diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index c1c120d0cf..dfda3a4f94 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -21,7 +21,7 @@ "profileName": "mssql-container" } ], - "omnisharp.defaultLaunchSolution": "umbraco-netcore-only.sln", + "omnisharp.defaultLaunchSolution": "umbraco.sln", "omnisharp.enableDecompilationSupport": true, "omnisharp.enableRoslynAnalyzers": true }, diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md index e97b03e7e3..1acc6b602b 100644 --- a/.github/CODE_OF_CONDUCT.md +++ b/.github/CODE_OF_CONDUCT.md @@ -57,6 +57,7 @@ Or alternatively, you can reach out directly to any of the team members behind t * Sebastiaan Janssen (He, Him - Languages spoken: English, Dutch, Danish(Read)) [sebastiaan@umbraco.com](mailto:sebastiaan@umbraco.com) * Ilham Boulghallat (She, Her - Languages spoken: English, French, Arabic) [ilham@umbraco.com](mailto:ilham@umbraco.com) * Arnold Visser (He, Him - Languages spoken: English, Dutch) [arnold@umbraco.com](mailto:arnold@umbraco.com) +* Emma Burstow (She, Her - Languages spoken: English) [ema@umbraco.com](mailto:ema@umbraco.com) The review process is done with full respect for the privacy and security of the reporter of any incident. @@ -89,4 +90,4 @@ Consequence: A permanent ban from any sort of public interaction within the comm ## Attribution This Code of Conduct is adapted from the Contributor Covenant, version 2.0, available at [https://www.contributor-covenant.org/version/2/0/code_of_conduct.html](https://www.contributor-covenant.org/version/2/0/code_of_conduct.html). -This Code of Conduct will be maintained and reviewed by the team listed above. \ No newline at end of file +This Code of Conduct will be maintained and reviewed by the team listed above. diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 33d3e851c7..add4e13c77 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -28,7 +28,7 @@ jobs: dotnet-version: '6.0.x' - name: dotnet build - run: dotnet build umbraco-netcore-only.sln # also runs npm build + run: dotnet build umbraco.sln -c SkipTests - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v1 diff --git a/.gitignore b/.gitignore index c69474ac30..59b050f634 100644 --- a/.gitignore +++ b/.gitignore @@ -93,6 +93,7 @@ tests/Umbraco.Tests.AcceptanceTest/cypress/support/chainable.ts tests/Umbraco.Tests.AcceptanceTest/cypress/videos/ tests/Umbraco.Tests.Integration.SqlCe/DatabaseContextTests.sdf tests/Umbraco.Tests.Integration.SqlCe/umbraco/Data/TEMP/ +tests/Umbraco.Tests.Integration/appsettings.Tests.Local.json tests/Umbraco.Tests.Integration/TEMP/* tests/Umbraco.Tests.Integration/umbraco/Data/ tests/Umbraco.Tests.Integration/umbraco/logs/ diff --git a/.globalconfig b/.globalconfig index 8342ab4580..8c0929382d 100644 --- a/.globalconfig +++ b/.globalconfig @@ -49,6 +49,7 @@ dotnet_analyzer_diagnostic.category-StyleCop.CSharp.MaintainabilityRules.severit dotnet_analyzer_diagnostic.category-StyleCop.CSharp.LayoutRules.severity = suggestion dotnet_diagnostic.SA1636.severity = none # SA1636: File header copyright text should match +dotnet_diagnostic.SA1101.severity = none # PrefixLocalCallsWithThis - stylecop appears to be ignoring dotnet_style_qualification_for_* dotnet_diagnostic.SA1503.severity = warning # BracesMustNotBeOmitted dotnet_diagnostic.SA1117.severity = warning # ParametersMustBeOnSameLineOrSeparateLines diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 9d2a5248e8..1d4324a34d 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -42,7 +42,7 @@ "type": "process", "args": [ "build", - "${workspaceFolder}/src/umbraco-netcore-only.sln", + "${workspaceFolder}/src/umbraco.sln", "/property:GenerateFullPaths=true", "/consoleloggerparameters:NoSummary" ], diff --git a/build/NuSpecs/UmbracoCms.SqlCe.nuspec b/build/NuSpecs/UmbracoCms.SqlCe.nuspec deleted file mode 100644 index 0a95a02d2a..0000000000 --- a/build/NuSpecs/UmbracoCms.SqlCe.nuspec +++ /dev/null @@ -1,43 +0,0 @@ - - - - Umbraco.Cms.SqlCe - 9.0.0 - Umbraco Cms Sql Ce Add-on - Umbraco HQ - Umbraco HQ - MIT - https://umbraco.com/ - https://umbraco.com/dist/nuget/logo-small.png - false - Contains the SQL CE assemblies needed to run Umbraco Cms. This package only contains assemblies and can be used for package development. Use the UmbracoCms package to setup Umbraco in Visual Studio as an ASP.NET Core project. - Contains the SQL CE assemblies needed to run Umbraco Cms - en-US - umbraco - - - - - - - - - - - - - - - - - - - - - - - diff --git a/build/NuSpecs/UmbracoCms.nuspec b/build/NuSpecs/UmbracoCms.nuspec index 00515660fe..127fd28220 100644 --- a/build/NuSpecs/UmbracoCms.nuspec +++ b/build/NuSpecs/UmbracoCms.nuspec @@ -20,6 +20,8 @@ + + diff --git a/src/ApiDocs/umbracotemplate/partials/class.tmpl.partial b/src/ApiDocs/umbracotemplate/partials/class.tmpl.partial index 9153a863a4..aa50d597ba 100644 --- a/src/ApiDocs/umbracotemplate/partials/class.tmpl.partial +++ b/src/ApiDocs/umbracotemplate/partials/class.tmpl.partial @@ -15,8 +15,8 @@
{{item.name.0.value}}
{{/inheritance.0}} -
{{__global.namespace}}:{{namespace}}
-
{{__global.assembly}}:{{assemblies.0}}.dll
+
{{__global.namespace}}: {{{namespace.specName.0.value}}}
+
{{__global.assembly}}: {{assemblies.0}}.dll
{{__global.syntax}}
{{syntax.content.0.value}}
diff --git a/src/JsonSchema/AppSettings.cs b/src/JsonSchema/AppSettings.cs index 73c5ea18f5..f9aa6b500c 100644 --- a/src/JsonSchema/AppSettings.cs +++ b/src/JsonSchema/AppSettings.cs @@ -3,6 +3,7 @@ using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Deploy.Core.Configuration.DebugConfiguration; using Umbraco.Deploy.Core.Configuration.DeployConfiguration; using Umbraco.Deploy.Core.Configuration.DeployProjectConfiguration; using Umbraco.Forms.Core.Configuration; @@ -127,6 +128,8 @@ namespace JsonSchema public DeploySettings Settings { get; set; } public DeployProjectConfig Project { get; set; } + + public DebugSettings Debug { get; set; } } } } diff --git a/src/JsonSchema/JsonSchema.csproj b/src/JsonSchema/JsonSchema.csproj index f0652e7e98..e46fc3ee4b 100644 --- a/src/JsonSchema/JsonSchema.csproj +++ b/src/JsonSchema/JsonSchema.csproj @@ -13,6 +13,8 @@ + + diff --git a/src/Umbraco.Cms.Persistence.SqlServer/Constants.cs b/src/Umbraco.Cms.Persistence.SqlServer/Constants.cs new file mode 100644 index 0000000000..19ec0738c8 --- /dev/null +++ b/src/Umbraco.Cms.Persistence.SqlServer/Constants.cs @@ -0,0 +1,12 @@ +namespace Umbraco.Cms.Persistence.SqlServer; + +/// +/// Constants related to SQLite. +/// +public static class Constants +{ + /// + /// SQLite provider name. + /// + public const string ProviderName = "Microsoft.Data.SqlClient"; +} diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/ColumnInSchemaDto.cs b/src/Umbraco.Cms.Persistence.SqlServer/Dtos/ColumnInSchemaDto.cs similarity index 91% rename from src/Umbraco.Infrastructure/Persistence/Dtos/ColumnInSchemaDto.cs rename to src/Umbraco.Cms.Persistence.SqlServer/Dtos/ColumnInSchemaDto.cs index 961a54bf68..65bd0b5d65 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/ColumnInSchemaDto.cs +++ b/src/Umbraco.Cms.Persistence.SqlServer/Dtos/ColumnInSchemaDto.cs @@ -1,6 +1,6 @@ using NPoco; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Persistence.SqlServer.Dtos { internal class ColumnInSchemaDto { diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/ConstraintPerColumnDto.cs b/src/Umbraco.Cms.Persistence.SqlServer/Dtos/ConstraintPerColumnDto.cs similarity index 86% rename from src/Umbraco.Infrastructure/Persistence/Dtos/ConstraintPerColumnDto.cs rename to src/Umbraco.Cms.Persistence.SqlServer/Dtos/ConstraintPerColumnDto.cs index e9aff45305..351979570c 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/ConstraintPerColumnDto.cs +++ b/src/Umbraco.Cms.Persistence.SqlServer/Dtos/ConstraintPerColumnDto.cs @@ -1,6 +1,6 @@ using NPoco; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Persistence.SqlServer.Dtos { internal class ConstraintPerColumnDto { diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/ConstraintPerTableDto.cs b/src/Umbraco.Cms.Persistence.SqlServer/Dtos/ConstraintPerTableDto.cs similarity index 82% rename from src/Umbraco.Infrastructure/Persistence/Dtos/ConstraintPerTableDto.cs rename to src/Umbraco.Cms.Persistence.SqlServer/Dtos/ConstraintPerTableDto.cs index c8f9a1abbf..3a633d4e0e 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/ConstraintPerTableDto.cs +++ b/src/Umbraco.Cms.Persistence.SqlServer/Dtos/ConstraintPerTableDto.cs @@ -1,6 +1,6 @@ using NPoco; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Persistence.SqlServer.Dtos { internal class ConstraintPerTableDto { diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/DefaultConstraintPerColumnDto.cs b/src/Umbraco.Cms.Persistence.SqlServer/Dtos/DefaultConstraintPerColumnDto.cs similarity index 88% rename from src/Umbraco.Infrastructure/Persistence/Dtos/DefaultConstraintPerColumnDto.cs rename to src/Umbraco.Cms.Persistence.SqlServer/Dtos/DefaultConstraintPerColumnDto.cs index 1441133daa..e0e1dfbe2f 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/DefaultConstraintPerColumnDto.cs +++ b/src/Umbraco.Cms.Persistence.SqlServer/Dtos/DefaultConstraintPerColumnDto.cs @@ -1,6 +1,6 @@ using NPoco; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Persistence.SqlServer.Dtos { internal class DefaultConstraintPerColumnDto { diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/DefinedIndexDto.cs b/src/Umbraco.Cms.Persistence.SqlServer/Dtos/DefinedIndexDto.cs similarity index 88% rename from src/Umbraco.Infrastructure/Persistence/Dtos/DefinedIndexDto.cs rename to src/Umbraco.Cms.Persistence.SqlServer/Dtos/DefinedIndexDto.cs index 287757dc9f..e78f354e46 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/DefinedIndexDto.cs +++ b/src/Umbraco.Cms.Persistence.SqlServer/Dtos/DefinedIndexDto.cs @@ -1,6 +1,6 @@ using NPoco; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Persistence.SqlServer.Dtos { internal class DefinedIndexDto { diff --git a/src/Umbraco.Cms.Persistence.SqlServer/Interceptors/SqlServerAddMiniProfilerInterceptor.cs b/src/Umbraco.Cms.Persistence.SqlServer/Interceptors/SqlServerAddMiniProfilerInterceptor.cs new file mode 100644 index 0000000000..7c5df6c497 --- /dev/null +++ b/src/Umbraco.Cms.Persistence.SqlServer/Interceptors/SqlServerAddMiniProfilerInterceptor.cs @@ -0,0 +1,11 @@ +using System.Data.Common; +using NPoco; +using StackExchange.Profiling; + +namespace Umbraco.Cms.Persistence.SqlServer.Interceptors; + +public class SqlServerAddMiniProfilerInterceptor : SqlServerConnectionInterceptor +{ + public override DbConnection OnConnectionOpened(IDatabase database, DbConnection conn) + => new StackExchange.Profiling.Data.ProfiledDbConnection(conn, MiniProfiler.Current); +} diff --git a/src/Umbraco.Cms.Persistence.SqlServer/Interceptors/SqlServerAddRetryPolicyInterceptor.cs b/src/Umbraco.Cms.Persistence.SqlServer/Interceptors/SqlServerAddRetryPolicyInterceptor.cs new file mode 100644 index 0000000000..bdf5745d42 --- /dev/null +++ b/src/Umbraco.Cms.Persistence.SqlServer/Interceptors/SqlServerAddRetryPolicyInterceptor.cs @@ -0,0 +1,34 @@ +using System.Data.Common; +using Microsoft.Extensions.Options; +using NPoco; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Infrastructure.Persistence.FaultHandling; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Persistence.SqlServer.Interceptors; + +public class SqlServerAddRetryPolicyInterceptor : SqlServerConnectionInterceptor +{ + private readonly IOptionsMonitor _connectionStrings; + + public SqlServerAddRetryPolicyInterceptor(IOptionsMonitor connectionStrings) + => _connectionStrings = connectionStrings; + + public override DbConnection OnConnectionOpened(IDatabase database, DbConnection conn) + { + if (!_connectionStrings.CurrentValue.IsConnectionStringConfigured()) + { + return conn; + } + + RetryPolicy? connectionRetryPolicy = RetryPolicyFactory.GetDefaultSqlConnectionRetryPolicyByConnectionString(_connectionStrings.CurrentValue.ConnectionString); + RetryPolicy? commandRetryPolicy = RetryPolicyFactory.GetDefaultSqlCommandRetryPolicyByConnectionString(_connectionStrings.CurrentValue.ConnectionString); + + if (connectionRetryPolicy == null && commandRetryPolicy == null) + { + return conn; + } + + return new RetryDbConnection(conn, connectionRetryPolicy, commandRetryPolicy); + } +} diff --git a/src/Umbraco.Cms.Persistence.SqlServer/Interceptors/SqlServerConnectionInterceptor.cs b/src/Umbraco.Cms.Persistence.SqlServer/Interceptors/SqlServerConnectionInterceptor.cs new file mode 100644 index 0000000000..499a8a05fe --- /dev/null +++ b/src/Umbraco.Cms.Persistence.SqlServer/Interceptors/SqlServerConnectionInterceptor.cs @@ -0,0 +1,16 @@ +using System.Data.Common; +using NPoco; +using Umbraco.Cms.Infrastructure.Persistence; + +namespace Umbraco.Cms.Persistence.SqlServer.Interceptors; + +public abstract class SqlServerConnectionInterceptor : IProviderSpecificConnectionInterceptor +{ + public string ProviderName => Constants.ProviderName; + + public abstract DbConnection OnConnectionOpened(IDatabase database, DbConnection conn); + + public virtual void OnConnectionClosing(IDatabase database, DbConnection conn) + { + } +} diff --git a/src/Umbraco.Infrastructure/Persistence/BulkDataReader.cs b/src/Umbraco.Cms.Persistence.SqlServer/Services/BulkDataReader.cs similarity index 99% rename from src/Umbraco.Infrastructure/Persistence/BulkDataReader.cs rename to src/Umbraco.Cms.Persistence.SqlServer/Services/BulkDataReader.cs index f03b60d778..e9784ce270 100644 --- a/src/Umbraco.Infrastructure/Persistence/BulkDataReader.cs +++ b/src/Umbraco.Cms.Persistence.SqlServer/Services/BulkDataReader.cs @@ -1,5 +1,3 @@ -using System; -using System.Collections.Generic; using System.Collections.ObjectModel; using System.Data; using System.Data.Common; @@ -7,7 +5,7 @@ using System.Diagnostics; using System.Globalization; using Microsoft.Data.SqlClient; -namespace Umbraco.Cms.Infrastructure.Persistence +namespace Umbraco.Cms.Persistence.SqlServer.Services { /// /// A base implementation of that is suitable for . diff --git a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/MicrosoftSqlSyntaxProviderBase.cs b/src/Umbraco.Cms.Persistence.SqlServer/Services/MicrosoftSqlSyntaxProviderBase.cs similarity index 78% rename from src/Umbraco.Infrastructure/Persistence/SqlSyntax/MicrosoftSqlSyntaxProviderBase.cs rename to src/Umbraco.Cms.Persistence.SqlServer/Services/MicrosoftSqlSyntaxProviderBase.cs index c9b05dd9a0..4856e1e117 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/MicrosoftSqlSyntaxProviderBase.cs +++ b/src/Umbraco.Cms.Persistence.SqlServer/Services/MicrosoftSqlSyntaxProviderBase.cs @@ -1,11 +1,12 @@ -using System; using System.Data; -using System.Linq; +using Microsoft.Extensions.Logging; +using NPoco; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Persistence; -using Umbraco.Cms.Infrastructure.Persistence.Querying; +using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; +using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; -namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax +namespace Umbraco.Cms.Persistence.SqlServer.Services { /// /// Abstract class for defining MS sql implementations @@ -14,8 +15,12 @@ namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax public abstract class MicrosoftSqlSyntaxProviderBase : SqlSyntaxProviderBase where TSyntax : ISqlSyntaxProvider { + private readonly ILogger _logger; + protected MicrosoftSqlSyntaxProviderBase() { + _logger = StaticApplicationLogging.CreateLogger(); + AutoIncrementDefinition = "IDENTITY(1,1)"; GuidColumnDefinition = "UniqueIdentifier"; RealColumnDefinition = "FLOAT"; @@ -34,7 +39,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax if (tableName?.Contains(".") == false) return $"[{tableName}]"; - var tableNameParts = tableName?.Split(Constants.CharArrays.Period, 2); + var tableNameParts = tableName?.Split(Cms.Core.Constants.CharArrays.Period, 2); return $"[{tableNameParts?[0]}].[{tableNameParts?[1]}]"; } @@ -175,5 +180,42 @@ namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax } return sqlDbType; } + + public override void HandleCreateTable(IDatabase database, TableDefinition tableDefinition, bool skipKeysAndIndexes = false) + { + var createSql = Format(tableDefinition); + var createPrimaryKeySql = FormatPrimaryKey(tableDefinition); + List foreignSql = Format(tableDefinition.ForeignKeys); + + _logger.LogInformation("Create table:\n {Sql}", createSql); + database.Execute(new Sql(createSql)); + + if (skipKeysAndIndexes) + { + return; + } + + //If any statements exists for the primary key execute them here + if (string.IsNullOrEmpty(createPrimaryKeySql) == false) + { + _logger.LogInformation("Create Primary Key:\n {Sql}", createPrimaryKeySql); + database.Execute(new Sql(createPrimaryKeySql)); + } + + List indexSql = Format(tableDefinition.Indexes); + //Loop through index statements and execute sql + foreach (var sql in indexSql) + { + _logger.LogInformation("Create Index:\n {Sql}", sql); + database.Execute(new Sql(sql)); + } + + //Loop through foreignkey statements and execute sql + foreach (var sql in foreignSql) + { + _logger.LogInformation("Create Foreign Key:\n {Sql}", sql); + database.Execute(new Sql(sql)); + } + } } } diff --git a/src/Umbraco.Infrastructure/Persistence/PocoDataDataReader.cs b/src/Umbraco.Cms.Persistence.SqlServer/Services/PocoDataDataReader.cs similarity index 97% rename from src/Umbraco.Infrastructure/Persistence/PocoDataDataReader.cs rename to src/Umbraco.Cms.Persistence.SqlServer/Services/PocoDataDataReader.cs index 17dc7937a6..8a05e78258 100644 --- a/src/Umbraco.Infrastructure/Persistence/PocoDataDataReader.cs +++ b/src/Umbraco.Cms.Persistence.SqlServer/Services/PocoDataDataReader.cs @@ -1,14 +1,10 @@ -using System; -using System.Collections.Generic; using System.Data; -using System.Linq; using NPoco; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; -using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence +namespace Umbraco.Cms.Persistence.SqlServer.Services { /// /// A data reader used for reading collections of PocoData entity types diff --git a/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlAzureDatabaseProviderMetadata.cs b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlAzureDatabaseProviderMetadata.cs new file mode 100644 index 0000000000..cf74a8549f --- /dev/null +++ b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlAzureDatabaseProviderMetadata.cs @@ -0,0 +1,101 @@ +using System.Runtime.Serialization; +using Umbraco.Cms.Core.Install.Models; +using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Persistence.SqlServer.Services; + +/// +/// Provider metadata for SQL Azure +/// +[DataContract] +public class SqlAzureDatabaseProviderMetadata : IDatabaseProviderMetadata +{ + /// + public Guid Id => new ("7858e827-8951-4fe0-a7fe-6883011b1f1b"); + + /// + public int SortOrder => 3; + + /// + public string DisplayName => "Azure SQL"; + + /// + public string DefaultDatabaseName => string.Empty; + + /// + public string ProviderName => Constants.ProviderName; + + /// + public bool SupportsQuickInstall => false; + + /// + public bool IsAvailable => true; + + /// + public bool RequiresServer => true; + + /// + public string ServerPlaceholder => "umbraco-database.database.windows.net"; + + /// + public bool RequiresCredentials => true; + + /// + public bool SupportsIntegratedAuthentication => false; + + /// + public bool RequiresConnectionTest => true; + + /// + public bool ForceCreateDatabase => false; + + /// + public string GenerateConnectionString(DatabaseModel databaseModel) + { + var server = databaseModel.Server; + var databaseName = databaseModel.DatabaseName; + var user = databaseModel.Login; + var password = databaseModel.Password; + + if (server.Contains(".") && ServerStartsWithTcp(server) == false) + server = $"tcp:{server}"; + + if (server.Contains(".") == false && ServerStartsWithTcp(server)) + { + string serverName = server.Contains(",") + ? server.Substring(0, server.IndexOf(",", StringComparison.Ordinal)) + : server; + + var portAddition = string.Empty; + + if (server.Contains(",")) + portAddition = server.Substring(server.IndexOf(",", StringComparison.Ordinal)); + + server = $"{serverName}.database.windows.net{portAddition}"; + } + + if (ServerStartsWithTcp(server) == false) + server = $"tcp:{server}.database.windows.net"; + + if (server.Contains(",") == false) + server = $"{server},1433"; + + if (user.Contains("@") == false) + { + var userDomain = server; + + if (ServerStartsWithTcp(server)) + userDomain = userDomain.Substring(userDomain.IndexOf(":", StringComparison.Ordinal) + 1); + + if (userDomain.Contains(".")) + userDomain = userDomain.Substring(0, userDomain.IndexOf(".", StringComparison.Ordinal)); + + user = $"{user}@{userDomain}"; + } + + return $"Server={server};Database={databaseName};User ID={user};Password={password}"; + } + + private static bool ServerStartsWithTcp(string server) => server.InvariantStartsWith("tcp:"); +} diff --git a/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlLocalDbDatabaseProviderMetadata.cs b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlLocalDbDatabaseProviderMetadata.cs new file mode 100644 index 0000000000..84e784731f --- /dev/null +++ b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlLocalDbDatabaseProviderMetadata.cs @@ -0,0 +1,66 @@ +using System.Runtime.Serialization; +using Microsoft.Data.SqlClient; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Install.Models; +using Umbraco.Cms.Infrastructure.Persistence; + +namespace Umbraco.Cms.Persistence.SqlServer.Services; + +/// +/// Provider metadata for SQL Server LocalDb +/// +[DataContract] +public class SqlLocalDbDatabaseProviderMetadata : IDatabaseProviderMetadata +{ + /// + public Guid Id => new ("05a7e9ed-aa6a-43af-a309-63422c87c675"); + + /// + public int SortOrder => 1; + + /// + public string DisplayName => "SQL Server Express LocalDB"; + + /// + public string DefaultDatabaseName => Core.Constants.System.UmbracoDefaultDatabaseName; + + /// + public string ProviderName => Constants.ProviderName; + + /// + public bool SupportsQuickInstall => true; + + /// + public bool IsAvailable => new LocalDb().IsAvailable; + + /// + public bool RequiresServer => false; + + /// + public string ServerPlaceholder => null; + + /// + public bool RequiresCredentials => false; + + /// + public bool SupportsIntegratedAuthentication => false; + + /// + public bool RequiresConnectionTest => false; + + /// + public bool ForceCreateDatabase => true; + + /// + public string GenerateConnectionString(DatabaseModel databaseModel) + { + var builder = new SqlConnectionStringBuilder + { + DataSource = @"(localdb)\MSSQLLocalDB", + AttachDBFilename = @$"{ConnectionStrings.DataDirectoryPlaceholder}\{databaseModel.DatabaseName}.mdf", + IntegratedSecurity = true + }; + + return builder.ConnectionString; + } +} diff --git a/src/Umbraco.Infrastructure/Persistence/SqlServerBulkSqlInsertProvider.cs b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerBulkSqlInsertProvider.cs similarity index 88% rename from src/Umbraco.Infrastructure/Persistence/SqlServerBulkSqlInsertProvider.cs rename to src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerBulkSqlInsertProvider.cs index ee2689b9e3..cfd30bbd90 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlServerBulkSqlInsertProvider.cs +++ b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerBulkSqlInsertProvider.cs @@ -1,20 +1,17 @@ -using System; -using System.Collections.Generic; using System.Data; -using System.Linq; using Microsoft.Data.SqlClient; using NPoco; -using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; +using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence +namespace Umbraco.Cms.Persistence.SqlServer.Services { /// /// A bulk sql insert provider for Sql Server /// public class SqlServerBulkSqlInsertProvider : IBulkSqlInsertProvider { - public string ProviderName => Cms.Core.Constants.DatabaseProviders.SqlServer; + public string ProviderName => Constants.ProviderName; public int BulkInsertRecords(IUmbracoDatabase database, IEnumerable records) { @@ -24,9 +21,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence var pocoData = database.PocoDataFactory.ForType(typeof(T)); if (pocoData == null) throw new InvalidOperationException("Could not find PocoData for " + typeof(T)); - return database.DatabaseType.IsSqlServer2008OrLater() - ? BulkInsertRecordsSqlServer(database, pocoData, recordsA) - : BasicBulkSqlInsertProvider.BulkInsertRecordsWithCommands(database, recordsA); + return BulkInsertRecordsSqlServer(database, pocoData, recordsA); } /// diff --git a/src/Umbraco.Infrastructure/Persistence/SqlServerDatabaseCreator.cs b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerDatabaseCreator.cs similarity index 92% rename from src/Umbraco.Infrastructure/Persistence/SqlServerDatabaseCreator.cs rename to src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerDatabaseCreator.cs index 63aab47047..205519d0b1 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlServerDatabaseCreator.cs +++ b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerDatabaseCreator.cs @@ -1,13 +1,11 @@ -using System; -using System.IO; using Microsoft.Data.SqlClient; -using Umbraco.Cms.Core; +using Umbraco.Cms.Infrastructure.Persistence; -namespace Umbraco.Cms.Infrastructure.Persistence +namespace Umbraco.Cms.Persistence.SqlServer.Services { public class SqlServerDatabaseCreator : IDatabaseCreator { - public string ProviderName => Constants.DatabaseProviders.SqlServer; + public string ProviderName => Constants.ProviderName; public void Create(string connectionString) { diff --git a/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerDatabaseProviderMetadata.cs b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerDatabaseProviderMetadata.cs new file mode 100644 index 0000000000..8c840f1778 --- /dev/null +++ b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerDatabaseProviderMetadata.cs @@ -0,0 +1,57 @@ +using System.Runtime.Serialization; +using Umbraco.Cms.Core.Install.Models; +using Umbraco.Cms.Infrastructure.Persistence; + +namespace Umbraco.Cms.Persistence.SqlServer.Services; + +/// +/// Provider metadata for SQL Server +/// +[DataContract] +public class SqlServerDatabaseProviderMetadata : IDatabaseProviderMetadata +{ + /// + public Guid Id => new ("5e1ad149-1951-4b74-90bf-2ac2aada9e73"); + + /// + public int SortOrder => 2; + + /// + public string DisplayName => "SQL Server"; + + /// + public string DefaultDatabaseName => string.Empty; + + /// + public string ProviderName => Constants.ProviderName; + + /// + public bool SupportsQuickInstall => false; + + /// + public bool IsAvailable => true; + + /// + public bool RequiresServer => true; + + /// + public string ServerPlaceholder => "(local)\\SQLEXPRESS"; + + /// + public bool RequiresCredentials => true; + + /// + public bool SupportsIntegratedAuthentication => true; + + /// + public bool RequiresConnectionTest => true; + + /// + public bool ForceCreateDatabase => false; + + /// + public string GenerateConnectionString(DatabaseModel databaseModel) => + databaseModel.IntegratedAuth + ? $"Server={databaseModel.Server};Database={databaseModel.DatabaseName};Integrated Security=true" + : $"Server={databaseModel.Server};Database={databaseModel.DatabaseName};User Id={databaseModel.Login};Password={databaseModel.Password}"; +} diff --git a/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerDistributedLockingMechanism.cs b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerDistributedLockingMechanism.cs new file mode 100644 index 0000000000..d85ef56e3b --- /dev/null +++ b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerDistributedLockingMechanism.cs @@ -0,0 +1,169 @@ +using System.Data; +using Microsoft.Data.SqlClient; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.DistributedLocking; +using Umbraco.Cms.Core.DistributedLocking.Exceptions; +using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Infrastructure.Scoping; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Persistence.SqlServer.Services; + +/// +/// SQL Server implementation of . +/// +public class SqlServerDistributedLockingMechanism : IDistributedLockingMechanism +{ + private readonly ILogger _logger; + private readonly Lazy _scopeAccessor; // Hooray it's a circular dependency. + private readonly IOptionsMonitor _globalSettings; + private readonly IOptionsMonitor _connectionStrings; + + /// + /// Initializes a new instance of the class. + /// + public SqlServerDistributedLockingMechanism( + ILogger logger, + Lazy scopeAccessor, + IOptionsMonitor globalSettings, + IOptionsMonitor connectionStrings) + { + _logger = logger; + _scopeAccessor = scopeAccessor; + _globalSettings = globalSettings; + _connectionStrings = connectionStrings; + } + + /// + public bool Enabled => _connectionStrings.CurrentValue.IsConnectionStringConfigured() && + _connectionStrings.CurrentValue.ProviderName == Constants.ProviderName; + + /// + public IDistributedLock ReadLock(int lockId, TimeSpan? obtainLockTimeout = null) + { + obtainLockTimeout ??= _globalSettings.CurrentValue.DistributedLockingReadLockDefaultTimeout; + return new SqlServerDistributedLock(this, lockId, DistributedLockType.ReadLock, obtainLockTimeout.Value); + } + + /// + public IDistributedLock WriteLock(int lockId, TimeSpan? obtainLockTimeout = null) + { + obtainLockTimeout ??= _globalSettings.CurrentValue.DistributedLockingWriteLockDefaultTimeout; + return new SqlServerDistributedLock(this, lockId, DistributedLockType.WriteLock, obtainLockTimeout.Value); + } + + private class SqlServerDistributedLock : IDistributedLock + { + private readonly SqlServerDistributedLockingMechanism _parent; + private readonly TimeSpan _timeout; + + public SqlServerDistributedLock( + SqlServerDistributedLockingMechanism parent, + int lockId, + DistributedLockType lockType, + TimeSpan timeout) + { + _parent = parent; + _timeout = timeout; + LockId = lockId; + LockType = lockType; + + _parent._logger.LogDebug("Requesting {lockType} for id {id}", LockType, LockId); + + try + { + switch (lockType) + { + case DistributedLockType.ReadLock: + ObtainReadLock(); + break; + case DistributedLockType.WriteLock: + ObtainWriteLock(); + break; + default: + throw new ArgumentOutOfRangeException(nameof(lockType), lockType, @"Unsupported lockType"); + } + } + catch (SqlException ex) when (ex.Number == 1222) + { + if (LockType == DistributedLockType.ReadLock) + { + throw new DistributedReadLockTimeoutException(LockId); + } + + throw new DistributedWriteLockTimeoutException(LockId); + } + + _parent._logger.LogDebug("Acquired {lockType} for id {id}", LockType, LockId); + } + + public int LockId { get; } + + public DistributedLockType LockType { get; } + + public void Dispose() + { + // Mostly no op, cleaned up by completing transaction in scope. + _parent._logger.LogDebug("Dropped {lockType} for id {id}", LockType, LockId); + } + + public override string ToString() + => $"SqlServerDistributedLock({LockId}, {LockType}"; + + private void ObtainReadLock() + { + IUmbracoDatabase db = _parent._scopeAccessor.Value.AmbientScope.Database; + + if (!db.InTransaction) + { + throw new InvalidOperationException("SqlServerDistributedLockingMechanism requires a transaction to function."); + } + + if (db.Transaction.IsolationLevel < IsolationLevel.ReadCommitted) + { + throw new InvalidOperationException("A transaction with minimum ReadCommitted isolation level is required."); + } + + const string query = "SELECT value FROM umbracoLock WITH (REPEATABLEREAD) WHERE id=@id"; + + db.Execute("SET LOCK_TIMEOUT " + _timeout.TotalMilliseconds + ";"); + + var i = db.ExecuteScalar(query, new {id = LockId}); + + if (i == null) + { + // ensure we are actually locking! + throw new ArgumentException(@$"LockObject with id={LockId} does not exist.", nameof(LockId)); + } + } + + private void ObtainWriteLock() + { + IUmbracoDatabase db = _parent._scopeAccessor.Value.AmbientScope.Database; + + if (!db.InTransaction) + { + throw new InvalidOperationException("SqlServerDistributedLockingMechanism requires a transaction to function."); + } + + if (db.Transaction.IsolationLevel < IsolationLevel.ReadCommitted) + { + throw new InvalidOperationException("A transaction with minimum ReadCommitted isolation level is required."); + } + + const string query = @"UPDATE umbracoLock WITH (REPEATABLEREAD) SET value = (CASE WHEN (value=1) THEN -1 ELSE 1 END) WHERE id=@id"; + + db.Execute("SET LOCK_TIMEOUT " + _timeout.TotalMilliseconds + ";"); + + var i = db.Execute(query, new {id = LockId}); + + if (i == 0) + { + // ensure we are actually locking! + throw new ArgumentException($"LockObject with id={LockId} does not exist."); + } + } + } +} diff --git a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerSyntaxProvider.cs similarity index 76% rename from src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs rename to src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerSyntaxProvider.cs index bffda81bd0..7b4feb0a98 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs +++ b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerSyntaxProvider.cs @@ -1,19 +1,17 @@ -using System; -using System.Collections.Generic; using System.Data; -using System.Diagnostics.CodeAnalysis; -using System.Linq; using Microsoft.Data.SqlClient; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using NPoco; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Persistence.SqlServer.Dtos; using Umbraco.Extensions; +using ColumnInfo = Umbraco.Cms.Infrastructure.Persistence.SqlSyntax.ColumnInfo; -namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax +namespace Umbraco.Cms.Persistence.SqlServer.Services { /// /// Represents an SqlSyntaxProvider for Sql Server. @@ -21,13 +19,20 @@ namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax public class SqlServerSyntaxProvider : MicrosoftSqlSyntaxProviderBase { private readonly IOptions _globalSettings; + private readonly ILogger _logger; public SqlServerSyntaxProvider(IOptions globalSettings) + : this(globalSettings, StaticApplicationLogging.CreateLogger()) { - _globalSettings = globalSettings; } - public override string ProviderName => Cms.Core.Constants.DatabaseProviders.SqlServer; + public SqlServerSyntaxProvider(IOptions globalSettings, ILogger logger) + { + _globalSettings = globalSettings; + _logger = logger; + } + + public override string ProviderName => Constants.ProviderName; public ServerVersionInfo? ServerVersion { get; private set; } @@ -57,6 +62,22 @@ namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax Azure = 5 } + public override DatabaseType GetUpdatedDatabaseType(DatabaseType current, string connectionString) + { + var setting = _globalSettings.Value.DatabaseFactoryServerVersion; + var fromSettings = false; + + if (setting.IsNullOrWhiteSpace() || !setting.StartsWith("SqlServer.") + || !Enum.TryParse(setting.Substring("SqlServer.".Length), out var versionName, true)) + { + versionName = GetSetVersion(connectionString, ProviderName, _logger).ProductVersionName; + } + + _logger.LogDebug("SqlServer {SqlServerVersion}, DatabaseType is {DatabaseType} ({Source}).", versionName, DatabaseType.SqlServer2012, fromSettings ? "settings" : "detected"); + + return DatabaseType.SqlServer2012; + } + public class ServerVersionInfo { public ServerVersionInfo() @@ -88,7 +109,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax private static VersionName MapProductVersion(string productVersion) { - var firstPart = string.IsNullOrWhiteSpace(productVersion) ? "??" : productVersion.Split(Constants.CharArrays.Period)[0]; + var firstPart = string.IsNullOrWhiteSpace(productVersion) ? "??" : productVersion.Split(Cms.Core.Constants.CharArrays.Period)[0]; switch (firstPart) { case "??": @@ -267,108 +288,7 @@ where tbl.[name]=@0 and col.[name]=@1;", tableName, columnName) return result > 0; } - public override void WriteLock(IDatabase db, TimeSpan timeout, int lockId) - { - // soon as we get Database, a transaction is started - - if (db.Transaction.IsolationLevel < IsolationLevel.ReadCommitted) - throw new InvalidOperationException("A transaction with minimum ReadCommitted isolation level is required."); - - ObtainWriteLock(db, timeout, lockId); - } - - public override void WriteLock(IDatabase db, params int[] lockIds) - { - WriteLock(db, _globalSettings.Value.SqlWriteLockTimeOut, lockIds); - } - - public void WriteLock(IDatabase db, TimeSpan timeout, params int[] lockIds) - { - if (db is null) - { - throw new ArgumentNullException(nameof(db)); - } - - if (db.Transaction is null) - { - throw new ArgumentException(nameof(db) + "." + nameof(db.Transaction) + " is null"); - } - - // soon as we get Database, a transaction is started - - if (db.Transaction.IsolationLevel < IsolationLevel.ReadCommitted) - { - throw new InvalidOperationException("A transaction with minimum ReadCommitted isolation level is required."); - } - - foreach (var lockId in lockIds) - { - ObtainWriteLock(db, timeout, lockId); - } - } - - private static void ObtainWriteLock(IDatabase db, TimeSpan timeout, int lockId) - { - db.Execute("SET LOCK_TIMEOUT " + timeout.TotalMilliseconds + ";"); - var i = db.Execute( - @"UPDATE umbracoLock WITH (REPEATABLEREAD) SET value = (CASE WHEN (value=1) THEN -1 ELSE 1 END) WHERE id=@id", - new {id = lockId}); - if (i == 0) // ensure we are actually locking! - { - throw new ArgumentException($"LockObject with id={lockId} does not exist."); - } - } - - public override void ReadLock(IDatabase db, TimeSpan timeout, int lockId) - { - // soon as we get Database, a transaction is started - - if (db.Transaction.IsolationLevel < IsolationLevel.ReadCommitted) - throw new InvalidOperationException("A transaction with minimum RepeatableRead isolation level is required."); - - ObtainReadLock(db, timeout, lockId); - } - - public override void ReadLock(IDatabase db, params int[] lockIds) - { - if (db is null) - { - throw new ArgumentNullException(nameof(db)); - } - - if (db.Transaction is null) - { - throw new ArgumentException(nameof(db) + "." + nameof(db.Transaction) + " is null"); - } - - // soon as we get Database, a transaction is started - - if (db.Transaction.IsolationLevel < IsolationLevel.ReadCommitted) - { - throw new InvalidOperationException("A transaction with minimum ReadCommitted isolation level is required."); - } - - foreach (var lockId in lockIds) - { - ObtainReadLock(db, null, lockId); - } - } - - private static void ObtainReadLock(IDatabase db, TimeSpan? timeout, int lockId) - { - if (timeout.HasValue) - { - db.Execute(@"SET LOCK_TIMEOUT " + timeout.Value.TotalMilliseconds + ";"); - } - - var i = db.ExecuteScalar("SELECT value FROM umbracoLock WITH (REPEATABLEREAD) WHERE id=@id", new {id = lockId}); - if (i == null) // ensure we are actually locking! - { - throw new ArgumentException($"LockObject with id={lockId} does not exist.", nameof(lockId)); - } - } - - public override string FormatColumnRename(string? tableName, string? oldName, string? newName) + public override string FormatColumnRename(string tableName, string oldName, string newName) { return string.Format(RenameColumn, tableName, oldName, newName); } @@ -434,5 +354,90 @@ where tbl.[name]=@0 and col.[name]=@1;", tableName, columnName) return string.Format(CreateIndex, GetIndexType(index.IndexType), " ", GetQuotedName(name), GetQuotedTableName(index.TableName), columns, includeColumns); } + + + public override Sql InsertForUpdateHint(Sql sql) + { + // go find the first FROM clause, and append the lock hint + Sql s = sql; + var updated = false; + + while (s != null) + { + var sqlText = SqlInspector.GetSqlText(s); + if (sqlText.StartsWith("FROM ", StringComparison.OrdinalIgnoreCase)) + { + SqlInspector.SetSqlText(s, sqlText + " WITH (UPDLOCK)"); + updated = true; + break; + } + + s = SqlInspector.GetSqlRhs(sql); + } + + if (updated) + SqlInspector.Reset(sql); + + return sql; + } + + public override Sql AppendForUpdateHint(Sql sql) + => sql.Append(" WITH (UPDLOCK) "); + + public override Sql.SqlJoinClause LeftJoinWithNestedJoin( + Sql sql, + Func, + Sql> nestedJoin, + string? alias = null) + { + Type type = typeof(TDto); + + var tableName = GetQuotedTableName(type.GetTableName()); + var join = tableName; + + if (alias != null) + { + var quotedAlias = GetQuotedTableName(alias); + join += " " + quotedAlias; + } + + var nestedSql = new Sql(sql.SqlContext); + nestedSql = nestedJoin(nestedSql); + + Sql.SqlJoinClause sqlJoin = sql.LeftJoin(join); + sql.Append(nestedSql); + return sqlJoin; + } + + #region Sql Inspection + + private static SqlInspectionUtilities _sqlInspector; + + private static SqlInspectionUtilities SqlInspector => _sqlInspector ?? (_sqlInspector = new SqlInspectionUtilities()); + + private class SqlInspectionUtilities + { + private readonly Func _getSqlText; + private readonly Action _setSqlText; + private readonly Func _getSqlRhs; + private readonly Action _setSqlFinal; + + public SqlInspectionUtilities() + { + (_getSqlText, _setSqlText) = ReflectionUtilities.EmitFieldGetterAndSetter("_sql"); + _getSqlRhs = ReflectionUtilities.EmitFieldGetter("_rhs"); + _setSqlFinal = ReflectionUtilities.EmitFieldSetter("_sqlFinal"); + } + + public string GetSqlText(Sql sql) => _getSqlText(sql); + + public void SetSqlText(Sql sql, string sqlText) => _setSqlText(sql, sqlText); + + public Sql GetSqlRhs(Sql sql) => _getSqlRhs(sql); + + public void Reset(Sql sql) => _setSqlFinal(sql, null); + } + + #endregion } } diff --git a/src/Umbraco.Cms.Persistence.SqlServer/SqlServerComposer.cs b/src/Umbraco.Cms.Persistence.SqlServer/SqlServerComposer.cs new file mode 100644 index 0000000000..60d64c09df --- /dev/null +++ b/src/Umbraco.Cms.Persistence.SqlServer/SqlServerComposer.cs @@ -0,0 +1,14 @@ +using Umbraco.Cms.Core.Composing; +using Umbraco.Cms.Core.DependencyInjection; + +namespace Umbraco.Cms.Persistence.SqlServer; + +/// +/// Automatically adds SQL Server support to Umbraco when this project is referenced. +/// +public class SqlServerComposer : IComposer +{ + /// + public void Compose(IUmbracoBuilder builder) + => builder.AddUmbracoSqlServerSupport(); +} diff --git a/src/Umbraco.Cms.Persistence.SqlServer/Umbraco.Cms.Persistence.SqlServer.csproj b/src/Umbraco.Cms.Persistence.SqlServer/Umbraco.Cms.Persistence.SqlServer.csproj new file mode 100644 index 0000000000..d73e5293f7 --- /dev/null +++ b/src/Umbraco.Cms.Persistence.SqlServer/Umbraco.Cms.Persistence.SqlServer.csproj @@ -0,0 +1,26 @@ + + + + net6.0 + enable + enable + + Umbraco.Cms.Persistence.SqlServer + Umbraco.Cms.Persistence.SqlServer + Adds support for SQL Server to Umbraco CMS. + + + + + + + + + <_Parameter1>Umbraco.Tests.Integration + + + <_Parameter1>Umbraco.Tests.UnitTests + + + + diff --git a/src/Umbraco.Cms.Persistence.SqlServer/UmbracoBuilderExtensions.cs b/src/Umbraco.Cms.Persistence.SqlServer/UmbracoBuilderExtensions.cs new file mode 100644 index 0000000000..5e4e68a0dc --- /dev/null +++ b/src/Umbraco.Cms.Persistence.SqlServer/UmbracoBuilderExtensions.cs @@ -0,0 +1,42 @@ +using System.Data.Common; +using Microsoft.Data.SqlClient; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.DistributedLocking; +using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; +using Umbraco.Cms.Persistence.SqlServer.Interceptors; +using Umbraco.Cms.Persistence.SqlServer.Services; + +namespace Umbraco.Cms.Persistence.SqlServer; + +/// +/// SQLite support extensions for IUmbracoBuilder. +/// +public static class UmbracoBuilderExtensions +{ + /// + /// Add required services for SQL Server support. + /// + public static IUmbracoBuilder AddUmbracoSqlServerSupport(this IUmbracoBuilder builder) + { + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); + + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); + + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); + + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); + + DbProviderFactories.UnregisterFactory(Constants.ProviderName); + DbProviderFactories.RegisterFactory(Constants.ProviderName, SqlClientFactory.Instance); + + return builder; + } +} diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Constants.cs b/src/Umbraco.Cms.Persistence.Sqlite/Constants.cs new file mode 100644 index 0000000000..76e408423c --- /dev/null +++ b/src/Umbraco.Cms.Persistence.Sqlite/Constants.cs @@ -0,0 +1,12 @@ +namespace Umbraco.Cms.Persistence.Sqlite; + +/// +/// Constants related to SQLite. +/// +public static class Constants +{ + /// + /// SQLite provider name. + /// + public const string ProviderName = "Microsoft.Data.SQLite"; +} diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Interceptors/SqliteAddMiniProfilerInterceptor.cs b/src/Umbraco.Cms.Persistence.Sqlite/Interceptors/SqliteAddMiniProfilerInterceptor.cs new file mode 100644 index 0000000000..eb76319040 --- /dev/null +++ b/src/Umbraco.Cms.Persistence.Sqlite/Interceptors/SqliteAddMiniProfilerInterceptor.cs @@ -0,0 +1,11 @@ +using System.Data.Common; +using NPoco; +using StackExchange.Profiling; + +namespace Umbraco.Cms.Persistence.Sqlite.Interceptors; + +public class SqliteAddMiniProfilerInterceptor : SqliteConnectionInterceptor +{ + public override DbConnection OnConnectionOpened(IDatabase database, DbConnection conn) + => new StackExchange.Profiling.Data.ProfiledDbConnection(conn, MiniProfiler.Current); +} diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Interceptors/SqliteAddPreferDeferredInterceptor.cs b/src/Umbraco.Cms.Persistence.Sqlite/Interceptors/SqliteAddPreferDeferredInterceptor.cs new file mode 100644 index 0000000000..ef22e9c0b6 --- /dev/null +++ b/src/Umbraco.Cms.Persistence.Sqlite/Interceptors/SqliteAddPreferDeferredInterceptor.cs @@ -0,0 +1,12 @@ +using System.Data.Common; +using Microsoft.Data.Sqlite; +using NPoco; +using Umbraco.Cms.Persistence.Sqlite.Services; + +namespace Umbraco.Cms.Persistence.Sqlite.Interceptors; + +public class SqliteAddPreferDeferredInterceptor : SqliteConnectionInterceptor +{ + public override DbConnection OnConnectionOpened(IDatabase database, DbConnection conn) + => new SqlitePreferDeferredTransactionsConnection(conn as SqliteConnection ?? throw new InvalidOperationException()); +} diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Interceptors/SqliteAddRetryPolicyInterceptor.cs b/src/Umbraco.Cms.Persistence.Sqlite/Interceptors/SqliteAddRetryPolicyInterceptor.cs new file mode 100644 index 0000000000..8010b8b696 --- /dev/null +++ b/src/Umbraco.Cms.Persistence.Sqlite/Interceptors/SqliteAddRetryPolicyInterceptor.cs @@ -0,0 +1,16 @@ +using System.Data.Common; +using NPoco; +using Umbraco.Cms.Infrastructure.Persistence.FaultHandling; +using Umbraco.Cms.Persistence.Sqlite.Services; + +namespace Umbraco.Cms.Persistence.Sqlite.Interceptors; + +public class SqliteAddRetryPolicyInterceptor : SqliteConnectionInterceptor +{ + public override DbConnection OnConnectionOpened(IDatabase database, DbConnection conn) + { + RetryStrategy retryStrategy = RetryStrategy.DefaultExponential; + var commandRetryPolicy = new RetryPolicy(new SqliteTransientErrorDetectionStrategy(), retryStrategy); + return new RetryDbConnection(conn, null, commandRetryPolicy); + } +} diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Interceptors/SqliteConnectionInterceptor.cs b/src/Umbraco.Cms.Persistence.Sqlite/Interceptors/SqliteConnectionInterceptor.cs new file mode 100644 index 0000000000..b74533d2f6 --- /dev/null +++ b/src/Umbraco.Cms.Persistence.Sqlite/Interceptors/SqliteConnectionInterceptor.cs @@ -0,0 +1,16 @@ +using System.Data.Common; +using NPoco; +using Umbraco.Cms.Infrastructure.Persistence; + +namespace Umbraco.Cms.Persistence.Sqlite.Interceptors; + +public abstract class SqliteConnectionInterceptor : IProviderSpecificConnectionInterceptor +{ + public string ProviderName => Constants.ProviderName; + + public abstract DbConnection OnConnectionOpened(IDatabase database, DbConnection conn); + + public virtual void OnConnectionClosing(IDatabase database, DbConnection conn) + { + } +} diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Mappers/SqliteGuidScalarMapper.cs b/src/Umbraco.Cms.Persistence.Sqlite/Mappers/SqliteGuidScalarMapper.cs new file mode 100644 index 0000000000..bd9bb1924d --- /dev/null +++ b/src/Umbraco.Cms.Persistence.Sqlite/Mappers/SqliteGuidScalarMapper.cs @@ -0,0 +1,24 @@ +using Umbraco.Cms.Infrastructure.Persistence; + +namespace Umbraco.Cms.Persistence.Sqlite.Mappers; + +public class SqliteGuidScalarMapper : ScalarMapper +{ + protected override Guid Map(object value) + => Guid.Parse($"{value}"); +} + +public class SqliteNullableGuidScalarMapper : ScalarMapper +{ + protected override Guid? Map(object? value) + { + if (value is null || value == DBNull.Value) + { + return default(Guid?); + } + + return Guid.TryParse($"{value}", out Guid result) + ? result + : default(Guid?); + } +} diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Mappers/SqlitePocoGuidMapper.cs b/src/Umbraco.Cms.Persistence.Sqlite/Mappers/SqlitePocoGuidMapper.cs new file mode 100644 index 0000000000..f7b2836f1a --- /dev/null +++ b/src/Umbraco.Cms.Persistence.Sqlite/Mappers/SqlitePocoGuidMapper.cs @@ -0,0 +1,33 @@ +using NPoco; + +namespace Umbraco.Cms.Persistence.Sqlite.Mappers; + +public class SqlitePocoGuidMapper : DefaultMapper +{ + public override Func GetFromDbConverter(Type destType, Type sourceType) + { + if (destType == typeof(Guid)) + { + return (value) => + { + var result = Guid.Parse($"{value}"); + return result; + }; + } + + if (destType == typeof(Guid?)) + { + return (value) => + { + if (Guid.TryParse($"{value}", out Guid result)) + { + return result; + } + + return default(Guid?); + }; + } + + return base.GetFromDbConverter(destType, sourceType); + } +} diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteBulkSqlInsertProvider.cs b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteBulkSqlInsertProvider.cs new file mode 100644 index 0000000000..895ee21ef6 --- /dev/null +++ b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteBulkSqlInsertProvider.cs @@ -0,0 +1,55 @@ +using NPoco; +using Umbraco.Cms.Infrastructure.Persistence; + +namespace Umbraco.Cms.Persistence.Sqlite.Services; + +/// +/// Implements for SQLite. +/// +public class SqliteBulkSqlInsertProvider : IBulkSqlInsertProvider +{ + public string ProviderName => Constants.ProviderName; + + public int BulkInsertRecords(IUmbracoDatabase database, IEnumerable records) + { + var recordsA = records.ToArray(); + if (recordsA.Length == 0) return 0; + + var pocoData = database.PocoDataFactory.ForType(typeof(T)); + if (pocoData == null) throw new InvalidOperationException("Could not find PocoData for " + typeof(T)); + + return BulkInsertRecordsSqlite(database, pocoData, recordsA); + } + + /// + /// Bulk-insert records using SqlServer BulkCopy method. + /// + /// The type of the records. + /// The database. + /// The PocoData object corresponding to the record's type. + /// The records. + /// The number of records that were inserted. + private int BulkInsertRecordsSqlite(IUmbracoDatabase database, PocoData pocoData, IEnumerable records) + { + var count = 0; + var inTrans = database.InTransaction; + + if (!inTrans) + { + database.BeginTransaction(); + } + + foreach (var record in records) + { + database.Insert(record); + count++; + } + + if (!inTrans) + { + database.CompleteTransaction(); + } + + return count; + } +} diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteDatabaseCreator.cs b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteDatabaseCreator.cs new file mode 100644 index 0000000000..43980b3b77 --- /dev/null +++ b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteDatabaseCreator.cs @@ -0,0 +1,48 @@ +using System.Diagnostics; +using Microsoft.Data.Sqlite; +using Umbraco.Cms.Infrastructure.Persistence; + +namespace Umbraco.Cms.Persistence.Sqlite.Services; + +/// +/// Implements for SQLite. +/// +public class SqliteDatabaseCreator : IDatabaseCreator +{ + /// + public string ProviderName => Constants.ProviderName; + + /// + /// Creates a SQLite database file. + /// + /// + /// + /// With journal_mode = wal we have snapshot isolation. + /// + /// + /// Concurrent read/write can take occur, committing a write transaction will have no impact + /// on open read transactions as they see only committed data from the point in time that they began reading. + /// + /// + /// A write transaction still requires exclusive access to database files so concurrent writes are not possible. + /// + /// + /// Read more Isolation in SQLite
+ /// Read more Write-Ahead Logging + ///
+ ///
+ public void Create(string connectionString) + { + using var connection = new SqliteConnection(connectionString); + connection.Open(); + + using SqliteCommand command = connection.CreateCommand(); + command.CommandText = "PRAGMA journal_mode = wal;"; + command.ExecuteNonQuery(); + + command.CommandText = "PRAGMA journal_mode"; + var mode = command.ExecuteScalar(); + + Debug.Assert(mode as string == "wal", "incorrect journal_mode"); + } +} diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteDatabaseProviderMetadata.cs b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteDatabaseProviderMetadata.cs new file mode 100644 index 0000000000..07e6db8b8f --- /dev/null +++ b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteDatabaseProviderMetadata.cs @@ -0,0 +1,72 @@ +using System.Runtime.Serialization; +using Microsoft.Data.Sqlite; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Install.Models; +using Umbraco.Cms.Infrastructure.Persistence; + +namespace Umbraco.Cms.Persistence.Sqlite.Services; + +[DataContract] +public class SqliteDatabaseProviderMetadata : IDatabaseProviderMetadata +{ + /// + public Guid Id => new ("530386a2-b219-4d5f-b68c-b965e14c9ac9"); + + /// + public int SortOrder => -1; + + /// + public string DisplayName => "SQLite"; + + /// + public string DefaultDatabaseName => Core.Constants.System.UmbracoDefaultDatabaseName; + + /// + public string ProviderName => Constants.ProviderName; + + /// + public bool SupportsQuickInstall => true; + + /// + public bool IsAvailable => true; + + /// + public bool RequiresServer => false; + + /// + public string ServerPlaceholder => null; + + /// + public bool RequiresCredentials => false; + + /// + public bool SupportsIntegratedAuthentication => false; + + /// + public bool RequiresConnectionTest => false; + + /// + /// + /// + /// Required to ensure database creator is used regardless of configured InstallMissingDatabase value. + /// + /// + /// Ensures database setup with journal_mode = wal; + /// + /// + public bool ForceCreateDatabase => true; + + /// + public string GenerateConnectionString(DatabaseModel databaseModel) + { + var builder = new SqliteConnectionStringBuilder + { + DataSource = $"{ConnectionStrings.DataDirectoryPlaceholder}/{databaseModel.DatabaseName}.sqlite.db", + ForeignKeys = true, + Pooling = true, + Cache = SqliteCacheMode.Shared, + }; + + return builder.ConnectionString; + } +} diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteDistributedLockingMechanism.cs b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteDistributedLockingMechanism.cs new file mode 100644 index 0000000000..c8c34603ab --- /dev/null +++ b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteDistributedLockingMechanism.cs @@ -0,0 +1,157 @@ +using System.Data; +using System.Data.Common; +using Microsoft.Data.SqlClient; +using Microsoft.Data.Sqlite; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.DistributedLocking; +using Umbraco.Cms.Core.DistributedLocking.Exceptions; +using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Infrastructure.Scoping; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Persistence.Sqlite.Services; + +public class SqliteDistributedLockingMechanism : IDistributedLockingMechanism +{ + private readonly ILogger _logger; + private readonly Lazy _scopeAccessor; + private readonly IOptionsMonitor _connectionStrings; + private readonly IOptionsMonitor _globalSettings; + + public SqliteDistributedLockingMechanism( + ILogger logger, + Lazy scopeAccessor, + IOptionsMonitor globalSettings, + IOptionsMonitor connectionStrings) + { + _logger = logger; + _scopeAccessor = scopeAccessor; + _connectionStrings = connectionStrings; + _globalSettings = globalSettings; + } + + /// + public bool Enabled => _connectionStrings.CurrentValue.IsConnectionStringConfigured() && + _connectionStrings.CurrentValue.ProviderName == Constants.ProviderName; + + // With journal_mode=wal we can always read a snapshot. + public IDistributedLock ReadLock(int lockId, TimeSpan? obtainLockTimeout = null) + { + obtainLockTimeout ??= _globalSettings.CurrentValue.DistributedLockingReadLockDefaultTimeout; + return new SqliteDistributedLock(this, lockId, DistributedLockType.ReadLock, obtainLockTimeout.Value); + } + + // With journal_mode=wal only a single write transaction can exist at a time. + public IDistributedLock WriteLock(int lockId, TimeSpan? obtainLockTimeout = null) + { + obtainLockTimeout ??= _globalSettings.CurrentValue.DistributedLockingWriteLockDefaultTimeout; + return new SqliteDistributedLock(this, lockId, DistributedLockType.WriteLock, obtainLockTimeout.Value); + } + + private class SqliteDistributedLock : IDistributedLock + { + private readonly SqliteDistributedLockingMechanism _parent; + private readonly TimeSpan _timeout; + + public SqliteDistributedLock( + SqliteDistributedLockingMechanism parent, + int lockId, + DistributedLockType lockType, + TimeSpan timeout) + { + _parent = parent; + _timeout = timeout; + LockId = lockId; + LockType = lockType; + + _parent._logger.LogDebug("Requesting {lockType} for id {id}", LockType, LockId); + + try + { + switch (lockType) + { + case DistributedLockType.ReadLock: + ObtainReadLock(); + break; + case DistributedLockType.WriteLock: + ObtainWriteLock(); + break; + default: + throw new ArgumentOutOfRangeException(nameof(lockType), lockType, @"Unsupported lockType"); + } + } + catch (SqlException ex) when (ex.Number == 1222) + { + if (LockType == DistributedLockType.ReadLock) + { + throw new DistributedReadLockTimeoutException(LockId); + } + + throw new DistributedWriteLockTimeoutException(LockId); + } + + _parent._logger.LogDebug("Acquired {lockType} for id {id}", LockType, LockId); + } + + public int LockId { get; } + + public DistributedLockType LockType { get; } + + public void Dispose() + { + // Mostly no op, cleaned up by completing transaction in scope. + _parent._logger.LogDebug("Dropped {lockType} for id {id}", LockType, LockId); + } + + public override string ToString() + => $"SqliteDistributedLock({LockId})"; + + // Can always obtain a read lock (snapshot isolation in wal mode) + // Mostly no-op just check that we didn't end up ReadUncommitted for real. + private void ObtainReadLock() + { + IUmbracoDatabase db = _parent._scopeAccessor.Value.AmbientScope.Database; + + if (!db.InTransaction) + { + throw new InvalidOperationException("SqliteDistributedLockingMechanism requires a transaction to function."); + } + } + + // Only one writer is possible at a time + // lock occurs for entire database as opposed to row/table. + private void ObtainWriteLock() + { + IUmbracoDatabase db = _parent._scopeAccessor.Value.AmbientScope.Database; + + if (!db.InTransaction) + { + throw new InvalidOperationException("SqliteDistributedLockingMechanism requires a transaction to function."); + } + + var query = @$"UPDATE umbracoLock SET value = (CASE WHEN (value=1) THEN -1 ELSE 1 END) WHERE id = {LockId}"; + + DbCommand command = db.CreateCommand(db.Connection, CommandType.Text, query); + + // imagine there is an existing writer, whilst elapsed time is < command timeout sqlite will busy loop + command.CommandTimeout = _timeout.Seconds; + + try + { + var i = command.ExecuteNonQuery(); + + if (i == 0) + { + // ensure we are actually locking! + throw new ArgumentException($"LockObject with id={LockId} does not exist."); + } + } + catch (SqliteException ex) when (ex.IsBusyOrLocked()) + { + throw new DistributedWriteLockTimeoutException(LockId); + } + } + } +} diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteExceptionExtensions.cs b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteExceptionExtensions.cs new file mode 100644 index 0000000000..4076718266 --- /dev/null +++ b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteExceptionExtensions.cs @@ -0,0 +1,12 @@ +using Microsoft.Data.Sqlite; + +namespace Umbraco.Cms.Persistence.Sqlite.Services; + +public static class SqliteExceptionExtensions +{ + public static bool IsBusyOrLocked(this SqliteException ex) => + ex.SqliteErrorCode + is SQLitePCL.raw.SQLITE_BUSY + or SQLitePCL.raw.SQLITE_LOCKED + or SQLitePCL.raw.SQLITE_LOCKED_SHAREDCACHE; +} diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqlitePreferDeferredTransactionsConnection.cs b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqlitePreferDeferredTransactionsConnection.cs new file mode 100644 index 0000000000..e4cee692a2 --- /dev/null +++ b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqlitePreferDeferredTransactionsConnection.cs @@ -0,0 +1,124 @@ +using System.Data; +using System.Data.Common; +using Microsoft.Data.Sqlite; + +namespace Umbraco.Cms.Persistence.Sqlite.Services; + +public class SqlitePreferDeferredTransactionsConnection : DbConnection +{ + private readonly SqliteConnection _inner; + + public SqlitePreferDeferredTransactionsConnection(SqliteConnection inner) + { + _inner = inner; + } + + protected override DbTransaction BeginDbTransaction(IsolationLevel isolationLevel) + => _inner.BeginTransaction(isolationLevel, deferred: true); // <-- The important bit + + public override void ChangeDatabase(string databaseName) + => _inner.ChangeDatabase(databaseName); + + public override void Close() + => _inner.Close(); + + public override void Open() + => _inner.Open(); + + public override string Database + => _inner.Database; + + public override ConnectionState State + => _inner.State; + + public override string DataSource + => _inner.DataSource; + + public override string ServerVersion + => _inner.ServerVersion; + + protected override DbCommand CreateDbCommand() + => new CommandWrapper(_inner.CreateCommand()); + + public override string ConnectionString + { + get => _inner.ConnectionString; + set => _inner.ConnectionString = value; + } + + private class CommandWrapper : DbCommand + { + private readonly DbCommand _inner; + + public CommandWrapper(DbCommand inner) + { + _inner = inner; + } + + public override void Cancel() + => _inner.Cancel(); + + public override int ExecuteNonQuery() + => _inner.ExecuteNonQuery(); + + public override object? ExecuteScalar() + => _inner.ExecuteScalar(); + + public override void Prepare() + => _inner.Prepare(); + + public override string CommandText + { + get => _inner.CommandText; + set => _inner.CommandText = value; + } + + public override int CommandTimeout + { + get => _inner.CommandTimeout; + set => _inner.CommandTimeout = value; + } + + public override CommandType CommandType + { + get => _inner.CommandType; + set => _inner.CommandType = value; + } + + public override UpdateRowSource UpdatedRowSource + { + get => _inner.UpdatedRowSource; + set => _inner.UpdatedRowSource = value; + } + + protected override DbConnection? DbConnection + { + get => _inner.Connection; + set + { + _inner.Connection = (value as SqlitePreferDeferredTransactionsConnection)?._inner; + } + } + + protected override DbParameterCollection DbParameterCollection + => _inner.Parameters; + + protected override DbTransaction? DbTransaction + { + get => _inner.Transaction; + set => _inner.Transaction = value; + } + + public override bool DesignTimeVisible + { + get => _inner.DesignTimeVisible; + set => _inner.DesignTimeVisible = value; + } + + protected override DbParameter CreateDbParameter() + => _inner.CreateParameter(); + + protected override DbDataReader ExecuteDbDataReader(CommandBehavior behavior) + => _inner.ExecuteReader(behavior); + } +} diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteSpecificMapperFactory.cs b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteSpecificMapperFactory.cs new file mode 100644 index 0000000000..cf1d707d69 --- /dev/null +++ b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteSpecificMapperFactory.cs @@ -0,0 +1,16 @@ +using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Persistence.Sqlite.Mappers; + +namespace Umbraco.Cms.Persistence.Sqlite.Services; + +/// +/// Implements for SQLite. +/// +public class SqliteSpecificMapperFactory : IProviderSpecificMapperFactory +{ + /// + public string ProviderName => Constants.ProviderName; + + /// + public NPocoMapperCollection Mappers => new NPocoMapperCollection(() => new[] { new SqlitePocoGuidMapper() }); +} diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteSyntaxProvider.cs b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteSyntaxProvider.cs new file mode 100644 index 0000000000..cc9d1ff279 --- /dev/null +++ b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteSyntaxProvider.cs @@ -0,0 +1,424 @@ +using System.Data; +using System.Data.Common; +using System.Linq.Expressions; +using System.Reflection; +using System.Text; +using System.Text.RegularExpressions; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using NPoco; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; +using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; +using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; +using Umbraco.Cms.Persistence.Sqlite.Mappers; +using Umbraco.Extensions; +using ColumnInfo = Umbraco.Cms.Infrastructure.Persistence.SqlSyntax.ColumnInfo; + +namespace Umbraco.Cms.Persistence.Sqlite.Services; + +/// +/// Implements for SQLite. +/// +public class SqliteSyntaxProvider : SqlSyntaxProviderBase +{ + private readonly IOptions _globalSettings; + private readonly ILogger _log; + private readonly IDictionary _scalarMappers; + + public SqliteSyntaxProvider(IOptions globalSettings, ILogger log) + { + _globalSettings = globalSettings; + _log = log; + + _scalarMappers = new Dictionary + { + [typeof(Guid)] = new SqliteGuidScalarMapper(), + [typeof(Guid?)] = new SqliteNullableGuidScalarMapper(), + }; + } + + /// + public override string ProviderName => Constants.ProviderName; + + public override string StringColumnDefinition => "TEXT COLLATE NOCASE"; + + public override string StringLengthUnicodeColumnDefinitionFormat => "TEXT COLLATE NOCASE"; + + /// + public override IsolationLevel DefaultIsolationLevel + => IsolationLevel.Serializable; + + /// + public override string DbProvider => Constants.ProviderName; + + + /// + public override bool SupportsIdentityInsert() => false; + + /// + public override bool SupportsClustered() => false; + + + public override string GetIndexType(IndexTypes indexTypes) + { + switch (indexTypes) + { + case IndexTypes.UniqueNonClustered: + return "UNIQUE"; + default: + return string.Empty; + } + } + + public override List Format(IEnumerable foreignKeys) + { + return foreignKeys.Select(Format).ToList(); + } + + public virtual string Format(ForeignKeyDefinition foreignKey) + { + var constraintName = string.IsNullOrEmpty(foreignKey.Name) + ? $"FK_{foreignKey.ForeignTable}_{foreignKey.PrimaryTable}_{foreignKey.PrimaryColumns.First()}" + : foreignKey.Name; + + var localColumn = GetQuotedColumnName(foreignKey.ForeignColumns.First()); + var remoteColumn = GetQuotedColumnName(foreignKey.PrimaryColumns.First()); + var remoteTable = GetQuotedTableName(foreignKey.PrimaryTable); + var onDelete = FormatCascade("DELETE", foreignKey.OnDelete); + var onUpdate = FormatCascade("UPDATE", foreignKey.OnUpdate); + + return + $"CONSTRAINT {constraintName} FOREIGN KEY ({localColumn}) REFERENCES {remoteTable} ({remoteColumn}) {onDelete} {onUpdate}"; + } + + /// + public override IEnumerable> GetDefinedIndexes(IDatabase db) + { + List items = db.Fetch( + @"SELECT + m.tbl_name AS tableName, + ilist.name AS indexName, + iinfo.name AS columnName, + ilist.[unique] AS isUnique + FROM + sqlite_master AS m, + pragma_index_list(m.name) AS ilist, + pragma_index_info(ilist.name) AS iinfo"); + + return items + .Where(x => !x.IndexName.StartsWith("sqlite_")) + .Select(item => + new Tuple(item.TableName, item.IndexName, item.ColumnName, item.IsUnique)) + .ToList(); + } + + + public override string ConvertIntegerToOrderableString => "substr('0000000000'||'{0}', -10, 10)"; + public override string ConvertDecimalToOrderableString => "substr('0000000000'||'{0}', -10, 10)"; + public override string ConvertDateToOrderableString => "{0}"; + + /// + public override string GetSpecialDbType(SpecialDbType dbType) => "TEXT COLLATE NOCASE"; + + /// + public override string GetSpecialDbType(SpecialDbType dbType, int customSize) => GetSpecialDbType(dbType); + + /// + public override bool TryGetDefaultConstraint(IDatabase db, string tableName, string columnName, + out string constraintName) + { + // TODO: SQLite + constraintName = string.Empty; + return false; + } + + public override string GetFieldNameForUpdate(Expression> fieldSelector, + string tableAlias = null) + { + var field = ExpressionHelper.FindProperty(fieldSelector).Item1 as PropertyInfo; + var fieldName = GetColumnName(field!); + + return GetQuotedColumnName(fieldName); + } + + private static string GetColumnName(PropertyInfo column) + { + ColumnAttribute? attr = column.FirstAttribute(); + return string.IsNullOrWhiteSpace(attr?.Name) ? column.Name : attr.Name; + } + + /// + protected override string FormatSystemMethods(SystemMethods systemMethod) + { + // TODO: SQLite + switch (systemMethod) + { + case SystemMethods.NewGuid: + return "NEWID()"; // No NEWID() in SQLite perhaps try RANDOM() + case SystemMethods.CurrentDateTime: + return "DATE()"; // No GETDATE() trying DATE() + } + + return null; + } + + /// + protected override string FormatIdentity(ColumnDefinition column) + { + /* NOTE: We need AUTOINCREMENT, adds overhead but makes magic ids not break everything. + * e.g. Cms.Core.Constants.Security.SuperUserId is -1 + * without the sqlite_sequence table we end up with the next user id = 0 + * but 0 is considered to not exist by our c# code and things explode */ + return column.IsIdentity ? "PRIMARY KEY AUTOINCREMENT" : string.Empty; + } + + public override string GetConcat(params string[] args) + { + return string.Join(" || ", args.AsEnumerable()); + } + + public override string GetColumn(DatabaseType dbType, string tableName, string columnName, string columnAlias, + string referenceName = null, bool forInsert = false) + { + if (forInsert) + { + return dbType.EscapeSqlIdentifier(columnName); + } + + return base.GetColumn(dbType, tableName, columnName, columnAlias, referenceName, forInsert); + } + + public override string FormatPrimaryKey(TableDefinition table) + { + ColumnDefinition? columnDefinition = table.Columns.FirstOrDefault(x => x.IsPrimaryKey); + if (columnDefinition == null) + { + return string.Empty; + } + + var constraintName = string.IsNullOrEmpty(columnDefinition.PrimaryKeyName) + ? $"PK_{table.Name}" + : columnDefinition.PrimaryKeyName; + + var columns = string.IsNullOrEmpty(columnDefinition.PrimaryKeyColumns) + ? GetQuotedColumnName(columnDefinition.Name) + : string.Join(", ", columnDefinition.PrimaryKeyColumns + .Split(Cms.Core.Constants.CharArrays.CommaSpace, StringSplitOptions.RemoveEmptyEntries) + .Select(GetQuotedColumnName)); + + // We can't name the PK if it's set as a column constraint so add an alternate at table level. + var constraintType = table.Columns.Any(x => x.IsIdentity) + ? "UNIQUE" + : "PRIMARY KEY"; + + return $"CONSTRAINT {constraintName} {constraintType} ({columns})"; + } + + + /// + public override Sql SelectTop(Sql sql, int top) + { + // SQLite uses LIMIT as opposed to TOP + // SELECT TOP 5 * FROM My_Table + // SELECT * FROM My_Table LIMIT 5; + + return sql.Append($"LIMIT {top}"); + } + + public virtual string Format(IEnumerable columns) + { + var sb = new StringBuilder(); + foreach (ColumnDefinition column in columns) + { + sb.AppendLine(", " + Format(column)); + } + + return sb.ToString().TrimStart(','); + } + + public override void HandleCreateTable(IDatabase database, TableDefinition tableDefinition, + bool skipKeysAndIndexes = false) + { + var columns = Format(tableDefinition.Columns); + var primaryKey = FormatPrimaryKey(tableDefinition); + List foreignKeys = Format(tableDefinition.ForeignKeys); + + var sb = new StringBuilder(); + sb.AppendLine($"CREATE TABLE {tableDefinition.Name}"); + sb.AppendLine("("); + sb.Append(columns); + + if (!string.IsNullOrEmpty(primaryKey) && !skipKeysAndIndexes) + { + sb.AppendLine($", {primaryKey}"); + } + + if (!skipKeysAndIndexes) + { + foreach (var foreignKey in foreignKeys) + { + sb.AppendLine($", {foreignKey}"); + } + } + + sb.AppendLine(")"); + + var createSql = sb.ToString(); + + _log.LogInformation("Create table:\n {Sql}", createSql); + database.Execute(new Sql(createSql)); + + if (skipKeysAndIndexes) + { + return; + } + + List indexSql = Format(tableDefinition.Indexes); + foreach (var sql in indexSql) + { + _log.LogInformation("Create Index:\n {Sql}", sql); + database.Execute(new Sql(sql)); + } + } + + public override IEnumerable GetTablesInSchema(IDatabase db) => + db.Fetch("select name from sqlite_master where type='table'") + .Where(x => !x.StartsWith("sqlite_")); + + public override IEnumerable GetColumnsInSchema(IDatabase db) + { + IEnumerable tables = GetTablesInSchema(db); + + db.OpenSharedConnection(); + foreach (var table in tables) + { + DbCommand? cmd = db.CreateCommand(db.Connection, CommandType.Text, $"PRAGMA table_info({table})"); + DbDataReader reader = cmd.ExecuteReader(); + + while (reader.Read()) + { + var ordinal = reader.GetInt32("cid"); + var columnName = reader.GetString("name"); + var type = reader.GetString("type"); + var notNull = reader.GetBoolean("notnull"); + yield return new ColumnInfo(table, columnName, ordinal, notNull, type); + } + } + } + + + /// + public override IEnumerable> GetConstraintsPerColumn(IDatabase db) + { + var items = db.Fetch("select * from sqlite_master where type = 'table'") + .Where(x => !x.Name.StartsWith("sqlite_")); + + List foundConstraints = new(); + foreach (SqliteMaster row in items) + { + var altPk = Regex.Match(row.Sql, @"CONSTRAINT (?PK_\w+)\s.*UNIQUE \(""(?.+?)""\)"); + if (altPk.Success) + { + var field = altPk.Groups["field"].Value; + var constraint = altPk.Groups["constraint"].Value; + foundConstraints.Add(new Constraint(row.Name, field, constraint)); + } + else + { + var identity = Regex.Match(row.Sql, @"""(?.+)"".*AUTOINCREMENT"); + if (identity.Success) + { + foundConstraints.Add(new Constraint(row.Name, identity.Groups["field"].Value, $"PK_{row.Name}")); + } + } + + var pk = Regex.Match(row.Sql, @"CONSTRAINT (?\w+)\s.*PRIMARY KEY \(""(?.+?)""\)"); + if (pk.Success) + { + var field = pk.Groups["field"].Value; + var constraint = pk.Groups["constraint"].Value; + foundConstraints.Add(new Constraint(row.Name, field, constraint)); + } + + var fkRegex = new Regex(@"CONSTRAINT (?\w+) FOREIGN KEY \(""(?.+?)""\) REFERENCES"); + var foreignKeys = fkRegex.Matches(row.Sql).Cast(); + { + foreach (var fk in foreignKeys) + { + var field = fk.Groups["field"].Value; + var constraint = fk.Groups["constraint"].Value; + foundConstraints.Add(new Constraint(row.Name, field, constraint)); + } + } + } + + // item.TableName, item.ColumnName, item.ConstraintName + return foundConstraints + .Select(x => Tuple.Create(x.TableName, x.ColumnName, x.ConstraintName)); + } + + public override Sql.SqlJoinClause LeftJoinWithNestedJoin( + Sql sql, + Func, + Sql> nestedJoin, + string? alias = null) + { + Type type = typeof(TDto); + + var tableName = GetQuotedTableName(type.GetTableName()); + var join = tableName; + string? quotedAlias = null; + + if (alias != null) + { + quotedAlias = GetQuotedTableName(alias); + join += " " + quotedAlias; + } + + var nestedSql = new Sql(sql.SqlContext); + nestedSql = nestedJoin(nestedSql); + + Sql.SqlJoinClause sqlJoin = sql.LeftJoin("(" + join); + sql.Append(nestedSql); + sql.Append($") {quotedAlias ?? tableName}"); + return sqlJoin; + } + + public override IDictionary ScalarMappers => _scalarMappers; + + private class Constraint + { + public string TableName { get; } + + public string ColumnName { get; } + + public string ConstraintName { get; } + + public Constraint(string tableName, string columnName, string constraintName) + { + TableName = tableName; + ColumnName = columnName; + ConstraintName = constraintName; + } + + public override string ToString() => ConstraintName; + } + + private class SqliteMaster + { + public string Type { get; set; } + public string Name { get; set; } + public string Sql { get; set; } + } + + private class IndexMeta + { + public string TableName { get; set; } + public string IndexName { get; set; } + public string ColumnName { get; set; } + public bool IsUnique { get; set; } + } +} diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteTransientErrorDetectionStrategy.cs b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteTransientErrorDetectionStrategy.cs new file mode 100644 index 0000000000..54414b2121 --- /dev/null +++ b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteTransientErrorDetectionStrategy.cs @@ -0,0 +1,17 @@ +using Microsoft.Data.Sqlite; +using Umbraco.Cms.Infrastructure.Persistence.FaultHandling; + +namespace Umbraco.Cms.Persistence.Sqlite.Services; + +public class SqliteTransientErrorDetectionStrategy : ITransientErrorDetectionStrategy +{ + public bool IsTransient(Exception ex) + { + if (ex is not SqliteException sqliteException) + { + return false; + } + + return sqliteException.IsTransient || sqliteException.IsBusyOrLocked(); + } +} diff --git a/src/Umbraco.Cms.Persistence.Sqlite/SqliteComposer.cs b/src/Umbraco.Cms.Persistence.Sqlite/SqliteComposer.cs new file mode 100644 index 0000000000..d638f713a2 --- /dev/null +++ b/src/Umbraco.Cms.Persistence.Sqlite/SqliteComposer.cs @@ -0,0 +1,14 @@ +using Umbraco.Cms.Core.Composing; +using Umbraco.Cms.Core.DependencyInjection; + +namespace Umbraco.Cms.Persistence.Sqlite; + +/// +/// Automatically adds SQLite support to Umbraco when this project is referenced. +/// +public class SqliteComposer : IComposer +{ + /// + public void Compose(IUmbracoBuilder builder) + => builder.AddUmbracoSqliteSupport(); +} diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Umbraco.Cms.Persistence.Sqlite.csproj b/src/Umbraco.Cms.Persistence.Sqlite/Umbraco.Cms.Persistence.Sqlite.csproj new file mode 100644 index 0000000000..3fce21af07 --- /dev/null +++ b/src/Umbraco.Cms.Persistence.Sqlite/Umbraco.Cms.Persistence.Sqlite.csproj @@ -0,0 +1,21 @@ + + + + net6.0 + enable + enable + + Umbraco.Cms.Persistence.Sqlite + Umbraco.Cms.Persistence.Sqlite + Adds support for SQLite to Umbraco CMS. + + + + + + + + + + + diff --git a/src/Umbraco.Cms.Persistence.Sqlite/UmbracoBuilderExtensions.cs b/src/Umbraco.Cms.Persistence.Sqlite/UmbracoBuilderExtensions.cs new file mode 100644 index 0000000000..0945b71270 --- /dev/null +++ b/src/Umbraco.Cms.Persistence.Sqlite/UmbracoBuilderExtensions.cs @@ -0,0 +1,41 @@ +using System.Data.Common; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.DistributedLocking; +using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; +using Umbraco.Cms.Persistence.Sqlite.Interceptors; +using Umbraco.Cms.Persistence.Sqlite.Services; + +namespace Umbraco.Cms.Persistence.Sqlite; + +/// +/// SQLite support extensions for IUmbracoBuilder. +/// +public static class UmbracoBuilderExtensions +{ + /// + /// Add required services for SQLite support. + /// + public static IUmbracoBuilder AddUmbracoSqliteSupport(this IUmbracoBuilder builder) + { + // TryAddEnumerable takes both TService and TImplementation into consideration (unlike TryAddSingleton) + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); + + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); + + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); + + DbProviderFactories.UnregisterFactory(Constants.ProviderName); + DbProviderFactories.RegisterFactory(Constants.ProviderName, Microsoft.Data.Sqlite.SqliteFactory.Instance); + + return builder; + } +} diff --git a/src/Umbraco.Core/Configuration/ConfigConnectionString.cs b/src/Umbraco.Core/Configuration/ConfigConnectionString.cs index 85c9332b30..e69de29bb2 100644 --- a/src/Umbraco.Core/Configuration/ConfigConnectionString.cs +++ b/src/Umbraco.Core/Configuration/ConfigConnectionString.cs @@ -1,92 +0,0 @@ -using System; -using System.Data.Common; - -namespace Umbraco.Cms.Core.Configuration -{ - public class ConfigConnectionString - { - public string Name { get; } - - public string? ConnectionString { get; } - - public string? ProviderName { get; } - - public ConfigConnectionString(string name, string? connectionString, string? providerName = null) - { - Name = name ?? throw new ArgumentNullException(nameof(name)); - ConnectionString = ParseConnectionString(connectionString, ref providerName); - ProviderName = providerName!; - } - - private static string? ParseConnectionString(string? connectionString, ref string? providerName) - { - if (string.IsNullOrEmpty(connectionString)) - { - return connectionString; - } - - var builder = new DbConnectionStringBuilder - { - ConnectionString = connectionString - }; - - // Replace data directory placeholder - const string attachDbFileNameKey = "AttachDbFileName"; - const string dataDirectoryPlaceholder = "|DataDirectory|"; - if (builder.TryGetValue(attachDbFileNameKey, out var attachDbFileNameValue) && - attachDbFileNameValue is string attachDbFileName && - attachDbFileName.Contains(dataDirectoryPlaceholder)) - { - var dataDirectory = AppDomain.CurrentDomain.GetData("DataDirectory")?.ToString(); - if (!string.IsNullOrEmpty(dataDirectory)) - { - builder[attachDbFileNameKey] = attachDbFileName.Replace(dataDirectoryPlaceholder, dataDirectory); - - // Mutate the existing connection string (note: the builder also lowercases the properties) - connectionString = builder.ToString(); - } - } - - // Also parse provider name now we already have a builder - if (string.IsNullOrEmpty(providerName)) - { - providerName = ParseProviderName(builder); - } - - return connectionString; - } - - /// - /// Parses the connection string to get the provider name. - /// - /// The connection string. - /// - /// The provider name or null is the connection string is empty. - /// - public static string? ParseProviderName(string? connectionString) - { - if (string.IsNullOrEmpty(connectionString)) - { - return null; - } - - var builder = new DbConnectionStringBuilder - { - ConnectionString = connectionString - }; - - return ParseProviderName(builder); - } - - private static string ParseProviderName(DbConnectionStringBuilder builder) - { - if ((builder.TryGetValue("Data Source", out var dataSource) || builder.TryGetValue("DataSource", out dataSource)) && - dataSource?.ToString()?.EndsWith(".sdf", StringComparison.OrdinalIgnoreCase) == true) - { - return Constants.DbProviderNames.SqlCe; - } - - return Constants.DbProviderNames.SqlServer; - } - } -} diff --git a/src/Umbraco.Core/Configuration/ConfigureConnectionStrings.cs b/src/Umbraco.Core/Configuration/ConfigureConnectionStrings.cs new file mode 100644 index 0000000000..174a65ac1e --- /dev/null +++ b/src/Umbraco.Core/Configuration/ConfigureConnectionStrings.cs @@ -0,0 +1,40 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.Configuration; + +/// +/// Configures ConnectionStrings. +/// +public class ConfigureConnectionStrings : IConfigureNamedOptions +{ + private readonly IConfiguration _configuration; + + /// + /// Initializes a new instance of the class. + /// + public ConfigureConnectionStrings(IConfiguration configuration) => _configuration = configuration; + + /// + public void Configure(ConnectionStrings options) => Configure(Constants.System.UmbracoConnectionName, options); + + /// + public void Configure(string name, ConnectionStrings options) + { + if (name == Options.DefaultName) + { + name = Constants.System.UmbracoConnectionName; + } + + if (options.IsConnectionStringConfigured()) + { + return; + } + + options.Name = name; + options.ConnectionString = _configuration.GetConnectionString(name); + options.ProviderName = _configuration.GetConnectionString($"{name}{ConnectionStrings.ProviderNamePostfix}") ?? ConnectionStrings.DefaultProviderName; + } +} diff --git a/src/Umbraco.Core/Configuration/Models/ConnectionStrings.cs b/src/Umbraco.Core/Configuration/Models/ConnectionStrings.cs index 44568c33c0..0642a6171c 100644 --- a/src/Umbraco.Core/Configuration/Models/ConnectionStrings.cs +++ b/src/Umbraco.Core/Configuration/Models/ConnectionStrings.cs @@ -1,31 +1,34 @@ -// Copyright (c) Umbraco. -// See LICENSE for more details. +using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Configuration.Models +namespace Umbraco.Cms.Core.Configuration.Models; + +[UmbracoOptions("ConnectionStrings")] +public class ConnectionStrings { - /// - /// Typed configuration options for connection strings. - /// - [UmbracoOptions("ConnectionStrings", BindNonPublicProperties = true)] - public class ConnectionStrings - { - // Backing field for UmbracoConnectionString to load from configuration value with key umbracoDbDSN. - // Attributes cannot be applied to map from keys that don't match, and have chosen to retain the key name - // used in configuration for older Umbraco versions. - // See: https://stackoverflow.com/a/54607296/489433 -#pragma warning disable SA1300 // Element should begin with upper-case letter -#pragma warning disable IDE1006 // Naming Styles - private string? umbracoDbDSN -#pragma warning restore IDE1006 // Naming Styles -#pragma warning restore SA1300 // Element should begin with upper-case letter - { - get => UmbracoConnectionString?.ConnectionString; - set => UmbracoConnectionString = new ConfigConnectionString(Constants.System.UmbracoConnectionName, value); - } + private string _connectionString; - /// - /// Gets or sets a value for the Umbraco database connection string.. - /// - public ConfigConnectionString UmbracoConnectionString { get; set; } = new ConfigConnectionString(Constants.System.UmbracoConnectionName, null); + /// + /// The default provider name when not present in configuration. + /// + public const string DefaultProviderName = "Microsoft.Data.SqlClient"; + + /// + /// The DataDirectory placeholder. + /// + public const string DataDirectoryPlaceholder = "|DataDirectory|"; + + /// + /// The postfix used to identify a connection strings provider setting. + /// + public const string ProviderNamePostfix = "_ProviderName"; + + public string Name { get; set; } + + public string ConnectionString + { + get => _connectionString; + set => _connectionString = value.ReplaceDataDirectoryPlaceholder(); } + + public string ProviderName { get; set; } = DefaultProviderName; } diff --git a/src/Umbraco.Core/Configuration/Models/ContentSettings.cs b/src/Umbraco.Core/Configuration/Models/ContentSettings.cs index 1caa81d80a..e6e5c7006f 100644 --- a/src/Umbraco.Core/Configuration/Models/ContentSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/ContentSettings.cs @@ -156,6 +156,9 @@ namespace Umbraco.Cms.Core.Configuration.Models internal const bool StaticShowDeprecatedPropertyEditors = false; internal const string StaticLoginBackgroundImage = "assets/img/login.jpg"; internal const string StaticLoginLogoImage = "assets/img/application/umbraco_logo_white.svg"; + internal const bool StaticHideBackOfficeLogo = false; + internal const bool StaticDisableDeleteWhenReferenced = false; + internal const bool StaticDisableUnpublishWhenReferenced = false; /// /// Gets or sets a value for the content notification settings. @@ -219,6 +222,24 @@ namespace Umbraco.Cms.Core.Configuration.Models [DefaultValue(StaticLoginLogoImage)] public string LoginLogoImage { get; set; } = StaticLoginLogoImage; + /// + /// Gets or sets a value indicating whether to hide the backoffice umbraco logo or not. + /// + [DefaultValue(StaticHideBackOfficeLogo)] + public bool HideBackOfficeLogo { get; set; } = StaticHideBackOfficeLogo; + + /// + /// Gets or sets a value indicating whether to disable the deletion of items referenced by other items. + /// + [DefaultValue(StaticDisableDeleteWhenReferenced)] + public bool DisableDeleteWhenReferenced { get; set; } = StaticDisableDeleteWhenReferenced; + + /// + /// Gets or sets a value indicating whether to disable the unpublishing of items referenced by other items. + /// + [DefaultValue(StaticDisableUnpublishWhenReferenced)] + public bool DisableUnpublishWhenReferenced { get; set; } = StaticDisableUnpublishWhenReferenced; + /// /// Get or sets the model representing the global content version cleanup policy /// diff --git a/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs b/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs index 0cd5ca5670..b355d81444 100644 --- a/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs @@ -27,8 +27,10 @@ namespace Umbraco.Cms.Core.Configuration.Models internal const bool StaticInstallMissingDatabase = false; internal const bool StaticDisableElectionForSingleServer = false; internal const string StaticNoNodesViewPath = "~/umbraco/UmbracoWebsite/NoNodes.cshtml"; - internal const string StaticSqlWriteLockTimeOut = "00:00:05"; + internal const string StaticDistributedLockingReadLockDefaultTimeout = "00:01:00"; + internal const string StaticDistributedLockingWriteLockDefaultTimeout = "00:00:05"; internal const bool StaticSanitizeTinyMce = false; + internal const int StaticMainDomReleaseSignalPollingInterval = 2000; /// /// Gets or sets a value for the reserved URLs (must end with a comma). @@ -137,6 +139,26 @@ namespace Umbraco.Cms.Core.Configuration.Models /// public string MainDomLock { get; set; } = string.Empty; + /// + /// Gets or sets a value to discriminate MainDom boundaries. + /// + /// Generally the default should suffice but useful for advanced scenarios e.g. azure deployment slot based zero downtime deployments. + /// + /// + public string MainDomKeyDiscriminator { get; set; } = string.Empty; + + /// + /// Gets or sets the duration (in milliseconds) for which the MainDomLock release signal polling task should sleep. + /// + /// + /// Doesn't apply to MainDomSemaphoreLock. + /// + /// The default value is 2000ms. + /// + /// + [DefaultValue(StaticMainDomReleaseSignalPollingInterval)] + public int MainDomReleaseSignalPollingInterval { get; set; } = StaticMainDomReleaseSignalPollingInterval; + /// /// Gets or sets the telemetry ID. /// @@ -174,18 +196,32 @@ namespace Umbraco.Cms.Core.Configuration.Models public bool IsPickupDirectoryLocationConfigured => !string.IsNullOrWhiteSpace(Smtp?.PickupDirectoryLocation); /// - /// Gets a value indicating whether TinyMCE scripting sanitization should be applied. + /// Gets or sets a value indicating whether TinyMCE scripting sanitization should be applied. /// [DefaultValue(StaticSanitizeTinyMce)] - public bool SanitizeTinyMce => StaticSanitizeTinyMce; + public bool SanitizeTinyMce { get; set; } = StaticSanitizeTinyMce; /// - /// Gets a value representing the time in milliseconds to lock the database for a write operation. + /// Gets or sets a value representing the maximum time to wait whilst attempting to obtain a distributed read lock. /// /// - /// The default value is 5000 milliseconds. + /// The default value is 60 seconds. /// - [DefaultValue(StaticSqlWriteLockTimeOut)] - public TimeSpan SqlWriteLockTimeOut { get; } = TimeSpan.Parse(StaticSqlWriteLockTimeOut); + [DefaultValue(StaticDistributedLockingReadLockDefaultTimeout)] + public TimeSpan DistributedLockingReadLockDefaultTimeout { get; set; } = TimeSpan.Parse(StaticDistributedLockingReadLockDefaultTimeout); + + /// + /// Gets or sets a value representing the maximum time to wait whilst attempting to obtain a distributed write lock. + /// + /// + /// The default value is 5 seconds. + /// + [DefaultValue(StaticDistributedLockingWriteLockDefaultTimeout)] + public TimeSpan DistributedLockingWriteLockDefaultTimeout { get; set; } = TimeSpan.Parse(StaticDistributedLockingWriteLockDefaultTimeout); + + /// + /// Gets or sets a value representing the DistributedLockingMechanism to use. + /// + public string DistributedLockingMechanism { get; set; } = string.Empty; } } diff --git a/src/Umbraco.Core/Configuration/Models/ModelsBuilderSettings.cs b/src/Umbraco.Core/Configuration/Models/ModelsBuilderSettings.cs index 2cf1f770b7..73d046de32 100644 --- a/src/Umbraco.Core/Configuration/Models/ModelsBuilderSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/ModelsBuilderSettings.cs @@ -49,6 +49,7 @@ namespace Umbraco.Cms.Core.Configuration.Models if (!ModelsMode.IsAuto()) { _flagOutOfDateModels = false; + return; } _flagOutOfDateModels = value; diff --git a/src/Umbraco.Core/Configuration/Models/RichTextEditorSettings.cs b/src/Umbraco.Core/Configuration/Models/RichTextEditorSettings.cs index 4bcbd19a1b..cd82376c57 100644 --- a/src/Umbraco.Core/Configuration/Models/RichTextEditorSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/RichTextEditorSettings.cs @@ -8,7 +8,7 @@ namespace Umbraco.Cms.Core.Configuration.Models [UmbracoOptions(Constants.Configuration.ConfigRichTextEditor)] public class RichTextEditorSettings { - internal const string StaticValidElements = "+a[id|style|rel|data-id|data-udi|rev|charset|hreflang|dir|lang|tabindex|accesskey|type|name|href|target|title|class|onfocus|onblur|onclick|ondblclick|onmousedown|onmouseup|onmouseover|onmousemove|onmouseout|onkeypress|onkeydown|onkeyup],-strong/-b[class|style],-em/-i[class|style],-strike[class|style],-u[class|style],#p[id|style|dir|class|align],-ol[class|reversed|start|style|type],-ul[class|style],-li[class|style],br[class],img[id|dir|lang|longdesc|usemap|style|class|src|onmouseover|onmouseout|border|alt=|title|hspace|vspace|width|height|align|umbracoorgwidth|umbracoorgheight|onresize|onresizestart|onresizeend|rel|data-id],-sub[style|class],-sup[style|class],-blockquote[dir|style|class],-table[border=0|cellspacing|cellpadding|width|height|class|align|summary|style|dir|id|lang|bgcolor|background|bordercolor],-tr[id|lang|dir|class|rowspan|width|height|align|valign|style|bgcolor|background|bordercolor],tbody[id|class],thead[id|class],tfoot[id|class],#td[id|lang|dir|class|colspan|rowspan|width|height|align|valign|style|bgcolor|background|bordercolor|scope],-th[id|lang|dir|class|colspan|rowspan|width|height|align|valign|style|scope],caption[id|lang|dir|class|style],-div[id|dir|class|align|style],-span[class|align|style],-pre[class|align|style],address[class|align|style],-h1[id|dir|class|align|style],-h2[id|dir|class|align|style],-h3[id|dir|class|align|style],-h4[id|dir|class|align|style],-h5[id|dir|class|align|style],-h6[id|style|dir|class|align|style],hr[class|style],small[class|style],dd[id|class|title|style|dir|lang],dl[id|class|title|style|dir|lang],dt[id|class|title|style|dir|lang],object[class|id|width|height|codebase|*],param[name|value|_value|class],embed[type|width|height|src|class|*],map[name|class],area[shape|coords|href|alt|target|class],bdo[class],button[class],iframe[*]"; + internal const string StaticValidElements = "+a[id|style|rel|data-id|data-udi|rev|charset|hreflang|dir|lang|tabindex|accesskey|type|name|href|target|title|class|onfocus|onblur|onclick|ondblclick|onmousedown|onmouseup|onmouseover|onmousemove|onmouseout|onkeypress|onkeydown|onkeyup],-strong/-b[class|style],-em/-i[class|style],-strike[class|style],-u[class|style],#p[id|style|dir|class|align],-ol[class|reversed|start|style|type],-ul[class|style],-li[class|style],br[class],img[id|dir|lang|longdesc|usemap|style|class|src|onmouseover|onmouseout|border|alt=|title|hspace|vspace|width|height|align|umbracoorgwidth|umbracoorgheight|onresize|onresizestart|onresizeend|rel|data-id],-sub[style|class],-sup[style|class],-blockquote[dir|style|class],-table[border=0|cellspacing|cellpadding|width|height|class|align|summary|style|dir|id|lang|bgcolor|background|bordercolor],-tr[id|lang|dir|class|rowspan|width|height|align|valign|style|bgcolor|background|bordercolor],tbody[id|class],thead[id|class],tfoot[id|class],#td[id|lang|dir|class|colspan|rowspan|width|height|align|valign|style|bgcolor|background|bordercolor|scope],-th[id|lang|dir|class|colspan|rowspan|width|height|align|valign|style|scope],caption[id|lang|dir|class|style],-div[id|dir|class|align|style],-span[class|align|style],-pre[class|align|style],address[class|align|style],-h1[id|dir|class|align|style],-h2[id|dir|class|align|style],-h3[id|dir|class|align|style],-h4[id|dir|class|align|style],-h5[id|dir|class|align|style],-h6[id|style|dir|class|align|style],hr[class|style],small[class|style],dd[id|class|title|style|dir|lang],dl[id|class|title|style|dir|lang],dt[id|class|title|style|dir|lang],object[class|id|width|height|codebase|*],param[name|value|_value|class],embed[type|width|height|src|class|*],map[name|class],area[shape|coords|href|alt|target|class],bdo[class],button[class],iframe[*],figure,figcaption"; internal const string StaticInvalidElements = "font"; private static readonly string[] s_default_plugins = new[] diff --git a/src/Umbraco.Core/Configuration/Models/Validation/GlobalSettingsValidator.cs b/src/Umbraco.Core/Configuration/Models/Validation/GlobalSettingsValidator.cs index 40b82ee09d..31d0779626 100644 --- a/src/Umbraco.Core/Configuration/Models/Validation/GlobalSettingsValidator.cs +++ b/src/Umbraco.Core/Configuration/Models/Validation/GlobalSettingsValidator.cs @@ -20,7 +20,7 @@ namespace Umbraco.Cms.Core.Configuration.Models.Validation return ValidateOptionsResult.Fail(message); } - if (!ValidateSqlWriteLockTimeOutSetting(options.SqlWriteLockTimeOut, out var message2)) + if (!ValidateSqlWriteLockTimeOutSetting(options.DistributedLockingWriteLockDefaultTimeout, out var message2)) { return ValidateOptionsResult.Fail(message2); } @@ -37,7 +37,7 @@ namespace Umbraco.Cms.Core.Configuration.Models.Validation const int maximumTimeOut = 20000; if (configuredTimeOut.TotalMilliseconds < minimumTimeOut || configuredTimeOut.TotalMilliseconds > maximumTimeOut) // between 0.1 and 20 seconds { - message = $"The `{Constants.Configuration.ConfigGlobal}:{nameof(GlobalSettings.SqlWriteLockTimeOut)}` setting is not between the minimum of {minimumTimeOut} ms and maximum of {maximumTimeOut} ms"; + message = $"The `{Constants.Configuration.ConfigGlobal}:{nameof(GlobalSettings.DistributedLockingWriteLockDefaultTimeout)}` setting is not between the minimum of {minimumTimeOut} ms and maximum of {maximumTimeOut} ms"; return false; } diff --git a/src/Umbraco.Core/Constants-Configuration.cs b/src/Umbraco.Core/Constants-Configuration.cs index bdbd13b2a4..b3963d64ef 100644 --- a/src/Umbraco.Core/Constants-Configuration.cs +++ b/src/Umbraco.Core/Constants-Configuration.cs @@ -17,6 +17,7 @@ namespace Umbraco.Cms.Core public const string ConfigCustomErrorsPrefix = ConfigPrefix + "CustomErrors:"; public const string ConfigGlobalPrefix = ConfigPrefix + "Global:"; public const string ConfigGlobalId = ConfigGlobalPrefix + "Id"; + public const string ConfigGlobalDistributedLockingMechanism = ConfigGlobalPrefix + "DistributedLockingMechanism"; public const string ConfigHostingPrefix = ConfigPrefix + "Hosting:"; public const string ConfigModelsBuilderPrefix = ConfigPrefix + "ModelsBuilder:"; public const string ConfigSecurityPrefix = ConfigPrefix + "Security:"; diff --git a/src/Umbraco.Core/Constants-DatabaseProviders.cs b/src/Umbraco.Core/Constants-DatabaseProviders.cs deleted file mode 100644 index 1fd16133e5..0000000000 --- a/src/Umbraco.Core/Constants-DatabaseProviders.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Umbraco.Cms.Core -{ - public static partial class Constants - { - public static class DatabaseProviders - { - public const string SqlCe = "System.Data.SqlServerCe.4.0"; - public const string SqlServer = "Microsoft.Data.SqlClient"; - } - } -} diff --git a/src/Umbraco.Core/Constants-HttpClients.cs b/src/Umbraco.Core/Constants-HttpClients.cs new file mode 100644 index 0000000000..474ec49a50 --- /dev/null +++ b/src/Umbraco.Core/Constants-HttpClients.cs @@ -0,0 +1,19 @@ +namespace Umbraco.Cms.Core +{ + /// + /// Defines constants. + /// + public static partial class Constants + { + /// + /// Defines constants for named http clients. + /// + public static class HttpClients + { + /// + /// Name for http client which ignores certificate errors. + /// + public const string IgnoreCertificateErrors = "Umbraco:HttpClients:IgnoreCertificateErrors"; + } + } +} diff --git a/src/Umbraco.Core/Constants-Icons.cs b/src/Umbraco.Core/Constants-Icons.cs index 62e19008dd..39980f116a 100644 --- a/src/Umbraco.Core/Constants-Icons.cs +++ b/src/Umbraco.Core/Constants-Icons.cs @@ -1,4 +1,4 @@ -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core { public static partial class Constants { @@ -9,6 +9,11 @@ /// public const string DefaultIcon = Content; + /// + /// System blueprint icon + /// + public const string Blueprint = "icon-blueprint"; + /// /// System content icon /// diff --git a/src/Umbraco.Core/Constants-System.cs b/src/Umbraco.Core/Constants-System.cs index ddff380c08..eeea929662 100644 --- a/src/Umbraco.Core/Constants-System.cs +++ b/src/Umbraco.Core/Constants-System.cs @@ -59,7 +59,10 @@ public const string RecycleBinMediaPathPrefix = "-1,-21,"; public const int DefaultLabelDataTypeId = -92; - public const string UmbracoConnectionName = "umbracoDbDSN"; + + public const string UmbracoDefaultDatabaseName = "Umbraco"; + + public const string UmbracoConnectionName = "umbracoDbDSN"; } } } diff --git a/src/Umbraco.Core/Constants-SystemDirectories.cs b/src/Umbraco.Core/Constants-SystemDirectories.cs index bf34aab989..f70dd199fc 100644 --- a/src/Umbraco.Core/Constants-SystemDirectories.cs +++ b/src/Umbraco.Core/Constants-SystemDirectories.cs @@ -1,3 +1,5 @@ +using System; + namespace Umbraco.Cms.Core { public static partial class Constants @@ -42,9 +44,11 @@ namespace Umbraco.Cms.Core public const string Install = "~/install"; public const string AppPlugins = "/App_Plugins"; - public static string AppPluginIcons => "/Backoffice/Icons"; - public const string CreatedPackages = "/created-packages"; + [Obsolete("Use PluginIcons instead")] + public static string AppPluginIcons => "/Backoffice/Icons"; + + public const string PluginIcons = "/backoffice/icons"; public const string MvcViews = "~/Views"; @@ -54,6 +58,8 @@ namespace Umbraco.Cms.Core public const string Packages = Data + "/packages"; + public const string CreatedPackages = Data + "/CreatedPackages"; + public const string Preview = Data + "/preview"; /// diff --git a/src/Umbraco.Core/ContentApps/DictionaryContentAppFactory.cs b/src/Umbraco.Core/ContentApps/DictionaryContentAppFactory.cs new file mode 100644 index 0000000000..b1fb31d2aa --- /dev/null +++ b/src/Umbraco.Core/ContentApps/DictionaryContentAppFactory.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Models.Membership; + +namespace Umbraco.Cms.Core.ContentApps +{ + internal class DictionaryContentAppFactory : IContentAppFactory + { + private const int Weight = -100; + + private ContentApp _dictionaryApp; + + public ContentApp GetContentAppFor(object source, IEnumerable userGroups) + { + switch (source) + { + case IDictionaryItem _: + return _dictionaryApp ??= new ContentApp + { + Alias = "dictionaryContent", + Name = "Content", + Icon = "icon-document", + View = "views/dictionary/views/content/content.html", + Weight = Weight + }; + default: + return null; + } + } + } +} diff --git a/src/Umbraco.Core/DependencyInjection/IUmbracoBuilder.cs b/src/Umbraco.Core/DependencyInjection/IUmbracoBuilder.cs index ec6669b177..9351bb52cf 100644 --- a/src/Umbraco.Core/DependencyInjection/IUmbracoBuilder.cs +++ b/src/Umbraco.Core/DependencyInjection/IUmbracoBuilder.cs @@ -31,7 +31,7 @@ namespace Umbraco.Cms.Core.DependencyInjection IProfiler Profiler { get; } AppCaches AppCaches { get; } - TBuilder WithCollectionBuilder() where TBuilder : ICollectionBuilder, new(); + TBuilder WithCollectionBuilder() where TBuilder : ICollectionBuilder; void Build(); } } diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Collections.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Collections.cs index 811ee35c14..a0ff6104a7 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Collections.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Collections.cs @@ -46,7 +46,8 @@ namespace Umbraco.Cms.Core.DependencyInjection .Append() .Append() .Append() - .Append(); + .Append() + .Append(); // all built-in finders in the correct order, // devs can then modify this list on application startup diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs index 58c57eb668..a3dc8b0e58 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs @@ -90,6 +90,8 @@ namespace Umbraco.Cms.Core.DependencyInjection .AddUmbracoOptions() .AddUmbracoOptions(); + builder.Services.AddSingleton, ConfigureConnectionStrings>(); + builder.Services.Configure(options => options.MergeReplacements(builder.Config)); return builder; diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs index ac5137e9ab..94b8b70c1b 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs @@ -103,7 +103,7 @@ namespace Umbraco.Cms.Core.DependencyInjection /// The type of the collection builder. /// The collection builder. public TBuilder WithCollectionBuilder() - where TBuilder : ICollectionBuilder, new() + where TBuilder : ICollectionBuilder { Type typeOfBuilder = typeof(TBuilder); @@ -112,7 +112,22 @@ namespace Umbraco.Cms.Core.DependencyInjection return (TBuilder)o; } - var builder = new TBuilder(); + TBuilder builder; + + if (typeof(TBuilder).GetConstructor(Type.EmptyTypes) != null) + { + builder = Activator.CreateInstance(); + } + else if (typeof(TBuilder).GetConstructor(new[] { typeof(IUmbracoBuilder) }) != null) + { + // Handle those collection builders which need a reference to umbraco builder i.e. DistributedLockingCollectionBuilder. + builder = (TBuilder)Activator.CreateInstance(typeof(TBuilder), this); + } + else + { + throw new InvalidOperationException("A CollectionBuilder must have either a parameterless constructor or a constructor whose only parameter is of type IUmbracoBuilder"); + } + _builders[typeOfBuilder] = builder; return builder; } @@ -265,6 +280,7 @@ namespace Umbraco.Cms.Core.DependencyInjection Services.AddSingleton(); // Register telemetry service used to gather data about installed packages + Services.AddUnique(); Services.AddUnique(); Services.AddUnique(); @@ -291,6 +307,7 @@ namespace Umbraco.Cms.Core.DependencyInjection Services.AddUnique(); Services.AddUnique(); Services.AddUnique(); + Services.AddUnique(); Services.AddUnique(factory => new ExternalLoginService( factory.GetRequiredService(), factory.GetRequiredService(), diff --git a/src/Umbraco.Core/DistributedLocking/DistributedLockType.cs b/src/Umbraco.Core/DistributedLocking/DistributedLockType.cs new file mode 100644 index 0000000000..01acd02c10 --- /dev/null +++ b/src/Umbraco.Core/DistributedLocking/DistributedLockType.cs @@ -0,0 +1,10 @@ +namespace Umbraco.Cms.Core.DistributedLocking; + +/// +/// Represents the type of distributed lock. +/// +public enum DistributedLockType +{ + ReadLock, + WriteLock +} diff --git a/src/Umbraco.Core/DistributedLocking/Exceptions/DistributedLockingException.cs b/src/Umbraco.Core/DistributedLocking/Exceptions/DistributedLockingException.cs new file mode 100644 index 0000000000..2f27929a6c --- /dev/null +++ b/src/Umbraco.Core/DistributedLocking/Exceptions/DistributedLockingException.cs @@ -0,0 +1,26 @@ +using System; + +namespace Umbraco.Cms.Core.DistributedLocking.Exceptions; + +/// +/// Base class for all DistributedLockingExceptions. +/// +public class DistributedLockingException : ApplicationException +{ + /// + /// Initializes a new instance of the class. + /// + public DistributedLockingException(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the class. + /// + // ReSharper disable once UnusedMember.Global + public DistributedLockingException(string message, Exception innerException) + : base(message, innerException) + { + } +} diff --git a/src/Umbraco.Core/DistributedLocking/Exceptions/DistributedLockingTimeoutException.cs b/src/Umbraco.Core/DistributedLocking/Exceptions/DistributedLockingTimeoutException.cs new file mode 100644 index 0000000000..9d65023790 --- /dev/null +++ b/src/Umbraco.Core/DistributedLocking/Exceptions/DistributedLockingTimeoutException.cs @@ -0,0 +1,15 @@ +namespace Umbraco.Cms.Core.DistributedLocking.Exceptions; + +/// +/// Base class for all DistributedLocking timeout related exceptions. +/// +public abstract class DistributedLockingTimeoutException : DistributedLockingException +{ + /// + /// Initializes a new instance of the class. + /// + protected DistributedLockingTimeoutException(int lockId, bool isWrite) + : base($"Failed to acquire {(isWrite ? "write" : "read")} lock for id: {lockId}.") + { + } +} diff --git a/src/Umbraco.Core/DistributedLocking/Exceptions/DistributedReadLockTimeoutException.cs b/src/Umbraco.Core/DistributedLocking/Exceptions/DistributedReadLockTimeoutException.cs new file mode 100644 index 0000000000..4d37238c0d --- /dev/null +++ b/src/Umbraco.Core/DistributedLocking/Exceptions/DistributedReadLockTimeoutException.cs @@ -0,0 +1,15 @@ +namespace Umbraco.Cms.Core.DistributedLocking.Exceptions; + +/// +/// Exception thrown when a read lock could not be obtained in a timely manner. +/// +public class DistributedReadLockTimeoutException : DistributedLockingTimeoutException +{ + /// + /// Initializes a new instance of the class. + /// + public DistributedReadLockTimeoutException(int lockId) + : base(lockId, false) + { + } +} diff --git a/src/Umbraco.Core/DistributedLocking/Exceptions/DistributedWriteLockTimeoutException.cs b/src/Umbraco.Core/DistributedLocking/Exceptions/DistributedWriteLockTimeoutException.cs new file mode 100644 index 0000000000..abf84470e0 --- /dev/null +++ b/src/Umbraco.Core/DistributedLocking/Exceptions/DistributedWriteLockTimeoutException.cs @@ -0,0 +1,15 @@ +namespace Umbraco.Cms.Core.DistributedLocking.Exceptions; + +/// +/// Exception thrown when a write lock could not be obtained in a timely manner. +/// +public class DistributedWriteLockTimeoutException : DistributedLockingTimeoutException +{ + /// + /// Initializes a new instance of the class. + /// + public DistributedWriteLockTimeoutException(int lockId) + : base(lockId, true) + { + } +} diff --git a/src/Umbraco.Core/DistributedLocking/IDistributedLock.cs b/src/Umbraco.Core/DistributedLocking/IDistributedLock.cs new file mode 100644 index 0000000000..202bb594bc --- /dev/null +++ b/src/Umbraco.Core/DistributedLocking/IDistributedLock.cs @@ -0,0 +1,19 @@ +using System; + +namespace Umbraco.Cms.Core.DistributedLocking; + +/// +/// Interface representing a DistributedLock. +/// +public interface IDistributedLock : IDisposable +{ + /// + /// Gets the LockId. + /// + int LockId { get; } + + /// + /// Gets the DistributedLockType. + /// + DistributedLockType LockType { get; } +} diff --git a/src/Umbraco.Core/DistributedLocking/IDistributedLockingMechanism.cs b/src/Umbraco.Core/DistributedLocking/IDistributedLockingMechanism.cs new file mode 100644 index 0000000000..5df8a23650 --- /dev/null +++ b/src/Umbraco.Core/DistributedLocking/IDistributedLockingMechanism.cs @@ -0,0 +1,51 @@ +using System; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.DistributedLocking.Exceptions; + +namespace Umbraco.Cms.Core.DistributedLocking; + +/// +/// Represents a class responsible for managing distributed locks. +/// +/// +/// In general the rules for distributed locks are as follows. +/// +/// +/// Cannot obtain a write lock if a read lock exists for same lock id (except during an upgrade from reader -> writer) +/// +/// +/// Cannot obtain a write lock if a write lock exists for same lock id. +/// +/// +/// Cannot obtain a read lock if a write lock exists for same lock id. +/// +/// +/// Can obtain a read lock if a read lock exists for same lock id. +/// +/// +/// +public interface IDistributedLockingMechanism +{ + /// + /// Gets a value indicating whether this distributed locking mechanism can be used. + /// + bool Enabled { get; } + + /// + /// Obtains a distributed read lock. + /// + /// + /// When timeout is null, implementations should use . + /// + /// Failed to obtain distributed read lock in time. + IDistributedLock ReadLock(int lockId, TimeSpan? obtainLockTimeout = null); + + /// + /// Obtains a distributed read lock. + /// + /// + /// When timeout is null, implementations should use . + /// + /// Failed to obtain distributed write lock in time. + IDistributedLock WriteLock(int lockId, TimeSpan? obtainLockTimeout = null); +} diff --git a/src/Umbraco.Core/DistributedLocking/IDistributedLockingMechanismFactory.cs b/src/Umbraco.Core/DistributedLocking/IDistributedLockingMechanismFactory.cs new file mode 100644 index 0000000000..1bd1cfe206 --- /dev/null +++ b/src/Umbraco.Core/DistributedLocking/IDistributedLockingMechanismFactory.cs @@ -0,0 +1,9 @@ +namespace Umbraco.Cms.Core.DistributedLocking; + +/// +/// Picks an appropriate IDistributedLockingMechanism when multiple are registered +/// +public interface IDistributedLockingMechanismFactory +{ + IDistributedLockingMechanism DistributedLockingMechanism { get; } +} diff --git a/src/Umbraco.Core/Events/RelateOnCopyNotificationHandler.cs b/src/Umbraco.Core/Events/RelateOnCopyNotificationHandler.cs index 44d2eb6ee7..f37d8723a7 100644 --- a/src/Umbraco.Core/Events/RelateOnCopyNotificationHandler.cs +++ b/src/Umbraco.Core/Events/RelateOnCopyNotificationHandler.cs @@ -33,7 +33,8 @@ namespace Umbraco.Cms.Core.Events Constants.Conventions.RelationTypes.RelateDocumentOnCopyName, true, Constants.ObjectTypes.Document, - Constants.ObjectTypes.Document); + Constants.ObjectTypes.Document, + false); _relationService.Save(relationType); } diff --git a/src/Umbraco.Core/Extensions/ConfigConnectionStringExtensions.cs b/src/Umbraco.Core/Extensions/ConfigConnectionStringExtensions.cs deleted file mode 100644 index 329c9e8202..0000000000 --- a/src/Umbraco.Core/Extensions/ConfigConnectionStringExtensions.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) Umbraco. -// See LICENSE for more details. - -using Umbraco.Cms.Core.Configuration; - -namespace Umbraco.Extensions -{ - public static class ConfigConnectionStringExtensions - { - public static bool IsConnectionStringConfigured(this ConfigConnectionString databaseSettings) - => databaseSettings != null && - !string.IsNullOrWhiteSpace(databaseSettings.ConnectionString) && - !string.IsNullOrWhiteSpace(databaseSettings.ProviderName); - } -} diff --git a/src/Umbraco.Core/Extensions/ConnectionStringExtensions.cs b/src/Umbraco.Core/Extensions/ConnectionStringExtensions.cs new file mode 100644 index 0000000000..6aa17055f9 --- /dev/null +++ b/src/Umbraco.Core/Extensions/ConnectionStringExtensions.cs @@ -0,0 +1,35 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System; +using Microsoft.Extensions.Configuration; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Configuration.Models; + +namespace Umbraco.Extensions +{ + public static class ConnectionStringExtensions + { + public static bool IsConnectionStringConfigured(this ConnectionStrings connectionString) + => connectionString != null && + !string.IsNullOrWhiteSpace(connectionString.ConnectionString) && + !string.IsNullOrWhiteSpace(connectionString.ProviderName); + + /// + /// Gets a connection string from configuration with placeholders replaced. + /// + public static string GetUmbracoConnectionString( + this IConfiguration configuration, + string connectionStringName = Constants.System.UmbracoConnectionName) => + configuration.GetConnectionString(connectionStringName).ReplaceDataDirectoryPlaceholder(); + + /// + /// Replaces instances of the |DataDirectory| placeholder in a string with the value of AppDomain DataDirectory. + /// + public static string ReplaceDataDirectoryPlaceholder(this string input) + { + var dataDirectory = AppDomain.CurrentDomain.GetData("DataDirectory")?.ToString(); + return input?.Replace(ConnectionStrings.DataDirectoryPlaceholder, dataDirectory); + } + } +} diff --git a/src/Umbraco.Core/Extensions/ObjectExtensions.cs b/src/Umbraco.Core/Extensions/ObjectExtensions.cs index 1265052308..1ba7e0fc4d 100644 --- a/src/Umbraco.Core/Extensions/ObjectExtensions.cs +++ b/src/Umbraco.Core/Extensions/ObjectExtensions.cs @@ -624,7 +624,6 @@ namespace Umbraco.Extensions if (type == typeof(sbyte)) return XmlConvert.ToString((sbyte)value); if (type == typeof(short)) return XmlConvert.ToString((short)value); if (type == typeof(TimeSpan)) return XmlConvert.ToString((TimeSpan)value); - if (type == typeof(bool)) return XmlConvert.ToString((bool)value); if (type == typeof(uint)) return XmlConvert.ToString((uint)value); if (type == typeof(ulong)) return XmlConvert.ToString((ulong)value); if (type == typeof(ushort)) return XmlConvert.ToString((ushort)value); diff --git a/src/Umbraco.Core/Install/InstallSteps/TelemetryIdentifierStep.cs b/src/Umbraco.Core/Install/InstallSteps/TelemetryIdentifierStep.cs index 6f4cd87bd9..5263ac935f 100644 --- a/src/Umbraco.Core/Install/InstallSteps/TelemetryIdentifierStep.cs +++ b/src/Umbraco.Core/Install/InstallSteps/TelemetryIdentifierStep.cs @@ -1,10 +1,13 @@ using System; using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Install.Models; +using Umbraco.Cms.Core.Telemetry; +using Umbraco.Cms.Web.Common.DependencyInjection; namespace Umbraco.Cms.Core.Install.InstallSteps { @@ -13,32 +16,30 @@ namespace Umbraco.Cms.Core.Install.InstallSteps PerformsAppRestart = false)] public class TelemetryIdentifierStep : InstallSetupStep { - private readonly ILogger _logger; private readonly IOptions _globalSettings; - private readonly IConfigManipulator _configManipulator; + private readonly ISiteIdentifierService _siteIdentifierService; - public TelemetryIdentifierStep(ILogger logger, IOptions globalSettings, IConfigManipulator configManipulator) + public TelemetryIdentifierStep( + IOptions globalSettings, + ISiteIdentifierService siteIdentifierService) { - _logger = logger; _globalSettings = globalSettings; - _configManipulator = configManipulator; + _siteIdentifierService = siteIdentifierService; + } + + [Obsolete("Use constructor that takes GlobalSettings and ISiteIdentifierService")] + public TelemetryIdentifierStep( + ILogger logger, + IOptions globalSettings, + IConfigManipulator configManipulator) + : this(globalSettings, StaticServiceProvider.Instance.GetRequiredService()) + { } public override Task ExecuteAsync(object model) { - // Generate GUID - var telemetrySiteIdentifier = Guid.NewGuid(); - - try - { - _configManipulator.SetGlobalId(telemetrySiteIdentifier.ToString()); - } - catch (Exception ex) - { - _logger.LogError(ex, "Couldn't update config files with a telemetry site identifier"); - } - - return Task.FromResult(null); + _siteIdentifierService.TryCreateSiteIdentifier(out _); + return Task.FromResult(null); } public override bool RequiresExecution(object model) diff --git a/src/Umbraco.Core/Install/Models/DatabaseModel.cs b/src/Umbraco.Core/Install/Models/DatabaseModel.cs index f321e8fc89..37cdcb9610 100644 --- a/src/Umbraco.Core/Install/Models/DatabaseModel.cs +++ b/src/Umbraco.Core/Install/Models/DatabaseModel.cs @@ -1,3 +1,4 @@ +using System; using System.Runtime.Serialization; namespace Umbraco.Cms.Core.Install.Models @@ -5,8 +6,11 @@ namespace Umbraco.Cms.Core.Install.Models [DataContract(Name = "database", Namespace = "")] public class DatabaseModel { - [DataMember(Name = "dbType")] - public DatabaseType DatabaseType { get; set; } = DatabaseType.SqlServer; + [DataMember(Name = "databaseProviderMetadataId")] + public Guid DatabaseProviderMetadataId { get; set; } + + [DataMember(Name = "providerName")] + public string ProviderName { get; set; } [DataMember(Name = "server")] public string Server { get; set; } = null!; diff --git a/src/Umbraco.Core/Install/Models/DatabaseType.cs b/src/Umbraco.Core/Install/Models/DatabaseType.cs deleted file mode 100644 index bc0616620f..0000000000 --- a/src/Umbraco.Core/Install/Models/DatabaseType.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Umbraco.Cms.Core.Install.Models -{ - public enum DatabaseType - { - SqlLocalDb, - SqlCe, - SqlServer, - SqlAzure, - Custom - } -} diff --git a/src/Umbraco.Core/Manifest/ManifestContentAppFactory.cs b/src/Umbraco.Core/Manifest/ManifestContentAppFactory.cs index c52bd81e21..c4bc87e9a2 100644 --- a/src/Umbraco.Core/Manifest/ManifestContentAppFactory.cs +++ b/src/Umbraco.Core/Manifest/ManifestContentAppFactory.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; @@ -70,6 +70,10 @@ namespace Umbraco.Cms.Core.Manifest partA = "contentType"; partB = contentType?.Alias; break; + case IDictionaryItem _: + partA = "dictionary"; + partB = "*"; //Not really a different type for dictionary items + break; default: return null; diff --git a/src/Umbraco.Core/Models/Content.cs b/src/Umbraco.Core/Models/Content.cs index d3b02e30a1..bc77e52624 100644 --- a/src/Umbraco.Core/Models/Content.cs +++ b/src/Umbraco.Core/Models/Content.cs @@ -198,7 +198,7 @@ namespace Umbraco.Cms.Core.Models public bool IsCulturePublished(string culture) // just check _publishInfos // a non-available culture could not become published anyways - => _publishInfos != null && _publishInfos.ContainsKey(culture); + => !culture.IsNullOrWhiteSpace() && _publishInfos != null && _publishInfos.ContainsKey(culture); /// public bool IsCultureEdited(string culture) diff --git a/src/Umbraco.Core/Models/ContentEditing/DictionaryDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/DictionaryDisplay.cs index 41e49ba34d..d8cfaf1104 100644 --- a/src/Umbraco.Core/Models/ContentEditing/DictionaryDisplay.cs +++ b/src/Umbraco.Core/Models/ContentEditing/DictionaryDisplay.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Runtime.Serialization; @@ -17,6 +17,7 @@ namespace Umbraco.Cms.Core.Models.ContentEditing { Notifications = new List(); Translations = new List(); + ContentApps = new List(); } /// @@ -37,5 +38,11 @@ namespace Umbraco.Cms.Core.Models.ContentEditing /// [DataMember(Name = "translations")] public List Translations { get; private set; } + + /// + /// Apps for the dictionary item + /// + [DataMember(Name = "apps")] + public List ContentApps { get; private set; } } } diff --git a/src/Umbraco.Core/Models/ContentEditing/HistoryCleanup.cs b/src/Umbraco.Core/Models/ContentEditing/HistoryCleanup.cs index b7bfb32808..a0d9bbbcb3 100644 --- a/src/Umbraco.Core/Models/ContentEditing/HistoryCleanup.cs +++ b/src/Umbraco.Core/Models/ContentEditing/HistoryCleanup.cs @@ -1,17 +1,34 @@ using System.Runtime.Serialization; +using Umbraco.Cms.Core.Models.Entities; namespace Umbraco.Cms.Core.Models.ContentEditing { [DataContract(Name = "historyCleanup", Namespace = "")] - public class HistoryCleanup + public class HistoryCleanup : BeingDirtyBase { + private bool _preventCleanup; + private int? _keepAllVersionsNewerThanDays; + private int? _keepLatestVersionPerDayForDays; + [DataMember(Name = "preventCleanup")] - public bool PreventCleanup { get; set; } + public bool PreventCleanup + { + get => _preventCleanup; + set => SetPropertyValueAndDetectChanges(value, ref _preventCleanup, nameof(PreventCleanup)); + } [DataMember(Name = "keepAllVersionsNewerThanDays")] - public int? KeepAllVersionsNewerThanDays { get; set; } + public int? KeepAllVersionsNewerThanDays + { + get => _keepAllVersionsNewerThanDays; + set => SetPropertyValueAndDetectChanges(value, ref _keepAllVersionsNewerThanDays, nameof(KeepAllVersionsNewerThanDays)); + } [DataMember(Name = "keepLatestVersionPerDayForDays")] - public int? KeepLatestVersionPerDayForDays { get; set; } + public int? KeepLatestVersionPerDayForDays + { + get => _keepLatestVersionPerDayForDays; + set => SetPropertyValueAndDetectChanges(value, ref _keepLatestVersionPerDayForDays, nameof(KeepLatestVersionPerDayForDays)); + } } } diff --git a/src/Umbraco.Core/Models/ContentEditing/RelationTypeDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/RelationTypeDisplay.cs index 0c649c092f..906fdf3a40 100644 --- a/src/Umbraco.Core/Models/ContentEditing/RelationTypeDisplay.cs +++ b/src/Umbraco.Core/Models/ContentEditing/RelationTypeDisplay.cs @@ -55,5 +55,11 @@ namespace Umbraco.Cms.Core.Models.ContentEditing /// [DataMember(Name = "notifications")] public List Notifications { get; private set; } + + /// + /// Gets or sets a boolean indicating whether the RelationType should be returned in "Used by"-queries. + /// + [DataMember(Name = "isDependency", IsRequired = true)] + public bool IsDependency { get; set; } } } diff --git a/src/Umbraco.Core/Models/ContentEditing/RelationTypeSave.cs b/src/Umbraco.Core/Models/ContentEditing/RelationTypeSave.cs index b72a03eec4..f541158095 100644 --- a/src/Umbraco.Core/Models/ContentEditing/RelationTypeSave.cs +++ b/src/Umbraco.Core/Models/ContentEditing/RelationTypeSave.cs @@ -23,5 +23,11 @@ namespace Umbraco.Cms.Core.Models.ContentEditing /// [DataMember(Name = "childObjectType", IsRequired = false)] public Guid? ChildObjectType { get; set; } + + /// + /// Gets or sets a boolean indicating whether the RelationType should be returned in "Used by"-queries. + /// + [DataMember(Name = "isDependency", IsRequired = true)] + public bool IsDependency { get; set; } } } diff --git a/src/Umbraco.Core/Models/ContentType.cs b/src/Umbraco.Core/Models/ContentType.cs index 07cc46c79b..92c1b6cfe6 100644 --- a/src/Umbraco.Core/Models/ContentType.cs +++ b/src/Umbraco.Core/Models/ContentType.cs @@ -64,8 +64,8 @@ namespace Umbraco.Cms.Core.Models /// we should not store direct entity /// [IgnoreDataMember] - public ITemplate? DefaultTemplate => - AllowedTemplates?.FirstOrDefault(x => x != null && x.Id == DefaultTemplateId); + public ITemplate DefaultTemplate => + AllowedTemplates.FirstOrDefault(x => x != null && x.Id == DefaultTemplateId); [DataMember] @@ -82,21 +82,27 @@ namespace Umbraco.Cms.Core.Models /// we should not store direct entity /// [DataMember] - public IEnumerable? AllowedTemplates + public IEnumerable AllowedTemplates { get => _allowedTemplates; set { SetPropertyValueAndDetectChanges(value, ref _allowedTemplates, nameof(AllowedTemplates), TemplateComparer); - if (_allowedTemplates?.Any(x => x.Id == _defaultTemplate) == false) + if (_allowedTemplates.Any(x => x.Id == _defaultTemplate) == false) { DefaultTemplateId = 0; } } } - public HistoryCleanup? HistoryCleanup { get; set; } + private HistoryCleanup? _historyCleanup; + + public HistoryCleanup? HistoryCleanup + { + get => _historyCleanup; + set => SetPropertyValueAndDetectChanges(value, ref _historyCleanup, nameof(HistoryCleanup)); + } /// /// Determines if AllowedTemplates contains templateId @@ -162,5 +168,8 @@ namespace Umbraco.Cms.Core.Models /// IContentType IContentType.DeepCloneWithResetIdentities(string newAlias) => (IContentType)DeepCloneWithResetIdentities(newAlias); + + /// + public override bool IsDirty() => base.IsDirty() || HistoryCleanup.IsDirty(); } } diff --git a/src/Umbraco.Core/Models/IRelationType.cs b/src/Umbraco.Core/Models/IRelationType.cs index c48e13fdc3..cbc485f64b 100644 --- a/src/Umbraco.Core/Models/IRelationType.cs +++ b/src/Umbraco.Core/Models/IRelationType.cs @@ -4,6 +4,15 @@ using Umbraco.Cms.Core.Models.Entities; namespace Umbraco.Cms.Core.Models { + public interface IRelationTypeWithIsDependency : IRelationType + { + /// + /// Gets or sets a boolean indicating whether the RelationType should be returned in "Used by"-queries. + /// + [DataMember] + bool IsDependency { get; set; } + } + public interface IRelationType : IEntity, IRememberBeingDirty { /// diff --git a/src/Umbraco.Core/Models/Mapping/CommonMapper.cs b/src/Umbraco.Core/Models/Mapping/CommonMapper.cs index ae96b57afe..3832654f45 100644 --- a/src/Umbraco.Core/Models/Mapping/CommonMapper.cs +++ b/src/Umbraco.Core/Models/Mapping/CommonMapper.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using Umbraco.Cms.Core.ContentApps; @@ -48,6 +48,11 @@ namespace Umbraco.Cms.Core.Models.Mapping } public IEnumerable GetContentApps(IUmbracoEntity source) + { + return GetContentAppsForEntity(source); + } + + public IEnumerable GetContentAppsForEntity(IEntity source) { var apps = _contentAppDefinitions.GetContentAppsFor(source).ToArray(); diff --git a/src/Umbraco.Core/Models/Mapping/ContentTypeMapDefinition.cs b/src/Umbraco.Core/Models/Mapping/ContentTypeMapDefinition.cs index d93420ace6..6a2dd0d305 100644 --- a/src/Umbraco.Core/Models/Mapping/ContentTypeMapDefinition.cs +++ b/src/Umbraco.Core/Models/Mapping/ContentTypeMapDefinition.cs @@ -133,7 +133,7 @@ namespace Umbraco.Cms.Core.Models.Mapping if (target is IContentTypeWithHistoryCleanup targetWithHistoryCleanup) { - targetWithHistoryCleanup.HistoryCleanup = source.HistoryCleanup; + MapHistoryCleanup(source, targetWithHistoryCleanup); } target.AllowedTemplates = source.AllowedTemplates? @@ -147,6 +147,34 @@ namespace Umbraco.Cms.Core.Models.Mapping : _fileService.GetTemplate(source.DefaultTemplate)); } + private static void MapHistoryCleanup(DocumentTypeSave source, IContentTypeWithHistoryCleanup target) + { + // If source history cleanup is null we don't have to map all properties + if (source.HistoryCleanup is null) + { + target.HistoryCleanup = null; + return; + } + + // We need to reset the dirty properties, because it is otherwise true, just because the json serializer has set properties + target.HistoryCleanup.ResetDirtyProperties(false); + if (target.HistoryCleanup.PreventCleanup != source.HistoryCleanup.PreventCleanup) + { + target.HistoryCleanup.PreventCleanup = source.HistoryCleanup.PreventCleanup; + } + + if (target.HistoryCleanup.KeepAllVersionsNewerThanDays != source.HistoryCleanup.KeepAllVersionsNewerThanDays) + { + target.HistoryCleanup.KeepAllVersionsNewerThanDays = source.HistoryCleanup.KeepAllVersionsNewerThanDays; + } + + if (target.HistoryCleanup.KeepLatestVersionPerDayForDays != + source.HistoryCleanup.KeepLatestVersionPerDayForDays) + { + target.HistoryCleanup.KeepLatestVersionPerDayForDays = source.HistoryCleanup.KeepLatestVersionPerDayForDays; + } + } + // no MapAll - take care private void Map(MediaTypeSave source, IMediaType target, MapperContext context) { @@ -196,7 +224,7 @@ namespace Umbraco.Cms.Core.Models.Mapping target.AllowCultureVariant = source.VariesByCulture(); target.AllowSegmentVariant = source.VariesBySegment(); - target.ContentApps = _commonMapper.GetContentApps(source); + target.ContentApps = _commonMapper.GetContentAppsForEntity(source); //sync templates if (source.AllowedTemplates is not null) @@ -331,7 +359,10 @@ namespace Umbraco.Cms.Core.Models.Mapping if (source.GroupId > 0) { - target.PropertyGroupId = new Lazy(() => source.GroupId, false); + if (target.PropertyGroupId?.Value != source.GroupId) + { + target.PropertyGroupId = new Lazy(() => source.GroupId, false); + } } target.Alias = source.Alias; @@ -526,7 +557,15 @@ namespace Umbraco.Cms.Core.Models.Mapping target.Thumbnail = source.Thumbnail; target.AllowedAsRoot = source.AllowAsRoot; - target.AllowedContentTypes = source.AllowedContentTypes.Select((t, i) => new ContentTypeSort(t, i)); + + bool allowedContentTypesUnchanged = target.AllowedContentTypes.Select(x => x.Id.Value) + .SequenceEqual(source.AllowedContentTypes); + + if (allowedContentTypesUnchanged is false) + { + target.AllowedContentTypes = source.AllowedContentTypes.Select((t, i) => new ContentTypeSort(t, i)); + } + if (!(target is IMemberType)) { @@ -578,7 +617,7 @@ namespace Umbraco.Cms.Core.Models.Mapping // ensure no duplicate alias, then assign the group properties collection EnsureUniqueAliases(destProperties); - if (destGroup is not null) + if (destGroup is not null && (destGroup.PropertyTypes.SupportsPublishing != isPublishing || destGroup.PropertyTypes.SequenceEqual(destProperties) is false)) { destGroup.PropertyTypes = new PropertyTypeCollection(isPublishing, destProperties); destGroups.Add(destGroup); @@ -587,7 +626,11 @@ namespace Umbraco.Cms.Core.Models.Mapping // ensure no duplicate name, then assign the groups collection EnsureUniqueAliases(destGroups); - target.PropertyGroups = new PropertyGroupCollection(destGroups); + + if (target.PropertyGroups.SequenceEqual(destGroups) is false) + { + target.PropertyGroups = new PropertyGroupCollection(destGroups); + } // because the property groups collection was rebuilt, there is no need to remove // the old groups - they are just gone and will be cleared by the repository diff --git a/src/Umbraco.Core/Models/Mapping/DictionaryMapDefinition.cs b/src/Umbraco.Core/Models/Mapping/DictionaryMapDefinition.cs index 290742eb1a..f355cba47d 100644 --- a/src/Umbraco.Core/Models/Mapping/DictionaryMapDefinition.cs +++ b/src/Umbraco.Core/Models/Mapping/DictionaryMapDefinition.cs @@ -1,6 +1,7 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; +using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Services; @@ -14,12 +15,20 @@ namespace Umbraco.Cms.Core.Models.Mapping public class DictionaryMapDefinition : IMapDefinition { private readonly ILocalizationService _localizationService; + private readonly CommonMapper _commonMapper; + [Obsolete("Use the constructor with the CommonMapper")] public DictionaryMapDefinition(ILocalizationService localizationService) { _localizationService = localizationService; } + public DictionaryMapDefinition(ILocalizationService localizationService, CommonMapper commonMapper) + { + _localizationService = localizationService; + _commonMapper = commonMapper; + } + public void DefineMaps(IUmbracoMapper mapper) { mapper.Define((source, context) => new EntityBasic(), Map); @@ -44,6 +53,10 @@ namespace Umbraco.Cms.Core.Models.Mapping target.Name = source.ItemKey; target.ParentId = source.ParentId ?? Guid.Empty; target.Udi = Udi.Create(Constants.UdiEntityType.DictionaryItem, source.Key); + if (_commonMapper != null) + { + target.ContentApps.AddRange(_commonMapper.GetContentAppsForEntity(source)); + } // build up the path to make it possible to set active item in tree // TODO: check if there is a better way diff --git a/src/Umbraco.Core/Models/Mapping/RelationMapDefinition.cs b/src/Umbraco.Core/Models/Mapping/RelationMapDefinition.cs index 714fb8431f..b0aaab9537 100644 --- a/src/Umbraco.Core/Models/Mapping/RelationMapDefinition.cs +++ b/src/Umbraco.Core/Models/Mapping/RelationMapDefinition.cs @@ -30,6 +30,11 @@ namespace Umbraco.Cms.Core.Models.Mapping target.ChildObjectType = source.ChildObjectType; target.Id = source.Id; target.IsBidirectional = source.IsBidirectional; + + if (source is IRelationTypeWithIsDependency sourceWithIsDependency) + { + target.IsDependency = sourceWithIsDependency.IsDependency; + } target.Key = source.Key; target.Name = source.Name; target.Alias = source.Alias; @@ -77,6 +82,11 @@ namespace Umbraco.Cms.Core.Models.Mapping target.ChildObjectType = source.ChildObjectType; target.Id = source.Id.TryConvertTo().Result; target.IsBidirectional = source.IsBidirectional; + if (target is IRelationTypeWithIsDependency targetWithIsDependency) + { + targetWithIsDependency.IsDependency = source.IsDependency; + } + target.Key = source.Key; target.Name = source.Name; target.ParentObjectType = source.ParentObjectType; diff --git a/src/Umbraco.Core/Models/RelationItem.cs b/src/Umbraco.Core/Models/RelationItem.cs new file mode 100644 index 0000000000..cebbc20951 --- /dev/null +++ b/src/Umbraco.Core/Models/RelationItem.cs @@ -0,0 +1,44 @@ +using System; +using System.Runtime.Serialization; +using Umbraco.Cms.Core.Models.Entities; + +namespace Umbraco.Cms.Core.Models +{ + [DataContract(Name = "relationItem", Namespace = "")] + public class RelationItem + { + [DataMember(Name = "id")] + public int NodeId { get; set; } + + [DataMember(Name = "key")] + public Guid NodeKey { get; set; } + + [DataMember(Name = "name")] + public string NodeName { get; set; } + + [DataMember(Name = "type")] + public string NodeType { get; set; } + + [DataMember(Name = "udi")] + public Udi NodeUdi => Udi.Create(NodeType, NodeKey); + + [DataMember(Name = "icon")] + public string ContentTypeIcon { get; set; } + + [DataMember(Name = "alias")] + public string ContentTypeAlias { get; set; } + + [DataMember(Name = "contentTypeName")] + public string ContentTypeName { get; set; } + + [DataMember(Name = "relationTypeName")] + public string RelationTypeName { get; set; } + + [DataMember(Name = "relationTypeIsBidirectional")] + public bool RelationTypeIsBidirectional { get; set; } + + [DataMember(Name = "relationTypeIsDependency")] + public bool RelationTypeIsDependency { get; set; } + + } +} diff --git a/src/Umbraco.Core/Models/RelationType.cs b/src/Umbraco.Core/Models/RelationType.cs index 2b59b52262..fcc5ab699e 100644 --- a/src/Umbraco.Core/Models/RelationType.cs +++ b/src/Umbraco.Core/Models/RelationType.cs @@ -9,20 +9,28 @@ namespace Umbraco.Cms.Core.Models /// [Serializable] [DataContract(IsReference = true)] - public class RelationType : EntityBase, IRelationType + public class RelationType : EntityBase, IRelationType, IRelationTypeWithIsDependency { private string _name; private string _alias; private bool _isBidirectional; + private bool _isDependency; private Guid? _parentObjectType; private Guid? _childObjectType; public RelationType(string alias, string name) - : this(name: name, alias: alias, false, null, null) + : this(name: name, alias: alias, false, null, null, false) { } + [Obsolete("Use ctor with isDependency parameter")] public RelationType(string name, string alias, bool isBidrectional, Guid? parentObjectType, Guid? childObjectType) + :this(name,alias,isBidrectional, parentObjectType, childObjectType, false) + { + + } + + public RelationType(string name, string alias, bool isBidrectional, Guid? parentObjectType, Guid? childObjectType, bool isDependency) { 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)); @@ -32,6 +40,7 @@ namespace Umbraco.Cms.Core.Models _name = name; _alias = alias; _isBidirectional = isBidrectional; + _isDependency = isDependency; _parentObjectType = parentObjectType; _childObjectType = childObjectType; } @@ -88,5 +97,11 @@ namespace Umbraco.Cms.Core.Models set => SetPropertyValueAndDetectChanges(value, ref _childObjectType, nameof(ChildObjectType)); } + + public bool IsDependency + { + get => _isDependency; + set => SetPropertyValueAndDetectChanges(value, ref _isDependency, nameof(IsDependency)); + } } } diff --git a/src/Umbraco.Core/Packaging/PackagesRepository.cs b/src/Umbraco.Core/Packaging/PackagesRepository.cs index 4183575070..4e68cf9f2e 100644 --- a/src/Umbraco.Core/Packaging/PackagesRepository.cs +++ b/src/Umbraco.Core/Packaging/PackagesRepository.cs @@ -5,7 +5,6 @@ using System.Globalization; using System.IO; using System.IO.Compression; using System.Linq; -using System.Text; using System.Xml.Linq; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; @@ -33,7 +32,7 @@ namespace Umbraco.Cms.Core.Packaging private readonly IEntityXmlSerializer _serializer; private readonly IHostingEnvironment _hostingEnvironment; private readonly string _packageRepositoryFileName; - private readonly string _mediaFolderPath; + private readonly string _createdPackagesFolderPath; private readonly string _packagesFolderPath; private readonly string _tempFolderPath; private readonly PackageDefinitionXmlParser _parser; @@ -93,7 +92,7 @@ namespace Umbraco.Cms.Core.Packaging _tempFolderPath = tempFolderPath ?? Constants.SystemDirectories.TempData.EnsureEndsWith('/') + "PackageFiles"; _packagesFolderPath = packagesFolderPath ?? Constants.SystemDirectories.Packages; - _mediaFolderPath = mediaFolderPath ?? Path.Combine(globalSettings.Value.UmbracoMediaPhysicalRootPath, Constants.SystemDirectories.CreatedPackages); + _createdPackagesFolderPath = mediaFolderPath ?? Constants.SystemDirectories.CreatedPackages; _parser = new PackageDefinitionXmlParser(); _mediaService = mediaService; @@ -250,15 +249,8 @@ namespace Umbraco.Cms.Core.Packaging } } - - - var directoryName = - _hostingEnvironment.MapPathWebRoot(Path.Combine(_mediaFolderPath, definition.Name.Replace(' ', '_'))); - - if (Directory.Exists(directoryName) == false) - { - Directory.CreateDirectory(directoryName); - } + var directoryName = _hostingEnvironment.MapPathContentRoot(Path.Combine(_createdPackagesFolderPath, definition.Name.Replace(' ', '_'))); + Directory.CreateDirectory(directoryName); var finalPackagePath = Path.Combine(directoryName, fileName); @@ -276,14 +268,14 @@ namespace Umbraco.Cms.Core.Packaging } finally { - //Clean up + // Clean up Directory.Delete(temporaryPath, true); } } private void ValidatePackage(PackageDefinition definition) { - //ensure it's valid + // ensure it's valid var context = new ValidationContext(definition, serviceProvider: null, items: null); var results = new List(); var isValid = Validator.TryValidateObject(definition, context, results); @@ -740,7 +732,6 @@ namespace Umbraco.Cms.Core.Packaging private XDocument EnsureStorage(out string packagesFile) { var packagesFolder = _hostingEnvironment.MapPathContentRoot(_packagesFolderPath); - //ensure it exists Directory.CreateDirectory(packagesFolder); packagesFile = _hostingEnvironment.MapPathContentRoot(CreatedPackagesFile); @@ -748,6 +739,8 @@ namespace Umbraco.Cms.Core.Packaging { var xml = new XDocument(new XElement("packages")); xml.Save(packagesFile); + + return xml; } var packagesXml = XDocument.Load(packagesFile); @@ -757,9 +750,16 @@ namespace Umbraco.Cms.Core.Packaging public void DeleteLocalRepositoryFiles() { var packagesFile = _hostingEnvironment.MapPathContentRoot(CreatedPackagesFile); - File.Delete(packagesFile); + if (File.Exists(packagesFile)) + { + File.Delete(packagesFile); + } + var packagesFolder = _hostingEnvironment.MapPathContentRoot(_packagesFolderPath); - Directory.Delete(packagesFolder); + if (Directory.Exists(packagesFolder)) + { + Directory.Delete(packagesFolder); + } } } } diff --git a/src/Umbraco.Core/Persistence/Constants-DbProviderNames.cs b/src/Umbraco.Core/Persistence/Constants-DbProviderNames.cs deleted file mode 100644 index bd95776dea..0000000000 --- a/src/Umbraco.Core/Persistence/Constants-DbProviderNames.cs +++ /dev/null @@ -1,12 +0,0 @@ - // ReSharper disable once CheckNamespace -namespace Umbraco.Cms.Core -{ - static partial class Constants - { - public static class DbProviderNames - { - public const string SqlServer = "Microsoft.Data.SqlClient"; - public const string SqlCe = "System.Data.SqlServerCe.4.0"; - } - } -} diff --git a/src/Umbraco.Core/Persistence/Constants-Locks.cs b/src/Umbraco.Core/Persistence/Constants-Locks.cs index 5312bf6886..3c0b2c4d28 100644 --- a/src/Umbraco.Core/Persistence/Constants-Locks.cs +++ b/src/Umbraco.Core/Persistence/Constants-Locks.cs @@ -65,6 +65,11 @@ namespace Umbraco.Cms.Core /// All languages. /// public const int Languages = -340; + + /// + /// ScheduledPublishing job. + /// + public const int ScheduledPublishing = -341; } } } diff --git a/src/Umbraco.Core/Persistence/Repositories/IRelationRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IRelationRepository.cs index 484cd1917b..0165b9eb39 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IRelationRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IRelationRepository.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Entities; diff --git a/src/Umbraco.Core/Persistence/Repositories/ITrackedReferencesRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ITrackedReferencesRepository.cs new file mode 100644 index 0000000000..42746a9565 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Repositories/ITrackedReferencesRepository.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Entities; + +namespace Umbraco.Cms.Core.Persistence.Repositories +{ + public interface ITrackedReferencesRepository + { + IEnumerable GetPagedRelationsForItems(int[] ids, long pageIndex, int pageSize, bool filterMustBeIsDependency,out long totalRecords); + IEnumerable GetPagedItemsWithRelations(int[] ids, long pageIndex, int pageSize, bool filterMustBeIsDependency,out long totalRecords); + IEnumerable GetPagedDescendantsInReferences(int parentId, long pageIndex, int pageSize, bool filterMustBeIsDependency,out long totalRecords); + } +} diff --git a/src/Umbraco.Core/PropertyEditors/DataValueEditor.cs b/src/Umbraco.Core/PropertyEditors/DataValueEditor.cs index ab84f7131b..b8e3e597a4 100644 --- a/src/Umbraco.Core/PropertyEditors/DataValueEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/DataValueEditor.cs @@ -6,7 +6,6 @@ using System.Linq; using System.Runtime.Serialization; using System.Xml.Linq; using Microsoft.Extensions.Logging; -using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Editors; @@ -158,17 +157,15 @@ namespace Umbraco.Cms.Core.PropertyEditors /// ValueType was out of range. internal Attempt TryConvertValueToCrlType(object? value) { - // Ensure empty string values are converted to null - if (value is string s && string.IsNullOrWhiteSpace(s)) + // Ensure empty string and JSON values are converted to null + if (value is string stringValue && string.IsNullOrWhiteSpace(stringValue)) { value = null; } - - // Ensure JSON is serialized properly (without indentation or converted to null when empty) - if (value is not null && ValueType.InvariantEquals(ValueTypes.Json)) + else if (value is not string && ValueType.InvariantEquals(ValueTypes.Json)) { + // Only serialize value when it's not already a string var jsonValue = _jsonSerializer?.Serialize(value); - if (jsonValue?.DetectIsEmptyJson() ?? false) { value = null; diff --git a/src/Umbraco.Core/Routing/DefaultUrlProvider.cs b/src/Umbraco.Core/Routing/DefaultUrlProvider.cs index 1b75eb1aeb..7e3a46f7a8 100644 --- a/src/Umbraco.Core/Routing/DefaultUrlProvider.cs +++ b/src/Umbraco.Core/Routing/DefaultUrlProvider.cs @@ -161,7 +161,7 @@ namespace Umbraco.Cms.Core.Routing : DomainUtilities.DomainForNode(umbracoContext.PublishedSnapshot.Domains, _siteDomainMapper, int.Parse(route.Substring(0, pos), CultureInfo.InvariantCulture), current, culture); var defaultCulture = _localizationService.GetDefaultLanguageIsoCode(); - if (domainUri is not null || culture is null || culture.Equals(defaultCulture, StringComparison.InvariantCultureIgnoreCase)) + if (domainUri is not null || string.IsNullOrEmpty(culture) || culture.Equals(defaultCulture, StringComparison.InvariantCultureIgnoreCase)) { var url = AssembleUrl(domainUri, path, current, mode).ToString(); return UrlInfo.Url(url, culture); diff --git a/src/Umbraco.Core/Routing/UrlProviderExtensions.cs b/src/Umbraco.Core/Routing/UrlProviderExtensions.cs index 07878be7f5..7686b1a49a 100644 --- a/src/Umbraco.Core/Routing/UrlProviderExtensions.cs +++ b/src/Umbraco.Core/Routing/UrlProviderExtensions.cs @@ -269,16 +269,10 @@ namespace Umbraco.Extensions if (!pcr.HasPublishedContent()) { - var logMsg = nameof(DetectCollisionAsync) + + const string logMsg = nameof(DetectCollisionAsync) + " did not resolve a content item for original url: {Url}, translated to {TranslatedUrl} and culture: {Culture}"; - if (pcr.IgnorePublishedContentCollisions) - { - logger.LogDebug(logMsg, url, uri, culture); - } - else - { - logger.LogDebug(logMsg, url, uri, culture); - } + + logger.LogDebug(logMsg, url, uri, culture); var urlInfo = UrlInfo.Message(textService.Localize("content", "routeErrorCannotRoute"), culture); return Attempt.Succeed(urlInfo); diff --git a/src/Umbraco.Core/Runtime/IMainDomKeyGenerator.cs b/src/Umbraco.Core/Runtime/IMainDomKeyGenerator.cs new file mode 100644 index 0000000000..5b8fb819e6 --- /dev/null +++ b/src/Umbraco.Core/Runtime/IMainDomKeyGenerator.cs @@ -0,0 +1,13 @@ +namespace Umbraco.Cms.Core.Runtime +{ + /// + /// Defines a class which can generate a distinct key for a MainDom boundary. + /// + public interface IMainDomKeyGenerator + { + /// + /// Returns a key that signifies a MainDom boundary. + /// + string GenerateKey(); + } +} diff --git a/src/Umbraco.Core/Runtime/MainDom.cs b/src/Umbraco.Core/Runtime/MainDom.cs index e09a8d2205..0198382b2a 100644 --- a/src/Umbraco.Core/Runtime/MainDom.cs +++ b/src/Umbraco.Core/Runtime/MainDom.cs @@ -87,7 +87,7 @@ namespace Umbraco.Cms.Core.Runtime if (_isMainDom.HasValue == false) { - throw new InvalidOperationException("Register called when MainDom has not been acquired"); + throw new InvalidOperationException("Register called before IsMainDom has been established"); } else if (_isMainDom == false) { @@ -225,7 +225,7 @@ namespace Umbraco.Cms.Core.Runtime { if (!_isMainDom.HasValue) { - throw new InvalidOperationException("MainDom has not been acquired yet"); + throw new InvalidOperationException("IsMainDom has not been established yet"); } return _isMainDom.Value; } diff --git a/src/Umbraco.Core/Runtime/MainDomSemaphoreLock.cs b/src/Umbraco.Core/Runtime/MainDomSemaphoreLock.cs index cd205a0542..5d2248906e 100644 --- a/src/Umbraco.Core/Runtime/MainDomSemaphoreLock.cs +++ b/src/Umbraco.Core/Runtime/MainDomSemaphoreLock.cs @@ -1,4 +1,5 @@ using System; +using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; @@ -22,6 +23,11 @@ namespace Umbraco.Cms.Core.Runtime public MainDomSemaphoreLock(ILogger logger, IHostingEnvironment hostingEnvironment) { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + throw new PlatformNotSupportedException("MainDomSemaphoreLock is only supported on Windows."); + } + var mainDomId = MainDom.GetMainDomId(hostingEnvironment); var lockName = "UMBRACO-" + mainDomId + "-MAINDOM-LCK"; _systemLock = new SystemLock(lockName); diff --git a/src/Umbraco.Core/Security/LegacyPasswordSecurity.cs b/src/Umbraco.Core/Security/LegacyPasswordSecurity.cs index 8f6813e7ba..35528a48ca 100644 --- a/src/Umbraco.Core/Security/LegacyPasswordSecurity.cs +++ b/src/Umbraco.Core/Security/LegacyPasswordSecurity.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.ComponentModel; using System.Security.Cryptography; using System.Text; @@ -144,7 +144,7 @@ namespace Umbraco.Cms.Core.Security var saltBytes = Convert.FromBase64String(salt); byte[] inArray; - var hashAlgorithm = GetHashAlgorithm(algorithmType); + using var hashAlgorithm = GetHashAlgorithm(algorithmType); var algorithm = hashAlgorithm as KeyedHashAlgorithm; if (algorithm != null) { @@ -209,11 +209,21 @@ namespace Umbraco.Cms.Core.Security { // This is for the v6-v8 hashing algorithm if (algorithm.InvariantEquals(Constants.Security.AspNetUmbraco8PasswordHashAlgorithmName)) + { return true; + } + + // Default validation value for old machine keys (switched to HMACSHA256 aspnet 4 https://docs.microsoft.com/en-us/aspnet/whitepapers/aspnet4/breaking-changes) + if (algorithm.InvariantEquals("SHA1")) + { + return true; + } // This is for the <= v4 hashing algorithm if (IsLegacySHA1Algorithm(algorithm)) + { return true; + } return false; } @@ -227,7 +237,7 @@ namespace Umbraco.Cms.Core.Security /// The encoded password. private string HashLegacySHA1Password(string password) { - var hashAlgorithm = GetLegacySHA1Algorithm(password); + using var hashAlgorithm = GetLegacySHA1Algorithm(password); var hash = Convert.ToBase64String(hashAlgorithm.ComputeHash(Encoding.Unicode.GetBytes(password))); return hash; } diff --git a/src/Umbraco.Core/Services/IRelationService.cs b/src/Umbraco.Core/Services/IRelationService.cs index 3f303eb12f..c4060cd695 100644 --- a/src/Umbraco.Core/Services/IRelationService.cs +++ b/src/Umbraco.Core/Services/IRelationService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Entities; @@ -215,7 +215,7 @@ namespace Umbraco.Cms.Core.Services /// /// /// - /// + /// An enumerable list of IEnumerable GetPagedParentEntitiesByChildId(int id, long pageIndex, int pageSize, out long totalChildren, params UmbracoObjectTypes[] entityTypes); /// @@ -225,7 +225,7 @@ namespace Umbraco.Cms.Core.Services /// /// /// - /// + /// An enumerable list of IEnumerable GetPagedChildEntitiesByParentId(int id, long pageIndex, int pageSize, out long totalChildren, params UmbracoObjectTypes[] entityTypes); /// diff --git a/src/Umbraco.Core/Services/IServerRegistrationService.cs b/src/Umbraco.Core/Services/IServerRegistrationService.cs index f9aaf7e33f..e469de9a06 100644 --- a/src/Umbraco.Core/Services/IServerRegistrationService.cs +++ b/src/Umbraco.Core/Services/IServerRegistrationService.cs @@ -37,6 +37,17 @@ namespace Umbraco.Cms.Core.Services /// from the database. IEnumerable? GetActiveServers(bool refresh = false); + /// + /// Return all servers (active and inactive). + /// + /// A value indicating whether to force-refresh the cache. + /// All servers. + /// By default this method will rely on the repository's cache, which is updated each + /// time the current server is touched, and the period depends on the configuration. Use the + /// parameter to force a cache refresh and reload all servers + /// from the database. + IEnumerable GetServers(bool refresh = false); + /// /// Gets the role of the current server. /// diff --git a/src/Umbraco.Core/Services/ITrackedReferencesService.cs b/src/Umbraco.Core/Services/ITrackedReferencesService.cs new file mode 100644 index 0000000000..eee8a324df --- /dev/null +++ b/src/Umbraco.Core/Services/ITrackedReferencesService.cs @@ -0,0 +1,15 @@ +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Core.Services +{ + public interface ITrackedReferencesService + { + PagedResult GetPagedRelationsForItems(int[] ids, long pageIndex, int pageSize, bool filterMustBeIsDependency); + + + PagedResult GetPagedDescendantsInReferences(int parentId, long pageIndex, int pageSize, bool filterMustBeIsDependency); + + + PagedResult GetPagedItemsWithRelations(int[] ids, long pageIndex, int pageSize, bool filterMustBeIsDependency); + } +} diff --git a/src/Umbraco.Core/Services/LocalizedTextServiceFileSources.cs b/src/Umbraco.Core/Services/LocalizedTextServiceFileSources.cs index 9c9132d9bb..f26843b028 100644 --- a/src/Umbraco.Core/Services/LocalizedTextServiceFileSources.cs +++ b/src/Umbraco.Core/Services/LocalizedTextServiceFileSources.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Globalization; using System.IO; diff --git a/src/Umbraco.Core/Services/RelationService.cs b/src/Umbraco.Core/Services/RelationService.cs index 3122e6e1f8..667622c5c1 100644 --- a/src/Umbraco.Core/Services/RelationService.cs +++ b/src/Umbraco.Core/Services/RelationService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using Microsoft.Extensions.Logging; diff --git a/src/Umbraco.Core/Services/TrackedReferencesService.cs b/src/Umbraco.Core/Services/TrackedReferencesService.cs new file mode 100644 index 0000000000..c43d2ca57b --- /dev/null +++ b/src/Umbraco.Core/Services/TrackedReferencesService.cs @@ -0,0 +1,50 @@ +using System.Linq; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Core.Scoping; + +namespace Umbraco.Cms.Core.Services +{ + public class TrackedReferencesService : ITrackedReferencesService + { + private readonly ITrackedReferencesRepository _trackedReferencesRepository; + private readonly IScopeProvider _scopeProvider; + private readonly IEntityService _entityService; + + public TrackedReferencesService(ITrackedReferencesRepository trackedReferencesRepository, IScopeProvider scopeProvider, IEntityService entityService) + { + _trackedReferencesRepository = trackedReferencesRepository; + _scopeProvider = scopeProvider; + _entityService = entityService; + } + + public PagedResult GetPagedRelationsForItems(int[] ids, long pageIndex, int pageSize, bool filterMustBeIsDependency) + { + using IScope scope = _scopeProvider.CreateScope(autoComplete: true); + var items = _trackedReferencesRepository.GetPagedRelationsForItems(ids, pageIndex, pageSize, filterMustBeIsDependency, out var totalItems); + + return new PagedResult(totalItems, pageIndex+1, pageSize) { Items = items }; + } + + public PagedResult GetPagedItemsWithRelations(int[] ids, long pageIndex, int pageSize, bool filterMustBeIsDependency) + { + using IScope scope = _scopeProvider.CreateScope(autoComplete: true); + var items = _trackedReferencesRepository.GetPagedItemsWithRelations(ids, pageIndex, pageSize, filterMustBeIsDependency, out var totalItems); + + return new PagedResult(totalItems, pageIndex+1, pageSize) { Items = items }; + } + + public PagedResult GetPagedDescendantsInReferences(int parentId, long pageIndex, int pageSize, bool filterMustBeIsDependency) + { + using IScope scope = _scopeProvider.CreateScope(autoComplete: true); + + var items = _trackedReferencesRepository.GetPagedDescendantsInReferences( + parentId, + pageIndex, + pageSize, + filterMustBeIsDependency, + out var totalItems); + return new PagedResult(totalItems, pageIndex+1, pageSize) { Items = items }; + } + } +} diff --git a/src/Umbraco.Core/Telemetry/ISiteIdentifierService.cs b/src/Umbraco.Core/Telemetry/ISiteIdentifierService.cs new file mode 100644 index 0000000000..7fd0ee5a85 --- /dev/null +++ b/src/Umbraco.Core/Telemetry/ISiteIdentifierService.cs @@ -0,0 +1,31 @@ +using System; + +namespace Umbraco.Cms.Core.Telemetry +{ + /// + /// Used to get and create the site identifier + /// + public interface ISiteIdentifierService + { + + /// + /// Tries to get the site identifier + /// + /// True if success. + bool TryGetSiteIdentifier(out Guid siteIdentifier); + + /// + /// Creates the site identifier and writes it to config. + /// + /// asd. + /// True if success. + bool TryCreateSiteIdentifier(out Guid createdGuid); + + /// + /// Tries to get the site identifier or otherwise create it if it doesn't exist. + /// + /// The out parameter for the existing or create site identifier. + /// True if success. + bool TryGetOrCreateSiteIdentifier(out Guid siteIdentifier); + } +} diff --git a/src/Umbraco.Core/Telemetry/SiteIdentifierService.cs b/src/Umbraco.Core/Telemetry/SiteIdentifierService.cs new file mode 100644 index 0000000000..b6e40665c1 --- /dev/null +++ b/src/Umbraco.Core/Telemetry/SiteIdentifierService.cs @@ -0,0 +1,81 @@ +using System; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration; +using Umbraco.Cms.Core.Configuration.Models; + +namespace Umbraco.Cms.Core.Telemetry +{ + /// + internal class SiteIdentifierService : ISiteIdentifierService + { + private GlobalSettings _globalSettings; + private readonly IConfigManipulator _configManipulator; + private readonly ILogger _logger; + + public SiteIdentifierService( + IOptionsMonitor optionsMonitor, + IConfigManipulator configManipulator, + ILogger logger) + { + _globalSettings = optionsMonitor.CurrentValue; + optionsMonitor.OnChange(globalSettings => _globalSettings = globalSettings); + _configManipulator = configManipulator; + _logger = logger; + } + + /// + public bool TryGetSiteIdentifier(out Guid siteIdentifier) + { + // Parse telemetry string as a GUID & verify its a GUID and not some random string + // since users may have messed with or decided to empty the app setting or put in something random + if (Guid.TryParse(_globalSettings.Id, out var parsedTelemetryId) is false + || parsedTelemetryId == Guid.Empty) + { + siteIdentifier = Guid.Empty; + return false; + } + + siteIdentifier = parsedTelemetryId; + return true; + } + + /// + public bool TryGetOrCreateSiteIdentifier(out Guid siteIdentifier) + { + if (TryGetSiteIdentifier(out Guid existingId)) + { + siteIdentifier = existingId; + return true; + } + + if (TryCreateSiteIdentifier(out Guid createdId)) + { + siteIdentifier = createdId; + return true; + } + + siteIdentifier = Guid.Empty; + return false; + } + + /// + public bool TryCreateSiteIdentifier(out Guid createdGuid) + { + createdGuid = Guid.NewGuid(); + + try + { + _configManipulator.SetGlobalId(createdGuid.ToString()); + } + catch (Exception ex) + { + _logger.LogError(ex, "Couldn't update config files with a telemetry site identifier"); + createdGuid = Guid.Empty; + return false; + } + + return true; + } + } +} diff --git a/src/Umbraco.Core/Telemetry/TelemetryService.cs b/src/Umbraco.Core/Telemetry/TelemetryService.cs index 7927772e0c..ce3696fca4 100644 --- a/src/Umbraco.Core/Telemetry/TelemetryService.cs +++ b/src/Umbraco.Core/Telemetry/TelemetryService.cs @@ -3,9 +3,7 @@ using System; using System.Collections.Generic; -using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration; -using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Manifest; using Umbraco.Cms.Core.Telemetry.Models; using Umbraco.Extensions; @@ -15,27 +13,27 @@ namespace Umbraco.Cms.Core.Telemetry /// internal class TelemetryService : ITelemetryService { - private readonly IOptionsMonitor _globalSettings; private readonly IManifestParser _manifestParser; private readonly IUmbracoVersion _umbracoVersion; + private readonly ISiteIdentifierService _siteIdentifierService; /// /// Initializes a new instance of the class. /// public TelemetryService( - IOptionsMonitor globalSettings, IManifestParser manifestParser, - IUmbracoVersion umbracoVersion) + IUmbracoVersion umbracoVersion, + ISiteIdentifierService siteIdentifierService) { _manifestParser = manifestParser; _umbracoVersion = umbracoVersion; - _globalSettings = globalSettings; + _siteIdentifierService = siteIdentifierService; } /// public bool TryGetTelemetryReportData(out TelemetryReportData? telemetryReportData) { - if (TryGetTelemetryId(out Guid telemetryId) is false) + if (_siteIdentifierService.TryGetOrCreateSiteIdentifier(out Guid telemetryId) is false) { telemetryReportData = null; return false; @@ -45,28 +43,14 @@ namespace Umbraco.Cms.Core.Telemetry { Id = telemetryId, Version = _umbracoVersion.SemanticVersion.ToSemanticStringWithoutBuild(), - Packages = GetPackageTelemetry() + Packages = GetPackageTelemetry(), }; return true; } - private bool TryGetTelemetryId(out Guid telemetryId) - { - // Parse telemetry string as a GUID & verify its a GUID and not some random string - // since users may have messed with or decided to empty the app setting or put in something random - if (Guid.TryParse(_globalSettings.CurrentValue.Id, out Guid parsedTelemetryId) is false) - { - telemetryId = Guid.Empty; - return false; - } - - telemetryId = parsedTelemetryId; - return true; - } - private IEnumerable GetPackageTelemetry() { - List packages = new (); + List packages = new(); IEnumerable manifests = _manifestParser.GetManifests(); foreach (PackageManifest manifest in manifests) @@ -79,7 +63,7 @@ namespace Umbraco.Cms.Core.Telemetry packages.Add(new PackageTelemetry { Name = manifest.PackageName, - Version = manifest.Version ?? string.Empty + Version = manifest.Version ?? string.Empty, }); } diff --git a/src/Umbraco.Examine.Lucene/LuceneIndexDiagnostics.cs b/src/Umbraco.Examine.Lucene/LuceneIndexDiagnostics.cs index ba3945c6e5..33230a296d 100644 --- a/src/Umbraco.Examine.Lucene/LuceneIndexDiagnostics.cs +++ b/src/Umbraco.Examine.Lucene/LuceneIndexDiagnostics.cs @@ -27,7 +27,11 @@ namespace Umbraco.Cms.Infrastructure.Examine IOptionsMonitor indexOptions) { _hostingEnvironment = hostingEnvironment; - _indexOptions = indexOptions.Get(index.Name); + if (indexOptions != null) + { + _indexOptions = indexOptions.Get(index.Name); + + } Index = index; Logger = logger; } @@ -35,7 +39,7 @@ namespace Umbraco.Cms.Infrastructure.Examine public LuceneIndex Index { get; } public ILogger Logger { get; } - + public Attempt IsHealthy() { @@ -72,12 +76,12 @@ namespace Umbraco.Cms.Infrastructure.Examine { d[nameof(LuceneDirectoryIndexOptions.DirectoryFactory)] = _indexOptions.DirectoryFactory.GetType(); } - + if (_indexOptions.IndexDeletionPolicy != null) { d[nameof(LuceneDirectoryIndexOptions.IndexDeletionPolicy)] = _indexOptions.IndexDeletionPolicy.GetType(); - } - + } + } return d; diff --git a/src/Umbraco.Infrastructure/Configuration/JsonConfigManipulator.cs b/src/Umbraco.Infrastructure/Configuration/JsonConfigManipulator.cs index 0bec8bc580..48c0540c67 100644 --- a/src/Umbraco.Infrastructure/Configuration/JsonConfigManipulator.cs +++ b/src/Umbraco.Infrastructure/Configuration/JsonConfigManipulator.cs @@ -2,26 +2,46 @@ using System; using System.IO; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration.Json; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using Umbraco.Cms.Core.Configuration; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Web.Common.DependencyInjection; namespace Umbraco.Cms.Core.Configuration { public class JsonConfigManipulator : IConfigManipulator { private readonly IConfiguration _configuration; + private readonly ILogger _logger; private readonly object _locker = new object(); - public JsonConfigManipulator(IConfiguration configuration) => _configuration = configuration; + [Obsolete] + public JsonConfigManipulator(IConfiguration configuration) + : this(configuration, StaticServiceProvider.Instance.GetRequiredService>()) + { } - public string UmbracoConnectionPath { get; } = $"ConnectionStrings:{ Cms.Core.Constants.System.UmbracoConnectionName}"; + public JsonConfigManipulator( + IConfiguration configuration, + ILogger logger) + { + _configuration = configuration; + _logger = logger; + } + + public string UmbracoConnectionPath { get; } = $"ConnectionStrings:{Cms.Core.Constants.System.UmbracoConnectionName}"; public void RemoveConnectionString() { var provider = GetJsonConfigurationProvider(UmbracoConnectionPath); var json = GetJson(provider); + if (json is null) + { + _logger.LogWarning("Failed to remove connection string from JSON configuration."); + return; + } RemoveJsonKey(json, UmbracoConnectionPath); @@ -33,6 +53,11 @@ namespace Umbraco.Cms.Core.Configuration var provider = GetJsonConfigurationProvider(); var json = GetJson(provider); + if (json is null) + { + _logger.LogWarning("Failed to save connection string in JSON configuration."); + return; + } var item = GetConnectionItem(connectionString, providerName); @@ -50,6 +75,11 @@ namespace Umbraco.Cms.Core.Configuration var provider = GetJsonConfigurationProvider(); var json = GetJson(provider); + if (json is null) + { + _logger.LogWarning("Failed to save configuration key \"{Key}\" in JSON configuration.", key); + return; + } JToken? token = json; foreach (var propertyName in key.Split(new[] { ':' })) @@ -79,6 +109,11 @@ namespace Umbraco.Cms.Core.Configuration var provider = GetJsonConfigurationProvider(); var json = GetJson(provider); + if (json is null) + { + _logger.LogWarning("Failed to save enabled/disabled state for redirect URL tracking in JSON configuration."); + return; + } var item = GetDisableRedirectUrlItem(disable); @@ -95,6 +130,11 @@ namespace Umbraco.Cms.Core.Configuration var provider = GetJsonConfigurationProvider(); var json = GetJson(provider); + if (json is null) + { + _logger.LogWarning("Failed to save global identifier in JSON configuration."); + return; + } var item = GetGlobalIdItem(id); @@ -155,8 +195,10 @@ namespace Umbraco.Cms.Core.Configuration writer.WriteStartObject(); writer.WritePropertyName("ConnectionStrings"); writer.WriteStartObject(); - writer.WritePropertyName(Cms.Core.Constants.System.UmbracoConnectionName); + writer.WritePropertyName(Constants.System.UmbracoConnectionName); writer.WriteValue(connectionString); + writer.WritePropertyName($"{Constants.System.UmbracoConnectionName}{ConnectionStrings.ProviderNamePostfix}"); + writer.WriteValue(providerName); writer.WriteEndObject(); writer.WriteEndObject(); @@ -182,13 +224,20 @@ namespace Umbraco.Cms.Core.Configuration { var jsonFilePath = Path.Combine(physicalFileProvider.Root, provider.Source.Path); - using (var sw = new StreamWriter(jsonFilePath, false)) - using (var jsonTextWriter = new JsonTextWriter(sw) + try { - Formatting = Formatting.Indented, - }) + using (var sw = new StreamWriter(jsonFilePath, false)) + using (var jsonTextWriter = new JsonTextWriter(sw) + { + Formatting = Formatting.Indented, + }) + { + json?.WriteTo(jsonTextWriter); + } + } + catch (IOException exception) { - json?.WriteTo(jsonTextWriter); + _logger.LogWarning(exception, "JSON configuration could not be written: {path}", jsonFilePath); } } } @@ -198,19 +247,25 @@ namespace Umbraco.Cms.Core.Configuration { lock (_locker) { - if (provider.Source.FileProvider is PhysicalFileProvider physicalFileProvider) + if (provider.Source.FileProvider is not PhysicalFileProvider physicalFileProvider) { - var jsonFilePath = Path.Combine(physicalFileProvider.Root, provider.Source.Path); - - var serializer = new JsonSerializer(); - using (var sr = new StreamReader(jsonFilePath)) - using (var jsonTextReader = new JsonTextReader(sr)) - { - return serializer.Deserialize(jsonTextReader); - } + return null; } - return null; + var jsonFilePath = Path.Combine(physicalFileProvider.Root, provider.Source.Path); + + try + { + var serializer = new JsonSerializer(); + using var sr = new StreamReader(jsonFilePath); + using var jsonTextReader = new JsonTextReader(sr); + return serializer.Deserialize(jsonTextReader); + } + catch (IOException exception) + { + _logger.LogWarning(exception, "JSON configuration could not be read: {path}", jsonFilePath); + return null; + } } } diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs index 5a4faa6c43..17ca813b3f 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs @@ -9,6 +9,7 @@ using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.DistributedLocking; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Handlers; using Umbraco.Cms.Core.HealthChecks.NotificationMethods; @@ -36,6 +37,7 @@ using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Core.Templates; using Umbraco.Cms.Core.Trees; using Umbraco.Cms.Core.Web; +using Umbraco.Cms.Infrastructure.DistributedLocking; using Umbraco.Cms.Infrastructure.Examine; using Umbraco.Cms.Infrastructure.HealthChecks; using Umbraco.Cms.Infrastructure.HostedServices; @@ -68,6 +70,7 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection .AddMainDom() .AddLogging(); + builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(factory => factory.GetRequiredService().SqlContext); builder.NPocoMappers().Add(); @@ -220,6 +223,7 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection private static IUmbracoBuilder AddMainDom(this IUmbracoBuilder builder) { + builder.Services.AddSingleton(); builder.Services.AddSingleton(factory => { var globalSettings = factory.GetRequiredService>(); @@ -228,21 +232,29 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection var dbCreator = factory.GetRequiredService(); var databaseSchemaCreatorFactory = factory.GetRequiredService(); - var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); var loggerFactory = factory.GetRequiredService(); var npocoMappers = factory.GetRequiredService(); + var mainDomKeyGenerator = factory.GetRequiredService(); - return globalSettings.Value.MainDomLock.Equals("SqlMainDomLock") || isWindows == false - ? (IMainDomLock)new SqlMainDomLock( - loggerFactory.CreateLogger(), + switch (globalSettings.Value.MainDomLock) + { + case "SqlMainDomLock": + return new SqlMainDomLock( loggerFactory, globalSettings, connectionStrings, dbCreator, - hostingEnvironment, + mainDomKeyGenerator, databaseSchemaCreatorFactory, - npocoMappers) - : new MainDomSemaphoreLock(loggerFactory.CreateLogger(), hostingEnvironment); + npocoMappers); + + case "MainDomSemaphoreLock": + return new MainDomSemaphoreLock(loggerFactory.CreateLogger(), hostingEnvironment); + + case "FileSystemMainDomLock": + default: + return new FileSystemMainDomLock(loggerFactory.CreateLogger(), mainDomKeyGenerator, hostingEnvironment, factory.GetRequiredService>()); + } }); return builder; diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.FileSystems.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.FileSystems.cs index ccb515182e..fbb32671a1 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.FileSystems.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.FileSystems.cs @@ -1,3 +1,4 @@ +using System.IO; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -49,7 +50,7 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection ILogger logger = factory.GetRequiredService>(); GlobalSettings globalSettings = factory.GetRequiredService>().Value; - var rootPath = hostingEnvironment.MapPathWebRoot(globalSettings.UmbracoMediaPhysicalRootPath); + var rootPath = Path.IsPathRooted(globalSettings.UmbracoMediaPhysicalRootPath) ? globalSettings.UmbracoMediaPhysicalRootPath : hostingEnvironment.MapPathWebRoot(globalSettings.UmbracoMediaPhysicalRootPath); var rootUrl = hostingEnvironment.ToAbsolute(globalSettings.UmbracoMediaPath); return new PhysicalFileSystem(ioHelper, hostingEnvironment, logger, rootPath, rootUrl); }); diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Installer.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Installer.cs index e0958bfdb7..d750eb15e0 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Installer.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Installer.cs @@ -1,7 +1,10 @@ using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Install.InstallSteps; using Umbraco.Cms.Core.Install.Models; +using Umbraco.Cms.Core.Telemetry; using Umbraco.Cms.Infrastructure.Install; using Umbraco.Cms.Infrastructure.Install.InstallSteps; using Umbraco.Extensions; @@ -19,7 +22,12 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); - builder.Services.AddScoped(); + builder.Services.AddScoped(provider => + { + return new TelemetryIdentifierStep( + provider.GetRequiredService>(), + provider.GetRequiredService()); + }); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs index 511c09304d..734fcb5661 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs @@ -48,6 +48,7 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection builder.Services.AddUnique(); builder.Services.AddUnique(); builder.Services.AddUnique(); + builder.Services.AddUnique(); builder.Services.AddUnique(); builder.Services.AddUnique(); builder.Services.AddUnique(); diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs index 80d4dd9b3f..7c72b1ddce 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs @@ -42,10 +42,6 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection builder.Services.AddUnique(); builder.Services.AddUnique(); builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); builder.Services.AddUnique(); builder.Services.AddTransient(SourcesFactory); builder.Services.AddUnique(factory => CreatePackageRepository(factory, "createdPackages.config")); @@ -84,7 +80,8 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection var pluginLangFolders = appPlugins.Exists == false ? Enumerable.Empty() : appPlugins.GetDirectories() - .SelectMany(x => x.GetDirectories("Lang", SearchOption.AllDirectories)) + // Check for both Lang & lang to support case sensitive file systems. + .SelectMany(x => x.GetDirectories("?ang", SearchOption.AllDirectories).Where(x => x.Name.InvariantEquals("lang"))) .SelectMany(x => x.GetFiles("*.xml", SearchOption.TopDirectoryOnly)) .Select(x => new LocalizedTextServiceSupplementaryFileSource(x, false)); diff --git a/src/Umbraco.Infrastructure/DistributedLocking/DefaultDistributedLockingMechanismFactory.cs b/src/Umbraco.Infrastructure/DistributedLocking/DefaultDistributedLockingMechanismFactory.cs new file mode 100644 index 0000000000..401e399441 --- /dev/null +++ b/src/Umbraco.Infrastructure/DistributedLocking/DefaultDistributedLockingMechanismFactory.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.DistributedLocking; + +namespace Umbraco.Cms.Infrastructure.DistributedLocking; + +public class DefaultDistributedLockingMechanismFactory : IDistributedLockingMechanismFactory +{ + private object _lock = new(); + private bool _initialized; + private IDistributedLockingMechanism _distributedLockingMechanism; + + private readonly IOptionsMonitor _globalSettings; + private readonly IEnumerable _distributedLockingMechanisms; + + public DefaultDistributedLockingMechanismFactory( + IOptionsMonitor globalSettings, + IEnumerable distributedLockingMechanisms) + { + _globalSettings = globalSettings; + _distributedLockingMechanisms = distributedLockingMechanisms; + } + + public IDistributedLockingMechanism DistributedLockingMechanism + { + get + { + EnsureInitialized(); + + return _distributedLockingMechanism; + } + } + + private void EnsureInitialized() + => LazyInitializer.EnsureInitialized(ref _distributedLockingMechanism, ref _initialized, ref _lock, Initialize); + + private IDistributedLockingMechanism Initialize() + { + var configured = _globalSettings.CurrentValue.DistributedLockingMechanism; + + if (!string.IsNullOrEmpty(configured)) + { + IDistributedLockingMechanism value = _distributedLockingMechanisms + .FirstOrDefault(x => x.GetType().FullName?.EndsWith(configured) ?? false); + + if (value == null) + { + throw new InvalidOperationException($"Couldn't find DistributedLockingMechanism specified by global config: {configured}"); + } + } + + IDistributedLockingMechanism defaultMechanism = _distributedLockingMechanisms.FirstOrDefault(x => x.Enabled); + if (defaultMechanism != null) + { + return defaultMechanism; + } + + throw new InvalidOperationException($"Couldn't find an appropriate default distributed locking mechanism."); + } +} diff --git a/src/Umbraco.Infrastructure/Events/RelateOnTrashNotificationHandler.cs b/src/Umbraco.Infrastructure/Events/RelateOnTrashNotificationHandler.cs index 4222ac800e..b06248c79e 100644 --- a/src/Umbraco.Infrastructure/Events/RelateOnTrashNotificationHandler.cs +++ b/src/Umbraco.Infrastructure/Events/RelateOnTrashNotificationHandler.cs @@ -65,7 +65,7 @@ namespace Umbraco.Cms.Core.Events var documentObjectType = Constants.ObjectTypes.Document; const string relationTypeName = Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteName; - relationType = new RelationType(relationTypeName, relationTypeAlias, false, documentObjectType, documentObjectType); + relationType = new RelationType(relationTypeName, relationTypeAlias, false, documentObjectType, documentObjectType, false); _relationService.Save(relationType); } @@ -123,7 +123,7 @@ namespace Umbraco.Cms.Core.Events { var documentObjectType = Constants.ObjectTypes.Document; const string relationTypeName = Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteName; - relationType = new RelationType(relationTypeName, relationTypeAlias, false, documentObjectType, documentObjectType); + relationType = new RelationType(relationTypeName, relationTypeAlias, false, documentObjectType, documentObjectType, false); _relationService.Save(relationType); } diff --git a/src/Umbraco.Infrastructure/HostedServices/KeepAlive.cs b/src/Umbraco.Infrastructure/HostedServices/KeepAlive.cs index 9f0a82eb78..550cb56674 100644 --- a/src/Umbraco.Infrastructure/HostedServices/KeepAlive.cs +++ b/src/Umbraco.Infrastructure/HostedServices/KeepAlive.cs @@ -7,6 +7,7 @@ using System.Net.Http; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Logging; @@ -101,7 +102,7 @@ namespace Umbraco.Cms.Infrastructure.HostedServices try { var request = new HttpRequestMessage(HttpMethod.Get, keepAlivePingUrl); - HttpClient httpClient = _httpClientFactory.CreateClient(); + HttpClient httpClient = _httpClientFactory.CreateClient(Constants.HttpClients.IgnoreCertificateErrors); _ = await httpClient.SendAsync(request); } catch (Exception ex) diff --git a/src/Umbraco.Infrastructure/HostedServices/RecurringHostedServiceBase.cs b/src/Umbraco.Infrastructure/HostedServices/RecurringHostedServiceBase.cs index ad21c2982b..34b13c6e5a 100644 --- a/src/Umbraco.Infrastructure/HostedServices/RecurringHostedServiceBase.cs +++ b/src/Umbraco.Infrastructure/HostedServices/RecurringHostedServiceBase.cs @@ -24,6 +24,7 @@ namespace Umbraco.Cms.Infrastructure.HostedServices private TimeSpan _period; private readonly TimeSpan _delay; private Timer? _timer; + private bool _disposedValue; /// /// Initializes a new instance of the class. @@ -84,7 +85,24 @@ namespace Umbraco.Cms.Infrastructure.HostedServices return Task.CompletedTask; } + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + _timer?.Dispose(); + } + + _disposedValue = true; + } + } + /// - public void Dispose() => _timer?.Dispose(); + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } } } diff --git a/src/Umbraco.Infrastructure/HostedServices/ReportSiteTask.cs b/src/Umbraco.Infrastructure/HostedServices/ReportSiteTask.cs index 753dc542b7..431409a240 100644 --- a/src/Umbraco.Infrastructure/HostedServices/ReportSiteTask.cs +++ b/src/Umbraco.Infrastructure/HostedServices/ReportSiteTask.cs @@ -59,9 +59,6 @@ namespace Umbraco.Cms.Infrastructure.HostedServices // Send data to LIVE telemetry s_httpClient.BaseAddress = new Uri("https://telemetry.umbraco.com/"); - // Set a low timeout - no need to use a larger default timeout for this POST request - s_httpClient.Timeout = new TimeSpan(0, 0, 1); - #if DEBUG // Send data to DEBUG telemetry service s_httpClient.BaseAddress = new Uri("https://telemetry.rainbowsrock.net/"); diff --git a/src/Umbraco.Infrastructure/HostedServices/ScheduledPublishing.cs b/src/Umbraco.Infrastructure/HostedServices/ScheduledPublishing.cs index d806bab5ad..7b8c87ceaa 100644 --- a/src/Umbraco.Infrastructure/HostedServices/ScheduledPublishing.cs +++ b/src/Umbraco.Infrastructure/HostedServices/ScheduledPublishing.cs @@ -5,13 +5,15 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Runtime; -using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Sync; using Umbraco.Cms.Core.Web; +using Umbraco.Cms.Web.Common.DependencyInjection; namespace Umbraco.Cms.Infrastructure.HostedServices { @@ -27,20 +29,13 @@ namespace Umbraco.Cms.Infrastructure.HostedServices private readonly IMainDom _mainDom; private readonly IRuntimeState _runtimeState; private readonly IServerMessenger _serverMessenger; + private readonly IScopeProvider _scopeProvider; private readonly IServerRoleAccessor _serverRegistrar; private readonly IUmbracoContextFactory _umbracoContextFactory; - /// + /// /// Initializes a new instance of the class. /// - /// Representation of the state of the Umbraco runtime. - /// Representation of the main application domain. - /// Provider of server registrations to the distributed cache. - /// Service for handling content operations. - /// Service for creating and managing Umbraco context. - /// The typed logger. - /// Service broadcasting cache notifications to registered servers. - /// Creates and manages instances. public ScheduledPublishing( IRuntimeState runtimeState, IMainDom mainDom, @@ -48,7 +43,8 @@ namespace Umbraco.Cms.Infrastructure.HostedServices IContentService contentService, IUmbracoContextFactory umbracoContextFactory, ILogger logger, - IServerMessenger serverMessenger) + IServerMessenger serverMessenger, + IScopeProvider scopeProvider) : base(TimeSpan.FromMinutes(1), DefaultDelay) { _runtimeState = runtimeState; @@ -58,6 +54,7 @@ namespace Umbraco.Cms.Infrastructure.HostedServices _umbracoContextFactory = umbracoContextFactory; _logger = logger; _serverMessenger = serverMessenger; + _scopeProvider = scopeProvider; } public override Task PerformExecuteAsync(object? state) @@ -93,8 +90,6 @@ namespace Umbraco.Cms.Infrastructure.HostedServices try { - // We don't need an explicit scope here because PerformScheduledPublish creates it's own scope - // so it's safe as it will create it's own ambient scope. // Ensure we run with an UmbracoContext, because this will run in a background task, // and developers may be using the UmbracoContext in the event handlers. @@ -105,6 +100,14 @@ namespace Umbraco.Cms.Infrastructure.HostedServices // - and we should definitively *not* have to flush it here (should be auto) using UmbracoContextReference contextReference = _umbracoContextFactory.EnsureUmbracoContext(); + using IScope scope = _scopeProvider.CreateScope(autoComplete: true); + + /* We used to assume that there will never be two instances running concurrently where (IsMainDom && ServerRole == SchedulingPublisher) + * However this is possible during an azure deployment slot swap for the SchedulingPublisher instance when trying to achieve zero downtime deployments. + * If we take a distributed write lock, we are certain that the multiple instances of the job will not run in parallel. + * It's possible that during the swapping process we may run this job more frequently than intended but this is not of great concern and it's + * only until the old SchedulingPublisher shuts down. */ + scope.EagerWriteLock(Constants.Locks.ScheduledPublishing); try { // Run diff --git a/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/InstructionProcessTask.cs b/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/InstructionProcessTask.cs index 54214ba08c..77a7e0226f 100644 --- a/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/InstructionProcessTask.cs +++ b/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/InstructionProcessTask.cs @@ -20,6 +20,7 @@ namespace Umbraco.Cms.Infrastructure.HostedServices.ServerRegistration private readonly IRuntimeState _runtimeState; private readonly IServerMessenger _messenger; private readonly ILogger _logger; + private bool _disposedValue; /// /// Initializes a new instance of the class. @@ -54,5 +55,20 @@ namespace Umbraco.Cms.Infrastructure.HostedServices.ServerRegistration return Task.CompletedTask; } + + protected override void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing && _messenger is IDisposable disposable) + { + disposable.Dispose(); + } + + _disposedValue = true; + } + + base.Dispose(disposing); + } } } diff --git a/src/Umbraco.Infrastructure/IPublishedContentQuery.cs b/src/Umbraco.Infrastructure/IPublishedContentQuery.cs index d5f41c0a02..b244cb93de 100644 --- a/src/Umbraco.Infrastructure/IPublishedContentQuery.cs +++ b/src/Umbraco.Infrastructure/IPublishedContentQuery.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Xml.XPath; using Examine.Search; @@ -8,31 +8,46 @@ using Umbraco.Cms.Core.Xml; namespace Umbraco.Cms.Core { /// - /// Query methods used for accessing strongly typed content in templates + /// Query methods used for accessing strongly typed content in templates. /// public interface IPublishedContentQuery { IPublishedContent? Content(int id); + IPublishedContent Content(Guid id); + IPublishedContent? Content(Udi id); + IPublishedContent? Content(object id); + IPublishedContent ContentSingleAtXPath(string xpath, params XPathVariable[] vars); + IEnumerable Content(IEnumerable ids); + IEnumerable Content(IEnumerable ids); IEnumerable Content(IEnumerable ids); + IEnumerable ContentAtXPath(string xpath, params XPathVariable[] vars); + IEnumerable ContentAtXPath(XPathExpression xpath, params XPathVariable[] vars); + IEnumerable ContentAtRoot(); IPublishedContent? Media(int id); + IPublishedContent Media(Guid id); + IPublishedContent? Media(Udi id); IPublishedContent? Media(object id); + IEnumerable Media(IEnumerable ids); + IEnumerable Media(IEnumerable ids); + IEnumerable Media(IEnumerable ids); + IEnumerable MediaAtRoot(); /// @@ -44,7 +59,7 @@ namespace Umbraco.Cms.Core /// The total amount of records. /// The culture (defaults to a culture insensitive search). /// The name of the index to search (defaults to ). - /// The fields to load in the results of the search (defaults to all fields loaded). + /// This parameter is no longer used, because the results are loaded from the published snapshot using the single item ID field. /// /// The search results. /// diff --git a/src/Umbraco.Infrastructure/Install/InstallHelper.cs b/src/Umbraco.Infrastructure/Install/InstallHelper.cs index cd7dfcbf0c..671dc85c4f 100644 --- a/src/Umbraco.Infrastructure/Install/InstallHelper.cs +++ b/src/Umbraco.Infrastructure/Install/InstallHelper.cs @@ -95,9 +95,10 @@ namespace Umbraco.Cms.Infrastructure.Install /// /// true if this is a brand new install; otherwise, false. /// - private bool IsBrandNewInstall => _connectionStrings.CurrentValue.UmbracoConnectionString?.IsConnectionStringConfigured() != true || - _databaseBuilder.IsDatabaseConfigured == false || - _databaseBuilder.CanConnectToDatabase == false || - _databaseBuilder.IsUmbracoInstalled() == false; + private bool IsBrandNewInstall => + _connectionStrings.Get(Constants.System.UmbracoConnectionName).IsConnectionStringConfigured() == false || + _databaseBuilder.IsDatabaseConfigured == false || + _databaseBuilder.CanConnectToDatabase == false || + _databaseBuilder.IsUmbracoInstalled() == false; } } diff --git a/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseConfigureStep.cs b/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseConfigureStep.cs index d211253ae2..44879f31b8 100644 --- a/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseConfigureStep.cs +++ b/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseConfigureStep.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using System.Runtime.InteropServices; +using System.Linq; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -20,116 +20,55 @@ namespace Umbraco.Cms.Infrastructure.Install.InstallSteps { private readonly DatabaseBuilder _databaseBuilder; private readonly ILogger _logger; + private readonly IEnumerable _databaseProviderMetadata; private readonly IOptionsMonitor _connectionStrings; - public DatabaseConfigureStep(DatabaseBuilder databaseBuilder, IOptionsMonitor connectionStrings, ILogger logger) + public DatabaseConfigureStep( + DatabaseBuilder databaseBuilder, + IOptionsMonitor connectionStrings, + ILogger logger, + IEnumerable databaseProviderMetadata) { _databaseBuilder = databaseBuilder; _connectionStrings = connectionStrings; _logger = logger; + _databaseProviderMetadata = databaseProviderMetadata; } - public override Task ExecuteAsync(DatabaseModel database) + public override Task ExecuteAsync(DatabaseModel databaseSettings) { - //if the database model is null then we will apply the defaults - if (database == null) - { - database = new DatabaseModel(); - - if (IsLocalDbAvailable()) - { - database.DatabaseType = DatabaseType.SqlLocalDb; - } - else if (IsSqlCeAvailable()) - { - database.DatabaseType = DatabaseType.SqlCe; - } - } - - if (_databaseBuilder.CanConnect(database.DatabaseType.ToString(), database.ConnectionString, database.Server, database.DatabaseName, database.Login, database.Password, database.IntegratedAuth) == false) + if (!_databaseBuilder.ConfigureDatabaseConnection(databaseSettings, isTrialRun: false)) { throw new InstallException("Could not connect to the database"); } - ConfigureConnection(database); - return Task.FromResult(null); } - private void ConfigureConnection(DatabaseModel database) - { - if (database.ConnectionString.IsNullOrWhiteSpace() == false) - { - _databaseBuilder.ConfigureDatabaseConnection(database.ConnectionString!); - } - else if (database.DatabaseType == DatabaseType.SqlLocalDb) - { - _databaseBuilder.ConfigureSqlLocalDbDatabaseConnection(); - } - else if (database.DatabaseType == DatabaseType.SqlCe) - { - _databaseBuilder.ConfigureEmbeddedDatabaseConnection(); - } - else if (database.IntegratedAuth) - { - _databaseBuilder.ConfigureIntegratedSecurityDatabaseConnection(database.Server, database.DatabaseName); - } - else - { - var password = database.Password?.Replace("'", "''"); - password = string.Format("'{0}'", password); - - _databaseBuilder.ConfigureDatabaseConnection(database.Server, database.DatabaseName, database.Login, password, database.DatabaseType.ToString()); - } - } - public override object ViewModel { get { - var databases = new List() - { - new { name = "Microsoft SQL Server", id = DatabaseType.SqlServer.ToString() }, - new { name = "Microsoft SQL Azure", id = DatabaseType.SqlAzure.ToString() }, - new { name = "Custom connection string", id = DatabaseType.Custom.ToString() }, - }; - - if (IsSqlCeAvailable()) - { - databases.Insert(0, new { name = "Microsoft SQL Server Compact (SQL CE)", id = DatabaseType.SqlCe.ToString() }); - } - - if (IsLocalDbAvailable()) - { - // Ensure this is always inserted as first when available - databases.Insert(0, new { name = "Microsoft SQL Server Express (LocalDB)", id = DatabaseType.SqlLocalDb.ToString() }); - } + var options = _databaseProviderMetadata + .Where(x => x.IsAvailable) + .OrderBy(x => x.SortOrder) + .ToList(); return new { - databases + databases = options }; } } - public static bool IsLocalDbAvailable() => new LocalDb().IsAvailable; - - public static bool IsSqlCeAvailable() => - // NOTE: Type.GetType will only return types that are currently loaded into the appdomain. In this case - // that is ok because we know if this is availalbe we will have manually loaded it into the appdomain. - // Else we'd have to use Assembly.LoadFrom and need to know the DLL location here which we don't need to do. - RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && - !(Type.GetType("Umbraco.Cms.Persistence.SqlCe.SqlCeSyntaxProvider, Umbraco.Persistence.SqlCe") is null); - public override string View => ShouldDisplayView() ? base.View : ""; - public override bool RequiresExecution(DatabaseModel model) => ShouldDisplayView(); private bool ShouldDisplayView() { //If the connection string is already present in web.config we don't need to show the settings page and we jump to installing/upgrading. - var databaseSettings = _connectionStrings.CurrentValue.UmbracoConnectionString; + var databaseSettings = _connectionStrings.Get(Core.Constants.System.UmbracoConnectionName); if (databaseSettings.IsConnectionStringConfigured()) { diff --git a/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseUpgradeStep.cs b/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseUpgradeStep.cs index c0e272a6aa..25494ff925 100644 --- a/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseUpgradeStep.cs +++ b/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseUpgradeStep.cs @@ -77,7 +77,7 @@ namespace Umbraco.Cms.Infrastructure.Install.InstallSteps return false; } - var databaseSettings = _connectionStrings.CurrentValue.UmbracoConnectionString; + var databaseSettings = _connectionStrings.Get(Core.Constants.System.UmbracoConnectionName); if (databaseSettings.IsConnectionStringConfigured()) { diff --git a/src/Umbraco.Infrastructure/Install/InstallSteps/NewInstallStep.cs b/src/Umbraco.Infrastructure/Install/InstallSteps/NewInstallStep.cs index 11372c89ae..38ac000452 100644 --- a/src/Umbraco.Infrastructure/Install/InstallSteps/NewInstallStep.cs +++ b/src/Umbraco.Infrastructure/Install/InstallSteps/NewInstallStep.cs @@ -1,5 +1,7 @@ using System; +using System.Collections.Generic; using System.Collections.Specialized; +using System.Linq; using System.Net.Http; using System.Text; using System.Threading.Tasks; @@ -37,6 +39,7 @@ namespace Umbraco.Cms.Infrastructure.Install.InstallSteps private readonly ICookieManager _cookieManager; private readonly IBackOfficeUserManager _userManager; private readonly IDbProviderFactoryCreator _dbProviderFactoryCreator; + private readonly IEnumerable _databaseProviderMetadata; public NewInstallStep( IUserService userService, @@ -47,7 +50,8 @@ namespace Umbraco.Cms.Infrastructure.Install.InstallSteps IOptionsMonitor connectionStrings, ICookieManager cookieManager, IBackOfficeUserManager userManager, - IDbProviderFactoryCreator dbProviderFactoryCreator) + IDbProviderFactoryCreator dbProviderFactoryCreator, + IEnumerable databaseProviderMetadata) { _userService = userService ?? throw new ArgumentNullException(nameof(userService)); _databaseBuilder = databaseBuilder ?? throw new ArgumentNullException(nameof(databaseBuilder)); @@ -58,6 +62,7 @@ namespace Umbraco.Cms.Infrastructure.Install.InstallSteps _cookieManager = cookieManager; _userManager = userManager ?? throw new ArgumentNullException(nameof(userManager)); _dbProviderFactoryCreator = dbProviderFactoryCreator ?? throw new ArgumentNullException(nameof(dbProviderFactoryCreator)); + _databaseProviderMetadata = databaseProviderMetadata; } public override async Task ExecuteAsync(UserModel user) @@ -113,11 +118,22 @@ namespace Umbraco.Cms.Infrastructure.Install.InstallSteps { get { + var quickInstallSettings = _databaseProviderMetadata + .Where(x => x.SupportsQuickInstall) + .Where(x => x.IsAvailable) + .OrderBy(x => x.SortOrder) + .Select(x => new + { + displayName = x.DisplayName, + defaultDatabaseName = x.DefaultDatabaseName, + }) + .FirstOrDefault(); + return new { minCharLength = _passwordConfiguration.RequiredLength, minNonAlphaNumericLength = _passwordConfiguration.GetMinNonAlphaNumericChars(), - quickInstallAvailable = DatabaseConfigureStep.IsSqlCeAvailable() || DatabaseConfigureStep.IsLocalDbAvailable(), + quickInstallSettings, customInstallAvailable = !GetInstallState().HasFlag(InstallState.ConnectionStringConfigured) }; } @@ -139,10 +155,11 @@ namespace Umbraco.Cms.Infrastructure.Install.InstallSteps { var installState = InstallState.Unknown; + // TODO: we need to do a null check here since this could be entirely missing and we end up with a null ref // exception in the installer. - var databaseSettings = _connectionStrings.CurrentValue.UmbracoConnectionString; + var databaseSettings = _connectionStrings.Get(Constants.System.UmbracoConnectionName); var hasConnString = databaseSettings != null && _databaseBuilder.IsDatabaseConfigured; if (hasConnString) diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/ExpressionFilter.cs b/src/Umbraco.Infrastructure/Logging/Viewer/ExpressionFilter.cs index ddf18a12ce..70c05dc2a4 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/ExpressionFilter.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/ExpressionFilter.cs @@ -1,7 +1,8 @@ -using System; +using System; using System.Linq; using Serilog.Events; -using Serilog.Filters.Expressions; +using Serilog.Expressions; +using Umbraco.Cms.Infrastructure.Logging.Viewer; using Umbraco.Extensions; namespace Umbraco.Cms.Core.Logging.Viewer @@ -10,32 +11,43 @@ namespace Umbraco.Cms.Core.Logging.Viewer internal class ExpressionFilter : ILogFilter { private readonly Func? _filter; - private const string ExpressionOperators = "()+=*<>%-"; + private const string s_expressionOperators = "()+=*<>%-"; public ExpressionFilter(string? filterExpression) { Func? filter; + // Our custom Serilog Functions to extend Serilog.Expressions + // In this case we are plugging the gap for the missing Has() + // function from porting away from Serilog.Filters.Expressions to Serilog.Expressions + // Along with patching support for the more verbose built in property names + var customSerilogFunctions = new SerilogLegacyNameResolver(typeof(SerilogExpressionsFunctions)); + if (string.IsNullOrEmpty(filterExpression)) { return; } // If the expression is one word and doesn't contain a serilog operator then we can perform a like search - if (!filterExpression.Contains(" ") && !filterExpression.ContainsAny(ExpressionOperators.Select(c => c))) + if (!filterExpression.Contains(" ") && !filterExpression.ContainsAny(s_expressionOperators.Select(c => c))) { filter = PerformMessageLikeFilter(filterExpression); } else // check if it's a valid expression { // If the expression evaluates then make it into a filter - if (FilterLanguage.TryCreateFilter(filterExpression, out var eval, out _)) + if (SerilogExpression.TryCompile(filterExpression, null, customSerilogFunctions, out CompiledExpression compiled, out var error)) { - filter = evt => true.Equals(eval(evt)); + filter = evt => + { + LogEventPropertyValue result = compiled(evt); + return ExpressionResult.IsTrue(result); + }; } else { - //Assume the expression was a search string and make a Like filter from that + // 'error' describes a syntax error, where it was unable to compile an expression + // Assume the expression was a search string and make a Like filter from that filter = PerformMessageLikeFilter(filterExpression); } } @@ -50,10 +62,15 @@ namespace Umbraco.Cms.Core.Logging.Viewer private Func? PerformMessageLikeFilter(string filterExpression) { - var filterSearch = $"@Message like '%{FilterLanguage.EscapeLikeExpressionContent(filterExpression)}%'"; - if (FilterLanguage.TryCreateFilter(filterSearch, out var eval, out _)) + var filterSearch = $"@Message like '%{SerilogExpression.EscapeLikeExpressionContent(filterExpression)}%'"; + if (SerilogExpression.TryCompile(filterSearch, out CompiledExpression compiled, out var error)) { - return evt => true.Equals(eval(evt)); + // `compiled` is a function that can be executed against `LogEvent`s: + return evt => + { + LogEventPropertyValue result = compiled(evt); + return ExpressionResult.IsTrue(result); + }; } return null; diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/SerilogExpressionsFunctions.cs b/src/Umbraco.Infrastructure/Logging/Viewer/SerilogExpressionsFunctions.cs new file mode 100644 index 0000000000..92b16b9729 --- /dev/null +++ b/src/Umbraco.Infrastructure/Logging/Viewer/SerilogExpressionsFunctions.cs @@ -0,0 +1,14 @@ +using Serilog.Events; + +namespace Umbraco.Cms.Infrastructure.Logging.Viewer +{ + public class SerilogExpressionsFunctions + { + // This Has() code is the same as the renamed IsDefined() function + // Added this to help backport and ensure saved queries continue to work if using Has() + public static LogEventPropertyValue? Has(LogEventPropertyValue? value) + { + return new ScalarValue(value != null); + } + } +} diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/SerilogLegacyNameResolver.cs b/src/Umbraco.Infrastructure/Logging/Viewer/SerilogLegacyNameResolver.cs new file mode 100644 index 0000000000..0472a8ea16 --- /dev/null +++ b/src/Umbraco.Infrastructure/Logging/Viewer/SerilogLegacyNameResolver.cs @@ -0,0 +1,38 @@ +using System; +using Serilog.Expressions; + +namespace Umbraco.Cms.Infrastructure.Logging.Viewer +{ + /// + /// Inherits Serilog's StaticMemberNameResolver to ensure we get same functionality + /// Of easily allowing any static methods definied in the passed in class/type + /// To extend as functions to use for filtering logs such as Has() and any other custom ones + /// + public class SerilogLegacyNameResolver : StaticMemberNameResolver + { + public SerilogLegacyNameResolver(Type type) : base(type) + { + } + + /// + /// Allows us to fix the gap from migrating away from Serilog.Filters.Expressions + /// So we can still support the more verbose built in property names such as + /// Exception, Level, MessageTemplate etc + /// + public override bool TryResolveBuiltInPropertyName(string alias, out string target) + { + target = alias switch + { + "Exception" => "x", + "Level" => "l", + "Message" => "m", + "MessageTemplate" => "mt", + "Properties" => "p", + "Timestamp" => "t", + _ => null + }; + + return target != null; + } + } +} diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/CreateTableOfDtoBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/CreateTableOfDtoBuilder.cs index f0362e42a3..5aac9e90f7 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/CreateTableOfDtoBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/CreateTableOfDtoBuilder.cs @@ -33,15 +33,8 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Table } var tableDefinition = DefinitionFactory.GetTableDefinition(TypeOfDto, syntax); - ExecuteSql(syntax.Format(tableDefinition)); - if (WithoutKeysAndIndexes) - return; - - ExecuteSql(syntax.FormatPrimaryKey(tableDefinition)); - foreach (var sql in syntax.Format(tableDefinition.ForeignKeys)) - ExecuteSql(sql); - foreach (var sql in syntax.Format(tableDefinition.Indexes)) - ExecuteSql(sql); + syntax.HandleCreateTable(_context.Database, tableDefinition, WithoutKeysAndIndexes); + _context.BuildingExpression = false; } private void ExecuteSql(string sql) diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/KeysAndIndexes/DeleteKeysAndIndexesBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/KeysAndIndexes/DeleteKeysAndIndexesBuilder.cs index c7411b3b69..09c6e04fe1 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/KeysAndIndexes/DeleteKeysAndIndexesBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/KeysAndIndexes/DeleteKeysAndIndexesBuilder.cs @@ -6,6 +6,21 @@ using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.KeysAndIndexes { + /// + /// + /// Assuming we stick with the current migrations setup this will need to be altered to + /// delegate to SQL syntax provider (we can drop indexes but not PK/FK). + /// + /// + /// 1. For SQLite, rename table.
+ /// 2. Create new table with expected keys.
+ /// 3. Insert into new from renamed
+ /// 4. Drop renamed.
+ ///
+ /// + /// Read more SQL Features That SQLite Does Not Implement + /// + ///
public class DeleteKeysAndIndexesBuilder : IExecutableBuilder { private readonly IMigrationContext _context; diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs index 30c881511d..cf06d38672 100644 --- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs @@ -1,9 +1,14 @@ using System; +using System.Collections.Generic; +using System.Data.Common; +using System.Linq; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Install; +using Umbraco.Cms.Core.Install.Models; using Umbraco.Cms.Core.Migrations; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; @@ -32,6 +37,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install private readonly IOptionsMonitor _connectionStrings; private readonly IMigrationPlanExecutor _migrationPlanExecutor; private readonly DatabaseSchemaCreatorFactory _databaseSchemaCreatorFactory; + private readonly IEnumerable _databaseProviderMetadata; private DatabaseSchemaResult? _databaseSchemaValidationResult; @@ -50,7 +56,8 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install IOptionsMonitor globalSettings, IOptionsMonitor connectionStrings, IMigrationPlanExecutor migrationPlanExecutor, - DatabaseSchemaCreatorFactory databaseSchemaCreatorFactory) + DatabaseSchemaCreatorFactory databaseSchemaCreatorFactory, + IEnumerable databaseProviderMetadata) { _scopeProvider = scopeProvider; _scopeAccessor = scopeAccessor; @@ -83,32 +90,9 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install /// /// Verifies whether a it is possible to connect to a database. /// - public bool CanConnect(string databaseType, string? connectionString, string server, string database, string login, string password, bool integratedAuth) + public bool CanConnect(string? connectionString, string providerName) { - // we do not test SqlCE or LocalDB connections - if (databaseType.InvariantContains("SqlCe") || databaseType.InvariantContains("SqlLocalDb")) - return true; - - string providerName; - - if (string.IsNullOrWhiteSpace(connectionString) == false) - { - providerName = ConfigConnectionString.ParseProviderName(connectionString)!; - } - else if (integratedAuth) - { - // has to be Sql Server - providerName = Constants.DbProviderNames.SqlServer; - connectionString = GetIntegratedSecurityDatabaseConnectionString(server, database); - } - else - { - connectionString = GetDatabaseConnectionString( - server, database, login, password, - databaseType, out providerName); - } - - var factory = _dbProviderFactoryCreator.CreateFactory(providerName); + DbProviderFactory factory = _dbProviderFactoryCreator.CreateFactory(providerName); return DbConnectionExtensions.IsConnectionAvailable(connectionString, factory); } @@ -147,65 +131,60 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install #region Configure Connection String - public const string EmbeddedDatabaseConnectionString = @"Data Source=|DataDirectory|\Umbraco.sdf;Flush Interval=1"; - - /// - /// Configures a connection string for the embedded database. - /// - public void ConfigureEmbeddedDatabaseConnection() + public bool ConfigureDatabaseConnection(DatabaseModel databaseSettings, bool isTrialRun) { - const string connectionString = EmbeddedDatabaseConnectionString; - const string providerName = Constants.DbProviderNames.SqlCe; + IDatabaseProviderMetadata providerMeta; - _configManipulator.SaveConnectionString(connectionString, providerName); - Configure(connectionString, providerName, true); + // if the database model is null then we will attempt quick install. + if (databaseSettings == null) + { + providerMeta = _databaseProviderMetadata + .OrderBy(x => x.SortOrder) + .Where(x => x.SupportsQuickInstall) + .FirstOrDefault(x => x.IsAvailable); + + databaseSettings = new DatabaseModel + { + DatabaseName = providerMeta?.DefaultDatabaseName, + }; + } + else + { + providerMeta = _databaseProviderMetadata + .FirstOrDefault(x => x.Id == databaseSettings.DatabaseProviderMetadataId); + } + + if (providerMeta == null) + { + throw new InstallException("Unable to determine database provider configuration."); + } + + var connectionString = providerMeta.GenerateConnectionString(databaseSettings); + var providerName = databaseSettings.ProviderName ?? providerMeta.ProviderName; + + if (providerMeta.RequiresConnectionTest && !CanConnect(connectionString, providerName)) + { + return false; + } + + if (!isTrialRun) + { + _configManipulator.SaveConnectionString(connectionString, providerName); + Configure(connectionString, providerName, _globalSettings.CurrentValue.InstallMissingDatabase || providerMeta.ForceCreateDatabase); + } + + return true; } - public const string LocalDbConnectionString = @"Data Source=(localdb)\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\Umbraco.mdf;Integrated Security=True"; - - public void ConfigureSqlLocalDbDatabaseConnection() - { - string connectionString = LocalDbConnectionString; - const string providerName = Constants.DbProviderNames.SqlServer; - - _configManipulator.SaveConnectionString(connectionString, providerName); - Configure(connectionString, providerName, true); - } - - /// - /// Configures a connection string that has been entered manually. - /// - /// A connection string. - /// Has to be SQL Server - public void ConfigureDatabaseConnection(string connectionString) - { - _configManipulator.SaveConnectionString(connectionString, null); - Configure(connectionString, null, _globalSettings.CurrentValue.InstallMissingDatabase); - } - - /// - /// Configures a connection string from the installer. - /// - /// The name or address of the database server. - /// The name of the database. - /// The user name. - /// The user password. - /// The name of the provider (Sql, Sql Azure, Sql Ce). - public void ConfigureDatabaseConnection(string server, string databaseName, string user, string password, string databaseProvider) - { - var connectionString = GetDatabaseConnectionString(server, databaseName, user, password, databaseProvider, out var providerName); - - _configManipulator.SaveConnectionString(connectionString, providerName); - Configure(connectionString, providerName, _globalSettings.CurrentValue.InstallMissingDatabase); - } private void Configure(string connectionString, string? providerName, bool installMissingDatabase) { // Update existing connection string - var umbracoConnectionString = new ConfigConnectionString(Constants.System.UmbracoConnectionName, connectionString, providerName); - _connectionStrings.CurrentValue.UmbracoConnectionString = umbracoConnectionString; + var umbracoConnectionString = _connectionStrings.Get(Core.Constants.System.UmbracoConnectionName); + umbracoConnectionString.ConnectionString = connectionString; + umbracoConnectionString.ProviderName = providerName; - _databaseFactory.Configure(umbracoConnectionString.ConnectionString, umbracoConnectionString.ProviderName); + _databaseFactory.Configure(umbracoConnectionString); if (installMissingDatabase) { @@ -213,102 +192,6 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install } } - /// - /// Gets a connection string from the installer. - /// - /// The name or address of the database server. - /// The name of the database. - /// The user name. - /// The user password. - /// The name of the provider (Sql, Sql Azure, Sql Ce). - /// - /// A connection string. - public static string GetDatabaseConnectionString(string server, string databaseName, string user, string password, string databaseProvider, out string providerName) - { - providerName = Constants.DbProviderNames.SqlServer; - - if (databaseProvider.InvariantContains("Azure")) - return GetAzureConnectionString(server, databaseName, user, password); - - return $"server={server};database={databaseName};user id={user};password={password}"; - } - - /// - /// Configures a connection string using Microsoft SQL Server integrated security. - /// - /// The name or address of the database server. - /// The name of the database - public void ConfigureIntegratedSecurityDatabaseConnection(string server, string databaseName) - { - var connectionString = GetIntegratedSecurityDatabaseConnectionString(server, databaseName); - const string providerName = Constants.DbProviderNames.SqlServer; - - _configManipulator.SaveConnectionString(connectionString, providerName); - _databaseFactory.Configure(connectionString, providerName); - } - - /// - /// Gets a connection string using Microsoft SQL Server integrated security. - /// - /// The name or address of the database server. - /// The name of the database - /// A connection string. - public static string GetIntegratedSecurityDatabaseConnectionString(string server, string databaseName) - { - return $"Server={server};Database={databaseName};Integrated Security=true"; - } - - /// - /// Gets an Azure connection string. - /// - /// The name or address of the database server. - /// The name of the database. - /// The user name. - /// The user password. - /// A connection string. - public static string GetAzureConnectionString(string server, string databaseName, string user, string password) - { - if (server.Contains(".") && ServerStartsWithTcp(server) == false) - server = $"tcp:{server}"; - - if (server.Contains(".") == false && ServerStartsWithTcp(server)) - { - string serverName = server.Contains(",") - ? server.Substring(0, server.IndexOf(",", StringComparison.Ordinal)) - : server; - - var portAddition = string.Empty; - - if (server.Contains(",")) - portAddition = server.Substring(server.IndexOf(",", StringComparison.Ordinal)); - - server = $"{serverName}.database.windows.net{portAddition}"; - } - - if (ServerStartsWithTcp(server) == false) - server = $"tcp:{server}.database.windows.net"; - - if (server.Contains(",") == false) - server = $"{server},1433"; - - if (user.Contains("@") == false) - { - var userDomain = server; - - if (ServerStartsWithTcp(server)) - userDomain = userDomain.Substring(userDomain.IndexOf(":", StringComparison.Ordinal) + 1); - - if (userDomain.Contains(".")) - userDomain = userDomain.Substring(0, userDomain.IndexOf(".", StringComparison.Ordinal)); - - user = $"{user}@{userDomain}"; - } - - return $"Server={server};Database={databaseName};User ID={user};Password={password}"; - } - - private static bool ServerStartsWithTcp(string server) => server.InvariantStartsWith("tcp:"); - #endregion #region Database Schema diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs index fc6f669f7d..fb4ac3ec7e 100644 --- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs @@ -79,7 +79,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install if (tableName.Equals(Cms.Core.Constants.DatabaseSchema.Tables.LogViewerQuery)) CreateLogViewerQueryData(); - _logger.LogInformation("Done creating table {TableName} data.", tableName); + _logger.LogInformation("Completed creating data in {TableName}", tableName); } private void CreateNodeData() @@ -173,6 +173,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Cms.Core.Constants.Locks.Domains, Name = "Domains" }); _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Cms.Core.Constants.Locks.KeyValues, Name = "KeyValues" }); _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Cms.Core.Constants.Locks.Languages, Name = "Languages" }); + _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Cms.Core.Constants.Locks.ScheduledPublishing, Name = "ScheduledPublishing" }); _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Cms.Core.Constants.Locks.MainDom, Name = "MainDom" }); } @@ -418,21 +419,21 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install private void CreateRelationTypeData() { - var relationType = new RelationTypeDto { Id = 1, Alias = Cms.Core.Constants.Conventions.RelationTypes.RelateDocumentOnCopyAlias, ChildObjectType = Cms.Core.Constants.ObjectTypes.Document, ParentObjectType = Cms.Core.Constants.ObjectTypes.Document, Dual = true, Name = Cms.Core.Constants.Conventions.RelationTypes.RelateDocumentOnCopyName }; + var relationType = new RelationTypeDto { Id = 1, Alias = Cms.Core.Constants.Conventions.RelationTypes.RelateDocumentOnCopyAlias, ChildObjectType = Cms.Core.Constants.ObjectTypes.Document, ParentObjectType = Cms.Core.Constants.ObjectTypes.Document, Dual = true, Name = Cms.Core.Constants.Conventions.RelationTypes.RelateDocumentOnCopyName, IsDependency = false}; relationType.UniqueId = CreateUniqueRelationTypeId(relationType.Alias, relationType.Name); _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.RelationType, "id", false, relationType); - relationType = new RelationTypeDto { Id = 2, Alias = Cms.Core.Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteAlias, ChildObjectType = Cms.Core.Constants.ObjectTypes.Document, ParentObjectType = Cms.Core.Constants.ObjectTypes.Document, Dual = false, Name = Cms.Core.Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteName }; + relationType = new RelationTypeDto { Id = 2, Alias = Cms.Core.Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteAlias, ChildObjectType = Cms.Core.Constants.ObjectTypes.Document, ParentObjectType = Cms.Core.Constants.ObjectTypes.Document, Dual = false, Name = Cms.Core.Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteName, IsDependency = false }; relationType.UniqueId = CreateUniqueRelationTypeId(relationType.Alias, relationType.Name); _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.RelationType, "id", false, relationType); - relationType = new RelationTypeDto { Id = 3, Alias = Cms.Core.Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteAlias, ChildObjectType = Cms.Core.Constants.ObjectTypes.Media, ParentObjectType = Cms.Core.Constants.ObjectTypes.Media, Dual = false, Name = Cms.Core.Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteName }; + relationType = new RelationTypeDto { Id = 3, Alias = Cms.Core.Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteAlias, ChildObjectType = Cms.Core.Constants.ObjectTypes.Media, ParentObjectType = Cms.Core.Constants.ObjectTypes.Media, Dual = false, Name = Cms.Core.Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteName, IsDependency = false }; relationType.UniqueId = CreateUniqueRelationTypeId(relationType.Alias, relationType.Name); _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.RelationType, "id", false, relationType); - relationType = new RelationTypeDto { Id = 4, Alias = Cms.Core.Constants.Conventions.RelationTypes.RelatedMediaAlias, ChildObjectType = null, ParentObjectType = null, Dual = false, Name = Cms.Core.Constants.Conventions.RelationTypes.RelatedMediaName }; + relationType = new RelationTypeDto { Id = 4, Alias = Cms.Core.Constants.Conventions.RelationTypes.RelatedMediaAlias, ChildObjectType = null, ParentObjectType = null, Dual = false, Name = Cms.Core.Constants.Conventions.RelationTypes.RelatedMediaName, IsDependency = true }; relationType.UniqueId = CreateUniqueRelationTypeId(relationType.Alias, relationType.Name); _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.RelationType, "id", false, relationType); - relationType = new RelationTypeDto { Id = 5, Alias = Cms.Core.Constants.Conventions.RelationTypes.RelatedDocumentAlias, ChildObjectType = null, ParentObjectType = null, Dual = false, Name = Cms.Core.Constants.Conventions.RelationTypes.RelatedDocumentName }; + relationType = new RelationTypeDto { Id = 5, Alias = Cms.Core.Constants.Conventions.RelationTypes.RelatedDocumentAlias, ChildObjectType = null, ParentObjectType = null, Dual = false, Name = Cms.Core.Constants.Conventions.RelationTypes.RelatedDocumentName, IsDependency = true }; relationType.UniqueId = CreateUniqueRelationTypeId(relationType.Alias, relationType.Name); _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.RelationType, "id", false, relationType); } diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs index 3058fbfdf0..6134d45803 100644 --- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs @@ -90,8 +90,12 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install private readonly ILoggerFactory _loggerFactory; private readonly IUmbracoVersion _umbracoVersion; - public DatabaseSchemaCreator(IUmbracoDatabase? database, ILogger logger, - ILoggerFactory loggerFactory, IUmbracoVersion umbracoVersion, IEventAggregator eventAggregator) + public DatabaseSchemaCreator( + IUmbracoDatabase? database, + ILogger logger, + ILoggerFactory loggerFactory, + IUmbracoVersion umbracoVersion, + IEventAggregator eventAggregator) { _database = database ?? throw new ArgumentNullException(nameof(database)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); @@ -154,8 +158,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install if (creatingNotification.Cancel == false) { - var dataCreation = new DatabaseDataCreator(_database, - _loggerFactory.CreateLogger(), _umbracoVersion); + var dataCreation = new DatabaseDataCreator(_database, _loggerFactory.CreateLogger(), _umbracoVersion); foreach (Type table in OrderedTables) { CreateTable(false, table, dataCreation); @@ -458,12 +461,6 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install TableDefinition tableDefinition = DefinitionFactory.GetTableDefinition(modelType, SqlSyntax); var tableName = tableDefinition.Name; - - var createSql = SqlSyntax.Format(tableDefinition); - var createPrimaryKeySql = SqlSyntax.FormatPrimaryKey(tableDefinition); - List foreignSql = SqlSyntax.Format(tableDefinition.ForeignKeys); - List indexSql = SqlSyntax.Format(tableDefinition.Indexes); - var tableExist = TableExists(tableName); if (string.IsNullOrEmpty(tableName)) { @@ -485,18 +482,11 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install } //Execute the Create Table sql - _database.Execute(new Sql(createSql)); - _logger.LogInformation("Create Table {TableName}: \n {Sql}", tableName, createSql); - - //If any statements exists for the primary key execute them here - if (string.IsNullOrEmpty(createPrimaryKeySql) == false) - { - _database.Execute(new Sql(createPrimaryKeySql)); - _logger.LogInformation("Create Primary Key:\n {Sql}", createPrimaryKeySql); - } + SqlSyntax.HandleCreateTable(_database, tableDefinition); if (SqlSyntax.SupportsIdentityInsert() && tableDefinition.Columns.Any(x => x.IsIdentity)) { + // This should probably delegate to whole thing to the syntax provider _database.Execute(new Sql($"SET IDENTITY_INSERT {SqlSyntax.GetQuotedTableName(tableName)} ON ")); } @@ -510,20 +500,6 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install _database.Execute(new Sql($"SET IDENTITY_INSERT {SqlSyntax.GetQuotedTableName(tableName)} OFF;")); } - //Loop through index statements and execute sql - foreach (var sql in indexSql) - { - _database.Execute(new Sql(sql)); - _logger.LogInformation("Create Index:\n {Sql}", sql); - } - - //Loop through foreignkey statements and execute sql - foreach (var sql in foreignSql) - { - _database.Execute(new Sql(sql)); - _logger.LogInformation("Create Foreign Key:\n {Sql}", sql); - } - if (overwrite) { _logger.LogInformation("Table {TableName} was recreated", tableName); diff --git a/src/Umbraco.Infrastructure/Migrations/MigrationBase_Extra.cs b/src/Umbraco.Infrastructure/Migrations/MigrationBase_Extra.cs index 1154d6151e..22f7771685 100644 --- a/src/Umbraco.Infrastructure/Migrations/MigrationBase_Extra.cs +++ b/src/Umbraco.Infrastructure/Migrations/MigrationBase_Extra.cs @@ -86,18 +86,8 @@ namespace Umbraco.Cms.Infrastructure.Migrations protected void ReplaceColumn(string tableName, string currentName, string newName) { - if (DatabaseType.IsSqlCe()) - { - AddColumn(tableName, newName, out var sqls); - Execute.Sql($"UPDATE {SqlSyntax.GetQuotedTableName(tableName)} SET {SqlSyntax.GetQuotedColumnName(newName)}={SqlSyntax.GetQuotedColumnName(currentName)}").Do(); - foreach (var sql in sqls) Execute.Sql(sql).Do(); - Delete.Column(currentName).FromTable(tableName).Do(); - } - else - { - Execute.Sql(SqlSyntax.FormatColumnRename(tableName, currentName, newName)).Do(); - AlterColumn(tableName, newName); - } + Execute.Sql(SqlSyntax.FormatColumnRename(tableName, currentName, newName)).Do(); + AlterColumn(tableName, newName); } protected bool TableExists(string tableName) diff --git a/src/Umbraco.Infrastructure/Migrations/MigrationExpressionBase.cs b/src/Umbraco.Infrastructure/Migrations/MigrationExpressionBase.cs index e89b91690f..4838467197 100644 --- a/src/Umbraco.Infrastructure/Migrations/MigrationExpressionBase.cs +++ b/src/Umbraco.Infrastructure/Migrations/MigrationExpressionBase.cs @@ -120,7 +120,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations protected void AppendStatementSeparator(StringBuilder stmtBuilder) { stmtBuilder.AppendLine(";"); - if (DatabaseType.IsSqlServerOrCe()) + if (DatabaseType.IsSqlServer()) stmtBuilder.AppendLine("GO"); } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs index f2c5662a74..34077943c8 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs @@ -17,6 +17,7 @@ using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0; using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_1_0; using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_2_0; using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_3_0; +using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_4_0; using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade @@ -243,6 +244,9 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade // to 8.17.0 To("{153865E9-7332-4C2A-9F9D-F20AEE078EC7}"); + // Hack to support migration from 8.18 + To("{03482BB0-CF13-475C-845E-ECB8319DBE3C}"); + // This should be safe to execute again. We need it with a new name to ensure updates from all the following has executed this step. // - 8.15.0 RC - Current state: {4695D0C9-0729-4976-985B-048D503665D8} // - 8.15.0 Final - Current state: {5C424554-A32D-4852-8ED1-A13508187901} @@ -278,6 +282,9 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade To("{CA7A1D9D-C9D4-4914-BC0A-459E7B9C3C8C}"); To("{0828F206-DCF7-4F73-ABBB-6792275532EB}"); + // TO 9.4.0 + To("{DBBA1EA0-25A1-4863-90FB-5D306FB6F1E1}"); + To("{DED98755-4059-41BB-ADBD-3FEAB12D1D7B}"); } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/RenameMediaVersionTable.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/RenameMediaVersionTable.cs index 632b6f8a6c..fa88f17422 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/RenameMediaVersionTable.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/RenameMediaVersionTable.cs @@ -18,24 +18,12 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 AddColumn("id", out var sqls); - if (Database.DatabaseType.IsSqlCe()) - { - // SQLCE does not support UPDATE...FROM - var versions = Database.Fetch($@"SELECT v.versionId, v.id -FROM cmsContentVersion v -JOIN umbracoNode n on v.contentId=n.id -WHERE n.nodeObjectType='{Cms.Core.Constants.ObjectTypes.Media}'"); - foreach (var t in versions) - Execute.Sql($"UPDATE {Cms.Core.Constants.DatabaseSchema.Tables.MediaVersion} SET id={t.id} WHERE versionId='{t.versionId}'").Do(); - } - else - { - Database.Execute($@"UPDATE {Cms.Core.Constants.DatabaseSchema.Tables.MediaVersion} SET id=v.id + Database.Execute($@"UPDATE {Cms.Core.Constants.DatabaseSchema.Tables.MediaVersion} SET id=v.id FROM {Cms.Core.Constants.DatabaseSchema.Tables.MediaVersion} m JOIN cmsContentVersion v on m.versionId = v.versionId JOIN umbracoNode n on v.contentId=n.id WHERE n.nodeObjectType='{Cms.Core.Constants.ObjectTypes.Media}'"); - } + foreach (var sql in sqls) Execute.Sql(sql).Do(); diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/VariantsMigration.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/VariantsMigration.cs index 8dbc7d59b3..f0fbb63729 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/VariantsMigration.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/VariantsMigration.cs @@ -76,19 +76,9 @@ HAVING COUNT(v2.id) <> 1").Any()) { Alter.Table(PreTables.PropertyData).AddColumn("versionId2").AsInt32().Nullable().Do(); - if (Database.DatabaseType.IsSqlCe()) - { - // SQLCE does not support UPDATE...FROM - var versions = Database.Fetch($"SELECT id, versionId FROM {PreTables.ContentVersion}"); - foreach (var t in versions) - Database.Execute($"UPDATE {PreTables.PropertyData} SET versionId2=@v2 WHERE versionId=@v1", new { v1 = t.versionId, v2 = t.id }); - } - else - { - Database.Execute($@"UPDATE {PreTables.PropertyData} SET versionId2={PreTables.ContentVersion}.id + Database.Execute($@"UPDATE {PreTables.PropertyData} SET versionId2={PreTables.ContentVersion}.id FROM {PreTables.ContentVersion} INNER JOIN {PreTables.PropertyData} ON {PreTables.ContentVersion}.versionId = {PreTables.PropertyData}.versionId"); - } Delete.Column("versionId").FromTable(PreTables.PropertyData).Do(); ReplaceColumn(PreTables.PropertyData, "versionId2", "versionId"); @@ -181,40 +171,16 @@ INNER JOIN {PreTables.PropertyData} ON {PreTables.ContentVersion}.versionId = {P ReplaceColumn(PreTables.ContentVersion, "ContentId", "nodeId"); // populate contentVersion text, current and userId columns for documents - if (Database.DatabaseType.IsSqlCe()) - { - // SQLCE does not support UPDATE...FROM - var documents = Database.Fetch($"SELECT versionId, text, published, newest, documentUser FROM {PreTables.Document}"); - foreach (var t in documents) - Database.Execute($@"UPDATE {PreTables.ContentVersion} SET text=@text, {SqlSyntax.GetQuotedColumnName("current")}=@current, userId=@userId WHERE versionId=@versionId", - new { text = t.text, current = t.newest && !t.published, userId = t.documentUser, versionId = t.versionId }); - } - else - { - Database.Execute($@"UPDATE {PreTables.ContentVersion} SET text=d.text, {SqlSyntax.GetQuotedColumnName("current")}=(d.newest & ~d.published), userId=d.documentUser + Database.Execute($@"UPDATE {PreTables.ContentVersion} SET text=d.text, {SqlSyntax.GetQuotedColumnName("current")}=(d.newest & ~d.published), userId=d.documentUser FROM {PreTables.ContentVersion} v INNER JOIN {PreTables.Document} d ON d.versionId = v.versionId"); - } + // populate contentVersion text and current columns for non-documents, userId is default - if (Database.DatabaseType.IsSqlCe()) - { - // SQLCE does not support UPDATE...FROM - var otherContent = Database.Fetch($@"SELECT cver.versionId, n.text + Database.Execute($@"UPDATE {PreTables.ContentVersion} SET text=n.text, {SqlSyntax.GetQuotedColumnName("current")}=1, userId=0 FROM {PreTables.ContentVersion} cver JOIN {SqlSyntax.GetQuotedTableName(Cms.Core.Constants.DatabaseSchema.Tables.Node)} n ON cver.nodeId=n.id WHERE cver.versionId NOT IN (SELECT versionId FROM {SqlSyntax.GetQuotedTableName(PreTables.Document)})"); - foreach (var t in otherContent) - Database.Execute($@"UPDATE {PreTables.ContentVersion} SET text=@text, {SqlSyntax.GetQuotedColumnName("current")}=1, userId=0 WHERE versionId=@versionId", - new { text = t.text, versionId = t.versionId }); - } - else - { - Database.Execute($@"UPDATE {PreTables.ContentVersion} SET text=n.text, {SqlSyntax.GetQuotedColumnName("current")}=1, userId=0 -FROM {PreTables.ContentVersion} cver -JOIN {SqlSyntax.GetQuotedTableName(Cms.Core.Constants.DatabaseSchema.Tables.Node)} n ON cver.nodeId=n.id -WHERE cver.versionId NOT IN (SELECT versionId FROM {SqlSyntax.GetQuotedTableName(PreTables.Document)})"); - } // create table Create.Table(withoutKeysAndIndexes: true).Do(); diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_15_0/AddCmsContentNuByteColumn.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_15_0/AddCmsContentNuByteColumn.cs index a63cb7c1e5..10db1964e0 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_15_0/AddCmsContentNuByteColumn.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_15_0/AddCmsContentNuByteColumn.cs @@ -17,22 +17,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_15_0 protected override void Migrate() { - // allow null for the `data` field - if (DatabaseType.IsSqlCe()) - { - // SQLCE does not support altering NTEXT, so we have to jump through some hoops to do it - // All column ordering must remain the same as what is defined in the DTO so we need to create a temp table, - // drop orig and then re-create/copy. - Create.Table(withoutKeysAndIndexes: true).Do(); - Execute.Sql($"INSERT INTO [{TempTableName}] SELECT nodeId, published, data, rv FROM [{Constants.DatabaseSchema.Tables.NodeData}]").Do(); - Delete.Table(Constants.DatabaseSchema.Tables.NodeData).Do(); - Create.Table().Do(); - Execute.Sql($"INSERT INTO [{Constants.DatabaseSchema.Tables.NodeData}] SELECT nodeId, published, data, rv, NULL FROM [{TempTableName}]").Do(); - } - else - { - AlterColumn(Constants.DatabaseSchema.Tables.NodeData, "data"); - } + AlterColumn(Constants.DatabaseSchema.Tables.NodeData, "data"); var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToList(); AddColumnIfNotExists(columns, "dataRaw"); diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_15_0/UpdateCmsPropertyGroupIdSeed.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_15_0/UpdateCmsPropertyGroupIdSeed.cs index 868343374d..496e12a1fa 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_15_0/UpdateCmsPropertyGroupIdSeed.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_15_0/UpdateCmsPropertyGroupIdSeed.cs @@ -10,10 +10,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_15_0 protected override void Migrate() { - if (DatabaseType.IsSqlCe()) - { - Database.Execute(Sql("ALTER TABLE [cmsPropertyTypeGroup] ALTER COLUMN [id] IDENTITY (56,1)")); - } + // NOOP - was sql ce only } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/DictionaryTablesIndexes.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/DictionaryTablesIndexes.cs index b050f90d82..44034c5e45 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/DictionaryTablesIndexes.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/DictionaryTablesIndexes.cs @@ -109,22 +109,6 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0 { string columnSpecification; - // If using SQL CE, we don't have access to COUNT (DISTINCT *) or CONCAT, so will need to do this by querying all records. - if (DatabaseType.IsSqlCe()) - { - columnSpecification = columns.Length == 1 - ? StringConvertedAndQuotedColumnName(columns[0]) - : $"{string.Join(" + ", columns.Select(x => StringConvertedAndQuotedColumnName(x)))}"; - - var allRecordsQuery = Database.SqlContext.Sql() - .Select(columnSpecification) - .From(); - - var allRecords = Database.Fetch(allRecordsQuery); - - return allRecords.Distinct().Count(); - } - columnSpecification = columns.Length == 1 ? QuoteColumnName(columns[0]) : $"CONCAT({string.Join(",", columns.Select(QuoteColumnName))})"; diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/ExternalLoginTableIndexes.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/ExternalLoginTableIndexes.cs index f350ed633c..db7f17eee3 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/ExternalLoginTableIndexes.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/ExternalLoginTableIndexes.cs @@ -1,6 +1,10 @@ +using System; using System.Collections.Generic; using System.Linq; +using NPoco; using Umbraco.Cms.Core; +using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; +using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; using Umbraco.Cms.Infrastructure.Persistence.Dtos; namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0 @@ -20,14 +24,14 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0 { // Before adding these indexes we need to remove duplicate data. // Get all logins by latest - var logins = Database.Fetch() + var logins = Database.Fetch() .OrderByDescending(x => x.CreateDate) .ToList(); var toDelete = new List(); // used to track duplicates so they can be removed var keys = new HashSet<(string, string)>(); - foreach(ExternalLoginDto login in logins) + foreach(ExternalLoginTokenTable.LegacyExternalLoginDto login in logins) { if (!keys.Add((login.ProviderKey, login.LoginProvider))) { @@ -37,16 +41,16 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0 } if (toDelete.Count > 0) { - Database.DeleteMany().Where(x => toDelete.Contains(x.Id)).Execute(); - } + Database.DeleteMany().Where(x => toDelete.Contains(x.Id)).Execute(); + } - var indexName1 = "IX_" + ExternalLoginDto.TableName + "_LoginProvider"; + var indexName1 = "IX_" + ExternalLoginTokenTable.LegacyExternalLoginDto.TableName + "_LoginProvider"; if (!IndexExists(indexName1)) { Create .Index(indexName1) - .OnTable(ExternalLoginDto.TableName) + .OnTable(ExternalLoginTokenTable.LegacyExternalLoginDto.TableName) .OnColumn("loginProvider") .Ascending() .WithOptions() @@ -56,13 +60,13 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0 .Do(); } - var indexName2 = "IX_" + ExternalLoginDto.TableName + "_ProviderKey"; + var indexName2 = "IX_" + ExternalLoginTokenTable.LegacyExternalLoginDto.TableName + "_ProviderKey"; if (!IndexExists(indexName2)) { Create .Index(indexName2) - .OnTable(ExternalLoginDto.TableName) + .OnTable(ExternalLoginTokenTable.LegacyExternalLoginDto.TableName) .OnColumn("loginProvider").Ascending() .OnColumn("providerKey").Ascending() .WithOptions() @@ -70,5 +74,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0 .Do(); } } + + } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/ExternalLoginTableIndexesFixup.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/ExternalLoginTableIndexesFixup.cs index 5efb914eb7..2c77b301ce 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/ExternalLoginTableIndexesFixup.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/ExternalLoginTableIndexesFixup.cs @@ -14,29 +14,29 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0 protected override void Migrate() { - var indexName1 = "IX_" + ExternalLoginDto.TableName + "_LoginProvider"; - var indexName2 = "IX_" + ExternalLoginDto.TableName + "_ProviderKey"; + var indexName1 = "IX_" + ExternalLoginTokenTable.LegacyExternalLoginDto.TableName + "_LoginProvider"; + var indexName2 = "IX_" + ExternalLoginTokenTable.LegacyExternalLoginDto.TableName + "_ProviderKey"; if (IndexExists(indexName1)) { // drop it since the previous migration index was wrong, and we // need to modify a column that belons to it - Delete.Index(indexName1).OnTable(ExternalLoginDto.TableName).Do(); + Delete.Index(indexName1).OnTable(ExternalLoginTokenTable.LegacyExternalLoginDto.TableName).Do(); } if (IndexExists(indexName2)) { // drop since it's using a column we're about to modify - Delete.Index(indexName2).OnTable(ExternalLoginDto.TableName).Do(); + Delete.Index(indexName2).OnTable(ExternalLoginTokenTable.LegacyExternalLoginDto.TableName).Do(); } // then fixup the length of the loginProvider column - AlterColumn(ExternalLoginDto.TableName, "loginProvider"); + AlterColumn(ExternalLoginTokenTable.LegacyExternalLoginDto.TableName, "loginProvider"); // create it with the correct definition Create .Index(indexName1) - .OnTable(ExternalLoginDto.TableName) + .OnTable(ExternalLoginTokenTable.LegacyExternalLoginDto.TableName) .OnColumn("loginProvider").Ascending() .OnColumn("userId").Ascending() .WithOptions() @@ -48,9 +48,9 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0 // re-create the original Create .Index(indexName2) - .OnTable(ExternalLoginDto.TableName) + .OnTable(ExternalLoginTokenTable.LegacyExternalLoginDto.TableName) .OnColumn("loginProvider").Ascending() - .OnColumn("providerKey").Ascending() + .OnColumn("providerKey").Ascending() .WithOptions() .NonClustered() .Do(); diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/ExternalLoginTokenTable.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/ExternalLoginTokenTable.cs index 8dd43f1834..851d986c7c 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/ExternalLoginTokenTable.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/ExternalLoginTokenTable.cs @@ -1,10 +1,14 @@ +using System; using System.Collections.Generic; +using NPoco; +using Umbraco.Cms.Core; +using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; +using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0 { - public class ExternalLoginTokenTable : MigrationBase { public ExternalLoginTokenTable(IMigrationContext context) @@ -13,7 +17,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0 } /// - /// Adds new External Login token table + /// Adds new External Login token table /// protected override void Migrate() { @@ -25,5 +29,53 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0 Create.Table().Do(); } + + [TableName(TableName)] + [ExplicitColumns] + [PrimaryKey("Id")] + internal class LegacyExternalLoginDto + { + public const string TableName = Constants.DatabaseSchema.Tables.ExternalLogin; + + [Column("id")] [PrimaryKeyColumn] public int Id { get; set; } + + [Obsolete( + "This only exists to ensure you can upgrade using external logins from umbraco version where this was used to the new where it is not used")] + [Column("userId")] + public int? UserId { get; set; } + + + /// + /// Used to store the name of the provider (i.e. Facebook, Google) + /// + [Column("loginProvider")] + [Length(400)] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Index(IndexTypes.UniqueNonClustered, ForColumns = "loginProvider,userOrMemberKey", + Name = "IX_" + TableName + "_LoginProvider")] + public string LoginProvider { get; set; } + + /// + /// Stores the key the provider uses to lookup the login + /// + [Column("providerKey")] + [Length(4000)] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Index(IndexTypes.NonClustered, ForColumns = "loginProvider,providerKey", + Name = "IX_" + TableName + "_ProviderKey")] + public string ProviderKey { get; set; } + + [Column("createDate")] + [Constraint(Default = SystemMethods.CurrentDateTime)] + public DateTime CreateDate { get; set; } + + /// + /// Used to store any arbitrary data for the user and external provider - like user tokens returned from the provider + /// + [Column("userData")] + [NullSetting(NullSetting = NullSettings.Null)] + [SpecialDbType(SpecialDbTypes.NTEXT)] + public string UserData { get; set; } + } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_3_0/UpdateExternalLoginToUseKeyInsteadOfId.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_3_0/UpdateExternalLoginToUseKeyInsteadOfId.cs index 4c7104e762..6b74c49f67 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_3_0/UpdateExternalLoginToUseKeyInsteadOfId.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_3_0/UpdateExternalLoginToUseKeyInsteadOfId.cs @@ -35,22 +35,8 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_3_0 //special trick to add the column without constraints and return the sql to add them later AddColumn("userOrMemberKey", out var sqls); - - if (DatabaseType.IsSqlCe()) - { - var userIds = Database.Fetch(Sql().Select("userId").From()); - - foreach (int userId in userIds) - { - Execute.Sql($"UPDATE {ExternalLoginDto.TableName} SET userOrMemberKey = '{userId.ToGuid()}' WHERE userId = {userId}").Do(); - } - } - else - { - //populate the new columns with the userId as a Guid. Same method as IntExtensions.ToGuid. - Execute.Sql($"UPDATE {ExternalLoginDto.TableName} SET userOrMemberKey = CAST(CONVERT(char(8), CONVERT(BINARY(4), userId), 2) + '-0000-0000-0000-000000000000' AS UNIQUEIDENTIFIER)").Do(); - - } + //populate the new columns with the userId as a Guid. Same method as IntExtensions.ToGuid. + Execute.Sql($"UPDATE {ExternalLoginDto.TableName} SET userOrMemberKey = CAST(CONVERT(char(8), CONVERT(BINARY(4), userId), 2) + '-0000-0000-0000-000000000000' AS UNIQUEIDENTIFIER)").Do(); //now apply constraints (NOT NULL) to new table foreach (var sql in sqls) Execute.Sql(sql).Do(); diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_4_0/AddScheduledPublishingLock.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_4_0/AddScheduledPublishingLock.cs new file mode 100644 index 0000000000..01cfb22a3d --- /dev/null +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_4_0/AddScheduledPublishingLock.cs @@ -0,0 +1,15 @@ +using Umbraco.Cms.Infrastructure.Persistence.Dtos; + +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_4_0 +{ + internal class AddScheduledPublishingLock : MigrationBase + { + public AddScheduledPublishingLock(IMigrationContext context) + : base(context) + { + } + + protected override void Migrate() => + Database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Cms.Core.Constants.Locks.ScheduledPublishing, Name = "ScheduledPublishing" }); + } +} diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_4_0/UpdateRelationTypesToHandleDependencies.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_4_0/UpdateRelationTypesToHandleDependencies.cs new file mode 100644 index 0000000000..1c8fe7ed72 --- /dev/null +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_4_0/UpdateRelationTypesToHandleDependencies.cs @@ -0,0 +1,34 @@ +using System.Linq; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Extensions; + + +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_4_0 +{ + internal class UpdateRelationTypesToHandleDependencies : MigrationBase + { + public UpdateRelationTypesToHandleDependencies(IMigrationContext context) + : base(context) + { + } + + protected override void Migrate() + { + var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToList(); + + AddColumnIfNotExists(columns, "isDependency"); + + var aliasesWithDependencies = new[] + { + Core.Constants.Conventions.RelationTypes.RelatedDocumentAlias, + Core.Constants.Conventions.RelationTypes.RelatedMediaAlias + }; + + Database.Execute( + Sql() + .Update(u => u.Set(x => x.IsDependency, true)) + .WhereIn(x => x.Alias, aliasesWithDependencies)); + + } + } +} diff --git a/src/Umbraco.Infrastructure/Persistence/BasicBulkSqlInsertProvider.cs b/src/Umbraco.Infrastructure/Persistence/BasicBulkSqlInsertProvider.cs deleted file mode 100644 index a5524b44de..0000000000 --- a/src/Umbraco.Infrastructure/Persistence/BasicBulkSqlInsertProvider.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Umbraco.Extensions; - -namespace Umbraco.Cms.Infrastructure.Persistence -{ - /// - /// A provider that just generates insert commands - /// - public class BasicBulkSqlInsertProvider : IBulkSqlInsertProvider - { - public string ProviderName => Cms.Core.Constants.DatabaseProviders.SqlServer; - - public int BulkInsertRecords(IUmbracoDatabase database, IEnumerable records) - { - if (!records.Any()) return 0; - - return BulkInsertRecordsWithCommands(database, records.ToArray()); - } - - /// - /// Bulk-insert records using commands. - /// - /// The type of the records. - /// The database. - /// The records. - /// The number of records that were inserted. - internal static int BulkInsertRecordsWithCommands(IUmbracoDatabase database, T[] records) - { - foreach (var command in database.GenerateBulkInsertCommands(records)) - command.ExecuteNonQuery(); - - return records.Length; // what else? - } - } -} diff --git a/src/Umbraco.Infrastructure/Persistence/CustomConnectionStringDatabaseProviderMetadata.cs b/src/Umbraco.Infrastructure/Persistence/CustomConnectionStringDatabaseProviderMetadata.cs new file mode 100644 index 0000000000..c9984d69ea --- /dev/null +++ b/src/Umbraco.Infrastructure/Persistence/CustomConnectionStringDatabaseProviderMetadata.cs @@ -0,0 +1,55 @@ +using System; +using System.Runtime.Serialization; +using Umbraco.Cms.Core.Install.Models; + +namespace Umbraco.Cms.Infrastructure.Persistence; + +/// +/// Provider metadata for custom connection string setup. +/// +[DataContract] +public class CustomConnectionStringDatabaseProviderMetadata : IDatabaseProviderMetadata +{ + /// + public Guid Id => new("42c0eafd-1650-4bdb-8cf6-d226e8941698"); + + /// + public int SortOrder => int.MaxValue; + + /// + public string DisplayName => "Custom"; + + /// + public string DefaultDatabaseName => string.Empty; + + /// + public string ProviderName => null; + + /// + public bool SupportsQuickInstall => false; + + /// + public bool IsAvailable => true; + + /// + public bool RequiresServer => false; + + /// + public string ServerPlaceholder => null; + + /// + public bool RequiresCredentials => false; + + /// + public bool SupportsIntegratedAuthentication => false; + + /// + public bool RequiresConnectionTest => true; + + /// + public bool ForceCreateDatabase => false; + + /// + public string GenerateConnectionString(DatabaseModel databaseModel) + => databaseModel.ConnectionString; +} diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/DefinitionFactory.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/DefinitionFactory.cs index a2507a17f2..9e26c2722a 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/DefinitionFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/DefinitionFactory.cs @@ -9,7 +9,7 @@ using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions { - internal static class DefinitionFactory + public static class DefinitionFactory { public static TableDefinition GetTableDefinition(Type modelType, ISqlSyntaxProvider sqlSyntax) { diff --git a/src/Umbraco.Infrastructure/Persistence/DbProviderFactoryCreator.cs b/src/Umbraco.Infrastructure/Persistence/DbProviderFactoryCreator.cs index f3f0902456..59e8e9c386 100644 --- a/src/Umbraco.Infrastructure/Persistence/DbProviderFactoryCreator.cs +++ b/src/Umbraco.Infrastructure/Persistence/DbProviderFactoryCreator.cs @@ -10,6 +10,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence public class DbProviderFactoryCreator : IDbProviderFactoryCreator { private readonly Func _getFactory; + private readonly IEnumerable _providerSpecificInterceptors; private readonly IDictionary _databaseCreators; private readonly IDictionary _syntaxProviders; private readonly IDictionary _bulkSqlInsertProviders; @@ -20,9 +21,12 @@ namespace Umbraco.Cms.Infrastructure.Persistence IEnumerable syntaxProviders, IEnumerable bulkSqlInsertProviders, IEnumerable databaseCreators, - IEnumerable providerSpecificMapperFactories) + IEnumerable providerSpecificMapperFactories, + IEnumerable providerSpecificInterceptors) + { _getFactory = getFactory; + _providerSpecificInterceptors = providerSpecificInterceptors; _databaseCreators = databaseCreators.ToDictionary(x => x.ProviderName); _syntaxProviders = syntaxProviders.ToDictionary(x => x.ProviderName); _bulkSqlInsertProviders = bulkSqlInsertProviders.ToDictionary(x => x.ProviderName); @@ -50,10 +54,9 @@ namespace Umbraco.Cms.Infrastructure.Persistence public IBulkSqlInsertProvider CreateBulkSqlInsertProvider(string providerName) { - if (!_bulkSqlInsertProviders.TryGetValue(providerName, out var result)) { - return new BasicBulkSqlInsertProvider(); + throw new InvalidOperationException($"Unknown provider name \"{providerName}\""); } return result; @@ -76,5 +79,8 @@ namespace Umbraco.Cms.Infrastructure.Persistence return new NPocoMapperCollection(() => Enumerable.Empty()); } + + public IEnumerable GetProviderSpecificInterceptors(string providerName) + => _providerSpecificInterceptors.Where(x => x.ProviderName == providerName); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/RelationTypeDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/RelationTypeDto.cs index 28cbcaed2d..d1cb5cc278 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/RelationTypeDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/RelationTypeDto.cs @@ -39,6 +39,10 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Dtos [NullSetting(NullSetting = NullSettings.NotNull)] [Length(100)] [Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoRelationType_alias")] - public string Alias { get; set; } = null!; + public string Alias { get; set; } = null!; + + [Constraint(Default = "0")] + [Column("isDependency")] + public bool IsDependency { get; set; } } } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/RelationTypeFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/RelationTypeFactory.cs index 0e304f1fcb..57b1831c9d 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/RelationTypeFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/RelationTypeFactory.cs @@ -9,7 +9,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Factories public static IRelationType BuildEntity(RelationTypeDto dto) { - var entity = new RelationType(dto.Name, dto.Alias, dto.Dual, dto.ParentObjectType, dto.ChildObjectType); + var entity = new RelationType(dto.Name, dto.Alias, dto.Dual, dto.ParentObjectType, dto.ChildObjectType, dto.IsDependency); try { @@ -30,11 +30,17 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Factories public static RelationTypeDto BuildDto(IRelationType entity) { + var isDependency = false; + if (entity is IRelationTypeWithIsDependency relationTypeWithIsDependency) + { + isDependency = relationTypeWithIsDependency.IsDependency; + } var dto = new RelationTypeDto { Alias = entity.Alias, ChildObjectType = entity.ChildObjectType, Dual = entity.IsBidirectional, + IsDependency = isDependency, Name = entity.Name ?? string.Empty, ParentObjectType = entity.ParentObjectType, UniqueId = entity.Key @@ -47,6 +53,8 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Factories return dto; } + + #endregion } } diff --git a/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryDbConnection.cs b/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryDbConnection.cs index bd03c44c7a..57b14bf0a9 100644 --- a/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryDbConnection.cs +++ b/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryDbConnection.cs @@ -6,7 +6,7 @@ using Transaction = System.Transactions.Transaction; namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling { - class RetryDbConnection : DbConnection + public class RetryDbConnection : DbConnection { private DbConnection _inner; private readonly RetryPolicy _conRetryPolicy; diff --git a/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryPolicyFactory.cs b/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryPolicyFactory.cs index a88ae39982..6a4e257af7 100644 --- a/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryPolicyFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryPolicyFactory.cs @@ -2,6 +2,8 @@ namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling { + // TODO: These should move to Persistence.SqlServer + /// /// Provides a factory class for instantiating application-specific retry policies. /// diff --git a/src/Umbraco.Infrastructure/Persistence/IDatabaseProviderMetadata.cs b/src/Umbraco.Infrastructure/Persistence/IDatabaseProviderMetadata.cs new file mode 100644 index 0000000000..41baa9c4f8 --- /dev/null +++ b/src/Umbraco.Infrastructure/Persistence/IDatabaseProviderMetadata.cs @@ -0,0 +1,90 @@ +using System; +using System.Runtime.Serialization; +using Umbraco.Cms.Core.Install.Models; + +namespace Umbraco.Cms.Infrastructure.Persistence; + +public interface IDatabaseProviderMetadata +{ + /// + /// Gets a unique identifier for this set of metadata used for filtering. + /// + [DataMember(Name = "id")] + Guid Id { get; } + + /// + /// Gets a value to determine display order and quick install priority. + /// + [DataMember(Name = "sortOrder")] + int SortOrder { get; } + + /// + /// Gets a friendly name to describe the provider. + /// + [DataMember(Name = "displayName")] + string DisplayName { get; } + + /// + /// Gets the default database name for the provider. + /// + [DataMember(Name = "defaultDatabaseName")] + string DefaultDatabaseName { get; } + + /// + /// Gets the database factory provider name. + /// + [DataMember(Name = "providerName")] + string ProviderName { get; } + + /// + /// Gets a value indicating whether can be used for one click install. + /// + [DataMember(Name = "supportsQuickInstall")] + bool SupportsQuickInstall { get; } + + /// + /// Gets a value indicating whether should be available for selection. + /// + [DataMember(Name = "isAvailable")] + bool IsAvailable { get; } + + /// + /// Gets a value indicating whether the server/hostname field must be populated. + /// + [DataMember(Name = "requiresServer")] + bool RequiresServer { get; } + + /// + /// Gets a value used as input placeholder for server/hostnmae field. + /// + [DataMember(Name = "serverPlaceholder")] + string ServerPlaceholder { get; } + + /// + /// Gets a value indicating whether a username and password are required (in general) to connect to the database + /// + [DataMember(Name = "requiresCredentials")] + bool RequiresCredentials { get; } + + /// + /// Gets a value indicating whether integrated authentication is supported (e.g. SQL Server & Oracle). + /// + [DataMember(Name = "supportsIntegratedAuthentication")] + bool SupportsIntegratedAuthentication { get; } + + /// + /// Gets a value indicating whether the connection should be tested before continuing install process. + /// + [DataMember(Name = "requiresConnectionTest")] + bool RequiresConnectionTest { get; } + + /// + /// Gets a value indicating to ignore the value of GlobalSettings.InstallMissingDatabase + /// + public bool ForceCreateDatabase { get; } + + /// + /// Creates a connection string for this provider. + /// + string GenerateConnectionString(DatabaseModel databaseModel); +} diff --git a/src/Umbraco.Infrastructure/Persistence/IDbProviderFactoryCreator.cs b/src/Umbraco.Infrastructure/Persistence/IDbProviderFactoryCreator.cs index 52daf7e351..f9bdf00f8f 100644 --- a/src/Umbraco.Infrastructure/Persistence/IDbProviderFactoryCreator.cs +++ b/src/Umbraco.Infrastructure/Persistence/IDbProviderFactoryCreator.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Data.Common; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; @@ -11,5 +12,6 @@ namespace Umbraco.Cms.Infrastructure.Persistence IBulkSqlInsertProvider CreateBulkSqlInsertProvider(string providerName); void CreateDatabase(string providerName, string connectionString); NPocoMapperCollection ProviderSpecificMappers(string providerName); + IEnumerable GetProviderSpecificInterceptors(string providerName); } } diff --git a/src/Umbraco.Infrastructure/Persistence/IProviderSpecificInterceptor.cs b/src/Umbraco.Infrastructure/Persistence/IProviderSpecificInterceptor.cs new file mode 100644 index 0000000000..736ba80854 --- /dev/null +++ b/src/Umbraco.Infrastructure/Persistence/IProviderSpecificInterceptor.cs @@ -0,0 +1,28 @@ +using NPoco; + +namespace Umbraco.Cms.Infrastructure.Persistence; + +public interface IProviderSpecificInterceptor : IInterceptor +{ + string ProviderName { get; } +} + +public interface IProviderSpecificExecutingInterceptor : IProviderSpecificInterceptor, IExecutingInterceptor +{ +} + +public interface IProviderSpecificConnectionInterceptor : IProviderSpecificInterceptor, IConnectionInterceptor +{ +} + +public interface IProviderSpecificExceptionInterceptor : IProviderSpecificInterceptor, IExceptionInterceptor +{ +} + +public interface IProviderSpecificDataInterceptor : IProviderSpecificInterceptor, IDataInterceptor +{ +} + +public interface IProviderSpecificTransactionInterceptor: IProviderSpecificInterceptor, ITransactionInterceptor +{ +} diff --git a/src/Umbraco.Infrastructure/Persistence/IScalarMapper.cs b/src/Umbraco.Infrastructure/Persistence/IScalarMapper.cs new file mode 100644 index 0000000000..29f1128a44 --- /dev/null +++ b/src/Umbraco.Infrastructure/Persistence/IScalarMapper.cs @@ -0,0 +1,13 @@ +namespace Umbraco.Cms.Infrastructure.Persistence; + +/// +/// Provides a mapping function for +/// +public interface IScalarMapper +{ + /// + /// Performs a mapping operation for a scalar value. + /// + object Map(object value); +} + diff --git a/src/Umbraco.Infrastructure/Persistence/IUmbracoDatabaseFactory.cs b/src/Umbraco.Infrastructure/Persistence/IUmbracoDatabaseFactory.cs index 382eda81b0..43d7c461ca 100644 --- a/src/Umbraco.Infrastructure/Persistence/IUmbracoDatabaseFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/IUmbracoDatabaseFactory.cs @@ -1,4 +1,5 @@ using System; +using Umbraco.Cms.Core.Configuration.Models; namespace Umbraco.Cms.Infrastructure.Persistence { @@ -51,7 +52,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence /// /// Configures the database factory. /// - void Configure(string? connectionString, string? providerName); + void Configure(ConnectionStrings umbracoConnectionString); /// /// Gets the . diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/RelationTypeMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/RelationTypeMapper.cs index 965a659631..732563fef7 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/RelationTypeMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/RelationTypeMapper.cs @@ -22,6 +22,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Mappers DefineMap(nameof(RelationType.Alias), nameof(RelationTypeDto.Alias)); DefineMap(nameof(RelationType.ChildObjectType), nameof(RelationTypeDto.ChildObjectType)); DefineMap(nameof(RelationType.IsBidirectional), nameof(RelationTypeDto.Dual)); + DefineMap(nameof(RelationType.IsDependency), nameof(RelationTypeDto.IsDependency)); DefineMap(nameof(RelationType.Name), nameof(RelationTypeDto.Name)); DefineMap(nameof(RelationType.ParentObjectType), nameof(RelationTypeDto.ParentObjectType)); } diff --git a/src/Umbraco.Infrastructure/Persistence/NPocoDatabaseTypeExtensions.cs b/src/Umbraco.Infrastructure/Persistence/NPocoDatabaseTypeExtensions.cs index d692656f0f..b349824591 100644 --- a/src/Umbraco.Infrastructure/Persistence/NPocoDatabaseTypeExtensions.cs +++ b/src/Umbraco.Infrastructure/Persistence/NPocoDatabaseTypeExtensions.cs @@ -1,35 +1,19 @@ -using NPoco; +using System; +using NPoco; namespace Umbraco.Cms.Infrastructure.Persistence { internal static class NPocoDatabaseTypeExtensions { - public static bool IsSqlServer(this DatabaseType databaseType) - { + [Obsolete("Usage of this method indicates a code smell.")] + public static bool IsSqlServer(this DatabaseType databaseType) => // note that because SqlServerDatabaseType is the base class for // all Sql Server types eg SqlServer2012DatabaseType, this will // test *any* version of Sql Server. - return databaseType is NPoco.DatabaseTypes.SqlServerDatabaseType; - } + databaseType is NPoco.DatabaseTypes.SqlServerDatabaseType; - public static bool IsSqlServer2008OrLater(this DatabaseType databaseType) - { - return databaseType is NPoco.DatabaseTypes.SqlServer2008DatabaseType; - } - - public static bool IsSqlServer2012OrLater(this DatabaseType databaseType) - { - return databaseType is NPoco.DatabaseTypes.SqlServer2012DatabaseType; - } - - public static bool IsSqlCe(this DatabaseType databaseType) - { - return databaseType is NPoco.DatabaseTypes.SqlServerCEDatabaseType; - } - - public static bool IsSqlServerOrCe(this DatabaseType databaseType) - { - return databaseType.IsSqlServer() || databaseType.IsSqlCe(); - } + [Obsolete("Usage of this method indicates a code smell.")] + public static bool IsSqlite(this DatabaseType databaseType) + => databaseType is NPoco.DatabaseTypes.SQLiteDatabaseType; } } diff --git a/src/Umbraco.Infrastructure/Persistence/NPocoSqlExtensions.cs b/src/Umbraco.Infrastructure/Persistence/NPocoSqlExtensions.cs index 7c4498292b..2d2df7fe4d 100644 --- a/src/Umbraco.Infrastructure/Persistence/NPocoSqlExtensions.cs +++ b/src/Umbraco.Infrastructure/Persistence/NPocoSqlExtensions.cs @@ -72,7 +72,27 @@ namespace Umbraco.Extensions /// The Sql statement. public static Sql WhereIn(this Sql sql, Expression> field, Sql? values) { - return sql.WhereIn(field, values, false); + return WhereIn(sql, field, values, false, null); + } + + public static Sql WhereIn(this Sql sql, Expression> field, Sql values, string tableAlias) + { + return sql.WhereIn(field, values, false, tableAlias); + } + + + public static Sql WhereLike(this Sql sql, Expression> fieldSelector, Sql valuesSql) + { + var fieldName = sql.SqlContext.SqlSyntax.GetFieldName(fieldSelector); + sql.Where(fieldName + " LIKE (" + valuesSql.SQL + ")", valuesSql.Arguments); + return sql; + } + + public static Sql WhereLike(this Sql sql, Expression> fieldSelector, string likeValue) + { + var fieldName = sql.SqlContext.SqlSyntax.GetFieldName(fieldSelector); + sql.Where(fieldName + " LIKE ('" + likeValue + "')"); + return sql; } /// @@ -130,7 +150,12 @@ namespace Umbraco.Extensions private static Sql WhereIn(this Sql sql, Expression> fieldSelector, Sql? valuesSql, bool not) { - var fieldName = sql.SqlContext.SqlSyntax.GetFieldName(fieldSelector); + return WhereIn(sql, fieldSelector, valuesSql, not, null); + } + + private static Sql WhereIn(this Sql sql, Expression> fieldSelector, Sql? valuesSql, bool not, string tableAlias) + { + var fieldName = sql.SqlContext.SqlSyntax.GetFieldName(fieldSelector, tableAlias); sql.Where(fieldName + (not ? " NOT" : "") +" IN (" + valuesSql?.SQL + ")", valuesSql?.Arguments); return sql; } @@ -252,7 +277,7 @@ namespace Umbraco.Extensions /// The Sql statement. public static Sql OrderByDescending(this Sql sql, Expression> field) { - return sql.OrderBy("(" + sql.SqlContext.SqlSyntax.GetFieldName(field) + ") DESC"); + return sql.OrderByDescending(sql.SqlContext.SqlSyntax.GetFieldName(field)); } /// @@ -268,7 +293,7 @@ namespace Umbraco.Extensions var columns = fields.Length == 0 ? sql.GetColumns(withAlias: false) : fields.Select(x => sqlSyntax.GetFieldName(x)).ToArray(); - return sql.OrderBy(columns.Select(x => x + " DESC")); + return sql.OrderByDescending(columns); } /// @@ -406,20 +431,11 @@ namespace Umbraco.Extensions /// An optional alias for the joined table. /// A SqlJoin statement. /// Nested statement produces LEFT JOIN xxx JOIN yyy ON ... ON ... - public static Sql.SqlJoinClause LeftJoin(this Sql sql, Func, Sql> nestedJoin, string? alias = null) - { - var type = typeof(TDto); - var tableName = type.GetTableName(); - var join = sql.SqlContext.SqlSyntax.GetQuotedTableName(tableName); - if (alias != null) join += " " + sql.SqlContext.SqlSyntax.GetQuotedTableName(alias); - - var nestedSql = new Sql(sql.SqlContext); - nestedSql = nestedJoin(nestedSql); - - var sqlJoin = sql.LeftJoin(join); - sql.Append(nestedSql); - return sqlJoin; - } + public static Sql.SqlJoinClause LeftJoin( + this Sql sql, + Func, Sql> nestedJoin, + string? alias = null) => + sql.SqlContext.SqlSyntax.LeftJoinWithNestedJoin(sql, nestedJoin, alias); /// /// Appends a RIGHT JOIN clause to the Sql statement. @@ -897,6 +913,15 @@ namespace Umbraco.Extensions return string.Join(", ", sql.GetColumns(columnExpressions: fields, withAlias: false)); } + /// + /// Gets fields for a Dto. + /// + public static string ColumnsForInsert(this Sql sql, params Expression>[] fields) + { + if (sql == null) throw new ArgumentNullException(nameof(sql)); + return string.Join(", ", sql.GetColumns(columnExpressions: fields, withAlias: false, forInsert: true)); + } + /// /// Gets fields for a Dto. /// @@ -929,7 +954,8 @@ namespace Umbraco.Extensions var type = typeof(TDto); var tableName = type.GetTableName(); - sql.Append($"DELETE {sql.SqlContext.SqlSyntax.GetQuotedTableName(tableName)}"); + // FROM optional SQL server, but not elsewhere. + sql.Append($"DELETE FROM {sql.SqlContext.SqlSyntax.GetQuotedTableName(tableName)}"); return sql; } @@ -998,7 +1024,7 @@ namespace Umbraco.Extensions public SqlUpd Set(Expression> fieldSelector, object? value) { - var fieldName = _sqlContext.SqlSyntax.GetFieldName(fieldSelector); + var fieldName = _sqlContext.SqlSyntax.GetFieldNameForUpdate(fieldSelector); _setExpressions.Add(new Tuple(fieldName, value)); return this; } @@ -1015,62 +1041,14 @@ namespace Umbraco.Extensions /// /// The Sql statement. /// The Sql statement. - /// NOTE: This method will not work for all queries, only simple ones! + /// + /// NOTE: This method will not work for all queries, only simple ones! + /// public static Sql ForUpdate(this Sql sql) - { - // go find the first FROM clause, and append the lock hint - Sql s = sql; - var updated = false; + => sql.SqlContext.SqlSyntax.InsertForUpdateHint(sql); - while (s != null) - { - var sqlText = SqlInspector.GetSqlText(s); - if (sqlText.StartsWith("FROM ", StringComparison.OrdinalIgnoreCase)) - { - SqlInspector.SetSqlText(s, sqlText + " WITH (UPDLOCK)"); - updated = true; - break; - } - - s = SqlInspector.GetSqlRhs(sql); - } - - if (updated) - SqlInspector.Reset(sql); - - return sql; - } - - #endregion - - #region Sql Inspection - - private static SqlInspectionUtilities? _sqlInspector; - - private static SqlInspectionUtilities SqlInspector => _sqlInspector ?? (_sqlInspector = new SqlInspectionUtilities()); - - private class SqlInspectionUtilities - { - private readonly Func _getSqlText; - private readonly Action _setSqlText; - private readonly Func _getSqlRhs; - private readonly Action _setSqlFinal; - - public SqlInspectionUtilities() - { - (_getSqlText, _setSqlText) = ReflectionUtilities.EmitFieldGetterAndSetter("_sql"); - _getSqlRhs = ReflectionUtilities.EmitFieldGetter("_rhs"); - _setSqlFinal = ReflectionUtilities.EmitFieldSetter("_sqlFinal"); - } - - public string GetSqlText(Sql sql) => _getSqlText(sql); - - public void SetSqlText(Sql sql, string sqlText) => _setSqlText(sql, sqlText); - - public Sql GetSqlRhs(Sql sql) => _getSqlRhs(sql); - - public void Reset(Sql sql) => _setSqlFinal(sql, null); - } + public static Sql AppendForUpdateHint(this Sql sql) + => sql.SqlContext.SqlSyntax.AppendForUpdateHint(sql); #endregion @@ -1095,7 +1073,7 @@ namespace Umbraco.Extensions #region Utilities - private static string[] GetColumns(this Sql sql, string? tableAlias = null, string? referenceName = null, Expression>[]? columnExpressions = null, bool withAlias = true) + private static string[] GetColumns(this Sql sql, string? tableAlias = null, string? referenceName = null, Expression>[]? columnExpressions = null, bool withAlias = true, bool forInsert = false) { var pd = sql.SqlContext.PocoDataFactory.ForType(typeof (TDto)); var tableName = tableAlias ?? pd.TableInfo.TableName; @@ -1135,24 +1113,11 @@ namespace Umbraco.Extensions } return queryColumns - .Select(x => GetColumn(sql.SqlContext.DatabaseType, tableName, x.Value.ColumnName, GetAlias(x.Value), referenceName)) + .Select(x => sql.SqlContext.SqlSyntax.GetColumn(sql.SqlContext.DatabaseType, tableName, x.Value.ColumnName, GetAlias(x.Value), referenceName, forInsert: forInsert)) .ToArray(); } - private static string GetColumn(DatabaseType dbType, string tableName, string columnName, string? columnAlias, string? referenceName = null) - { - tableName = dbType.EscapeTableName(tableName); - columnName = dbType.EscapeSqlIdentifier(columnName); - var column = tableName + "." + columnName; - if (columnAlias == null) return column; - - referenceName = referenceName == null ? string.Empty : referenceName + "__"; - columnAlias = dbType.EscapeSqlIdentifier(referenceName + columnAlias); - column += " AS " + columnAlias; - return column; - } - - private static string GetTableName(this Type type) + public static string GetTableName(this Type type) { // TODO: returning string.Empty for now // BUT the code bits that calls this method cannot deal with string.Empty so we diff --git a/src/Umbraco.Infrastructure/Persistence/Querying/ExpressionVisitorBase.cs b/src/Umbraco.Infrastructure/Persistence/Querying/ExpressionVisitorBase.cs index 3eb7c5d58b..f399f66aca 100644 --- a/src/Umbraco.Infrastructure/Persistence/Querying/ExpressionVisitorBase.cs +++ b/src/Umbraco.Infrastructure/Persistence/Querying/ExpressionVisitorBase.cs @@ -649,7 +649,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Querying if (m.Arguments.Count == 2) { var n1 = Visit(m.Arguments[0]); - var f = m.Arguments[2]; + var f = m.Arguments[1]; if (!(f is Expression> fl)) throw new NotSupportedException("Expression is not a proper lambda."); var ff = fl.Compile(); diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepository.cs index d42e4bab11..9e2f0257b6 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepository.cs @@ -92,8 +92,8 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement var sqlClause = Sql() .SelectAll() - .From() - .RightJoin() + .From() + .LeftJoin() .On(left => left.Id, right => right.PropertyTypeGroupId) .InnerJoin() .On(left => left.DataTypeId, right => right.NodeId); diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs index 3d2f00d298..4fb347d448 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs @@ -811,21 +811,18 @@ AND umbracoNode.id <> @id", //now we need to insert names into these 2 tables based on the invariant data //insert rows into the versionCultureVariationDto table based on the data from contentVersionDto for the default lang - var cols = Sql().Columns(x => x.VersionId, x => x.Name, - x => x.UpdateUserId, x => x.UpdateDate, x => x.LanguageId); + var cols = Sql().ColumnsForInsert(x => x.VersionId, x => x.Name, x => x.UpdateUserId, x => x.UpdateDate, x => x.LanguageId); sqlSelect = Sql().Select(x => x.Id, x => x.Text, x => x.UserId, x => x.VersionDate) .Append($", {defaultLanguageId}") //default language ID .From() .InnerJoin().On(x => x.NodeId, x => x.NodeId) .Where(x => x.ContentTypeId == contentType.Id); - var sqlInsert = Sql($"INSERT INTO {ContentVersionCultureVariationDto.TableName} ({cols})") - .Append(sqlSelect); + var sqlInsert = Sql($"INSERT INTO {ContentVersionCultureVariationDto.TableName} ({cols})").Append(sqlSelect); Database.Execute(sqlInsert); //insert rows into the documentCultureVariation table - cols = Sql().Columns(x => x.NodeId, x => x.Edited, x => x.Published, - x => x.Name, x => x.Available, x => x.LanguageId); + cols = Sql().ColumnsForInsert(x => x.NodeId, x => x.Edited, x => x.Published, x => x.Name, x => x.Available, x => x.LanguageId); sqlSelect = Sql().Select(x => x.NodeId, x => x.Edited, x => x.Published) .AndSelect(x => x.Text) .Append($", 1, {defaultLanguageId}") //make Available + default language ID @@ -847,7 +844,10 @@ AND umbracoNode.id <> @id", } /// - private void CopyTagData(int? sourceLanguageId, int? targetLanguageId, IReadOnlyCollection propertyTypeIds, + private void CopyTagData( + int? sourceLanguageId, + int? targetLanguageId, + IReadOnlyCollection propertyTypeIds, IReadOnlyCollection? contentTypeIds = null) { // note: important to use SqlNullableEquals for nullable types, cannot directly compare language identifiers @@ -919,7 +919,7 @@ AND umbracoNode.id <> @id", .WhereNull(x => x.Id, "xtags") // ie, not exists .Where(x => x.LanguageId.SqlNullableEquals(sourceLanguageId, -1)); - var cols = Sql().Columns(x => x.Text, x => x.Group, x => x.LanguageId); + var cols = Sql().ColumnsForInsert(x => x.Text, x => x.Group, x => x.LanguageId); var sqlInsertTags = Sql($"INSERT INTO {TagDto.TableName} ({cols})").Append(sqlSelectTagsToInsert); Database.Execute(sqlInsertTags); @@ -951,10 +951,8 @@ AND umbracoNode.id <> @id", .Where(x => x.LanguageId.SqlNullableEquals(sourceLanguageId, -1)) .WhereIn(x => x.PropertyTypeId, propertyTypeIds); - var relationColumnsToInsert = - Sql().Columns(x => x.NodeId, x => x.PropertyTypeId, x => x.TagId); - var sqlInsertRelations = Sql($"INSERT INTO {TagRelationshipDto.TableName} ({relationColumnsToInsert})") - .Append(sqlSelectRelationsToInsert); + var relationColumnsToInsert = Sql().ColumnsForInsert(x => x.NodeId, x => x.PropertyTypeId, x => x.TagId); + var sqlInsertRelations = Sql($"INSERT INTO {TagRelationshipDto.TableName} ({relationColumnsToInsert})").Append(sqlSelectRelationsToInsert); Database.Execute(sqlInsertRelations); @@ -1044,11 +1042,8 @@ AND umbracoNode.id <> @id", //now insert all property data into the target language that exists under the source language var targetLanguageIdS = targetLanguageId.HasValue ? targetLanguageId.ToString() : "NULL"; - var cols = Sql().Columns(x => x.VersionId, x => x.PropertyTypeId, x => x.Segment, - x => x.IntegerValue, x => x.DecimalValue, x => x.DateValue, x => x.VarcharValue, x => x.TextValue, - x => x.LanguageId); - var sqlSelectData = Sql().Select(x => x.VersionId, x => x.PropertyTypeId, x => x.Segment, - x => x.IntegerValue, x => x.DecimalValue, x => x.DateValue, x => x.VarcharValue, x => x.TextValue) + var cols = Sql().ColumnsForInsert(x => x.VersionId, x => x.PropertyTypeId, x => x.Segment, x => x.IntegerValue, x => x.DecimalValue, x => x.DateValue, x => x.VarcharValue, x => x.TextValue, x => x.LanguageId); + var sqlSelectData = Sql().Select(x => x.VersionId, x => x.PropertyTypeId, x => x.Segment, x => x.IntegerValue, x => x.DecimalValue, x => x.DateValue, x => x.VarcharValue, x => x.TextValue) .Append(", " + targetLanguageIdS) //default language ID .From(); diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/CreatedPackageSchemaRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/CreatedPackageSchemaRepository.cs index e3dd6a9d68..6ca327dfab 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/CreatedPackageSchemaRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/CreatedPackageSchemaRepository.cs @@ -39,7 +39,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement private readonly IMacroService _macroService; private readonly IContentTypeService _contentTypeService; private readonly string _tempFolderPath; - private readonly string _mediaFolderPath; + private readonly string _createdPackagesFolderPath; /// /// Initializes a new instance of the class. @@ -76,9 +76,8 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement _macroService = macroService; _contentTypeService = contentTypeService; _xmlParser = new PackageDefinitionXmlParser(); - _mediaFolderPath = mediaFolderPath ?? Path.Combine(globalSettings.Value.UmbracoMediaPhysicalRootPath, Constants.SystemDirectories.CreatedPackages); - _tempFolderPath = - tempFolderPath ?? Constants.SystemDirectories.TempData.EnsureEndsWith('/') + "PackageFiles"; + _createdPackagesFolderPath = mediaFolderPath ?? Constants.SystemDirectories.CreatedPackages; + _tempFolderPath = tempFolderPath ?? Constants.SystemDirectories.TempData + "/PackageFiles"; } public IEnumerable GetAll() @@ -144,7 +143,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement .Delete() .Where(x => x.Id == id); - _umbracoDatabase.Delete(query); + _umbracoDatabase.Execute(query); } public bool SavePackage(PackageDefinition definition) @@ -175,10 +174,8 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement }; // Set the ids, we have to save in database first to get the Id - definition.PackageId = dto.PackageId; - var result = _umbracoDatabase!.Insert(dto); - var decimalResult = result.SafeCast(); - definition.Id = decimal.ToInt32(decimalResult); + _umbracoDatabase!.Insert(dto); + definition.Id = dto.Id; } // Save snapshot locally, we do this to the updated packagePath @@ -199,17 +196,12 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement public string ExportPackage(PackageDefinition definition) { - // Ensure it's valid ValidatePackage(definition); // Create a folder for building this package - var temporaryPath = - _hostingEnvironment.MapPathContentRoot(_tempFolderPath.EnsureEndsWith('/') + Guid.NewGuid()); - if (Directory.Exists(temporaryPath) == false) - { - Directory.CreateDirectory(temporaryPath); - } + var temporaryPath = _hostingEnvironment.MapPathContentRoot(Path.Combine(_tempFolderPath, Guid.NewGuid().ToString())); + Directory.CreateDirectory(temporaryPath); try { @@ -225,8 +217,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement PackageTemplates(definition, root); PackageStylesheets(definition, root); PackageStaticFiles(definition.Scripts, root, "Scripts", "Script", _fileSystems.ScriptsFileSystem!); - PackageStaticFiles(definition.PartialViews, root, "PartialViews", "View", - _fileSystems.PartialViewsFileSystem!); + PackageStaticFiles(definition.PartialViews, root, "PartialViews", "View", _fileSystems.PartialViewsFileSystem!); PackageMacros(definition, root); PackageDictionaryItems(definition, root); PackageLanguages(definition, root); @@ -272,27 +263,25 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement } } - var directoryName = - _hostingEnvironment.MapPathWebRoot( - Path.Combine(_mediaFolderPath, definition.Name.Replace(' ', '_'))); - - if (Directory.Exists(directoryName) == false) - { - Directory.CreateDirectory(directoryName); - } + var directoryName = _hostingEnvironment.MapPathContentRoot(Path.Combine(_createdPackagesFolderPath, definition.Name.Replace(' ', '_'))); + Directory.CreateDirectory(directoryName); var finalPackagePath = Path.Combine(directoryName, fileName); - if (File.Exists(finalPackagePath)) + // Clean existing files + foreach (var packagePath in new[] { - File.Delete(finalPackagePath); - } - - if (File.Exists(finalPackagePath.Replace("zip", "xml"))) - { - File.Delete(finalPackagePath.Replace("zip", "xml")); + definition.PackagePath, + finalPackagePath + }) + { + if (File.Exists(packagePath)) + { + File.Delete(packagePath); + } } + // Move to final package path File.Move(tempPackagePath, finalPackagePath); definition.PackagePath = finalPackagePath; diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepository.cs index 8bf59d67db..ef0f02540e 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepository.cs @@ -356,7 +356,8 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement .On((node, dcv, lang) => node.NodeId == dcv.NodeId && lang.Id == dcv.LanguageId, aliasRight: "dcv") // for selected nodes - .WhereIn(x => x.NodeId, ids); + .WhereIn(x => x.NodeId, ids) + .OrderBy(x => x.Id); } // gets the full sql for a given object type and a given unique id diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ExternalLoginRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ExternalLoginRepository.cs index 6f0f72460e..14b42dc92c 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ExternalLoginRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ExternalLoginRepository.cs @@ -300,7 +300,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement => forUpdate ? Sql() .Select(r => r.Select(x => x.ExternalLoginDto)) .From() - .Append(" WITH (UPDLOCK)") // ensure these table values are locked for updates, the ForUpdate ext method does not work here + .AppendForUpdateHint() // ensure these table values are locked for updates, the ForUpdate ext method does not work here .InnerJoin() .On(x => x.ExternalLoginId, x => x.Id) : Sql() diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RelationRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RelationRepository.cs index 374f4deda5..dde11ead71 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RelationRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RelationRepository.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Linq.Expressions; using Microsoft.Extensions.Logging; using NPoco; using Umbraco.Cms.Core; @@ -175,6 +176,11 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement } public IEnumerable GetPagedParentEntitiesByChildId(int childId, long pageIndex, int pageSize, out long totalRecords, params Guid[] entityTypes) + { + return GetPagedParentEntitiesByChildId(childId, pageIndex, pageSize, out totalRecords, new int[0], entityTypes); + } + + public IEnumerable GetPagedParentEntitiesByChildId(int childId, long pageIndex, int pageSize, out long totalRecords, int[] relationTypes, params Guid[] entityTypes) { // var contentObjectTypes = new[] { Constants.ObjectTypes.Document, Constants.ObjectTypes.Media, Constants.ObjectTypes.Member } // we could pass in the contentObjectTypes so that the entity repository sql is configured to do full entity lookups so that we get the full data @@ -188,10 +194,36 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement sql.Where(rel => rel.ChildId == childId); sql.Where((rel, node) => rel.ParentId == childId || node.NodeId != childId); + + if (relationTypes != null && relationTypes.Any()) + { + sql.WhereIn(rel => rel.RelationType, relationTypes); + } + }); + } + + public IEnumerable GetPagedParentEntitiesByChildIds(int[] childIds, long pageIndex, int pageSize, out long totalRecords, int[] relationTypes, params Guid[] entityTypes) + { + return _entityRepository.GetPagedResultsByQuery(Query(), entityTypes, pageIndex, pageSize, out totalRecords, null, null, sql => + { + SqlJoinRelations(sql); + + sql.WhereIn(rel => rel.ChildId, childIds); + sql.WhereAny(s => s.WhereIn(rel => rel.ParentId, childIds), s => s.WhereNotIn(node => node.NodeId, childIds)); + + if (relationTypes != null && relationTypes.Any()) + { + sql.WhereIn(rel => rel.RelationType, relationTypes); + } }); } public IEnumerable GetPagedChildEntitiesByParentId(int parentId, long pageIndex, int pageSize, out long totalRecords, params Guid[] entityTypes) + { + return GetPagedChildEntitiesByParentId(parentId, pageIndex, pageSize, out totalRecords, new int[0], entityTypes); + } + + public IEnumerable GetPagedChildEntitiesByParentId(int parentId, long pageIndex, int pageSize, out long totalRecords, int[] relationTypes, params Guid[] entityTypes) { // var contentObjectTypes = new[] { Constants.ObjectTypes.Document, Constants.ObjectTypes.Media, Constants.ObjectTypes.Member } // we could pass in the contentObjectTypes so that the entity repository sql is configured to do full entity lookups so that we get the full data @@ -205,9 +237,29 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement sql.Where(rel => rel.ParentId == parentId); sql.Where((rel, node) => rel.ChildId == parentId || node.NodeId != parentId); + + if (relationTypes != null && relationTypes.Any()) + { + sql.WhereIn(rel => rel.RelationType, relationTypes); + } }); } + public IEnumerable GetPagedEntitiesForItemsInRelation(int[] itemIds, long pageIndex, int pageSize, out long totalRecords, params Guid[] entityTypes) + { + return _entityRepository.GetPagedResultsByQuery(Query(), entityTypes, pageIndex, pageSize, out totalRecords, null, null, sql => + { + SqlJoinRelations(sql); + + sql.WhereIn(rel => rel.ChildId, itemIds); + sql.Where((rel, node) => rel.ChildId == node.NodeId); + sql.Where(type => type.IsDependency); + }); + } + + + + public void Save(IEnumerable relations) { foreach (var hasIdentityGroup in relations.GroupBy(r => r.HasIdentity)) @@ -317,8 +369,11 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement public void DeleteByParent(int parentId, params string[] relationTypeAliases) { - if (Database.DatabaseType.IsSqlCe()) + // HACK: SQLite - hard to replace this without provider specific repositories/another ORM. + if (Database.DatabaseType.IsSqlite()) { + var query = Sql().Append(@"delete from umbracoRelation"); + var subQuery = Sql().Select(x => x.Id) .From() .InnerJoin().On(x => x.RelationType, x => x.Id) @@ -329,8 +384,9 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement subQuery.WhereIn(x => x.Alias, relationTypeAliases); } - Database.Execute(Sql().Delete().WhereIn(x => x.Id, subQuery)); + var fullQuery = query.WhereIn(x => x.Id, subQuery); + Database.Execute(fullQuery); } else { @@ -403,4 +459,42 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement sql.OrderByDescending(orderBy); } } + + internal class RelationItemDto + { + [Column(Name = "nodeId")] + public int ChildNodeId { get; set; } + + [Column(Name = "nodeKey")] + public Guid ChildNodeKey { get; set; } + + [Column(Name = "nodeName")] + public string ChildNodeName { get; set; } + + [Column(Name = "nodeObjectType")] + public Guid ChildNodeObjectType { get; set; } + + [Column(Name = "contentTypeIcon")] + public string ChildContentTypeIcon { get; set; } + + [Column(Name = "contentTypeAlias")] + public string ChildContentTypeAlias { get; set; } + + [Column(Name = "contentTypeName")] + public string ChildContentTypeName { get; set; } + + + + [Column(Name = "relationTypeName")] + public string RelationTypeName { get; set; } + + [Column(Name = "relationTypeAlias")] + public string RelationTypeAlias { get; set; } + + [Column(Name = "relationTypeIsDependency")] + public bool RelationTypeIsDependency { get; set; } + + [Column(Name = "relationTypeIsBidirectional")] + public bool RelationTypeIsBidirectional { get; set; } + } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/SimilarNodeName.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/SimilarNodeName.cs index 7021bab9d7..2621461a85 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/SimilarNodeName.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/SimilarNodeName.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Text.RegularExpressions; @@ -51,7 +51,22 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement } } - // no suffix - name without suffix does NOT exist, AND name with suffix does NOT exist + // no suffix - name without suffix does NOT exist - we can just use the name without suffix. + if (!model.Suffix.HasValue && !items.SimpleNameExists(model.Text)) + { + model.Suffix = StructuredName.NO_SUFFIX; + + return model.FullName; + } + + // suffix - name with suffix does NOT exist + // We can just return the full name as it is as there's no conflict. + if (model.Suffix.HasValue && !items.SimpleNameExists(model.FullName)) + { + return model.FullName; + } + + // no suffix - name without suffix does NOT exist, AND name with suffix does NOT exist if (!model.Suffix.HasValue && !items.SimpleNameExists(model.Text) && !items.SuffixedNameExists()) { model.Suffix = StructuredName.NO_SUFFIX; @@ -163,7 +178,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement internal static readonly uint? NO_SUFFIX = default; internal string Text { get; set; } - internal uint ? Suffix { get; set; } + internal uint? Suffix { get; set; } public string FullName { get diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TagRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TagRepository.cs index fd052d2741..ca171a9b01 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TagRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TagRepository.cs @@ -210,7 +210,16 @@ WHERE r.tagId IS NULL"; sql.Append(" UNION "); } - sql.Append("SELECT N'"); + // HACK: SQLite (or rather SQL server setup was a hack) + if (SqlContext.DatabaseType.IsSqlServer()) + { + sql.Append("SELECT N'"); + } + else + { + sql.Append("SELECT '"); + } + sql.Append(SqlSyntax.EscapeString(tag.Text)); sql.Append("' AS tag, '"); sql.Append(SqlSyntax.EscapeString(tag.Group)); diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TrackedReferencesRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TrackedReferencesRepository.cs new file mode 100644 index 0000000000..d92f0903f1 --- /dev/null +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TrackedReferencesRepository.cs @@ -0,0 +1,177 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NPoco; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Persistence; +using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Infrastructure.Scoping; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +{ + internal class TrackedReferencesRepository : ITrackedReferencesRepository + { + private readonly IScopeAccessor _scopeAccessor; + + public TrackedReferencesRepository(IScopeAccessor scopeAccessor) + { + _scopeAccessor = scopeAccessor; + } + + public IEnumerable GetPagedItemsWithRelations(int[] ids, long pageIndex, int pageSize, + bool filterMustBeIsDependency, out long totalRecords) + { + var sql = _scopeAccessor.AmbientScope.Database.SqlContext.Sql().Select( + "[pn].[id] as nodeId", + "[pn].[uniqueId] as nodeKey", + "[pn].[text] as nodeName", + "[pn].[nodeObjectType] as nodeObjectType", + "[ct].[icon] as contentTypeIcon", + "[ct].[alias] as contentTypeAlias", + "[ctn].[text] as contentTypeName", + "[umbracoRelationType].[alias] as relationTypeAlias", + "[umbracoRelationType].[name] as relationTypeName", + "[umbracoRelationType].[isDependency] as relationTypeIsDependency", + "[umbracoRelationType].[dual] as relationTypeIsBidirectional") + .From("r") + .InnerJoin("umbracoRelationType").On((left, right) => left.RelationType == right.Id, aliasLeft: "r", aliasRight:"umbracoRelationType") + .InnerJoin("cn").On((r, cn, rt) => (!rt.Dual && r.ParentId == cn.NodeId) || (rt.Dual && (r.ChildId == cn.NodeId || r.ParentId == cn.NodeId )), aliasLeft: "r", aliasRight:"cn", aliasOther: "umbracoRelationType" ) + .InnerJoin("pn").On((r, pn, cn) => (pn.NodeId == r.ChildId && cn.NodeId == r.ParentId) || (pn.NodeId == r.ParentId && cn.NodeId == r.ChildId), aliasLeft: "r", aliasRight:"pn", aliasOther:"cn" ) + .LeftJoin("c").On((left, right) => left.NodeId == right.NodeId, aliasLeft:"pn", aliasRight:"c") + .LeftJoin("ct").On((left, right) => left.ContentTypeId == right.NodeId, aliasLeft:"c", aliasRight:"ct") + .LeftJoin("ctn").On((left, right) => left.NodeId == right.NodeId, aliasLeft:"ct", aliasRight:"ctn"); + + if (ids.Any()) + { + sql = sql.Where(x => ids.Contains(x.NodeId), "pn"); + } + + if (filterMustBeIsDependency) + { + sql = sql.Where(rt => rt.IsDependency, "umbracoRelationType"); + } + + // Ordering is required for paging + sql = sql.OrderBy(x => x.Alias); + + var pagedResult = _scopeAccessor.AmbientScope.Database.Page(pageIndex + 1, pageSize, sql); + totalRecords = Convert.ToInt32(pagedResult.TotalItems); + + return pagedResult.Items.Select(MapDtoToEntity); + } + + public IEnumerable GetPagedDescendantsInReferences(int parentId, long pageIndex, int pageSize, bool filterMustBeIsDependency, + out long totalRecords) + { + var syntax = _scopeAccessor.AmbientScope.Database.SqlContext.SqlSyntax; + + // Gets the path of the parent with ",%" added + var subsubQuery = _scopeAccessor.AmbientScope.Database.SqlContext.Sql() + .Select(syntax.GetConcat("[node].[path]", "',%'")) + .From("node") + .Where(x => x.NodeId == parentId, "node"); + + + // Gets the descendants of the parent node + Sql subQuery = _scopeAccessor.AmbientScope.Database.SqlContext.Sql() + .Select(x => x.NodeId) + .From() + .WhereLike(x => x.Path, subsubQuery); + + // Get all relations where parent is in the sub query + var sql = _scopeAccessor.AmbientScope.Database.SqlContext.Sql().Select( + "[pn].[id] as nodeId", + "[pn].[uniqueId] as nodeKey", + "[pn].[text] as nodeName", + "[pn].[nodeObjectType] as nodeObjectType", + "[ct].[icon] as contentTypeIcon", + "[ct].[alias] as contentTypeAlias", + "[ctn].[text] as contentTypeName", + "[umbracoRelationType].[alias] as relationTypeAlias", + "[umbracoRelationType].[name] as relationTypeName", + "[umbracoRelationType].[isDependency] as relationTypeIsDependency", + "[umbracoRelationType].[dual] as relationTypeIsBidirectional") + .From("r") + .InnerJoin("umbracoRelationType").On((left, right) => left.RelationType == right.Id, aliasLeft: "r", aliasRight:"umbracoRelationType") + .InnerJoin("cn").On((r, cn, rt) => (!rt.Dual && r.ParentId == cn.NodeId) || (rt.Dual && (r.ChildId == cn.NodeId || r.ParentId == cn.NodeId )), aliasLeft: "r", aliasRight:"cn", aliasOther: "umbracoRelationType" ) + .InnerJoin("pn").On((r, pn, cn) => (pn.NodeId == r.ChildId && cn.NodeId == r.ParentId) || (pn.NodeId == r.ParentId && cn.NodeId == r.ChildId), aliasLeft: "r", aliasRight:"pn", aliasOther:"cn" ) + .LeftJoin("c").On((left, right) => left.NodeId == right.NodeId, aliasLeft:"pn", aliasRight:"c") + .LeftJoin("ct").On((left, right) => left.ContentTypeId == right.NodeId, aliasLeft:"c", aliasRight:"ct") + .LeftJoin("ctn").On((left, right) => left.NodeId == right.NodeId, aliasLeft:"ct", aliasRight:"ctn") + .WhereIn((System.Linq.Expressions.Expression>)(x => x.NodeId), subQuery, "pn"); + if (filterMustBeIsDependency) + { + sql = sql.Where(rt => rt.IsDependency, "umbracoRelationType"); + } + // Ordering is required for paging + sql = sql.OrderBy(x => x.Alias); + + var pagedResult = _scopeAccessor.AmbientScope.Database.Page(pageIndex + 1, pageSize, sql); + totalRecords = Convert.ToInt32(pagedResult.TotalItems); + + return pagedResult.Items.Select(MapDtoToEntity); + } + + public IEnumerable GetPagedRelationsForItems(int[] ids, long pageIndex, int pageSize, bool filterMustBeIsDependency, out long totalRecords) + { + var sql = _scopeAccessor.AmbientScope.Database.SqlContext.Sql().Select( + "[cn].[id] as nodeId", + "[cn].[uniqueId] as nodeKey", + "[cn].[text] as nodeName", + "[cn].[nodeObjectType] as nodeObjectType", + "[ct].[icon] as contentTypeIcon", + "[ct].[alias] as contentTypeAlias", + "[ctn].[text] as contentTypeName", + "[umbracoRelationType].[alias] as relationTypeAlias", + "[umbracoRelationType].[name] as relationTypeName", + "[umbracoRelationType].[isDependency] as relationTypeIsDependency", + "[umbracoRelationType].[dual] as relationTypeIsBidirectional") + .From("r") + .InnerJoin("umbracoRelationType").On((left, right) => left.RelationType == right.Id, aliasLeft: "r", aliasRight:"umbracoRelationType") + .InnerJoin("cn").On((r, cn, rt) => (!rt.Dual && r.ParentId == cn.NodeId) || (rt.Dual && (r.ChildId == cn.NodeId || r.ParentId == cn.NodeId )), aliasLeft: "r", aliasRight:"cn", aliasOther: "umbracoRelationType" ) + .InnerJoin("pn").On((r, pn, cn) => (pn.NodeId == r.ChildId && cn.NodeId == r.ParentId) || (pn.NodeId == r.ParentId && cn.NodeId == r.ChildId), aliasLeft: "r", aliasRight:"pn", aliasOther:"cn" ) + .LeftJoin("c").On((left, right) => left.NodeId == right.NodeId, aliasLeft:"cn", aliasRight:"c") + .LeftJoin("ct").On((left, right) => left.ContentTypeId == right.NodeId, aliasLeft:"c", aliasRight:"ct") + .LeftJoin("ctn").On((left, right) => left.NodeId == right.NodeId, aliasLeft:"ct", aliasRight:"ctn"); + + if (ids.Any()) + { + sql = sql.Where(x => ids.Contains(x.NodeId), "pn"); + } + + if (filterMustBeIsDependency) + { + sql = sql.Where(rt => rt.IsDependency, "umbracoRelationType"); + } + + // Ordering is required for paging + sql = sql.OrderBy(x => x.Alias); + + var pagedResult = _scopeAccessor.AmbientScope.Database.Page(pageIndex + 1, pageSize, sql); + totalRecords = Convert.ToInt32(pagedResult.TotalItems); + + return pagedResult.Items.Select(MapDtoToEntity); + } + + private RelationItem MapDtoToEntity(RelationItemDto dto) + { + var type = ObjectTypes.GetUdiType(dto.ChildNodeObjectType); + return new RelationItem() + { + NodeId = dto.ChildNodeId, + NodeKey = dto.ChildNodeKey, + NodeType = ObjectTypes.GetUdiType(dto.ChildNodeObjectType), + NodeName = dto.ChildNodeName, + RelationTypeName = dto.RelationTypeName, + RelationTypeIsBidirectional = dto.RelationTypeIsBidirectional, + RelationTypeIsDependency = dto.RelationTypeIsDependency, + ContentTypeAlias = dto.ChildContentTypeAlias, + ContentTypeIcon = dto.ChildContentTypeIcon, + ContentTypeName = dto.ChildContentTypeName, + }; + } + } +} diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs index e424565f32..7fce3581d1 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs @@ -225,10 +225,10 @@ SELECT 4 AS [Key], COUNT(id) AS [Value] FROM umbracoUser WHERE userDisabled = 0 // that query is going to run a *lot*, make it a template var t = SqlContext.Templates.Get("Umbraco.Core.UserRepository.ValidateLoginSession", s => s .Select() - .SelectTop(1) .From() .Where(x => x.SessionId == SqlTemplate.Arg("sessionId")) - .ForUpdate()); + .ForUpdate() + .SelectTop(1)); // Stick at end, SQL server syntax provider will insert at start of query after "select ", but sqlite will append limit to end. var sql = t.Sql(sessionId); diff --git a/src/Umbraco.Infrastructure/Persistence/ScalarMapper.cs b/src/Umbraco.Infrastructure/Persistence/ScalarMapper.cs new file mode 100644 index 0000000000..9b6eada924 --- /dev/null +++ b/src/Umbraco.Infrastructure/Persistence/ScalarMapper.cs @@ -0,0 +1,14 @@ +using System; + +namespace Umbraco.Cms.Infrastructure.Persistence; + +public abstract class ScalarMapper : IScalarMapper +{ + /// + /// Performs a strongly typed mapping operation for a scalar value. + /// + protected abstract T Map(object value); + + /// + object IScalarMapper.Map(object value) => Map(value); +} diff --git a/src/Umbraco.Infrastructure/Persistence/SqlServerDbProviderFactoryCreator.cs b/src/Umbraco.Infrastructure/Persistence/SqlServerDbProviderFactoryCreator.cs index 8dd45283e7..e69de29bb2 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlServerDbProviderFactoryCreator.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlServerDbProviderFactoryCreator.cs @@ -1,53 +0,0 @@ -using System; -using System.Data.Common; -using System.Linq; -using Microsoft.Extensions.Options; -using NPoco; -using Umbraco.Cms.Core.Configuration.Models; -using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; - -namespace Umbraco.Cms.Infrastructure.Persistence -{ - [Obsolete("This is only used for integration tests and should be moved into a test project.")] - public class SqlServerDbProviderFactoryCreator : IDbProviderFactoryCreator - { - private readonly Func _getFactory; - private readonly IOptions _globalSettings; - - public SqlServerDbProviderFactoryCreator(Func getFactory, IOptions globalSettings) - { - _getFactory = getFactory; - _globalSettings = globalSettings; - } - - public DbProviderFactory? CreateFactory(string? providerName) - { - if (string.IsNullOrEmpty(providerName)) return null; - - return _getFactory(providerName); - } - - // gets the sql syntax provider that corresponds, from attribute - public ISqlSyntaxProvider GetSqlSyntaxProvider(string providerName) - => providerName switch - { - Cms.Core.Constants.DbProviderNames.SqlCe => throw new NotSupportedException("SqlCe is not supported"), - Cms.Core.Constants.DbProviderNames.SqlServer => new SqlServerSyntaxProvider(_globalSettings), - _ => throw new InvalidOperationException($"Unknown provider name \"{providerName}\""), - }; - - public IBulkSqlInsertProvider CreateBulkSqlInsertProvider(string providerName) - => providerName switch - { - Cms.Core.Constants.DbProviderNames.SqlCe => throw new NotSupportedException("SqlCe is not supported"), - Cms.Core.Constants.DbProviderNames.SqlServer => new SqlServerBulkSqlInsertProvider(), - _ => new BasicBulkSqlInsertProvider(), - }; - - public void CreateDatabase(string providerName, string connectionString) - => throw new NotSupportedException("Embedded databases are not supported"); // TODO But LocalDB is? - - public NPocoMapperCollection ProviderSpecificMappers(string providerName) - => new NPocoMapperCollection(() => Enumerable.Empty()); - } -} diff --git a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/ColumnInfo.cs b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/ColumnInfo.cs index f177a25bc4..fed6e221b5 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/ColumnInfo.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/ColumnInfo.cs @@ -21,6 +21,15 @@ DataType = dataType; } + public ColumnInfo(string tableName, string columnName, int ordinal, bool isNullable, string dataType) + { + TableName = tableName; + ColumnName = columnName; + Ordinal = ordinal; + IsNullable = isNullable; + DataType = dataType; + } + public string TableName { get; set; } public string ColumnName { get; set; } public int Ordinal { get; set; } diff --git a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/ISqlSyntaxProvider.cs b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/ISqlSyntaxProvider.cs index e3de457894..316758a6f8 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/ISqlSyntaxProvider.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/ISqlSyntaxProvider.cs @@ -1,13 +1,13 @@ using System; using System.Collections.Generic; using System.Data; +using System.Linq.Expressions; using System.Diagnostics.CodeAnalysis; using System.Text.RegularExpressions; using NPoco; using Umbraco.Cms.Core.Persistence; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -using Umbraco.Cms.Infrastructure.Persistence.Querying; namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax { @@ -16,6 +16,8 @@ namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax /// public interface ISqlSyntaxProvider { + DatabaseType GetUpdatedDatabaseType(DatabaseType current, string connectionString); + string ProviderName { get; } string EscapeString(string val); @@ -25,6 +27,8 @@ namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax string GetStringColumnWildcardComparison(string column, int paramIndex, TextColumnType columnType); string GetConcat(params string[] args); + string GetColumn(DatabaseType dbType, string tableName, string columnName, string columnAlias, string referenceName = null, bool forInsert = false); + string GetQuotedTableName(string? tableName); string GetQuotedColumnName(string? columnName); string GetQuotedName(string? name); @@ -65,6 +69,8 @@ namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax string FormatColumnRename(string? tableName, string? oldName, string? newName); string FormatTableRename(string? oldName, string? newName); + void HandleCreateTable(IDatabase database, TableDefinition tableDefinition, bool skipKeysAndIndexes = false); + /// /// Gets a regex matching aliased fields. /// @@ -127,16 +133,33 @@ namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax /// The constraint name. /// A value indicating whether a default constraint was found. /// - /// Some database engines (e.g. SqlCe) may not have names for default constraints, + /// Some database engines may not have names for default constraints, /// in which case the function may return true, but is /// unspecified. /// bool TryGetDefaultConstraint(IDatabase db, string? tableName, string columnName, [MaybeNullWhen(false)] out string constraintName); - void ReadLock(IDatabase db, TimeSpan timeout, int lockId); - void WriteLock(IDatabase db, TimeSpan timeout, int lockId); - void ReadLock(IDatabase db, params int[] lockIds); - void WriteLock(IDatabase db, params int[] lockIds); + string GetFieldNameForUpdate(Expression> fieldSelector, string tableAlias = null); + + /// + /// Appends the relevant ForUpdate hint. + /// + Sql InsertForUpdateHint(Sql sql); + + /// + /// Appends the relevant ForUpdate hint. + /// + Sql AppendForUpdateHint(Sql sql); + + /// + /// Handles left join with nested join + /// + Sql.SqlJoinClause LeftJoinWithNestedJoin( + Sql sql, + Func, Sql> nestedJoin, + string alias = null); + + IDictionary ScalarMappers { get; } } } diff --git a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs index fd07c46685..da97287493 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs @@ -4,6 +4,7 @@ using System.Data; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; +using System.Linq.Expressions; using System.Text; using System.Text.RegularExpressions; using NPoco; @@ -61,7 +62,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax public string GetWildcardPlaceholder() => "%"; public string StringLengthNonUnicodeColumnDefinitionFormat { get; } = "VARCHAR({0})"; - public string StringLengthUnicodeColumnDefinitionFormat { get; } = "NVARCHAR({0})"; + public virtual string StringLengthUnicodeColumnDefinitionFormat { get; } = "NVARCHAR({0})"; public string DecimalColumnDefinitionFormat { get; } = "DECIMAL({0},{1})"; public string DefaultValueFormat { get; } = "DEFAULT ({0})"; @@ -70,7 +71,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax public int DefaultDecimalScale { get; } = 9; //Set by Constructor - public string StringColumnDefinition { get; } + public virtual string StringColumnDefinition { get; } public string StringLengthColumnDefinitionFormat { get; } public string AutoIncrementDefinition { get; protected set; } = "AUTOINCREMENT"; @@ -138,6 +139,8 @@ namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax return dbTypeMap.Create(); } + public virtual DatabaseType GetUpdatedDatabaseType(DatabaseType current, string connectionString) => current; + public abstract string ProviderName { get; } public virtual string EscapeString(string val) @@ -217,6 +220,22 @@ namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax return "NVARCHAR"; } + public virtual string GetSpecialDbType(SpecialDbType dbType, int customSize) => $"{GetSpecialDbType(dbType)}({customSize})"; + + public virtual string GetColumn(DatabaseType dbType, string tableName, string columnName, string columnAlias, string referenceName = null, bool forInsert = false) + { + tableName = GetQuotedTableName(tableName); + columnName = GetQuotedColumnName(columnName); + var column = tableName + "." + columnName; + if (columnAlias == null) return column; + + referenceName = referenceName == null ? string.Empty : referenceName + "__"; + columnAlias = GetQuotedColumnName(referenceName + columnAlias); + column += " AS " + columnAlias; + return column; + } + + public abstract IsolationLevel DefaultIsolationLevel { get; } public abstract string DbProvider { get; } @@ -244,16 +263,18 @@ namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax public abstract bool TryGetDefaultConstraint(IDatabase db, string? tableName, string columnName, [MaybeNullWhen(false)] out string constraintName); - public abstract void ReadLock(IDatabase db, params int[] lockIds); - public abstract void WriteLock(IDatabase db, params int[] lockIds); - public abstract void ReadLock(IDatabase db, TimeSpan timeout, int lockId); + public virtual string GetFieldNameForUpdate(Expression> fieldSelector, string tableAlias = null) => this.GetFieldName(fieldSelector, tableAlias); - public abstract void WriteLock(IDatabase db, TimeSpan timeout, int lockId); + public virtual Sql InsertForUpdateHint(Sql sql) => sql; - public virtual bool DoesTableExist(IDatabase db, string tableName) - { - return false; - } + public virtual Sql AppendForUpdateHint(Sql sql) => sql; + + public abstract Sql.SqlJoinClause LeftJoinWithNestedJoin(Sql sql, Func, Sql> nestedJoin, string alias = null); + + + public virtual IDictionary ScalarMappers => null; + + public virtual bool DoesTableExist(IDatabase db, string tableName) => GetTablesInSchema(db).Contains(tableName); public virtual bool SupportsClustered() { @@ -480,7 +501,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax { if (column.Size != default) { - return $"{GetSpecialDbType(column.CustomDbType.Value)}({column.Size})"; + return GetSpecialDbType(column.CustomDbType.Value, column.Size); } return GetSpecialDbType(column.CustomDbType.Value); @@ -555,6 +576,8 @@ namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax public abstract Sql SelectTop(Sql sql, int top); + public abstract void HandleCreateTable(IDatabase database, TableDefinition tableDefinition, bool skipKeysAndIndexes = false); + public virtual string DeleteDefaultConstraint => throw new NotSupportedException("Default constraints are not supported"); public virtual string CreateTable => "CREATE TABLE {0} ({1})"; diff --git a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderExtensions.cs b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderExtensions.cs index 9a28686ced..6c816f7c92 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderExtensions.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderExtensions.cs @@ -27,7 +27,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax /// /// This is used to generate a delete query that uses a sub-query to select the data, it is required because there's a very particular syntax that - /// needs to be used to work for all servers: SQLCE and MSSQL + /// needs to be used to work for all servers /// /// /// diff --git a/src/Umbraco.Infrastructure/Persistence/SqlSyntaxExtensions.cs b/src/Umbraco.Infrastructure/Persistence/SqlSyntaxExtensions.cs index 7f66b75880..fca90e9048 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlSyntaxExtensions.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlSyntaxExtensions.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq.Expressions; using System.Reflection; using NPoco; @@ -12,15 +12,6 @@ namespace Umbraco.Extensions /// public static class SqlSyntaxExtensions { - private static string GetTableName(this Type type) - { - // TODO: returning string.Empty for now - // BUT the code bits that calls this method cannot deal with string.Empty so we - // should either throw, or fix these code bits... - var attr = type.FirstAttribute(); - return string.IsNullOrWhiteSpace(attr?.Value) ? string.Empty : attr.Value; - } - private static string GetColumnName(this PropertyInfo column) { var attr = column.FirstAttribute(); diff --git a/src/Umbraco.Infrastructure/Persistence/UmbracoDatabase.cs b/src/Umbraco.Infrastructure/Persistence/UmbracoDatabase.cs index 30a97313da..4dd0c20aed 100644 --- a/src/Umbraco.Infrastructure/Persistence/UmbracoDatabase.cs +++ b/src/Umbraco.Infrastructure/Persistence/UmbracoDatabase.cs @@ -27,8 +27,6 @@ namespace Umbraco.Cms.Infrastructure.Persistence private readonly ILogger _logger; private readonly IBulkSqlInsertProvider? _bulkSqlInsertProvider; private readonly DatabaseSchemaCreatorFactory? _databaseSchemaCreatorFactory; - private readonly RetryPolicy? _connectionRetryPolicy; - private readonly RetryPolicy? _commandRetryPolicy; private readonly IEnumerable? _mapperCollection; private readonly Guid _instanceGuid = Guid.NewGuid(); private List? _commands; @@ -49,8 +47,6 @@ namespace Umbraco.Cms.Infrastructure.Persistence ILogger logger, IBulkSqlInsertProvider? bulkSqlInsertProvider, DatabaseSchemaCreatorFactory databaseSchemaCreatorFactory, - RetryPolicy? connectionRetryPolicy = null, - RetryPolicy? commandRetryPolicy = null, IEnumerable? mapperCollection = null) : base(connectionString, sqlContext.DatabaseType, provider, sqlContext.SqlSyntax.DefaultIsolationLevel) { @@ -58,8 +54,6 @@ namespace Umbraco.Cms.Infrastructure.Persistence _logger = logger; _bulkSqlInsertProvider = bulkSqlInsertProvider; _databaseSchemaCreatorFactory = databaseSchemaCreatorFactory; - _connectionRetryPolicy = connectionRetryPolicy; - _commandRetryPolicy = commandRetryPolicy; _mapperCollection = mapperCollection; Init(); @@ -99,27 +93,6 @@ namespace Umbraco.Cms.Infrastructure.Persistence /// public ISqlContext SqlContext { get; } - #region Temp - - // work around NPoco issue https://github.com/schotime/NPoco/issues/517 while we wait for the fix - public override DbCommand CreateCommand(DbConnection connection, CommandType commandType, string sql, params object[] args) - { - var command = base.CreateCommand(connection, commandType, sql, args); - - if (!DatabaseType.IsSqlCe()) - return command; - - foreach (DbParameter parameter in command.Parameters) - { - if (parameter.Value == DBNull.Value) - parameter.DbType = DbType.String; - } - - return command; - } - - #endregion - #region Testing, Debugging and Troubleshooting private bool _enableCount; @@ -218,12 +191,14 @@ namespace Umbraco.Cms.Infrastructure.Persistence #region OnSomething - // TODO: has new interceptors to replace OnSomething? - protected override DbConnection OnConnectionOpened(DbConnection connection) { - if (connection == null) throw new ArgumentNullException(nameof(connection)); + if (connection == null) + { + throw new ArgumentNullException(nameof(connection)); + } + // TODO: this should probably move to a SQL Server ProviderSpecificInterceptor. #if DEBUG_DATABASES // determines the database connection SPID for debugging if (DatabaseType.IsSqlServer()) @@ -239,15 +214,8 @@ namespace Umbraco.Cms.Infrastructure.Persistence // includes SqlCE _spid = 0; } + #endif - - // wrap the connection with a profiling connection that tracks timings - connection = new StackExchange.Profiling.Data.ProfiledDbConnection(connection, MiniProfiler.Current); - - // wrap the connection with a retrying connection - if (_connectionRetryPolicy != null || _commandRetryPolicy != null) - connection = new RetryDbConnection(connection, _connectionRetryPolicy, _commandRetryPolicy); - return connection; } @@ -354,5 +322,33 @@ namespace Umbraco.Cms.Infrastructure.Persistence public DbType DbType { get; } public int Size { get; } } + + /// + public new T ExecuteScalar(string sql, params object[] args) + => ExecuteScalar(new Sql(sql, args)); + + /// + public new T ExecuteScalar(Sql sql) + => ExecuteScalar(sql.SQL, CommandType.Text, sql.Arguments); + + /// + /// + /// Be nice if handled upstream GH issue + /// + public new T ExecuteScalar(string sql, CommandType commandType, params object[] args) + { + if (SqlContext.SqlSyntax.ScalarMappers == null) + { + return base.ExecuteScalar(sql, commandType, args); + } + + if (!SqlContext.SqlSyntax.ScalarMappers.TryGetValue(typeof(T), out IScalarMapper mapper)) + { + return base.ExecuteScalar(sql, commandType, args); + } + + var result = base.ExecuteScalar(sql, commandType, args); + return (T)mapper.Map(result); + } } } diff --git a/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseFactory.cs b/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseFactory.cs index 95c0f01093..137f4ddb62 100644 --- a/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseFactory.cs @@ -43,17 +43,16 @@ namespace Umbraco.Cms.Infrastructure.Persistence private DatabaseFactory? _npocoDatabaseFactory; private IPocoDataFactory? _pocoDataFactory; - private string? _providerName; private DatabaseType? _databaseType; private ISqlSyntaxProvider? _sqlSyntax; private IBulkSqlInsertProvider? _bulkSqlInsertProvider; - private RetryPolicy? _connectionRetryPolicy; - private RetryPolicy? _commandRetryPolicy; private NPoco.MapperCollection? _pocoMappers; private SqlContext _sqlContext = null!; private bool _upgrading; private bool _initialized; + private ConnectionStrings _umbracoConnectionString; + private DbProviderFactory? _dbProviderFactory = null; private DbProviderFactory? DbProviderFactory @@ -62,9 +61,9 @@ namespace Umbraco.Cms.Infrastructure.Persistence { if (_dbProviderFactory == null) { - _dbProviderFactory = string.IsNullOrWhiteSpace(_providerName) + _dbProviderFactory = string.IsNullOrWhiteSpace(ProviderName) ? null - : _dbProviderFactoryCreator.CreateFactory(_providerName); + : _dbProviderFactoryCreator.CreateFactory(ProviderName); } return _dbProviderFactory; @@ -74,45 +73,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence #region Constructors - /// - /// Initializes a new instance of the . - /// - /// Used by the other ctor and in tests. - internal UmbracoDatabaseFactory( - ILogger logger, - ILoggerFactory loggerFactory, - IOptions globalSettings, - IMapperCollection mappers, - IDbProviderFactoryCreator dbProviderFactoryCreator, - DatabaseSchemaCreatorFactory databaseSchemaCreatorFactory, - NPocoMapperCollection npocoMappers, - string? connectionString) - { - _globalSettings = globalSettings; - _mappers = mappers ?? throw new ArgumentNullException(nameof(mappers)); - _dbProviderFactoryCreator = dbProviderFactoryCreator ?? throw new ArgumentNullException(nameof(dbProviderFactoryCreator)); - _databaseSchemaCreatorFactory = databaseSchemaCreatorFactory ?? throw new ArgumentNullException(nameof(databaseSchemaCreatorFactory)); - _npocoMappers = npocoMappers; - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _loggerFactory = loggerFactory; - if (connectionString is null) - { - logger.LogDebug("Missing connection string, defer configuration."); - return; // not configured - } - - var configConnectionString = new ConfigConnectionString("Custom", connectionString); - // could as well be - // so need to test the values too - if (configConnectionString.IsConnectionStringConfigured() == false) - { - logger.LogDebug("Empty connection string or provider name, defer configuration."); - return; // not configured - } - - Configure(configConnectionString.ConnectionString!, configConnectionString.ProviderName!); - } /// /// Initializes a new instance of the . @@ -126,10 +87,24 @@ namespace Umbraco.Cms.Infrastructure.Persistence IMapperCollection mappers, IDbProviderFactoryCreator dbProviderFactoryCreator, DatabaseSchemaCreatorFactory databaseSchemaCreatorFactory, - NPocoMapperCollection npocoMappers): - this(logger, loggerFactory, globalSettings, mappers, dbProviderFactoryCreator, databaseSchemaCreatorFactory, npocoMappers, connectionStrings?.CurrentValue?.UmbracoConnectionString?.ConnectionString) + NPocoMapperCollection npocoMappers) { + _globalSettings = globalSettings; + _mappers = mappers ?? throw new ArgumentNullException(nameof(mappers)); + _dbProviderFactoryCreator = dbProviderFactoryCreator ?? throw new ArgumentNullException(nameof(dbProviderFactoryCreator)); + _databaseSchemaCreatorFactory = databaseSchemaCreatorFactory ?? throw new ArgumentNullException(nameof(databaseSchemaCreatorFactory)); + _npocoMappers = npocoMappers; + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _loggerFactory = loggerFactory; + ConnectionStrings umbracoConnectionString = connectionStrings.Get(Constants.System.UmbracoConnectionName); + if (!umbracoConnectionString.IsConnectionStringConfigured()) + { + logger.LogDebug("Missing connection string, defer configuration."); + return; // not configured + } + + Configure(umbracoConnectionString); } #endregion @@ -141,7 +116,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence { lock (_lock) { - return !ConnectionString.IsNullOrWhiteSpace() && !_providerName.IsNullOrWhiteSpace(); + return !ConnectionString.IsNullOrWhiteSpace() && !ProviderName.IsNullOrWhiteSpace(); } } } @@ -150,52 +125,17 @@ namespace Umbraco.Cms.Infrastructure.Persistence public bool Initialized => Volatile.Read(ref _initialized); /// - public string? ConnectionString { get; private set; } + public string? ConnectionString => _umbracoConnectionString?.ConnectionString; /// - public string? ProviderName => _providerName; + public string? ProviderName => _umbracoConnectionString?.ProviderName; /// public bool CanConnect => // actually tries to connect to the database (regardless of configured/initialized) - !ConnectionString.IsNullOrWhiteSpace() && !_providerName.IsNullOrWhiteSpace() && + !ConnectionString.IsNullOrWhiteSpace() && !ProviderName.IsNullOrWhiteSpace() && DbConnectionExtensions.IsConnectionAvailable(ConnectionString, DbProviderFactory); - private void UpdateSqlServerDatabaseType() - { - // replace NPoco database type by a more efficient one - var setting = _globalSettings.Value.DatabaseFactoryServerVersion; - var fromSettings = false; - - if (setting.IsNullOrWhiteSpace() || !setting.StartsWith("SqlServer.") - || !Enum.TryParse(setting.Substring("SqlServer.".Length), out var versionName, true)) - { - versionName = ((SqlServerSyntaxProvider?) _sqlSyntax)!.GetSetVersion(ConnectionString, _providerName, _logger).ProductVersionName; - } - else - { - fromSettings = true; - } - - switch (versionName) - { - case SqlServerSyntaxProvider.VersionName.V2008: - _databaseType = DatabaseType.SqlServer2008; - break; - case SqlServerSyntaxProvider.VersionName.V2012: - case SqlServerSyntaxProvider.VersionName.V2014: - case SqlServerSyntaxProvider.VersionName.V2016: - case SqlServerSyntaxProvider.VersionName.V2017: - case SqlServerSyntaxProvider.VersionName.V2019: - _databaseType = DatabaseType.SqlServer2012; - break; - // else leave unchanged - } - - _logger.LogDebug("SqlServer {SqlServerVersion}, DatabaseType is {DatabaseType} ({Source}).", - versionName, _databaseType, fromSettings ? "settings" : "detected"); - } - /// public ISqlContext SqlContext { @@ -224,18 +164,21 @@ namespace Umbraco.Cms.Infrastructure.Persistence public void ConfigureForUpgrade() => _upgrading = true; /// - public void Configure(string? connectionString, string? providerName) + public void Configure(ConnectionStrings umbracoConnectionString) { - if (connectionString.IsNullOrWhiteSpace()) throw new ArgumentNullException(nameof(connectionString)); - if (providerName.IsNullOrWhiteSpace()) throw new ArgumentNullException(nameof(providerName)); + if (umbracoConnectionString is null) + { + throw new ArgumentNullException(nameof(umbracoConnectionString)); + } lock (_lock) { if (Volatile.Read(ref _initialized)) + { throw new InvalidOperationException("Already initialized."); + } - ConnectionString = connectionString; - _providerName = providerName; + _umbracoConnectionString = umbracoConnectionString; } // rest to be lazy-initialized @@ -252,37 +195,31 @@ namespace Umbraco.Cms.Infrastructure.Persistence throw new InvalidOperationException("The factory has not been configured with a proper connection string."); } - if (_providerName.IsNullOrWhiteSpace()) + if (ProviderName.IsNullOrWhiteSpace()) { throw new InvalidOperationException("The factory has not been configured with a proper provider name."); } if (DbProviderFactory == null) { - throw new Exception($"Can't find a provider factory for provider name \"{_providerName}\"."); + throw new Exception($"Can't find a provider factory for provider name \"{ProviderName}\"."); } - _connectionRetryPolicy = RetryPolicyFactory.GetDefaultSqlConnectionRetryPolicyByConnectionString(ConnectionString!); - _commandRetryPolicy = RetryPolicyFactory.GetDefaultSqlCommandRetryPolicyByConnectionString(ConnectionString!); - - _databaseType = DatabaseType.Resolve(DbProviderFactory.GetType().Name, _providerName); + _databaseType = DatabaseType.Resolve(DbProviderFactory.GetType().Name, ProviderName); if (_databaseType == null) { - throw new Exception($"Can't find an NPoco database type for provider name \"{_providerName}\"."); + throw new Exception($"Can't find an NPoco database type for provider name \"{ProviderName}\"."); } - _sqlSyntax = _dbProviderFactoryCreator.GetSqlSyntaxProvider(_providerName!); + _sqlSyntax = _dbProviderFactoryCreator.GetSqlSyntaxProvider(ProviderName!); if (_sqlSyntax == null) { - throw new Exception($"Can't find a sql syntax provider for provider name \"{_providerName}\"."); + throw new Exception($"Can't find a sql syntax provider for provider name \"{ProviderName}\"."); } - _bulkSqlInsertProvider = _dbProviderFactoryCreator.CreateBulkSqlInsertProvider(_providerName!); + _bulkSqlInsertProvider = _dbProviderFactoryCreator.CreateBulkSqlInsertProvider(ProviderName!); - if (_databaseType.IsSqlServer()) - { - UpdateSqlServerDatabaseType(); - } + _databaseType = _sqlSyntax.GetUpdatedDatabaseType(_databaseType, ConnectionString); // ensure we have only 1 set of mappers, and 1 PocoDataFactory, for all database // so that everything NPoco is properly cached for the lifetime of the application @@ -290,16 +227,23 @@ namespace Umbraco.Cms.Infrastructure.Persistence // add all registered mappers for NPoco _pocoMappers.AddRange(_npocoMappers); - _pocoMappers.AddRange(_dbProviderFactoryCreator.ProviderSpecificMappers(_providerName!)); + _pocoMappers.AddRange(_dbProviderFactoryCreator.ProviderSpecificMappers(ProviderName!)); var factory = new FluentPocoDataFactory(GetPocoDataFactoryResolver, _pocoMappers); _pocoDataFactory = factory; var config = new FluentConfig(xmappers => factory); // create the database factory - _npocoDatabaseFactory = DatabaseFactory.Config(x => x - .UsingDatabase(CreateDatabaseInstance) // creating UmbracoDatabase instances - .WithFluentConfig(config)); // with proper configuration + _npocoDatabaseFactory = DatabaseFactory.Config(cfg => + { + cfg.UsingDatabase(CreateDatabaseInstance) // creating UmbracoDatabase instances + .WithFluentConfig(config); // with proper configuration + + foreach (IProviderSpecificInterceptor interceptor in _dbProviderFactoryCreator.GetProviderSpecificInterceptors(ProviderName)) + { + cfg.WithInterceptor(interceptor); + } + }); if (_npocoDatabaseFactory == null) { @@ -323,8 +267,6 @@ namespace Umbraco.Cms.Infrastructure.Persistence private InitializedPocoDataBuilder GetPocoDataFactoryResolver(Type type, IPocoDataFactory factory) => new UmbracoPocoDataBuilder(type, _pocoMappers, _upgrading).Init(); - - // method used by NPoco's UmbracoDatabaseFactory to actually create the database instance private UmbracoDatabase? CreateDatabaseInstance() { @@ -339,12 +281,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence _loggerFactory.CreateLogger(), _bulkSqlInsertProvider, _databaseSchemaCreatorFactory, - _connectionRetryPolicy, - _commandRetryPolicy, - _pocoMappers - ); - } - + _pocoMappers); protected override void DisposeResources() { diff --git a/src/Umbraco.Infrastructure/PublishedContentQuery.cs b/src/Umbraco.Infrastructure/PublishedContentQuery.cs index c765a95b1c..0c8a31d829 100644 --- a/src/Umbraco.Infrastructure/PublishedContentQuery.cs +++ b/src/Umbraco.Infrastructure/PublishedContentQuery.cs @@ -17,23 +17,25 @@ using Constants = Umbraco.Cms.Core.Constants; namespace Umbraco.Cms.Infrastructure { /// - /// A class used to query for published content, media items + /// A class used to query for published content, media items /// + /// public class PublishedContentQuery : IPublishedContentQuery { private readonly IExamineManager _examineManager; private readonly IPublishedSnapshot _publishedSnapshot; private readonly IVariationContextAccessor _variationContextAccessor; + private static readonly HashSet s_itemIdFieldNameHashSet = + new HashSet() { ExamineFieldNames.ItemIdFieldName }; + /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - public PublishedContentQuery(IPublishedSnapshot publishedSnapshot, - IVariationContextAccessor variationContextAccessor, IExamineManager examineManager) + public PublishedContentQuery(IPublishedSnapshot publishedSnapshot, IVariationContextAccessor variationContextAccessor, IExamineManager examineManager) { _publishedSnapshot = publishedSnapshot ?? throw new ArgumentNullException(nameof(publishedSnapshot)); - _variationContextAccessor = variationContextAccessor ?? - throw new ArgumentNullException(nameof(variationContextAccessor)); + _variationContextAccessor = variationContextAccessor ?? throw new ArgumentNullException(nameof(variationContextAccessor)); _examineManager = examineManager ?? throw new ArgumentNullException(nameof(examineManager)); } @@ -72,6 +74,7 @@ namespace Umbraco.Cms.Infrastructure return false; } } + private static bool ConvertIdObjectToUdi(object id, out Udi? guidId) { switch (id) @@ -93,160 +96,162 @@ namespace Umbraco.Cms.Infrastructure #region Content - public IPublishedContent? Content(int id) => ItemById(id, _publishedSnapshot.Content); + public IPublishedContent? Content(int id) + => ItemById(id, _publishedSnapshot.Content); - public IPublishedContent Content(Guid id) => ItemById(id, _publishedSnapshot.Content); + public IPublishedContent Content(Guid id) + => ItemById(id, _publishedSnapshot.Content); public IPublishedContent? Content(Udi? id) { - if (!(id is GuidUdi udi)) return null; + if (!(id is GuidUdi udi)) + { + return null; + } + return ItemById(udi.Guid, _publishedSnapshot.Content); } public IPublishedContent? Content(object id) { if (ConvertIdObjectToInt(id, out var intId)) + { return Content(intId); + } + if (ConvertIdObjectToGuid(id, out var guidId)) + { return Content(guidId); + } + if (ConvertIdObjectToUdi(id, out var udiId)) + { return Content(udiId); + } + return null; } - public IPublishedContent ContentSingleAtXPath(string xpath, params XPathVariable[] vars) => - ItemByXPath(xpath, vars, _publishedSnapshot.Content); + public IPublishedContent ContentSingleAtXPath(string xpath, params XPathVariable[] vars) + => ItemByXPath(xpath, vars, _publishedSnapshot.Content); - public IEnumerable Content(IEnumerable ids) => - ItemsByIds(_publishedSnapshot.Content, ids); + public IEnumerable Content(IEnumerable ids) + => ItemsByIds(_publishedSnapshot.Content, ids); - public IEnumerable Content(IEnumerable ids) => - ItemsByIds(_publishedSnapshot.Content, ids); + public IEnumerable Content(IEnumerable ids) + => ItemsByIds(_publishedSnapshot.Content, ids); public IEnumerable Content(IEnumerable ids) - { - return ids.Select(Content).WhereNotNull(); - } - public IEnumerable ContentAtXPath(string xpath, params XPathVariable[] vars) => - ItemsByXPath(xpath, vars, _publishedSnapshot.Content); + => ids.Select(Content).WhereNotNull(); - public IEnumerable ContentAtXPath(XPathExpression xpath, params XPathVariable[] vars) => - ItemsByXPath(xpath, vars, _publishedSnapshot.Content); + public IEnumerable ContentAtXPath(string xpath, params XPathVariable[] vars) + => ItemsByXPath(xpath, vars, _publishedSnapshot.Content); - public IEnumerable ContentAtRoot() => ItemsAtRoot(_publishedSnapshot.Content); + public IEnumerable ContentAtXPath(XPathExpression xpath, params XPathVariable[] vars) + => ItemsByXPath(xpath, vars, _publishedSnapshot.Content); + + public IEnumerable ContentAtRoot() + => ItemsAtRoot(_publishedSnapshot.Content); #endregion #region Media - public IPublishedContent? Media(int id) => ItemById(id, _publishedSnapshot.Media); + public IPublishedContent? Media(int id) + => ItemById(id, _publishedSnapshot.Media); - public IPublishedContent Media(Guid id) => ItemById(id, _publishedSnapshot.Media); + public IPublishedContent Media(Guid id) + => ItemById(id, _publishedSnapshot.Media); public IPublishedContent? Media(Udi? id) { - if (!(id is GuidUdi udi)) return null; + if (!(id is GuidUdi udi)) + { + return null; + } + return ItemById(udi.Guid, _publishedSnapshot.Media); } public IPublishedContent? Media(object id) { if (ConvertIdObjectToInt(id, out var intId)) + { return Media(intId); + } + if (ConvertIdObjectToGuid(id, out var guidId)) + { return Media(guidId); + } + if (ConvertIdObjectToUdi(id, out var udiId)) + { return Media(udiId); + } + return null; } - public IEnumerable Media(IEnumerable ids) => ItemsByIds(_publishedSnapshot.Media, ids); + public IEnumerable Media(IEnumerable ids) + => ItemsByIds(_publishedSnapshot.Media, ids); + public IEnumerable Media(IEnumerable ids) - { - return ids.Select(Media).WhereNotNull(); - } + => ids.Select(Media).WhereNotNull(); - public IEnumerable Media(IEnumerable ids) => ItemsByIds(_publishedSnapshot.Media, ids); + public IEnumerable Media(IEnumerable ids) + => ItemsByIds(_publishedSnapshot.Media, ids); - public IEnumerable MediaAtRoot() => ItemsAtRoot(_publishedSnapshot.Media); + public IEnumerable MediaAtRoot() + => ItemsAtRoot(_publishedSnapshot.Media); #endregion #region Used by Content/Media private static IPublishedContent? ItemById(int id, IPublishedCache cache) - { - var doc = cache.GetById(id); - return doc; - } + => cache.GetById(id); private static IPublishedContent ItemById(Guid id, IPublishedCache cache) - { - var doc = cache.GetById(id); - return doc; - } + => cache.GetById(id); private static IPublishedContent ItemByXPath(string xpath, XPathVariable[] vars, IPublishedCache cache) - { - var doc = cache.GetSingleByXPath(xpath, vars); - return doc; - } - - //NOTE: Not used? - //private IPublishedContent ItemByXPath(XPathExpression xpath, XPathVariable[] vars, IPublishedCache cache) - //{ - // var doc = cache.GetSingleByXPath(xpath, vars); - // return doc; - //} + => cache.GetSingleByXPath(xpath, vars); private static IEnumerable ItemsByIds(IPublishedCache cache, IEnumerable ids) - { - return ids.Select(eachId => ItemById(eachId, cache)).WhereNotNull(); - } + => ids.Select(eachId => ItemById(eachId, cache)).WhereNotNull(); private IEnumerable ItemsByIds(IPublishedCache cache, IEnumerable ids) - { - return ids.Select(eachId => ItemById(eachId, cache)).WhereNotNull(); - } + => ids.Select(eachId => ItemById(eachId, cache)).WhereNotNull(); - private static IEnumerable ItemsByXPath(string xpath, XPathVariable[] vars, - IPublishedCache cache) - { - var doc = cache.GetByXPath(xpath, vars); - return doc; - } + private static IEnumerable ItemsByXPath(string xpath, XPathVariable[] vars, IPublishedCache cache) + => cache.GetByXPath(xpath, vars); - private static IEnumerable ItemsByXPath(XPathExpression xpath, XPathVariable[] vars, - IPublishedCache cache) - { - var doc = cache.GetByXPath(xpath, vars); - return doc; - } + private static IEnumerable ItemsByXPath(XPathExpression xpath, XPathVariable[] vars, IPublishedCache cache) + => cache.GetByXPath(xpath, vars); - private static IEnumerable ItemsAtRoot(IPublishedCache cache) => cache.GetAtRoot(); + private static IEnumerable ItemsAtRoot(IPublishedCache cache) + => cache.GetAtRoot(); #endregion #region Search /// - public IEnumerable Search(string term, string culture = "*", - string indexName = Constants.UmbracoIndexes.ExternalIndexName) => - Search(term, 0, 0, out _, culture, indexName); + public IEnumerable Search(string term, string culture = "*", string indexName = Constants.UmbracoIndexes.ExternalIndexName) + => Search(term, 0, 0, out _, culture, indexName); /// public IEnumerable Search(string term, int skip, int take, out long totalRecords, string culture = "*", string indexName = Constants.UmbracoIndexes.ExternalIndexName, ISet? loadedFields = null) { if (skip < 0) { - throw new ArgumentOutOfRangeException(nameof(skip), skip, - "The value must be greater than or equal to zero."); + throw new ArgumentOutOfRangeException(nameof(skip), skip, "The value must be greater than or equal to zero."); } if (take < 0) { - throw new ArgumentOutOfRangeException(nameof(take), take, - "The value must be greater than or equal to zero."); + throw new ArgumentOutOfRangeException(nameof(take), take, "The value must be greater than or equal to zero."); } if (string.IsNullOrEmpty(indexName)) @@ -254,68 +259,67 @@ namespace Umbraco.Cms.Infrastructure indexName = Constants.UmbracoIndexes.ExternalIndexName; } - if (!_examineManager.TryGetIndex(indexName, out var index) || !(index is IUmbracoIndex umbIndex)) + if (!_examineManager.TryGetIndex(indexName, out var index) || index is not IUmbracoIndex umbIndex) { - throw new InvalidOperationException( - $"No index found by name {indexName} or is not of type {typeof(IUmbracoIndex)}"); + throw new InvalidOperationException($"No index found by name {indexName} or is not of type {typeof(IUmbracoIndex)}"); } var query = umbIndex.Searcher.CreateQuery(IndexTypes.Content); - IQueryExecutor queryExecutor; + IOrdering ordering; if (culture == "*") { // Search everything - queryExecutor = query.ManagedQuery(term); + ordering = query.ManagedQuery(term); } else if (string.IsNullOrWhiteSpace(culture)) { // Only search invariant - queryExecutor = query + ordering = query .Field(UmbracoExamineFieldNames.VariesByCultureFieldName, "n") // Must not vary by culture .And().ManagedQuery(term); } else { // Only search the specified culture - var fields = - umbIndex.GetCultureAndInvariantFields(culture) - .ToArray(); // Get all index fields suffixed with the culture name supplied - queryExecutor = query.ManagedQuery(term, fields); - } - if (loadedFields != null && queryExecutor is IBooleanOperation booleanOperation) - { - queryExecutor = booleanOperation.SelectFields(loadedFields); + var fields = umbIndex.GetCultureAndInvariantFields(culture).ToArray(); // Get all index fields suffixed with the culture name supplied + ordering = query.ManagedQuery(term, fields); } + // Only select item ID field, because results are loaded from the published snapshot based on this single value + var queryExecutor = ordering.SelectFields(s_itemIdFieldNameHashSet); + + var results = skip == 0 && take == 0 ? queryExecutor.Execute() : queryExecutor.Execute(QueryOptions.SkipTake(skip, take)); totalRecords = results.TotalItemCount; - return new CultureContextualSearchResults( - results.ToPublishedSearchResults(_publishedSnapshot.Content), _variationContextAccessor, - culture); + return new CultureContextualSearchResults(results.ToPublishedSearchResults(_publishedSnapshot.Content), _variationContextAccessor, culture); } /// - public IEnumerable Search(IQueryExecutor query) => Search(query, 0, 0, out _); + public IEnumerable Search(IQueryExecutor query) + => Search(query, 0, 0, out _); /// - public IEnumerable Search(IQueryExecutor query, int skip, int take, - out long totalRecords) + public IEnumerable Search(IQueryExecutor query, int skip, int take, out long totalRecords) { if (skip < 0) { - throw new ArgumentOutOfRangeException(nameof(skip), skip, - "The value must be greater than or equal to zero."); + throw new ArgumentOutOfRangeException(nameof(skip), skip, "The value must be greater than or equal to zero."); } if (take < 0) { - throw new ArgumentOutOfRangeException(nameof(take), take, - "The value must be greater than or equal to zero."); + throw new ArgumentOutOfRangeException(nameof(take), take, "The value must be greater than or equal to zero."); + } + + if (query is IOrdering ordering) + { + // Only select item ID field, because results are loaded from the published snapshot based on this single value + query = ordering.SelectFields(s_itemIdFieldNameHashSet); } var results = skip == 0 && take == 0 @@ -328,8 +332,7 @@ namespace Umbraco.Cms.Infrastructure } /// - /// This is used to contextualize the values in the search results when enumerating over them so that the correct - /// culture values are used + /// This is used to contextualize the values in the search results when enumerating over them, so that the correct culture values are used. /// private class CultureContextualSearchResults : IEnumerable { @@ -337,8 +340,7 @@ namespace Umbraco.Cms.Infrastructure private readonly IVariationContextAccessor _variationContextAccessor; private readonly IEnumerable _wrapped; - public CultureContextualSearchResults(IEnumerable wrapped, - IVariationContextAccessor variationContextAccessor, string culture) + public CultureContextualSearchResults(IEnumerable wrapped, IVariationContextAccessor variationContextAccessor, string culture) { _wrapped = wrapped; _variationContextAccessor = variationContextAccessor; @@ -347,20 +349,21 @@ namespace Umbraco.Cms.Infrastructure public IEnumerator GetEnumerator() { - //We need to change the current culture to what is requested and then change it back + // We need to change the current culture to what is requested and then change it back var originalContext = _variationContextAccessor.VariationContext; if (!_culture.IsNullOrWhiteSpace() && !_culture.InvariantEquals(originalContext?.Culture)) + { _variationContextAccessor.VariationContext = new VariationContext(_culture); + } - //now the IPublishedContent returned will be contextualized to the culture specified and will be reset when the enumerator is disposed - return new CultureContextualSearchResultsEnumerator(_wrapped.GetEnumerator(), _variationContextAccessor, - originalContext); + // Now the IPublishedContent returned will be contextualized to the culture specified and will be reset when the enumerator is disposed + return new CultureContextualSearchResultsEnumerator(_wrapped.GetEnumerator(), _variationContextAccessor, originalContext); } IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); /// - /// Resets the variation context when this is disposed + /// Resets the variation context when this is disposed. /// private class CultureContextualSearchResultsEnumerator : IEnumerator { @@ -368,30 +371,31 @@ namespace Umbraco.Cms.Infrastructure private readonly IVariationContextAccessor _variationContextAccessor; private readonly IEnumerator _wrapped; - public CultureContextualSearchResultsEnumerator(IEnumerator wrapped, - IVariationContextAccessor variationContextAccessor, VariationContext? originalContext) + public CultureContextualSearchResultsEnumerator( + IEnumerator wrapped, + IVariationContextAccessor variationContextAccessor, + VariationContext? originalContext) { _wrapped = wrapped; _variationContextAccessor = variationContextAccessor; _originalContext = originalContext; } + public PublishedSearchResult Current => _wrapped.Current; + + object IEnumerator.Current => Current; + public void Dispose() { _wrapped.Dispose(); - //reset + + // Reset to original variation context _variationContextAccessor.VariationContext = _originalContext; } public bool MoveNext() => _wrapped.MoveNext(); - public void Reset() - { - _wrapped.Reset(); - } - - public PublishedSearchResult Current => _wrapped.Current; - object IEnumerator.Current => Current; + public void Reset() => _wrapped.Reset(); } } diff --git a/src/Umbraco.Infrastructure/Routing/RedirectTrackingHandler.cs b/src/Umbraco.Infrastructure/Routing/RedirectTrackingHandler.cs index 869844557f..4b34cf41a9 100644 --- a/src/Umbraco.Infrastructure/Routing/RedirectTrackingHandler.cs +++ b/src/Umbraco.Infrastructure/Routing/RedirectTrackingHandler.cs @@ -99,25 +99,32 @@ namespace Umbraco.Cms.Core.Routing { return; } - var contentCache = publishedSnapshot?.Content; - var entityContent = contentCache?.GetById(entity.Id); - if (entityContent == null) + + IPublishedContentCache contentCache = publishedSnapshot?.Content; + IPublishedContent? entityContent = contentCache?.GetById(entity.Id); + if (entityContent is null) + { return; + } // get the default affected cultures by going up the tree until we find the first culture variant entity (default to no cultures) var defaultCultures = entityContent.AncestorsOrSelf()?.FirstOrDefault(a => a.Cultures.Any())?.Cultures.Keys.ToArray() ?? Array.Empty(); - foreach (var x in entityContent.DescendantsOrSelf(_variationContextAccessor)) + + foreach (IPublishedContent publishedContent in entityContent.DescendantsOrSelf(_variationContextAccessor)) { // if this entity defines specific cultures, use those instead of the default ones - var cultures = x.Cultures.Any() ? x.Cultures.Keys : defaultCultures; + IEnumerable cultures = publishedContent.Cultures.Any() ? publishedContent.Cultures.Keys : defaultCultures; foreach (var culture in cultures) { - var route = contentCache?.GetRouteById(x.Id, culture); + var route = contentCache?.GetRouteById(publishedContent.Id, culture); if (IsNotRoute(route)) + { continue; - oldRoutes[new ContentIdAndCulture(x.Id, culture)] = new ContentKeyAndOldRoute(x.Key, route!); + } + + oldRoutes[new ContentIdAndCulture(publishedContent.Id, culture)] = new ContentKeyAndOldRoute(publishedContent.Key, route!); } } } @@ -137,11 +144,14 @@ namespace Umbraco.Cms.Core.Routing return; } - foreach (var oldRoute in oldRoutes) + foreach (KeyValuePair oldRoute in oldRoutes) { var newRoute = contentCache.GetRouteById(oldRoute.Key.ContentId, oldRoute.Key.Culture); if (IsNotRoute(newRoute) || oldRoute.Value.OldRoute == newRoute) + { continue; + } + _redirectUrlService.Register(oldRoute.Value.OldRoute, oldRoute.Value.ContentKey, oldRoute.Key.Culture); } } diff --git a/src/Umbraco.Infrastructure/Runtime/DefaultMainDomKeyGenerator.cs b/src/Umbraco.Infrastructure/Runtime/DefaultMainDomKeyGenerator.cs new file mode 100644 index 0000000000..11944e776c --- /dev/null +++ b/src/Umbraco.Infrastructure/Runtime/DefaultMainDomKeyGenerator.cs @@ -0,0 +1,34 @@ +using System; +using System.Security.Cryptography; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Hosting; +using Umbraco.Cms.Core.Runtime; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Infrastructure.Runtime +{ + + internal class DefaultMainDomKeyGenerator : IMainDomKeyGenerator + { + private readonly IHostingEnvironment _hostingEnvironment; + private readonly IOptionsMonitor _globalSettings; + + public DefaultMainDomKeyGenerator(IHostingEnvironment hostingEnvironment, IOptionsMonitor globalSettings) + { + _hostingEnvironment = hostingEnvironment; + _globalSettings = globalSettings; + } + + public string GenerateKey() + { + var machineName = Environment.MachineName; + var mainDomId = MainDom.GetMainDomId(_hostingEnvironment); + var discriminator = _globalSettings.CurrentValue.MainDomKeyDiscriminator; + + var rawKey = $"{machineName}{mainDomId}{discriminator}"; + + return rawKey.GenerateHash(); + } + } +} diff --git a/src/Umbraco.Infrastructure/Runtime/FileSystemMainDomLock.cs b/src/Umbraco.Infrastructure/Runtime/FileSystemMainDomLock.cs new file mode 100644 index 0000000000..c4cbcef588 --- /dev/null +++ b/src/Umbraco.Infrastructure/Runtime/FileSystemMainDomLock.cs @@ -0,0 +1,122 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Hosting; +using Umbraco.Cms.Core.Runtime; + +namespace Umbraco.Cms.Infrastructure.Runtime +{ + internal class FileSystemMainDomLock : IMainDomLock + { + private readonly ILogger _logger; + private readonly IOptionsMonitor _globalSettings; + private readonly CancellationTokenSource _cancellationTokenSource = new(); + private readonly string _lockFilePath; + private readonly string _releaseSignalFilePath; + + private FileStream _lockFileStream; + private Task _listenForReleaseSignalFileTask; + + public FileSystemMainDomLock( + ILogger logger, + IMainDomKeyGenerator mainDomKeyGenerator, + IHostingEnvironment hostingEnvironment, + IOptionsMonitor globalSettings) + { + _logger = logger; + _globalSettings = globalSettings; + + var lockFileName = $"MainDom_{mainDomKeyGenerator.GenerateKey()}.lock"; + _lockFilePath = Path.Combine(hostingEnvironment.LocalTempPath, lockFileName); + _releaseSignalFilePath = $"{_lockFilePath}_release"; + } + + public Task AcquireLockAsync(int millisecondsTimeout) + { + var stopwatch = new Stopwatch(); + stopwatch.Start(); + + do + { + try + { + _logger.LogDebug("Attempting to obtain MainDom lock file handle {lockFilePath}", _lockFilePath); + _lockFileStream = File.Open(_lockFilePath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None); + DeleteLockReleaseSignalFile(); + return Task.FromResult(true); + } + catch (IOException) + { + _logger.LogDebug("Couldn't obtain MainDom lock file handle, signalling for release of {lockFilePath}", _lockFilePath); + CreateLockReleaseSignalFile(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Unexpected exception attempting to obtain MainDom lock file handle {lockFilePath}, giving up", _lockFilePath); + _lockFileStream?.Close(); + return Task.FromResult(false); + } + } + while (stopwatch.ElapsedMilliseconds < millisecondsTimeout); + + return Task.FromResult(false); + } + + public void CreateLockReleaseSignalFile() => + File.Open(_releaseSignalFilePath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite | FileShare.Delete) + .Close(); + + public void DeleteLockReleaseSignalFile() => + File.Delete(_releaseSignalFilePath); + + // Create a long running task to poll to check if anyone has created a lock release file. + public Task ListenAsync() + { + if (_listenForReleaseSignalFileTask != null) + { + return _listenForReleaseSignalFileTask; + } + + _listenForReleaseSignalFileTask = Task.Factory.StartNew( + ListeningLoop, + _cancellationTokenSource.Token, + TaskCreationOptions.LongRunning, + TaskScheduler.Default); + + return _listenForReleaseSignalFileTask; + } + + private void ListeningLoop() + { + while (true) + { + if (_cancellationTokenSource.IsCancellationRequested) + { + _logger.LogDebug("ListenAsync Task canceled, exiting loop"); + return; + } + + if (File.Exists(_releaseSignalFilePath)) + { + _logger.LogDebug("Found lock release signal file, releasing lock on {lockFilePath}", _lockFilePath); + _lockFileStream?.Close(); + _lockFileStream = null; + break; + } + + Thread.Sleep(_globalSettings.CurrentValue.MainDomReleaseSignalPollingInterval); + } + } + + public void Dispose() + { + _lockFileStream?.Close(); + _lockFileStream = null; + } + } +} diff --git a/src/Umbraco.Infrastructure/Runtime/RuntimeState.cs b/src/Umbraco.Infrastructure/Runtime/RuntimeState.cs index 36041d2c47..5ab9993f43 100644 --- a/src/Umbraco.Infrastructure/Runtime/RuntimeState.cs +++ b/src/Umbraco.Infrastructure/Runtime/RuntimeState.cs @@ -313,7 +313,6 @@ namespace Umbraco.Cms.Infrastructure.Runtime } private bool CanAutoInstallMissingDatabase(IUmbracoDatabaseFactory databaseFactory) - => databaseFactory.ProviderName == Constants.DatabaseProviders.SqlCe || - databaseFactory.ConnectionString?.InvariantContains("(localdb)") == true; + => databaseFactory.ConnectionString?.InvariantContains("(localdb)") == true; } } diff --git a/src/Umbraco.Infrastructure/Runtime/SqlMainDomLock.cs b/src/Umbraco.Infrastructure/Runtime/SqlMainDomLock.cs index 64f83eee35..9148167c6b 100644 --- a/src/Umbraco.Infrastructure/Runtime/SqlMainDomLock.cs +++ b/src/Umbraco.Infrastructure/Runtime/SqlMainDomLock.cs @@ -2,21 +2,17 @@ using System; using System.Data; using System.Diagnostics; using System.Linq; -using System.Security.Cryptography; using System.Threading; using System.Threading.Tasks; -using Microsoft.Data.SqlClient; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using NPoco; using Umbraco.Cms.Core.Configuration.Models; -using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Runtime; using Umbraco.Cms.Infrastructure.Migrations.Install; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Persistence.Mappers; -using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; using Umbraco.Extensions; using MapperCollection = Umbraco.Cms.Infrastructure.Persistence.Mappers.MapperCollection; @@ -29,10 +25,8 @@ namespace Umbraco.Cms.Infrastructure.Runtime private const string UpdatedSuffix = "_updated"; private readonly ILogger _logger; private readonly IOptions _globalSettings; - private readonly IHostingEnvironment _hostingEnvironment; private readonly IUmbracoDatabase? _db; private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); - private SqlServerSyntaxProvider _sqlServerSyntax; private bool _mainDomChanging = false; private readonly UmbracoDatabaseFactory _dbFactory; private bool _errorDuringAcquiring; @@ -41,56 +35,30 @@ namespace Umbraco.Cms.Infrastructure.Runtime private bool _acquireWhenTablesNotAvailable = false; public SqlMainDomLock( - ILogger logger, ILoggerFactory loggerFactory, IOptions globalSettings, IOptionsMonitor connectionStrings, IDbProviderFactoryCreator dbProviderFactoryCreator, - IHostingEnvironment hostingEnvironment, + IMainDomKeyGenerator mainDomKeyGenerator, DatabaseSchemaCreatorFactory databaseSchemaCreatorFactory, - NPocoMapperCollection npocoMappers, - string? connectionStringName) + NPocoMapperCollection npocoMappers) { // unique id for our appdomain, this is more unique than the appdomain id which is just an INT counter to its safer _lockId = Guid.NewGuid().ToString(); - _logger = logger; + _logger = loggerFactory.CreateLogger(); _globalSettings = globalSettings; - _sqlServerSyntax = new SqlServerSyntaxProvider(_globalSettings); - _hostingEnvironment = hostingEnvironment; + _dbFactory = new UmbracoDatabaseFactory( loggerFactory.CreateLogger(), loggerFactory, _globalSettings, + connectionStrings, new MapperCollection(() => Enumerable.Empty()), dbProviderFactoryCreator, databaseSchemaCreatorFactory, - npocoMappers, - connectionStringName); - MainDomKey = MainDomKeyPrefix + "-" + (Environment.MachineName + MainDom.GetMainDomId(_hostingEnvironment)).GenerateHash(); - } - - public SqlMainDomLock( - ILogger logger, - ILoggerFactory loggerFactory, - IOptions globalSettings, - IOptionsMonitor connectionStrings, - IDbProviderFactoryCreator dbProviderFactoryCreator, - IHostingEnvironment hostingEnvironment, - DatabaseSchemaCreatorFactory databaseSchemaCreatorFactory, - NPocoMapperCollection npocoMappers) - : this( - logger, - loggerFactory, - globalSettings, - connectionStrings, - dbProviderFactoryCreator, - hostingEnvironment, - databaseSchemaCreatorFactory, - npocoMappers, - connectionStrings.CurrentValue.UmbracoConnectionString.ConnectionString - ) - { + npocoMappers); + MainDomKey = MainDomKeyPrefix + "-" + mainDomKeyGenerator.GenerateKey(); } public async Task AcquireLockAsync(int millisecondsTimeout) @@ -101,13 +69,6 @@ namespace Umbraco.Cms.Infrastructure.Runtime return true; } - if (!(_dbFactory.SqlContext?.SqlSyntax is SqlServerSyntaxProvider sqlServerSyntaxProvider)) - { - throw new NotSupportedException("SqlMainDomLock is only supported for Sql Server"); - } - - _sqlServerSyntax = sqlServerSyntaxProvider; - _logger.LogDebug("Acquiring lock..."); var tempId = Guid.NewGuid().ToString(); @@ -127,25 +88,7 @@ namespace Umbraco.Cms.Infrastructure.Runtime return true; } - db!.BeginTransaction(IsolationLevel.ReadCommitted); - - try - { - // wait to get a write lock - _sqlServerSyntax.WriteLock(db, TimeSpan.FromMilliseconds(millisecondsTimeout), Cms.Core.Constants.Locks.MainDom); - } - catch(SqlException ex) - { - if (IsLockTimeoutException(ex)) - { - _logger.LogError(ex, "Sql timeout occurred, could not acquire MainDom."); - _errorDuringAcquiring = true; - return false; - } - - // unexpected (will be caught below) - throw; - } + db!.BeginTransaction(IsolationLevel.Serializable); var result = InsertLockRecord(tempId, db); //we change the row to a random Id to signal other MainDom to shutdown if (result == RecordPersistenceType.Insert) @@ -212,7 +155,7 @@ namespace Umbraco.Cms.Infrastructure.Runtime { // poll every couple of seconds // local testing shows the actual query to be executed from client/server is approx 300ms but would change depending on environment/IO - Thread.Sleep(2000); + Thread.Sleep(_globalSettings.Value.MainDomReleaseSignalPollingInterval); if (!_dbFactory.Configured) { @@ -256,9 +199,7 @@ namespace Umbraco.Cms.Infrastructure.Runtime InsertLockRecord(_lockId, db!); } - db!.BeginTransaction(IsolationLevel.ReadCommitted); - // get a read lock - _sqlServerSyntax.ReadLock(db, Cms.Core.Constants.Locks.MainDom); + db!.BeginTransaction(IsolationLevel.Serializable); if (!IsMainDomValue(_lockId, db)) { @@ -345,12 +286,11 @@ namespace Umbraco.Cms.Infrastructure.Runtime try { - transaction = db.GetTransaction(IsolationLevel.ReadCommitted); - // get a read lock - _sqlServerSyntax.ReadLock(db, Cms.Core.Constants.Locks.MainDom); + transaction = db.GetTransaction(IsolationLevel.Serializable); - // the row - var mainDomRows = db.Fetch("SELECT * FROM umbracoKeyValue WHERE [key] = @key", new { key = MainDomKey }); + // the row + var mainDomRows = db.Fetch("SELECT * FROM umbracoKeyValue WHERE [key] = @key", + new {key = MainDomKey}); if (mainDomRows.Count == 0 || mainDomRows[0].Value == updatedTempId) { @@ -359,8 +299,6 @@ namespace Umbraco.Cms.Infrastructure.Runtime // which indicates that we // can acquire it and it has shutdown. - _sqlServerSyntax.WriteLock(db, Cms.Core.Constants.Locks.MainDom); - // so now we update the row with our appdomain id InsertLockRecord(_lockId, db); _logger.LogDebug("Acquired with ID {LockId}", _lockId); @@ -378,12 +316,6 @@ namespace Umbraco.Cms.Infrastructure.Runtime } catch (Exception ex) { - if (IsLockTimeoutException(ex as SqlException)) - { - _logger.LogError(ex, "Sql timeout occurred, waiting for existing MainDom is canceled."); - _errorDuringAcquiring = true; - return false; - } // unexpected _logger.LogError(ex, "Unexpected error, waiting for existing MainDom is canceled."); _errorDuringAcquiring = true; @@ -416,9 +348,7 @@ namespace Umbraco.Cms.Infrastructure.Runtime try { - transaction = db.GetTransaction(IsolationLevel.ReadCommitted); - - _sqlServerSyntax.WriteLock(db, Cms.Core.Constants.Locks.MainDom); + transaction = db.GetTransaction(IsolationLevel.Serializable); // so now we update the row with our appdomain id InsertLockRecord(_lockId, db); @@ -427,13 +357,6 @@ namespace Umbraco.Cms.Infrastructure.Runtime } catch (Exception ex) { - if (IsLockTimeoutException(ex as SqlException)) - { - // something is wrong, we cannot acquire, not much we can do - _logger.LogError(ex, "Sql timeout occurred, could not forcibly acquire MainDom."); - _errorDuringAcquiring = true; - return false; - } _logger.LogError(ex, "Unexpected error, could not forcibly acquire MainDom."); _errorDuringAcquiring = true; return false; @@ -471,13 +394,6 @@ namespace Umbraco.Cms.Infrastructure.Runtime } - /// - /// Checks if the exception is an SQL timeout - /// - /// - /// - private bool IsLockTimeoutException(SqlException? sqlException) => sqlException?.Number == 1222; - #region IDisposable Support private bool _disposedValue = false; // To detect redundant calls @@ -502,10 +418,7 @@ namespace Umbraco.Cms.Infrastructure.Runtime try { db = _dbFactory.CreateDatabase(); - db!.BeginTransaction(IsolationLevel.ReadCommitted); - - // get a write lock - _sqlServerSyntax.WriteLock(db, Cms.Core.Constants.Locks.MainDom); + db!.BeginTransaction(IsolationLevel.Serializable); // When we are disposed, it means we have released the MainDom lock // and called all MainDom release callbacks, in this case diff --git a/src/Umbraco.Infrastructure/Scoping/Scope.cs b/src/Umbraco.Infrastructure/Scoping/Scope.cs index aa3c4bb4c9..d7a67f1db8 100644 --- a/src/Umbraco.Infrastructure/Scoping/Scope.cs +++ b/src/Umbraco.Infrastructure/Scoping/Scope.cs @@ -7,6 +7,7 @@ using System.Threading; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.DistributedLocking; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Infrastructure.Persistence; @@ -44,7 +45,7 @@ namespace Umbraco.Cms.Core.Scoping private IsolatedCaches? _isolatedCaches; private IScopedNotificationPublisher? _notificationPublisher; - private StackQueue<(LockType lockType, TimeSpan timeout, Guid instanceId, int lockId)>? _queuedLocks; + private StackQueue<(DistributedLockType lockType, TimeSpan timeout, Guid instanceId, int lockId)>? _queuedLocks; // This is all used to safely track read/write locks at given Scope levels so that // when we dispose we can verify that everything has been cleaned up correctly. @@ -52,6 +53,7 @@ namespace Umbraco.Cms.Core.Scoping private Dictionary>? _readLocksDictionary; private HashSet? _writeLocks; private Dictionary>? _writeLocksDictionary; + private Queue _acquiredLocks; // initializes a new scope private Scope( @@ -153,6 +155,8 @@ namespace Umbraco.Cms.Core.Scoping } else { + _acquiredLocks = new Queue(); + // the FS scope cannot be "on demand" like the rest, because we would need to hook into // every scoped FS to trigger the creation of shadow FS "on demand", and that would be // pretty pointless since if scopeFileSystems is true, we *know* we want to shadow @@ -204,9 +208,6 @@ namespace Umbraco.Cms.Core.Scoping { } - internal Dictionary>? ReadLocks => _readLocksDictionary; - - internal Dictionary>? WriteLocks => _writeLocksDictionary; // a value indicating whether to force call-context public bool CallContext @@ -277,25 +278,6 @@ namespace Umbraco.Cms.Core.Scoping } } - public IUmbracoDatabase? DatabaseOrNull - { - get - { - EnsureNotDisposed(); - if (ParentScope == null) - { - if (_database != null) - { - EnsureDbLocks(); - } - - return _database; - } - - return ParentScope.DatabaseOrNull; - } - } - // true if Umbraco.CoreDebugSettings.LogUncompletedScope appSetting is set to "true" private bool LogUncompletedScopes => _coreDebugSettings.LogIncompletedScopes; @@ -466,6 +448,11 @@ namespace Umbraco.Cms.Core.Scoping ClearLocks(InstanceId); if (ParentScope is null) { + while (!_acquiredLocks?.IsCollectionEmpty() ?? false) + { + _acquiredLocks.Dequeue().Dispose(); + } + // We're the parent scope, make sure that locks of all scopes has been cleared // Since we're only reading we don't have to be in a lock if (_readLocksDictionary?.Count > 0 || _writeLocksDictionary?.Count > 0) @@ -505,24 +492,24 @@ namespace Umbraco.Cms.Core.Scoping _disposed = true; } - public void EagerReadLock(params int[] lockIds) => EagerReadLockInner(Database, InstanceId, null, lockIds); + public void EagerReadLock(params int[] lockIds) => EagerReadLockInner(InstanceId, null, lockIds); /// public void ReadLock(params int[] lockIds) => LazyReadLockInner(InstanceId, lockIds); public void EagerReadLock(TimeSpan timeout, int lockId) => - EagerReadLockInner(Database, InstanceId, timeout, lockId); + EagerReadLockInner(InstanceId, timeout, lockId); /// public void ReadLock(TimeSpan timeout, int lockId) => LazyReadLockInner(InstanceId, timeout, lockId); - public void EagerWriteLock(params int[] lockIds) => EagerWriteLockInner(Database, InstanceId, null, lockIds); + public void EagerWriteLock(params int[] lockIds) => EagerWriteLockInner(InstanceId, null, lockIds); /// public void WriteLock(params int[] lockIds) => LazyWriteLockInner(InstanceId, lockIds); public void EagerWriteLock(TimeSpan timeout, int lockId) => - EagerWriteLockInner(Database, InstanceId, timeout, lockId); + EagerWriteLockInner(InstanceId, timeout, lockId); /// public void WriteLock(TimeSpan timeout, int lockId) => LazyWriteLockInner(InstanceId, timeout, lockId); @@ -598,7 +585,7 @@ namespace Umbraco.Cms.Core.Scoping { if (_queuedLocks?.Count > 0) { - LockType currentType = LockType.ReadLock; + DistributedLockType currentType = DistributedLockType.ReadLock; TimeSpan currentTimeout = TimeSpan.Zero; Guid currentInstanceId = InstanceId; var collectedIds = new HashSet(); @@ -606,7 +593,7 @@ namespace Umbraco.Cms.Core.Scoping var i = 0; while (_queuedLocks.Count > 0) { - (LockType lockType, TimeSpan timeout, Guid instanceId, var lockId) = _queuedLocks.Dequeue(); + (DistributedLockType lockType, TimeSpan timeout, Guid instanceId, var lockId) = _queuedLocks.Dequeue(); if (i == 0) { @@ -621,13 +608,13 @@ namespace Umbraco.Cms.Core.Scoping // process the lock ids collected switch (currentType) { - case LockType.ReadLock: - EagerReadLockInner(_database!, currentInstanceId, + case DistributedLockType.ReadLock: + EagerReadLockInner(currentInstanceId, currentTimeout == TimeSpan.Zero ? null : currentTimeout, collectedIds.ToArray()); break; - case LockType.WriteLock: - EagerWriteLockInner(_database!, currentInstanceId, + case DistributedLockType.WriteLock: + EagerWriteLockInner(currentInstanceId, currentTimeout == TimeSpan.Zero ? null : currentTimeout, collectedIds.ToArray()); break; @@ -647,12 +634,12 @@ namespace Umbraco.Cms.Core.Scoping // process the remaining switch (currentType) { - case LockType.ReadLock: - EagerReadLockInner(_database!, currentInstanceId, + case DistributedLockType.ReadLock: + EagerReadLockInner(currentInstanceId, currentTimeout == TimeSpan.Zero ? null : currentTimeout, collectedIds.ToArray()); break; - case LockType.WriteLock: - EagerWriteLockInner(_database!, currentInstanceId, + case DistributedLockType.WriteLock: + EagerWriteLockInner(currentInstanceId, currentTimeout == TimeSpan.Zero ? null : currentTimeout, collectedIds.ToArray()); break; } @@ -922,7 +909,7 @@ namespace Umbraco.Cms.Core.Scoping { // It's safe to assume that the locks on the top of the stack belong to this instance, // since any child scopes that might have added locks to the stack must be disposed before we try and dispose this instance. - (LockType lockType, TimeSpan timeout, Guid instanceId, int lockId) top = + (DistributedLockType lockType, TimeSpan timeout, Guid instanceId, int lockId) top = _queuedLocks.PeekStack(); if (top.instanceId == instanceId) { @@ -945,7 +932,7 @@ namespace Umbraco.Cms.Core.Scoping } else { - LazyLockInner(LockType.ReadLock, instanceId, lockIds); + LazyLockInner(DistributedLockType.ReadLock, instanceId, lockIds); } } @@ -957,7 +944,7 @@ namespace Umbraco.Cms.Core.Scoping } else { - LazyLockInner(LockType.ReadLock, instanceId, timeout, lockId); + LazyLockInner(DistributedLockType.ReadLock, instanceId, timeout, lockId); } } @@ -969,7 +956,7 @@ namespace Umbraco.Cms.Core.Scoping } else { - LazyLockInner(LockType.WriteLock, instanceId, lockIds); + LazyLockInner(DistributedLockType.WriteLock, instanceId, lockIds); } } @@ -981,17 +968,17 @@ namespace Umbraco.Cms.Core.Scoping } else { - LazyLockInner(LockType.WriteLock, instanceId, timeout, lockId); + LazyLockInner(DistributedLockType.WriteLock, instanceId, timeout, lockId); } } - private void LazyLockInner(LockType lockType, Guid instanceId, params int[] lockIds) + private void LazyLockInner(DistributedLockType lockType, Guid instanceId, params int[] lockIds) { lock (_lockQueueLocker) { if (_queuedLocks == null) { - _queuedLocks = new StackQueue<(LockType, TimeSpan, Guid, int)>(); + _queuedLocks = new StackQueue<(DistributedLockType, TimeSpan, Guid, int)>(); } foreach (var lockId in lockIds) @@ -1001,15 +988,16 @@ namespace Umbraco.Cms.Core.Scoping } } - private void LazyLockInner(LockType lockType, Guid instanceId, TimeSpan timeout, int lockId) + private void LazyLockInner(DistributedLockType lockType, Guid instanceId, TimeSpan timeout, int lockId) { lock (_lockQueueLocker) { if (_queuedLocks == null) { - _queuedLocks = new StackQueue<(LockType, TimeSpan, Guid, int)>(); + _queuedLocks = new StackQueue<(DistributedLockType, TimeSpan, Guid, int)>(); } + _queuedLocks.Enqueue((lockType, timeout, instanceId, lockId)); } } @@ -1020,18 +1008,31 @@ namespace Umbraco.Cms.Core.Scoping /// Instance ID of the requesting scope. /// Optional database timeout in milliseconds. /// Array of lock object identifiers. - private void EagerReadLockInner(IUmbracoDatabase db, Guid instanceId, TimeSpan? timeout, params int[] lockIds) + private void EagerReadLockInner(Guid instanceId, TimeSpan? timeout, params int[] lockIds) { if (ParentScope is not null) { // If we have a parent we delegate lock creation to parent. - ParentScope.EagerReadLockInner(db, instanceId, timeout, lockIds); + ParentScope.EagerReadLockInner(instanceId, timeout, lockIds); } else { - // We are the outermost scope, handle the lock request. - LockInner(db, instanceId, ref _readLocksDictionary, ref _readLocks, ObtainReadLock, - ObtainTimeoutReadLock, timeout, lockIds); + lock (_dictionaryLocker) + { + foreach (var lockId in lockIds) + { + IncrementLock(lockId, instanceId, ref _readLocksDictionary); + + // We are the outermost scope, handle the lock request. + LockInner( + instanceId, + ref _readLocksDictionary, + ref _readLocks, + ObtainReadLock, + timeout, + lockId); + } + } } } @@ -1041,18 +1042,31 @@ namespace Umbraco.Cms.Core.Scoping /// Instance ID of the requesting scope. /// Optional database timeout in milliseconds. /// Array of lock object identifiers. - private void EagerWriteLockInner(IUmbracoDatabase db, Guid instanceId, TimeSpan? timeout, params int[] lockIds) + private void EagerWriteLockInner(Guid instanceId, TimeSpan? timeout, params int[] lockIds) { if (ParentScope is not null) { // If we have a parent we delegate lock creation to parent. - ParentScope.EagerWriteLockInner(db, instanceId, timeout, lockIds); + ParentScope.EagerWriteLockInner(instanceId, timeout, lockIds); } else { - // We are the outermost scope, handle the lock request. - LockInner(db, instanceId, ref _writeLocksDictionary, ref _writeLocks, ObtainWriteLock, - ObtainTimeoutWriteLock, timeout, lockIds); + lock (_dictionaryLocker) + { + foreach (var lockId in lockIds) + { + IncrementLock(lockId, instanceId, ref _writeLocksDictionary); + + // We are the outermost scope, handle the lock request. + LockInner( + instanceId, + ref _writeLocksDictionary, + ref _writeLocks, + ObtainWriteLock, + timeout, + lockId); + } + } } } @@ -1062,90 +1076,56 @@ namespace Umbraco.Cms.Core.Scoping /// Instance ID of the scope requesting the lock. /// Reference to the applicable locks dictionary (ReadLocks or WriteLocks). /// Reference to the applicable locks hashset (_readLocks or _writeLocks). - /// Delegate used to request the lock from the database without a timeout. - /// Delegate used to request the lock from the database with a timeout. + /// Delegate used to request the lock from the locking mechanism. /// Optional timeout parameter to specify a timeout. - /// Lock identifiers to lock on. - private void LockInner(IUmbracoDatabase db, Guid instanceId, ref Dictionary>? locks, + /// Lock identifier. + private void LockInner( + Guid instanceId, + ref Dictionary>? locks, ref HashSet? locksSet, - Action obtainLock, Action obtainLockTimeout, + Action obtainLock, TimeSpan? timeout, - params int[] lockIds) + int lockId) { - lock (_dictionaryLocker) + locksSet ??= new HashSet(); + + // Only acquire the lock if we haven't done so yet. + if (!locksSet.Contains(lockId)) { - locksSet ??= new HashSet(); - foreach (var lockId in lockIds) - { - // Only acquire the lock if we haven't done so yet. - if (!locksSet.Contains(lockId)) - { - IncrementLock(lockId, instanceId, ref locks); - locksSet.Add(lockId); - try - { - if (timeout is null) - { - // We just want an ordinary lock. - obtainLock(db, lockId); - } - else - { - // We want a lock with a custom timeout - obtainLockTimeout(db, lockId, timeout.Value); - } - } - catch - { - // Something went wrong and we didn't get the lock - // Since we at this point have determined that we haven't got any lock with an ID of LockID, it's safe to completely remove it instead of decrementing. - locks?[instanceId].Remove(lockId); - // It needs to be removed from the HashSet as well, because that's how we determine to acquire a lock. - locksSet.Remove(lockId); - throw; - } - } - else - { - // We already have a lock, but need to update the dictionary for debugging purposes. - IncrementLock(lockId, instanceId, ref locks); - } - } + return; + } + + locksSet.Add(lockId); + try + { + obtainLock(lockId, timeout); + } + catch + { + // Something went wrong and we didn't get the lock + // Since we at this point have determined that we haven't got any lock with an ID of LockID, it's safe to completely remove it instead of decrementing. + locks[instanceId].Remove(lockId); + + // It needs to be removed from the HashSet as well, because that's how we determine to acquire a lock. + locksSet.Remove(lockId); + throw; } } - /// - /// Obtains an ordinary read lock. - /// - /// Lock object identifier to lock. - private void ObtainReadLock(IUmbracoDatabase db, int lockId) => SqlContext.SqlSyntax.ReadLock(db, lockId); - /// /// Obtains a read lock with a custom timeout. /// /// Lock object identifier to lock. /// TimeSpan specifying the timout period. - private void ObtainTimeoutReadLock(IUmbracoDatabase db, int lockId, TimeSpan timeout) => - SqlContext.SqlSyntax.ReadLock(db, timeout, lockId); - - /// - /// Obtains an ordinary write lock. - /// - /// Lock object identifier to lock. - private void ObtainWriteLock(IUmbracoDatabase db, int lockId) => SqlContext.SqlSyntax.WriteLock(db, lockId); + private void ObtainReadLock(int lockId, TimeSpan? timeout) + => _acquiredLocks.Enqueue(_scopeProvider.DistributedLockingMechanismFactory.DistributedLockingMechanism.ReadLock(lockId, timeout)); /// /// Obtains a write lock with a custom timeout. /// /// Lock object identifier to lock. /// TimeSpan specifying the timout period. - private void ObtainTimeoutWriteLock(IUmbracoDatabase db, int lockId, TimeSpan timeout) => - SqlContext.SqlSyntax.WriteLock(db, timeout, lockId); - - private enum LockType - { - ReadLock, - WriteLock - } + private void ObtainWriteLock(int lockId, TimeSpan? timeout) + => _acquiredLocks.Enqueue(_scopeProvider.DistributedLockingMechanismFactory.DistributedLockingMechanism.WriteLock(lockId, timeout)); } } diff --git a/src/Umbraco.Infrastructure/Scoping/ScopeProvider.cs b/src/Umbraco.Infrastructure/Scoping/ScopeProvider.cs index a453b3fcbd..901fec317c 100644 --- a/src/Umbraco.Infrastructure/Scoping/ScopeProvider.cs +++ b/src/Umbraco.Infrastructure/Scoping/ScopeProvider.cs @@ -11,6 +11,7 @@ using Umbraco.Extensions; using System.Collections.Generic; using System.Collections.Concurrent; using System.Threading; +using Umbraco.Cms.Core.DistributedLocking; using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Infrastructure.Scoping; @@ -38,6 +39,7 @@ namespace Umbraco.Cms.Core.Scoping private readonly IEventAggregator _eventAggregator; public ScopeProvider( + IDistributedLockingMechanismFactory distributedLockingMechanismFactory, IUmbracoDatabaseFactory databaseFactory, FileSystems fileSystems, IOptionsMonitor coreDebugSettings, @@ -45,6 +47,7 @@ namespace Umbraco.Cms.Core.Scoping IRequestCache requestCache, IEventAggregator eventAggregator) { + DistributedLockingMechanismFactory = distributedLockingMechanismFactory; DatabaseFactory = databaseFactory; _fileSystems = fileSystems; _coreDebugSettings = coreDebugSettings.CurrentValue; @@ -58,6 +61,8 @@ namespace Umbraco.Cms.Core.Scoping coreDebugSettings.OnChange(x => _coreDebugSettings = x); } + public IDistributedLockingMechanismFactory DistributedLockingMechanismFactory { get; } + public IUmbracoDatabaseFactory DatabaseFactory { get; } public ISqlContext SqlContext => DatabaseFactory.SqlContext; diff --git a/src/Umbraco.Infrastructure/Security/ClaimsIdentityExtensions.cs b/src/Umbraco.Infrastructure/Security/ClaimsIdentityExtensions.cs index de5b6206fc..9e11916223 100644 --- a/src/Umbraco.Infrastructure/Security/ClaimsIdentityExtensions.cs +++ b/src/Umbraco.Infrastructure/Security/ClaimsIdentityExtensions.cs @@ -1,5 +1,6 @@ // Copyright (c) Umbraco. // See LICENSE for more details. + using System.Linq; using System.Security.Claims; using Umbraco.Cms.Core; @@ -11,7 +12,8 @@ namespace Umbraco.Extensions { // Ignore these Claims when merging, these claims are dynamically added whenever the ticket // is re-issued and we don't want to merge old values of these. - private static readonly string[] s_ignoredClaims = new[] { ClaimTypes.CookiePath, Constants.Security.SessionIdClaimType }; + // We do however want to merge these when the SecurityStampValidator refreshes the principal since it's still the same login session + private static readonly string[] s_ignoredClaims = { ClaimTypes.CookiePath, Constants.Security.SessionIdClaimType }; public static void MergeAllClaims(this ClaimsIdentity destination, ClaimsIdentity source) { diff --git a/src/Umbraco.Infrastructure/Security/MemberUserStore.cs b/src/Umbraco.Infrastructure/Security/MemberUserStore.cs index 711c03b2e4..1b660ebf4e 100644 --- a/src/Umbraco.Infrastructure/Security/MemberUserStore.cs +++ b/src/Umbraco.Infrastructure/Security/MemberUserStore.cs @@ -181,6 +181,7 @@ namespace Umbraco.Cms.Core.Security { // we have to remember whether Logins property is dirty, since the UpdateMemberProperties will reset it. var isLoginsPropertyDirty = user.IsPropertyDirty(nameof(MemberIdentityUser.Logins)); + var isTokensPropertyDirty = user.IsPropertyDirty(nameof(MemberIdentityUser.LoginTokens)); MemberDataChangeType memberChangeType = UpdateMemberProperties(found, user); if (memberChangeType == MemberDataChangeType.FullSave) @@ -203,6 +204,16 @@ namespace Umbraco.Cms.Core.Security x.ProviderKey, x.UserData))); } + + if (isTokensPropertyDirty) + { + _externalLoginService.Save( + found.Key, + user.LoginTokens.Select(x => new ExternalLoginToken( + x.LoginProvider, + x.Name, + x.Value))); + } } return Task.FromResult(IdentityResult.Success); @@ -545,7 +556,38 @@ namespace Umbraco.Cms.Core.Security return found!; } - private MemberIdentityUser? AssignLoginsCallback(MemberIdentityUser? user) + /// + /// Overridden to support Umbraco's own data storage requirements + /// + /// + /// The base class's implementation of this calls into FindTokenAsync and AddUserTokenAsync, both methods will only work with ORMs that are change + /// tracking ORMs like EFCore. + /// + /// + public override Task SetTokenAsync(MemberIdentityUser user, string loginProvider, string name, string value, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + IIdentityUserToken token = user.LoginTokens.FirstOrDefault(x => x.LoginProvider.InvariantEquals(loginProvider) && x.Name.InvariantEquals(name)); + if (token == null) + { + user.LoginTokens.Add(new IdentityUserToken(loginProvider, name, value, user.Id)); + } + else + { + token.Value = value; + } + + return Task.CompletedTask; + } + + private MemberIdentityUser AssignLoginsCallback(MemberIdentityUser? user) { if (user != null) { diff --git a/src/Umbraco.Infrastructure/Services/Implement/ServerRegistrationService.cs b/src/Umbraco.Infrastructure/Services/Implement/ServerRegistrationService.cs index 2d795c734a..5f73281962 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/ServerRegistrationService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/ServerRegistrationService.cs @@ -133,13 +133,28 @@ namespace Umbraco.Cms.Core.Services.Implement /// time the current server is touched, and the period depends on the configuration. Use the /// parameter to force a cache refresh and reload active servers /// from the database. - public IEnumerable? GetActiveServers(bool refresh = false) + public IEnumerable? GetActiveServers(bool refresh = false) => GetServers(refresh).Where(x => x.IsActive); + + /// + /// Return all servers (active and inactive). + /// + /// A value indicating whether to force-refresh the cache. + /// All servers. + /// By default this method will rely on the repository's cache, which is updated each + /// time the current server is touched, and the period depends on the configuration. Use the + /// parameter to force a cache refresh and reload all servers + /// from the database. + public IEnumerable GetServers(bool refresh = false) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.ReadLock(Cms.Core.Constants.Locks.Servers); - if (refresh) ((ServerRegistrationRepository) _serverRegistrationRepository).ClearCache(); - return _serverRegistrationRepository.GetMany()?.Where(x => x.IsActive).ToArray(); // fast, cached // fast, cached + if (refresh) + { + ((ServerRegistrationRepository)_serverRegistrationRepository).ClearCache(); + } + + return _serverRegistrationRepository.GetMany().ToArray(); // fast, cached // fast, cached } } diff --git a/src/Umbraco.Infrastructure/Sync/DatabaseServerMessenger.cs b/src/Umbraco.Infrastructure/Sync/DatabaseServerMessenger.cs index 065b9b8640..f17487597c 100644 --- a/src/Umbraco.Infrastructure/Sync/DatabaseServerMessenger.cs +++ b/src/Umbraco.Infrastructure/Sync/DatabaseServerMessenger.cs @@ -19,7 +19,7 @@ namespace Umbraco.Cms.Infrastructure.Sync /// /// An that works by storing messages in the database. /// - public abstract class DatabaseServerMessenger : ServerMessengerBase + public abstract class DatabaseServerMessenger : ServerMessengerBase, IDisposable { /* * this messenger writes ALL instructions to the database, @@ -39,6 +39,7 @@ namespace Umbraco.Cms.Infrastructure.Sync private DateTime _lastPruned; private readonly Lazy _initialized; private bool _syncing; + private bool _disposedValue; private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); private readonly CancellationToken _cancellationToken; @@ -282,6 +283,28 @@ namespace Umbraco.Cms.Infrastructure.Sync } } + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + _syncIdle?.Dispose(); + } + + _disposedValue = true; + } + } + + /// + /// Dispose + /// + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + #endregion } } diff --git a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj index 178298a49c..24a3268fb3 100644 --- a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj +++ b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj @@ -42,8 +42,8 @@ + - diff --git a/src/Umbraco.Persistence.SqlCe/SqlCeBulkSqlInsertProvider.cs b/src/Umbraco.Persistence.SqlCe/SqlCeBulkSqlInsertProvider.cs deleted file mode 100644 index e6ed41548d..0000000000 --- a/src/Umbraco.Persistence.SqlCe/SqlCeBulkSqlInsertProvider.cs +++ /dev/null @@ -1,90 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Data; -using System.Data.SqlServerCe; -using System.Data.SqlTypes; -using System.Linq; -using NPoco; -using Umbraco.Cms.Infrastructure.Persistence; -using Umbraco.Extensions; -using Constants = Umbraco.Cms.Core.Constants; - -namespace Umbraco.Cms.Persistence.SqlCe -{ - public class SqlCeBulkSqlInsertProvider : IBulkSqlInsertProvider - { - public string ProviderName => Constants.DatabaseProviders.SqlCe; - - public int BulkInsertRecords(IUmbracoDatabase database, IEnumerable records) - { - if (!records.Any()) return 0; - - var pocoData = database.PocoDataFactory.ForType(typeof(T)); - if (pocoData == null) throw new InvalidOperationException("Could not find PocoData for " + typeof(T)); - - return BulkInsertRecordsSqlCe(database, pocoData, records.ToArray()); - - } - - /// - /// Bulk-insert records using SqlCE TableDirect method. - /// - /// The type of the records. - /// The database. - /// The PocoData object corresponding to the record's type. - /// The records. - /// The number of records that were inserted. - private static int BulkInsertRecordsSqlCe(IUmbracoDatabase database, PocoData pocoData, IEnumerable records) - { - var columns = pocoData.Columns.ToArray(); - - // create command against the original database.Connection - using (var command = database.CreateCommand(database.Connection, CommandType.TableDirect, string.Empty)) - { - command.CommandText = pocoData.TableInfo.TableName; - command.CommandType = CommandType.TableDirect; // TODO: why repeat? - // TODO: not supporting transactions? - //cmd.Transaction = GetTypedTransaction(db.Connection.); - - var count = 0; - var tCommand = NPocoDatabaseExtensions.GetTypedCommand(command); // execute on the real command - - // seems to cause problems, I think this is primarily used for retrieval, not inserting. - // see: https://msdn.microsoft.com/en-us/library/system.data.sqlserverce.sqlcecommand.indexname%28v=vs.100%29.aspx?f=255&MSPPError=-2147217396 - //tCommand.IndexName = pd.TableInfo.PrimaryKey; - - using (var resultSet = tCommand.ExecuteResultSet(ResultSetOptions.Updatable)) - { - var updatableRecord = resultSet.CreateRecord(); - foreach (var record in records) - { - for (var i = 0; i < columns.Length; i++) - { - // skip the index if this shouldn't be included (i.e. PK) - if (NPocoDatabaseExtensions.IncludeColumn(pocoData, columns[i])) - { - var val = columns[i].Value.GetValue(record); - - if (val is byte[]) - { - var bytes = val as byte[]; - updatableRecord.SetSqlBinary(i, new SqlBinary(bytes)); - } - else - { - updatableRecord.SetValue(i, val); - } - - updatableRecord.SetValue(i, val); - } - } - resultSet.Insert(updatableRecord); - count++; - } - } - - return count; - } - } - } -} diff --git a/src/Umbraco.Persistence.SqlCe/SqlCeDatabaseCreator.cs b/src/Umbraco.Persistence.SqlCe/SqlCeDatabaseCreator.cs deleted file mode 100644 index fd360be13a..0000000000 --- a/src/Umbraco.Persistence.SqlCe/SqlCeDatabaseCreator.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Umbraco.Cms.Infrastructure.Persistence; -using Constants = Umbraco.Cms.Core.Constants; - -namespace Umbraco.Cms.Persistence.SqlCe -{ - public class SqlCeDatabaseCreator : IDatabaseCreator - { - public string ProviderName => Constants.DatabaseProviders.SqlCe; - - public void Create(string connectionString) - { - using var engine = new System.Data.SqlServerCe.SqlCeEngine(connectionString); - engine.CreateDatabase(); - } - } -} diff --git a/src/Umbraco.Persistence.SqlCe/SqlCeImageMapper.cs b/src/Umbraco.Persistence.SqlCe/SqlCeImageMapper.cs deleted file mode 100644 index 8349056440..0000000000 --- a/src/Umbraco.Persistence.SqlCe/SqlCeImageMapper.cs +++ /dev/null @@ -1,70 +0,0 @@ -using System; -using System.Data; -using System.Data.Common; -using System.Data.SqlServerCe; -using System.Linq; -using System.Reflection; -using NPoco; -using Umbraco.Cms.Infrastructure.Persistence; - -namespace Umbraco.Cms.Persistence.SqlCe -{ - /// - /// Custom NPoco mapper for SqlCe - /// - /// - /// Work arounds to handle special columns - /// - public class SqlCeImageMapper : DefaultMapper - { - //private readonly IUmbracoDatabaseFactory _dbFactory; - - //public SqlCeImageMapper(IUmbracoDatabaseFactory dbFactory) => _dbFactory = dbFactory; - - public override Func GetToDbConverter(Type destType, MemberInfo sourceMemberInfo) - { - if (sourceMemberInfo.GetMemberInfoType() == typeof(byte[])) - { - return x => - { - //PocoData pd = _dbFactory.SqlContext.PocoDataFactory.ForType(sourceMemberInfo.DeclaringType); - //if (pd == null) - //{ - // return null; - //} - - //PocoColumn col = pd.AllColumns.FirstOrDefault(x => x.MemberInfoData.MemberInfo == sourceMemberInfo); - //if (col == null) - //{ - // return null; - //} - - return new SqlCeParameter - { - SqlDbType = SqlDbType.Image, - Value = x ?? Array.Empty() - }; - }; - } - return base.GetToDbConverter(destType, sourceMemberInfo); - } - - public override Func GetParameterConverter(DbCommand dbCommand, Type sourceType) - { - if (sourceType == typeof(byte[])) - { - return x => - { - var param = new SqlCeParameter - { - SqlDbType = SqlDbType.Image, - Value = x - }; - return param; - }; - - } - return base.GetParameterConverter(dbCommand, sourceType); - } - } -} diff --git a/src/Umbraco.Persistence.SqlCe/SqlCeSpecificMapperFactory.cs b/src/Umbraco.Persistence.SqlCe/SqlCeSpecificMapperFactory.cs deleted file mode 100644 index 3646218fce..0000000000 --- a/src/Umbraco.Persistence.SqlCe/SqlCeSpecificMapperFactory.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Umbraco.Cms.Core; -using Umbraco.Cms.Infrastructure.Persistence; - -namespace Umbraco.Cms.Persistence.SqlCe -{ - public class SqlCeSpecificMapperFactory : IProviderSpecificMapperFactory - { - public string ProviderName => Constants.DatabaseProviders.SqlCe; - public NPocoMapperCollection Mappers => new NPocoMapperCollection(() => new[] {new SqlCeImageMapper()}); - } -} diff --git a/src/Umbraco.Persistence.SqlCe/SqlCeSyntaxProvider.cs b/src/Umbraco.Persistence.SqlCe/SqlCeSyntaxProvider.cs deleted file mode 100644 index 62aa933a04..0000000000 --- a/src/Umbraco.Persistence.SqlCe/SqlCeSyntaxProvider.cs +++ /dev/null @@ -1,322 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Data; -using System.Linq; -using Microsoft.Extensions.Options; -using NPoco; -using Umbraco.Cms.Core.Configuration.Models; -using Umbraco.Cms.Infrastructure.Persistence; -using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; -using ColumnInfo = Umbraco.Cms.Infrastructure.Persistence.SqlSyntax.ColumnInfo; -using Constants = Umbraco.Cms.Core.Constants; - -namespace Umbraco.Cms.Persistence.SqlCe -{ - /// - /// Represents an SqlSyntaxProvider for Sql Ce - /// - public class SqlCeSyntaxProvider : MicrosoftSqlSyntaxProviderBase - { - private readonly IOptions _globalSettings; - - public SqlCeSyntaxProvider(IOptions globalSettings) - { - _globalSettings = globalSettings; - BlobColumnDefinition = "IMAGE"; - // NOTE: if this column type is used in sqlce, it will prob result in errors since - // SQLCE cannot support this type correctly without 2x columns and a lot of work arounds. - // We don't use this natively within Umbraco but 3rd parties might with SQL server. - DateTimeOffsetColumnDefinition = "DATETIME"; - } - - 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); - } - - public override bool SupportsClustered() - { - return false; - } - - /// - /// SqlCe doesn't support the Truncate Table syntax, so we just have to do a DELETE FROM which is slower but we have no choice. - /// - public override string TruncateTable - { - get { return "DELETE FROM {0}"; } - } - - public override string GetIndexType(IndexTypes indexTypes) - { - string indexType; - //NOTE Sql Ce doesn't support clustered indexes - if (indexTypes == IndexTypes.Clustered) - { - indexType = "NONCLUSTERED"; - } - else - { - indexType = indexTypes == IndexTypes.NonClustered - ? "NONCLUSTERED" - : "UNIQUE NONCLUSTERED"; - } - return indexType; - } - - public override string GetConcat(params string[] args) - { - return "(" + string.Join("+", args) + ")"; - } - - public override System.Data.IsolationLevel DefaultIsolationLevel => System.Data.IsolationLevel.RepeatableRead; - public override string DbProvider => "SqlServerCE"; - - public override string FormatColumnRename(string tableName, string oldName, string newName) - { - //NOTE Sql CE doesn't support renaming a column, so a new column needs to be created, then copy data and finally remove old column - //This assumes that the new column has been created, and that the old column will be deleted after this statement has run. - //http://stackoverflow.com/questions/3967353/microsoft-sql-compact-edition-rename-column - - return string.Format("UPDATE {0} SET {1} = {2}", tableName, newName, oldName); - } - - public override string FormatTableRename(string oldName, string newName) - { - return string.Format(RenameTable, oldName, newName); - } - - public override string FormatPrimaryKey(TableDefinition table) - { - var columnDefinition = table.Columns.FirstOrDefault(x => x.IsPrimaryKey); - if (columnDefinition == null) - return string.Empty; - - string constraintName = string.IsNullOrEmpty(columnDefinition.PrimaryKeyName) - ? string.Format("PK_{0}", table.Name) - : columnDefinition.PrimaryKeyName; - - string columns = string.IsNullOrEmpty(columnDefinition.PrimaryKeyColumns) - ? GetQuotedColumnName(columnDefinition.Name) - : string.Join(", ", columnDefinition.PrimaryKeyColumns - .Split(Constants.CharArrays.CommaSpace, StringSplitOptions.RemoveEmptyEntries) - .Select(GetQuotedColumnName)); - - return string.Format(CreateConstraint, - GetQuotedTableName(table.Name), - GetQuotedName(constraintName), - "PRIMARY KEY", - columns); - } - - public override IEnumerable GetTablesInSchema(IDatabase db) - { - var items = db.Fetch("SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES"); - return items.Select(x => x.TABLE_NAME).Cast().ToList(); - } - - public override IEnumerable GetColumnsInSchema(IDatabase db) - { - var items = db.Fetch("SELECT TABLE_NAME, COLUMN_NAME, ORDINAL_POSITION, COLUMN_DEFAULT, IS_NULLABLE, DATA_TYPE FROM INFORMATION_SCHEMA.COLUMNS"); - return - items.Select( - item => - new ColumnInfo(item.TABLE_NAME, item.COLUMN_NAME, item.ORDINAL_POSITION, item.COLUMN_DEFAULT, - item.IS_NULLABLE, item.DATA_TYPE)).ToList(); - } - - /// - public override IEnumerable> GetConstraintsPerTable(IDatabase db) - { - var items = db.Fetch("SELECT TABLE_NAME, CONSTRAINT_NAME FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS"); - return items.Select(item => new Tuple(item.TABLE_NAME, item.CONSTRAINT_NAME)).ToList(); - } - - /// - public override IEnumerable> GetConstraintsPerColumn(IDatabase db) - { - var items = db.Fetch("SELECT TABLE_NAME, COLUMN_NAME, CONSTRAINT_NAME FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE"); - return items.Select(item => new Tuple(item.TABLE_NAME, item.COLUMN_NAME, item.CONSTRAINT_NAME)).ToList(); - } - - /// - public override IEnumerable> GetDefinedIndexes(IDatabase db) - { - var items = - db.Fetch( - @"SELECT TABLE_NAME, INDEX_NAME, COLUMN_NAME, [UNIQUE] FROM INFORMATION_SCHEMA.INDEXES -WHERE PRIMARY_KEY=0 -ORDER BY TABLE_NAME, INDEX_NAME"); - return - items.Select( - item => new Tuple(item.TABLE_NAME, item.INDEX_NAME, item.COLUMN_NAME, item.UNIQUE)); - } - - /// - public override bool TryGetDefaultConstraint(IDatabase db, string tableName, string columnName, out string constraintName) - { - // cannot return a true default constraint name (does not exist on SqlCe) - // but we won't really need it anyways - just check whether there is a constraint - constraintName = null; - var hasDefault = db.Fetch(@"select column_hasdefault from information_schema.columns -where table_name=@0 and column_name=@1", tableName, columnName).FirstOrDefault(); - return hasDefault; - } - - public override bool DoesTableExist(IDatabase db, string tableName) - { - var result = - db.ExecuteScalar("SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = @TableName", - new { TableName = tableName }); - - return result > 0; - } - - public override void WriteLock(IDatabase db, TimeSpan timeout, int lockId) - { - // soon as we get Database, a transaction is started - - if (db.Transaction.IsolationLevel < IsolationLevel.RepeatableRead) - throw new InvalidOperationException("A transaction with minimum RepeatableRead isolation level is required."); - - ObtainWriteLock(db, timeout, lockId); - } - - public override void WriteLock(IDatabase db, params int[] lockIds) - { - // soon as we get Database, a transaction is started - - if (db.Transaction.IsolationLevel < IsolationLevel.RepeatableRead) - throw new InvalidOperationException("A transaction with minimum RepeatableRead isolation level is required."); - - var timeout = _globalSettings.Value.SqlWriteLockTimeOut; - - foreach (var lockId in lockIds) - { - ObtainWriteLock(db, timeout, lockId); - } - } - - private static void ObtainWriteLock(IDatabase db, TimeSpan timeout, int lockId) - { - db.Execute(@"SET LOCK_TIMEOUT " + timeout.TotalMilliseconds + ";"); - var i = db.Execute(@"UPDATE umbracoLock SET value = (CASE WHEN (value=1) THEN -1 ELSE 1 END) WHERE id=@id", new { id = lockId }); - if (i == 0) // ensure we are actually locking! - throw new ArgumentException($"LockObject with id={lockId} does not exist."); - } - - public override void ReadLock(IDatabase db, TimeSpan timeout, int lockId) - { - // soon as we get Database, a transaction is started - - if (db.Transaction.IsolationLevel < IsolationLevel.RepeatableRead) - throw new InvalidOperationException("A transaction with minimum RepeatableRead isolation level is required."); - - ObtainReadLock(db, timeout, lockId); - } - - public override void ReadLock(IDatabase db, params int[] lockIds) - { - // soon as we get Database, a transaction is started - - if (db.Transaction.IsolationLevel < IsolationLevel.RepeatableRead) - throw new InvalidOperationException("A transaction with minimum RepeatableRead isolation level is required."); - - foreach (var lockId in lockIds) - { - ObtainReadLock(db, null, lockId); - } - } - - private static void ObtainReadLock(IDatabase db, TimeSpan? timeout, int lockId) - { - if (timeout.HasValue) - { - db.Execute(@"SET LOCK_TIMEOUT " + timeout.Value.TotalMilliseconds + ";"); - } - - var i = db.ExecuteScalar("SELECT value FROM umbracoLock WHERE id=@id", new {id = lockId}); - - if (i == null) // ensure we are actually locking! - throw new ArgumentException($"LockObject with id={lockId} does not exist."); - } - - protected override string FormatIdentity(ColumnDefinition column) - { - return column.IsIdentity ? GetIdentityString(column) : string.Empty; - } - - private static string GetIdentityString(ColumnDefinition column) - { - if (column.Seeding != default(int)) - return string.Format("IDENTITY({0},1)", column.Seeding); - - return "IDENTITY(1,1)"; - } - - protected override string FormatSystemMethods(SystemMethods systemMethod) - { - switch (systemMethod) - { - case SystemMethods.NewGuid: - return "NEWID()"; - case SystemMethods.CurrentDateTime: - return "GETDATE()"; - //case SystemMethods.NewSequentialId: - // return "NEWSEQUENTIALID()"; - //case SystemMethods.CurrentUTCDateTime: - // return "GETUTCDATE()"; - } - - return null; - } - - public override string DeleteDefaultConstraint - { - get - { - return "ALTER TABLE {0} ALTER COLUMN {1} DROP DEFAULT"; - } - } - - public override string DropIndex { get { return "DROP INDEX {1}.{0}"; } } - public override string CreateIndex => "CREATE {0}{1}INDEX {2} ON {3} ({4})"; - public override string Format(IndexDefinition index) - { - var name = string.IsNullOrEmpty(index.Name) - ? $"IX_{index.TableName}_{index.ColumnName}" - : index.Name; - - var columns = index.Columns.Any() - ? string.Join(",", index.Columns.Select(x => GetQuotedColumnName(x.Name))) - : GetQuotedColumnName(index.ColumnName); - - - return string.Format(CreateIndex, GetIndexType(index.IndexType), " ", GetQuotedName(name), - GetQuotedTableName(index.TableName), columns); - } - - public override string GetSpecialDbType(SpecialDbType dbTypes) - { - // SqlCE does not have nvarchar(max) for now - if (dbTypes == SpecialDbType.NVARCHARMAX) - { - return "NTEXT"; - } - - return base.GetSpecialDbType(dbTypes); - } - public override SqlDbType GetSqlDbType(DbType dbType) - { - if (DbType.Binary == dbType) - { - return SqlDbType.Image; - } - return base.GetSqlDbType(dbType); - } - } -} diff --git a/src/Umbraco.Persistence.SqlCe/Umbraco.Persistence.SqlCe.csproj b/src/Umbraco.Persistence.SqlCe/Umbraco.Persistence.SqlCe.csproj deleted file mode 100644 index 86aaafa5ec..0000000000 --- a/src/Umbraco.Persistence.SqlCe/Umbraco.Persistence.SqlCe.csproj +++ /dev/null @@ -1,39 +0,0 @@ - - - - net472 - Umbraco.Cms.Persistence.SqlCe - - - - bin\Release\Umbraco.Persistence.SqlCe.xml - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - all - - - - - - - - - - - - - - - - diff --git a/src/Umbraco.PublishedCache.NuCache/CacheKeys.cs b/src/Umbraco.PublishedCache.NuCache/CacheKeys.cs index 3d8f14afd3..0ec6f0b7cb 100644 --- a/src/Umbraco.PublishedCache.NuCache/CacheKeys.cs +++ b/src/Umbraco.PublishedCache.NuCache/CacheKeys.cs @@ -13,9 +13,7 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache [MethodImpl(MethodImplOptions.AggressiveInlining)] private static string LangId(string culture) - { - return culture != null ? ("-L:" + culture) : string.Empty; - } + => string.IsNullOrEmpty(culture) ? string.Empty : ("-L:" + culture); public static string PublishedContentChildren(Guid contentUid, bool previewing) { diff --git a/src/Umbraco.PublishedCache.NuCache/ContentStore.cs b/src/Umbraco.PublishedCache.NuCache/ContentStore.cs index 240e6c8861..98fc4a3ffe 100644 --- a/src/Umbraco.PublishedCache.NuCache/ContentStore.cs +++ b/src/Umbraco.PublishedCache.NuCache/ContentStore.cs @@ -608,7 +608,10 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache throw new ArgumentException("Kit content cannot have children.", nameof(kit)); // ReSharper restore LocalizableElement - _logger.LogDebug("Set content ID: {KitNodeId}", kit.Node.Id); + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("Set content ID: {KitNodeId}", kit.Node.Id); + } // get existing _contentNodes.TryGetValue(kit.Node.Id, out var link); @@ -727,7 +730,11 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache previousNode = null; // there is no previous sibling } - _logger.LogDebug("Set {thisNodeId} with parent {thisNodeParentContentId}", thisNode.Id, thisNode.ParentContentId); + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("Set {thisNodeId} with parent {thisNodeParentContentId}", thisNode.Id, thisNode.ParentContentId); + } + SetValueLocked(_contentNodes, thisNode.Id, thisNode); // if we are initializing from the database source ensure the local db is updated @@ -784,7 +791,12 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache ok = false; continue; // skip that one } - _logger.LogDebug("Set {kitNodeId} with parent {kitNodeParentContentId}", kit.Node.Id, kit.Node.ParentContentId); + + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("Set {kitNodeId} with parent {kitNodeParentContentId}", kit.Node.Id, kit.Node.ParentContentId); + } + SetValueLocked(_contentNodes, kit.Node.Id, kit.Node); if (_localDb != null) RegisterChange(kit.Node.Id, kit); @@ -873,7 +885,11 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache if (link?.Value == null) return false; var content = link.Value; - _logger.LogDebug("Clear content ID: {ContentId}", content.Id); + + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("Clear content ID: {ContentId}", content.Id); + } // clear the entire branch ClearBranchLocked(content); diff --git a/src/Umbraco.PublishedCache.NuCache/Persistence/NuCacheContentRepository.cs b/src/Umbraco.PublishedCache.NuCache/Persistence/NuCacheContentRepository.cs index 8aa35c1608..9964195f34 100644 --- a/src/Umbraco.PublishedCache.NuCache/Persistence/NuCacheContentRepository.cs +++ b/src/Umbraco.PublishedCache.NuCache/Persistence/NuCacheContentRepository.cs @@ -364,7 +364,7 @@ AND cmsContentNu.nodeId IS NULL foreach (IProperty prop in content.Properties) { var pdatas = new List(); - foreach (IPropertyValue pvalue in prop.Values) + foreach (IPropertyValue pvalue in prop.Values.OrderBy(x => x.Culture)) { // sanitize - properties should be ok but ... never knows if (!prop.PropertyType.SupportsVariation(pvalue.Culture, pvalue.Segment)) diff --git a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs index 2f67da6caf..e34223fec1 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs @@ -105,7 +105,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers var keepOnlyKeys = new Dictionary { {"umbracoUrls", new[] {"authenticationApiBaseUrl", "serverVarsJs", "externalLoginsUrl", "currentUserApiBaseUrl", "previewHubUrl", "iconApiBaseUrl"}}, - {"umbracoSettings", new[] {"allowPasswordReset", "imageFileTypes", "maxFileSize", "loginBackgroundImage", "loginLogoImage", "canSendRequiredEmail", "usernameIsEmail", "minimumPasswordLength", "minimumPasswordNonAlphaNum"}}, + {"umbracoSettings", new[] {"allowPasswordReset", "imageFileTypes", "maxFileSize", "loginBackgroundImage", "loginLogoImage", "canSendRequiredEmail", "usernameIsEmail", "minimumPasswordLength", "minimumPasswordNonAlphaNum", "hideBackofficeLogo", "disableDeleteWhenReferenced", "disableUnpublishWhenReferenced"}}, {"application", new[] {"applicationPath", "cacheBuster"}}, {"isDebuggingEnabled", new string[] { }}, {"features", new [] {"disabledFeatures"}} @@ -383,6 +383,10 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers { "previewHubUrl", _previewRoutes.GetPreviewHubRoute() }, + { + "trackedReferencesApiBaseUrl", _linkGenerator.GetUmbracoApiServiceBaseUrl( + controller => controller.GetPagedReferences(0, 1, 1, false)) + } } }, { @@ -413,6 +417,9 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers {"allowPasswordReset", _securitySettings.AllowPasswordReset}, {"loginBackgroundImage", _contentSettings.LoginBackgroundImage}, {"loginLogoImage", _contentSettings.LoginLogoImage }, + {"hideBackofficeLogo", _contentSettings.HideBackOfficeLogo }, + {"disableDeleteWhenReferenced", _contentSettings.DisableDeleteWhenReferenced }, + {"disableUnpublishWhenReferenced", _contentSettings.DisableUnpublishWhenReferenced }, {"showUserInvite", _emailSender.CanSendRequiredEmail()}, {"canSendRequiredEmail", _emailSender.CanSendRequiredEmail()}, {"showAllowSegmentationForDocumentTypes", false}, diff --git a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs index 25abbf1fab..ef5f39540e 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs @@ -819,11 +819,17 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers contentItem.Variants.Where(x => x.Save).Select(x => x.Culture).ToArray(), defaultCulture); + //get the updated model + bool isBlueprint = contentItem.PersistedContent.Blueprint; + + var contentSavedHeader = isBlueprint ? "editBlueprintSavedHeader" : "editContentSavedHeader"; + var contentSavedText = isBlueprint ? "editBlueprintSavedText" : "editContentSavedText"; + switch (contentItem.Action) { case ContentSaveAction.Save: case ContentSaveAction.SaveNew: - SaveAndNotify(contentItem, saveMethod, variantCount, notifications, globalNotifications, "editContentSavedText", "editVariantSavedText", cultureForInvariantErrors, null, out wasCancelled); + SaveAndNotify(contentItem, saveMethod, variantCount, notifications, globalNotifications, contentSavedHeader, contentSavedText, "editVariantSavedText", cultureForInvariantErrors, null, out wasCancelled); break; case ContentSaveAction.Schedule: case ContentSaveAction.ScheduleNew: @@ -833,7 +839,8 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers wasCancelled = false; break; } - SaveAndNotify(contentItem, saveMethod, variantCount, notifications, globalNotifications, "editContentScheduledSavedText", "editVariantSavedText", cultureForInvariantErrors, contentSchedule, out wasCancelled); + + SaveAndNotify(contentItem, saveMethod, variantCount, notifications, globalNotifications, "editContentSavedHeader", "editContentScheduledSavedText", "editVariantSavedText", cultureForInvariantErrors, contentSchedule, out wasCancelled); break; case ContentSaveAction.SendPublish: @@ -882,7 +889,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (!await ValidatePublishBranchPermissionsAsync(contentItem)) { globalNotifications.AddErrorNotification( - _localizedTextService.Localize(null,"publish"), + _localizedTextService.Localize(null, "publish"), _localizedTextService.Localize("publish", "invalidPublishBranchPermissions")); wasCancelled = false; break; @@ -899,7 +906,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (!await ValidatePublishBranchPermissionsAsync(contentItem)) { globalNotifications.AddErrorNotification( - _localizedTextService.Localize(null,"publish"), + _localizedTextService.Localize(null, "publish"), _localizedTextService.Localize("publish", "invalidPublishBranchPermissions")); wasCancelled = false; break; @@ -913,7 +920,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers throw new ArgumentOutOfRangeException(); } - //get the updated model + // We have to map do display after we've actually saved the content, otherwise we'll miss information that's set when saving content, such as ID var display = mapToDisplay(contentItem.PersistedContent); //merge the tracked success messages with the outgoing model @@ -1040,7 +1047,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers /// private void SaveAndNotify(ContentItemSave contentItem, Func saveMethod, int variantCount, Dictionary notifications, SimpleNotificationModel globalNotifications, - string invariantSavedLocalizationAlias, string variantSavedLocalizationAlias, string cultureForInvariantErrors, ContentScheduleCollection contentSchedule, + string savedContentHeaderLocalizationAlias, string invariantSavedLocalizationAlias, string variantSavedLocalizationAlias, string cultureForInvariantErrors, ContentScheduleCollection contentSchedule, out bool wasCancelled) { var saveResult = saveMethod(contentItem.PersistedContent, contentSchedule); @@ -1060,15 +1067,15 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers var variantName = GetVariantName(culture, segment); AddSuccessNotification(notifications, culture, segment, - _localizedTextService.Localize("speechBubbles", "editContentSavedHeader"), - _localizedTextService.Localize(null,variantSavedLocalizationAlias, new[] { variantName })); + _localizedTextService.Localize("speechBubbles", savedContentHeaderLocalizationAlias), + _localizedTextService.Localize(null, variantSavedLocalizationAlias, new[] { variantName })); } } else if (ModelState.IsValid) { globalNotifications.AddSuccessNotification( - _localizedTextService.Localize("speechBubbles", "editContentSavedHeader"), - _localizedTextService.Localize("speechBubbles",invariantSavedLocalizationAlias)); + _localizedTextService.Localize("speechBubbles", savedContentHeaderLocalizationAlias), + _localizedTextService.Localize("speechBubbles", invariantSavedLocalizationAlias)); } } } @@ -2112,21 +2119,34 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers } var variantIndex = 0; + var defaultCulture = _allLangs.Value.Values.FirstOrDefault(x => x.IsDefault)?.IsoCode; - //loop through each variant, set the correct name and property values + // loop through each variant, set the correct name and property values foreach (var variant in contentSave.Variants) { - //Don't update anything for this variant if Save is not true - if (!variant.Save) continue; + // Don't update anything for this variant if Save is not true + if (!variant.Save) + { + continue; + } - //Don't update the name if it is empty + // Don't update the name if it is empty if (!variant.Name.IsNullOrWhiteSpace()) { if (contentSave.PersistedContent.ContentType.VariesByCulture()) { if (variant.Culture.IsNullOrWhiteSpace()) + { throw new InvalidOperationException($"Cannot set culture name without a culture."); + } + contentSave.PersistedContent.SetCultureName(variant.Name, variant.Culture); + + // If the variant culture is the default culture we also want to update the name on the Content itself. + if (variant.Culture.Equals(defaultCulture, StringComparison.InvariantCultureIgnoreCase)) + { + contentSave.PersistedContent.Name = variant.Name; + } } else { @@ -2134,7 +2154,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers } } - //This is important! We only want to process invariant properties with the first variant, for any other variant + // This is important! We only want to process invariant properties with the first variant, for any other variant // we need to exclude invariant properties from being processed, otherwise they will be double processed for the // same value which can cause some problems with things such as file uploads. var propertyCollection = variantIndex == 0 @@ -2142,10 +2162,10 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers : new ContentPropertyCollectionDto { Properties = variant.PropertyCollectionDto.Properties.Where( - x => !x.Culture.IsNullOrWhiteSpace() || !x.Segment.IsNullOrWhiteSpace()) + x => !x.Culture.IsNullOrWhiteSpace() || !x.Segment.IsNullOrWhiteSpace()), }; - //for each variant, map the property values + // for each variant, map the property values MapPropertyValuesForPersistence( contentSave, propertyCollection, @@ -2166,6 +2186,12 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers variantIndex++; } + // Map IsDirty cultures to edited cultures, to make it easier to verify changes on specific variants on Saving and Saved events. + IEnumerable editedCultures = contentSave.PersistedContent.CultureInfos.Values + .Where(x => x.IsDirty()) + .Select(x => x.Culture); + contentSave.PersistedContent.SetCultureEdited(editedCultures); + // handle template if (string.IsNullOrWhiteSpace(contentSave.TemplateAlias)) // cleared: clear if not already null { @@ -2176,10 +2202,10 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers } else // set: update if different { - var template = _fileService.GetTemplate(contentSave.TemplateAlias); - if (template == null) + ITemplate template = _fileService.GetTemplate(contentSave.TemplateAlias); + if (template is null) { - //ModelState.AddModelError("Template", "No template exists with the specified alias: " + contentItem.TemplateAlias); + // ModelState.AddModelError("Template", "No template exists with the specified alias: " + contentItem.TemplateAlias); _logger.LogWarning("No template exists with the specified alias: {TemplateAlias}", contentSave.TemplateAlias); } else if (template.Id != contentSave.PersistedContent.TemplateId) diff --git a/src/Umbraco.Web.BackOffice/Controllers/ContentControllerBase.cs b/src/Umbraco.Web.BackOffice/Controllers/ContentControllerBase.cs index f389641777..b22a7d715c 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ContentControllerBase.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ContentControllerBase.cs @@ -224,7 +224,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers { // if there's already a default event message, don't add our default one IEventMessagesFactory messages = EventMessages; - if (messages != null && messages.GetOrDefault().GetAll().Any(x => x.IsDefaultEventMessage)) + if (messages?.GetOrDefault()?.GetAll().Any(x => x.IsDefaultEventMessage) == true) { return; } diff --git a/src/Umbraco.Web.BackOffice/Controllers/ContentTypeController.cs b/src/Umbraco.Web.BackOffice/Controllers/ContentTypeController.cs index 9164588d7d..37ddd5e7cb 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ContentTypeController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ContentTypeController.cs @@ -433,7 +433,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers /// /// Returns all content type objects /// - [Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)] + [Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentsOrDocumentTypes)] public IEnumerable GetAll() { var types = _contentTypeService.GetAll(); diff --git a/src/Umbraco.Web.BackOffice/Controllers/DashboardController.cs b/src/Umbraco.Web.BackOffice/Controllers/DashboardController.cs index 8255dcd977..70fb864482 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/DashboardController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/DashboardController.cs @@ -7,6 +7,7 @@ using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Newtonsoft.Json; @@ -19,10 +20,12 @@ using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; +using Umbraco.Cms.Core.Telemetry; using Umbraco.Cms.Web.BackOffice.Filters; using Umbraco.Cms.Web.Common.Attributes; using Umbraco.Cms.Web.Common.Authorization; using Umbraco.Cms.Web.Common.Controllers; +using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Cms.Web.Common.Filters; using Umbraco.Extensions; using Constants = Umbraco.Cms.Core.Constants; @@ -43,10 +46,13 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers private readonly IDashboardService _dashboardService; private readonly IUmbracoVersion _umbracoVersion; private readonly IShortStringHelper _shortStringHelper; + private readonly ISiteIdentifierService _siteIdentifierService; private readonly ContentDashboardSettings _dashboardSettings; + /// /// Initializes a new instance of the with all its dependencies. /// + [ActivatorUtilitiesConstructor] public DashboardController( IBackOfficeSecurityAccessor backOfficeSecurityAccessor, AppCaches appCaches, @@ -54,7 +60,8 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers IDashboardService dashboardService, IUmbracoVersion umbracoVersion, IShortStringHelper shortStringHelper, - IOptionsSnapshot dashboardSettings) + IOptionsSnapshot dashboardSettings, + ISiteIdentifierService siteIdentifierService) { _backOfficeSecurityAccessor = backOfficeSecurityAccessor; @@ -63,6 +70,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers _dashboardService = dashboardService; _umbracoVersion = umbracoVersion; _shortStringHelper = shortStringHelper; + _siteIdentifierService = siteIdentifierService; _dashboardSettings = dashboardSettings.Value; } @@ -79,6 +87,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers var language = user.Language; var version = _umbracoVersion.SemanticVersion.ToSemanticStringWithoutBuild(); var isAdmin = user.IsAdmin(); + _siteIdentifierService.TryGetOrCreateSiteIdentifier(out Guid siteIdentifier); if (!IsAllowedUrl(baseUrl)) { @@ -90,14 +99,15 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers return JObject.Parse(errorJson); } - var url = string.Format("{0}{1}?section={2}&allowed={3}&lang={4}&version={5}&admin={6}", + var url = string.Format("{0}{1}?section={2}&allowed={3}&lang={4}&version={5}&admin={6}&siteid={7}", baseUrl, _dashboardSettings.ContentDashboardPath, section, allowedSections, language, version, - isAdmin); + isAdmin, + siteIdentifier); var key = "umbraco-dynamic-dashboard-" + language + allowedSections.Replace(",", "-") + section; var content = _appCaches.RuntimeCache.GetCacheItem(key); diff --git a/src/Umbraco.Web.BackOffice/Controllers/EntityController.cs b/src/Umbraco.Web.BackOffice/Controllers/EntityController.cs index a4588ed9f7..a01b88501f 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/EntityController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/EntityController.cs @@ -760,27 +760,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (id == Constants.System.Root && startNodes.Length > 0 && startNodes.Contains(Constants.System.Root) == false && !ignoreUserStartNodes) { - if (pageNumber > 0) - { - return new PagedResult(0, 0, 0); - } - - IEntitySlim[] nodes = _entityService.GetAll(objectType.Value, startNodes).ToArray(); - if (nodes.Length == 0) - { - return new PagedResult(0, 0, 0); - } - - if (pageSize < nodes.Length) - { - pageSize = nodes.Length; // bah - } - - var pr = new PagedResult(nodes.Length, pageNumber, pageSize) - { - Items = nodes.Select(_umbracoMapper.Map) - }; - return pr; + return new PagedResult(0, 0, 0); } // else proceed as usual diff --git a/src/Umbraco.Web.BackOffice/Controllers/ExamineManagementController.cs b/src/Umbraco.Web.BackOffice/Controllers/ExamineManagementController.cs index 008582b6b3..911e50d0a0 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ExamineManagementController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ExamineManagementController.cs @@ -64,7 +64,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers public ActionResult GetSearchResults(string searcherName, string query, int pageIndex = 0, int pageSize = 20) { - query = query.Trim(); + query = query?.Trim(); if (query.IsNullOrWhiteSpace()) { diff --git a/src/Umbraco.Web.BackOffice/Controllers/ImagesController.cs b/src/Umbraco.Web.BackOffice/Controllers/ImagesController.cs index 564d0dcdd9..a10d524c03 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ImagesController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ImagesController.cs @@ -54,12 +54,20 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers public IActionResult GetResized(string imagePath, int width) { var ext = Path.GetExtension(imagePath); - + + // check if imagePath is local to prevent open redirect + if (!Uri.IsWellFormedUriString(imagePath, UriKind.Relative)) + { + return Unauthorized(); + } + // we need to check if it is an image by extension if (_imageUrlGenerator.IsSupportedImageFormat(ext) == false) + { return NotFound(); - - //redirect to ImageProcessor thumbnail with rnd generated from last modified time of original media file + } + + // redirect to ImageProcessor thumbnail with rnd generated from last modified time of original media file DateTimeOffset? imageLastModified = null; try { @@ -80,8 +88,14 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers ImageCropMode = ImageCropMode.Max, CacheBusterValue = rnd }); - - return new RedirectResult(imageUrl, false); + if (Url.IsLocalUrl(imageUrl)) + { + return new LocalRedirectResult(imageUrl, false); + } + else + { + return Unauthorized(); + } } /// diff --git a/src/Umbraco.Web.BackOffice/Controllers/LanguageController.cs b/src/Umbraco.Web.BackOffice/Controllers/LanguageController.cs index 2cbf21fc7c..8dcdcb060b 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/LanguageController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/LanguageController.cs @@ -174,6 +174,19 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers return ValidationProblem(ModelState); } + // Update language + CultureInfo cultureAfterChange; + try + { + // language has the CultureName of the previous lang so we get information about new culture. + cultureAfterChange = CultureInfo.GetCultureInfo(language.IsoCode); + } + catch (CultureNotFoundException) + { + ModelState.AddModelError("IsoCode", "No Culture found with name " + language.IsoCode); + return ValidationProblem(ModelState); + } + existingById.CultureName = cultureAfterChange.DisplayName; existingById.IsDefault = language.IsDefault; existingById.FallbackLanguageId = language.FallbackLanguageId; existingById.IsoCode = language.IsoCode; diff --git a/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs b/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs index c3d947ac70..0dc6a45946 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs @@ -33,12 +33,9 @@ using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Infrastructure.Persistence; -using Umbraco.Cms.Web.BackOffice.ActionResults; using Umbraco.Cms.Web.BackOffice.Authorization; -using Umbraco.Cms.Web.BackOffice.Extensions; using Umbraco.Cms.Web.BackOffice.Filters; using Umbraco.Cms.Web.BackOffice.ModelBinders; -using Umbraco.Cms.Web.Common.ActionsResults; using Umbraco.Cms.Web.Common.Attributes; using Umbraco.Cms.Web.Common.Authorization; using Umbraco.Extensions; @@ -676,12 +673,19 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers { return new ActionResult(parentIdResult.Result); } + var parentId = parentIdResult.Value; if (!parentId.HasValue) { return NotFound("The passed id doesn't exist"); } + var isFolderAllowed = IsFolderCreationAllowedHere(parentId.Value); + if (isFolderAllowed == false) + { + return ValidationProblem(_localizedTextService.Localize("speechBubbles", "folderCreationNotAllowed")); + } + var f = _mediaService.CreateMedia(folder.Name, parentId.Value, Constants.Conventions.MediaTypes.Folder); _mediaService.Save(f, _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id); @@ -722,10 +726,14 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers var tempFiles = new PostedFiles(); - //in case we pass a path with a folder in it, we will create it and upload media to it. if (!string.IsNullOrEmpty(path)) { + if (!IsFolderCreationAllowedHere(parentId.Value)) + { + AddCancelMessage(tempFiles, _localizedTextService.Localize("speechBubbles", "folderUploadNotAllowed")); + return Ok(tempFiles); + } var folders = path.Split(Constants.CharArrays.ForwardSlash); @@ -735,7 +743,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers IMedia folderMediaItem; //if uploading directly to media root and not a subfolder - if (parentId == -1) + if (parentId == Constants.System.Root) { //look for matching folder folderMediaItem = @@ -768,11 +776,50 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers _mediaService.Save(folderMediaItem); } } + //set the media root to the folder id so uploaded files will end there. parentId = folderMediaItem.Id; } } + var mediaTypeAlias = string.Empty; + var allMediaTypes = _mediaTypeService.GetAll().ToList(); + var allowedContentTypes = new HashSet(); + + if (parentId != Constants.System.Root) + { + var mediaFolderItem = _mediaService.GetById(parentId.Value); + var mediaFolderType = allMediaTypes.FirstOrDefault(x => x.Alias == mediaFolderItem.ContentType.Alias); + + if (mediaFolderType != null) + { + IMediaType mediaTypeItem = null; + + foreach (ContentTypeSort allowedContentType in mediaFolderType.AllowedContentTypes) + { + IMediaType checkMediaTypeItem = allMediaTypes.FirstOrDefault(x => x.Id == allowedContentType.Id.Value); + allowedContentTypes.Add(checkMediaTypeItem); + + var fileProperty = checkMediaTypeItem?.CompositionPropertyTypes.FirstOrDefault(x => x.Alias == Constants.Conventions.Media.File); + if (fileProperty != null) + { + mediaTypeItem = checkMediaTypeItem; + } + } + + //Only set the permission-based mediaType if we only allow 1 specific file under this parent. + if (allowedContentTypes.Count == 1 && mediaTypeItem != null) + { + mediaTypeAlias = mediaTypeItem.Alias; + } + } + } + else + { + var typesAllowedAtRoot = allMediaTypes.Where(x => x.AllowedAsRoot).ToList(); + allowedContentTypes.UnionWith(typesAllowedAtRoot); + } + //get the files foreach (var formFile in file) { @@ -780,71 +827,82 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers var safeFileName = fileName.ToSafeFileName(ShortStringHelper); var ext = safeFileName.Substring(safeFileName.LastIndexOf('.') + 1).ToLower(); - if (_contentSettings.IsFileAllowedForUpload(ext)) - { - var mediaType = Constants.Conventions.MediaTypes.File; - - if (contentTypeAlias == Constants.Conventions.MediaTypes.AutoSelect) - { - var mediaTypes = _mediaTypeService.GetAll(); - // Look up MediaTypes - foreach (var mediaTypeItem in mediaTypes) - { - var fileProperty = mediaTypeItem.CompositionPropertyTypes.FirstOrDefault(x => x.Alias == "umbracoFile"); - if (fileProperty != null) - { - var dataTypeKey = fileProperty.DataTypeKey; - var dataType = _dataTypeService.GetDataType(dataTypeKey); - - if (dataType != null && dataType.Configuration is IFileExtensionsConfig fileExtensionsConfig) - { - var fileExtensions = fileExtensionsConfig.FileExtensions; - if (fileExtensions != null) - { - if (fileExtensions.Where(x => x.Value == ext).Count() != 0) - { - mediaType = mediaTypeItem.Alias; - break; - } - } - } - } - } - - // If media type is still File then let's check if it's an image. - if (mediaType == Constants.Conventions.MediaTypes.File && _imageUrlGenerator.SupportedImageFileTypes.Contains(ext)) - { - mediaType = Constants.Conventions.MediaTypes.Image; - } - } - else - { - mediaType = contentTypeAlias; - } - - var mediaItemName = fileName.ToFriendlyName(); - - var f = _mediaService.CreateMedia(mediaItemName, parentId.Value, mediaType, _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id); - - - await using (var stream = formFile.OpenReadStream()) - { - f.SetValue(_mediaFileManager, _mediaUrlGenerators, _shortStringHelper, _contentTypeBaseServiceProvider, Constants.Conventions.Media.File, fileName, stream); - } - - - var saveResult = _mediaService.Save(f, _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id); - if (saveResult == false) - { - AddCancelMessage(tempFiles, _localizedTextService.Localize("speechBubbles", "operationCancelledText") + " -- " + mediaItemName); - } - } - else + if (!_contentSettings.IsFileAllowedForUpload(ext)) { tempFiles.Notifications.Add(new BackOfficeNotification( _localizedTextService.Localize("speechBubbles", "operationFailedHeader"), _localizedTextService.Localize("media", "disallowedFileType"), NotificationStyle.Warning)); + continue; + } + + if (string.IsNullOrEmpty(mediaTypeAlias)) + { + mediaTypeAlias = Constants.Conventions.MediaTypes.File; + + if (contentTypeAlias == Constants.Conventions.MediaTypes.AutoSelect) + { + // Look up MediaTypes + foreach (var mediaTypeItem in allMediaTypes) + { + var fileProperty = mediaTypeItem.CompositionPropertyTypes.FirstOrDefault(x => x.Alias == Constants.Conventions.Media.File); + if (fileProperty == null) + { + continue; + } + + var dataTypeKey = fileProperty.DataTypeKey; + var dataType = _dataTypeService.GetDataType(dataTypeKey); + + if (dataType == null || dataType.Configuration is not IFileExtensionsConfig fileExtensionsConfig) + { + continue; + } + + var fileExtensions = fileExtensionsConfig.FileExtensions; + if (fileExtensions == null || fileExtensions.All(x => x.Value != ext)) + { + continue; + } + + mediaTypeAlias = mediaTypeItem.Alias; + break; + } + + // If media type is still File then let's check if it's an image. + if (mediaTypeAlias == Constants.Conventions.MediaTypes.File && _imageUrlGenerator.SupportedImageFileTypes.Contains(ext)) + { + mediaTypeAlias = Constants.Conventions.MediaTypes.Image; + } + } + else + { + mediaTypeAlias = contentTypeAlias; + } + } + + if (allowedContentTypes.Any(x => x.Alias == mediaTypeAlias) == false) + { + tempFiles.Notifications.Add(new BackOfficeNotification( + _localizedTextService.Localize("speechBubbles", "operationFailedHeader"), + _localizedTextService.Localize("media", "disallowedMediaType", new[] { mediaTypeAlias }), + NotificationStyle.Warning)); + continue; + } + + var mediaItemName = fileName.ToFriendlyName(); + + var createdMediaItem = _mediaService.CreateMedia(mediaItemName, parentId.Value, mediaTypeAlias, _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id); + + await using (var stream = formFile.OpenReadStream()) + { + createdMediaItem.SetValue(_mediaFileManager, _mediaUrlGenerators, _shortStringHelper, _contentTypeBaseServiceProvider, Constants.Conventions.Media.File, fileName, stream); + } + + var saveResult = _mediaService.Save(createdMediaItem, _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id); + if (saveResult == false) + { + AddCancelMessage(tempFiles, _localizedTextService.Localize("speechBubbles", "operationCancelledText") + " -- " + mediaItemName); } } @@ -861,6 +919,29 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers return Ok(tempFiles); } + private bool IsFolderCreationAllowedHere(int parentId) + { + var allMediaTypes = _mediaTypeService.GetAll().ToList(); + var isFolderAllowed = false; + if (parentId == Constants.System.Root) + { + var typesAllowedAtRoot = allMediaTypes.Where(ct => ct.AllowedAsRoot).ToList(); + isFolderAllowed = typesAllowedAtRoot.Any(x => x.Alias == Constants.Conventions.MediaTypes.Folder); + } + else + { + var parentMediaType = _mediaService.GetById(parentId); + var mediaFolderType = allMediaTypes.FirstOrDefault(x => x.Alias == parentMediaType.ContentType.Alias); + if (mediaFolderType != null) + { + isFolderAllowed = + mediaFolderType.AllowedContentTypes.Any(x => x.Alias == Constants.Conventions.MediaTypes.Folder); + } + } + + return isFolderAllowed; + } + private IMedia FindInChildren(int mediaId, string nameToFind, string contentTypeAlias) { const int pageSize = 500; @@ -1001,7 +1082,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers return new ActionResult(toMove); } - + [Obsolete("Please use TrackedReferencesController.GetPagedReferences() instead. Scheduled for removal in V11.")] public PagedResult GetPagedReferences(int id, string entityType, int pageNumber = 1, int pageSize = 100) { if (pageNumber <= 0 || pageSize <= 0) diff --git a/src/Umbraco.Web.BackOffice/Controllers/RelationTypeController.cs b/src/Umbraco.Web.BackOffice/Controllers/RelationTypeController.cs index d63f6b0eda..197148a2a3 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/RelationTypeController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/RelationTypeController.cs @@ -148,7 +148,8 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers relationType.Name.ToSafeAlias(_shortStringHelper, true), relationType.IsBidirectional, relationType.ParentObjectType, - relationType.ChildObjectType); + relationType.ChildObjectType, + relationType.IsDependency); try { diff --git a/src/Umbraco.Web.BackOffice/Controllers/TrackedReferencesController.cs b/src/Umbraco.Web.BackOffice/Controllers/TrackedReferencesController.cs new file mode 100644 index 0000000000..2cef8d61af --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Controllers/TrackedReferencesController.cs @@ -0,0 +1,71 @@ +using System.Collections.Generic; +using System.Linq; +using System.Runtime.Serialization; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Web.BackOffice.ModelBinders; +using Umbraco.Cms.Web.Common.Attributes; +using Umbraco.Cms.Web.Common.Authorization; + +namespace Umbraco.Cms.Web.BackOffice.Controllers +{ + [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] + [Authorize(Policy = AuthorizationPolicies.SectionAccessContentOrMedia)] + public class TrackedReferencesController : BackOfficeNotificationsController + { + private readonly ITrackedReferencesService _relationService; + private readonly IEntityService _entityService; + + public TrackedReferencesController(ITrackedReferencesService relationService, + IEntityService entityService) + { + _relationService = relationService; + _entityService = entityService; + } + + // Used by info tabs on content, media etc. So this is basically finding childs of relations. + public ActionResult> GetPagedReferences(int id, int pageNumber = 1, + int pageSize = 100, bool filterMustBeIsDependency = false) + { + if (pageNumber <= 0 || pageSize <= 0) + { + return BadRequest("Both pageNumber and pageSize must be greater than zero"); + } + + return _relationService.GetPagedRelationsForItems(new []{id}, pageNumber - 1, pageSize, filterMustBeIsDependency); + } + + // Used on delete, finds + public ActionResult> GetPagedDescendantsInReferences(int parentId, int pageNumber = 1, int pageSize = 100, bool filterMustBeIsDependency = true) + { + if (pageNumber <= 0 || pageSize <= 0) + { + return BadRequest("Both pageNumber and pageSize must be greater than zero"); + } + + + return _relationService.GetPagedDescendantsInReferences(parentId, pageNumber - 1, pageSize, filterMustBeIsDependency); + + } + + // Used by unpublish content. So this is basically finding parents of relations. + [HttpGet] + [HttpPost] + public ActionResult> GetPagedReferencedItems([FromJsonPath] int[] ids, int pageNumber = 1, int pageSize = 100, bool filterMustBeIsDependency = true) + { + if (pageNumber <= 0 || pageSize <= 0) + { + return BadRequest("Both pageNumber and pageSize must be greater than zero"); + } + + return _relationService.GetPagedItemsWithRelations(ids, pageNumber - 1, pageSize, filterMustBeIsDependency); + + } + } + +} diff --git a/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs b/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs index 3f59d66a3f..ad766e034c 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs @@ -432,7 +432,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers { if (userSave == null) { - throw new ArgumentNullException("userSave"); + throw new ArgumentNullException(nameof(userSave)); } if (userSave.Message.IsNullOrWhiteSpace()) @@ -440,7 +440,6 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers ModelState.AddModelError("Message", "Message cannot be empty"); } - IUser user; if (_securitySettings.UsernameIsEmail) { // ensure it's the same @@ -449,16 +448,14 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers else { // first validate the username if we're showing it - var userResult = CheckUniqueUsername(userSave.Username, u => u.LastLoginDate != default || u.EmailConfirmedDate.HasValue); - if (!(userResult.Result is null)) + ActionResult userResult = CheckUniqueUsername(userSave.Username, u => u.LastLoginDate != default || u.EmailConfirmedDate.HasValue); + if (userResult.Result is not null) { return userResult.Result; } - - user = userResult.Value; } - user = CheckUniqueEmail(userSave.Email, u => u.LastLoginDate != default || u.EmailConfirmedDate.HasValue); + IUser user = CheckUniqueEmail(userSave.Email, u => u.LastLoginDate != default || u.EmailConfirmedDate.HasValue); if (ModelState.IsValid == false) { diff --git a/src/Umbraco.Web.BackOffice/Install/InstallApiController.cs b/src/Umbraco.Web.BackOffice/Install/InstallApiController.cs index eb6457074e..29c64174dd 100644 --- a/src/Umbraco.Web.BackOffice/Install/InstallApiController.cs +++ b/src/Umbraco.Web.BackOffice/Install/InstallApiController.cs @@ -61,12 +61,8 @@ namespace Umbraco.Cms.Web.BackOffice.Install internal InstallHelper InstallHelper { get; } - public bool PostValidateDatabaseConnection(DatabaseModel model) - { - var canConnect = _databaseBuilder.CanConnect(model.DatabaseType.ToString(), model.ConnectionString, - model.Server, model.DatabaseName, model.Login, model.Password, model.IntegratedAuth); - return canConnect; - } + public bool PostValidateDatabaseConnection(DatabaseModel databaseSettings) + => _databaseBuilder.ConfigureDatabaseConnection(databaseSettings, isTrialRun: true); /// /// Gets the install setup. diff --git a/src/Umbraco.Web.BackOffice/Mapping/ContentMapDefinition.cs b/src/Umbraco.Web.BackOffice/Mapping/ContentMapDefinition.cs index 50dd66ea19..e647309bd9 100644 --- a/src/Umbraco.Web.BackOffice/Mapping/ContentMapDefinition.cs +++ b/src/Umbraco.Web.BackOffice/Mapping/ContentMapDefinition.cs @@ -133,7 +133,7 @@ namespace Umbraco.Cms.Web.BackOffice.Mapping target.AllowedActions = GetActions(source, parent, context); target.AllowedTemplates = GetAllowedTemplates(source); - target.ContentApps = _commonMapper.GetContentApps(source); + target.ContentApps = _commonMapper.GetContentAppsForEntity(source); target.ContentTypeId = source.ContentType.Id; target.ContentTypeKey = source.ContentType.Key; target.ContentTypeAlias = source.ContentType.Alias; diff --git a/src/Umbraco.Web.BackOffice/Mapping/MediaMapDefinition.cs b/src/Umbraco.Web.BackOffice/Mapping/MediaMapDefinition.cs index 03bca1ee70..9bff86e90b 100644 --- a/src/Umbraco.Web.BackOffice/Mapping/MediaMapDefinition.cs +++ b/src/Umbraco.Web.BackOffice/Mapping/MediaMapDefinition.cs @@ -1,4 +1,4 @@ -using System; +using System; using Microsoft.Extensions.Options; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration.Models; @@ -57,7 +57,7 @@ namespace Umbraco.Cms.Web.BackOffice.Mapping // Umbraco.Code.MapAll -Properties -Errors -Edited -Updater -Alias -IsContainer private void Map(IMedia source, MediaItemDisplay target, MapperContext context) { - target.ContentApps = _commonMapper.GetContentApps(source); + target.ContentApps = _commonMapper.GetContentAppsForEntity(source); target.ContentType = _commonMapper.GetContentType(source, context); target.ContentTypeId = source.ContentType.Id; target.ContentTypeAlias = source.ContentType.Alias; diff --git a/src/Umbraco.Web.BackOffice/Mapping/MemberMapDefinition.cs b/src/Umbraco.Web.BackOffice/Mapping/MemberMapDefinition.cs index f0647b9efb..8da173ce68 100644 --- a/src/Umbraco.Web.BackOffice/Mapping/MemberMapDefinition.cs +++ b/src/Umbraco.Web.BackOffice/Mapping/MemberMapDefinition.cs @@ -41,7 +41,7 @@ namespace Umbraco.Cms.Web.BackOffice.Mapping // Umbraco.Code.MapAll -Trashed -IsContainer -VariesByCulture private void Map(IMember source, MemberDisplay target, MapperContext context) { - target.ContentApps = _commonMapper.GetContentApps(source); + target.ContentApps = _commonMapper.GetContentAppsForEntity(source); target.ContentType = _commonMapper.GetContentType(source, context); target.ContentTypeId = source.ContentType.Id; target.ContentTypeAlias = source.ContentType.Alias; diff --git a/src/Umbraco.Web.BackOffice/Security/BackOfficeAuthenticationBuilder.cs b/src/Umbraco.Web.BackOffice/Security/BackOfficeAuthenticationBuilder.cs index d62edcc1f9..8d4e04d2c0 100644 --- a/src/Umbraco.Web.BackOffice/Security/BackOfficeAuthenticationBuilder.cs +++ b/src/Umbraco.Web.BackOffice/Security/BackOfficeAuthenticationBuilder.cs @@ -58,16 +58,15 @@ namespace Umbraco.Cms.Web.BackOffice.Security // TODO: We could override and throw NotImplementedException for other methods? // Ensures that the sign in scheme is always the Umbraco back office external type - private class EnsureBackOfficeScheme : IPostConfigureOptions where TOptions : RemoteAuthenticationOptions + internal class EnsureBackOfficeScheme : IPostConfigureOptions where TOptions : RemoteAuthenticationOptions { public void PostConfigure(string name, TOptions options) { - if (!name.StartsWith(Constants.Security.BackOfficeExternalAuthenticationTypePrefix)) + // ensure logic only applies to backoffice authentication schemes + if (name.StartsWith(Constants.Security.BackOfficeExternalAuthenticationTypePrefix)) { - return; + options.SignInScheme = Constants.Security.BackOfficeExternalAuthenticationType; } - - options.SignInScheme = Constants.Security.BackOfficeExternalAuthenticationType; } } } diff --git a/src/Umbraco.Web.BackOffice/Security/ConfigureBackOfficeCookieOptions.cs b/src/Umbraco.Web.BackOffice/Security/ConfigureBackOfficeCookieOptions.cs index 66c3cecbae..43086f8c63 100644 --- a/src/Umbraco.Web.BackOffice/Security/ConfigureBackOfficeCookieOptions.cs +++ b/src/Umbraco.Web.BackOffice/Security/ConfigureBackOfficeCookieOptions.cs @@ -15,6 +15,7 @@ using Umbraco.Cms.Core.Net; using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Web; +using Umbraco.Cms.Web.BackOffice.Controllers; using Umbraco.Extensions; namespace Umbraco.Cms.Web.BackOffice.Security @@ -92,7 +93,7 @@ namespace Umbraco.Cms.Web.BackOffice.Security /// public void Configure(CookieAuthenticationOptions options) { - options.SlidingExpiration = true; + options.SlidingExpiration = false; options.ExpireTimeSpan = _globalSettings.TimeOut; options.Cookie.Domain = _securitySettings.AuthCookieDomain; options.Cookie.Name = _securitySettings.AuthCookieName; @@ -150,8 +151,6 @@ namespace Umbraco.Cms.Web.BackOffice.Security // ensure the thread culture is set backOfficeIdentity.EnsureCulture(); - await EnsureValidSessionId(ctx); - await securityStampValidator.ValidateAsync(ctx); EnsureTicketRenewalIfKeepUserLoggedIn(ctx); // add or update a claim to track when the cookie expires, we use this to track time remaining @@ -163,6 +162,28 @@ namespace Umbraco.Cms.Web.BackOffice.Security Constants.Security.BackOfficeAuthenticationType, backOfficeIdentity)); + await securityStampValidator.ValidateAsync(ctx); + + // This might have been called from GetRemainingTimeoutSeconds, in this case we don't want to ensure valid session + // since that in it self will keep the session valid since we renew the lastVerified date. + // Similarly don't renew the token + if (IsRemainingSecondsRequest(ctx)) + { + return; + } + + // This relies on IssuedUtc, so call it before updating it. + await EnsureValidSessionId(ctx); + + // We have to manually specify Issued and Expires, + // because the SecurityStampValidator refreshes the principal every 30 minutes, + // When the principal is refreshed the Issued is update to time of refresh, however, the Expires remains unchanged + // When we then try and renew, the difference of issued and expires effectively becomes the new ExpireTimeSpan + // meaning we effectively lose 30 minutes of our ExpireTimeSpan for EVERY principal refresh if we don't + // https://github.com/dotnet/aspnetcore/blob/main/src/Security/Authentication/Cookies/src/CookieAuthenticationHandler.cs#L115 + ctx.Properties.IssuedUtc = _systemClock.UtcNow; + ctx.Properties.ExpiresUtc = _systemClock.UtcNow.Add(_globalSettings.TimeOut); + ctx.ShouldRenew = true; }, OnSigningIn = ctx => { @@ -226,7 +247,7 @@ namespace Umbraco.Cms.Web.BackOffice.Security } return Task.CompletedTask; - } + }, }; } @@ -276,5 +297,21 @@ namespace Umbraco.Cms.Web.BackOffice.Security } } } + + private bool IsRemainingSecondsRequest(CookieValidatePrincipalContext context) + { + var routeValues = context.HttpContext.Request.RouteValues; + if (routeValues.TryGetValue("controller", out var controllerName) && + routeValues.TryGetValue("action", out var action)) + { + if (controllerName?.ToString() == ControllerExtensions.GetControllerName() + && action?.ToString() == nameof(AuthenticationController.GetRemainingTimeoutSeconds)) + { + return true; + } + } + + return false; + } } } diff --git a/src/Umbraco.Web.BackOffice/Services/ConflictingRouteService.cs b/src/Umbraco.Web.BackOffice/Services/ConflictingRouteService.cs index af8d0d877e..e610ca1ee7 100644 --- a/src/Umbraco.Web.BackOffice/Services/ConflictingRouteService.cs +++ b/src/Umbraco.Web.BackOffice/Services/ConflictingRouteService.cs @@ -1,6 +1,8 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Reflection; +using Microsoft.AspNetCore.Routing; using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Web.Common.Attributes; @@ -15,7 +17,10 @@ namespace Umbraco.Cms.Web.BackOffice.Services /// /// Initializes a new instance of the class. /// - public ConflictingRouteService(TypeLoader typeLoader) => _typeLoader = typeLoader; + public ConflictingRouteService(TypeLoader typeLoader) + { + _typeLoader = typeLoader; + } /// public bool HasConflictingRoutes(out string controllerName) diff --git a/src/Umbraco.Web.BackOffice/Services/IconService.cs b/src/Umbraco.Web.BackOffice/Services/IconService.cs index 8c5968e6b0..5c9dc0e8eb 100644 --- a/src/Umbraco.Web.BackOffice/Services/IconService.cs +++ b/src/Umbraco.Web.BackOffice/Services/IconService.cs @@ -106,8 +106,18 @@ namespace Umbraco.Cms.Web.BackOffice.Services // iterate sub directories of app plugins foreach (var dir in appPlugins.EnumerateDirectories()) { - var iconPath = _hostingEnvironment.MapPathContentRoot($"{Constants.SystemDirectories.AppPlugins}/{dir.Name}{Constants.SystemDirectories.AppPluginIcons}"); - if (Directory.Exists(iconPath)) + // AppPluginIcons path was previoulsy the wrong case, so we first check for the prefered directory + // and then check the legacy directory. + var iconPath = _hostingEnvironment.MapPathContentRoot($"{Constants.SystemDirectories.AppPlugins}/{dir.Name}{Constants.SystemDirectories.PluginIcons}"); + var iconPathExists = Directory.Exists(iconPath); + + if (!iconPathExists) + { + iconPath = _hostingEnvironment.MapPathContentRoot($"{Constants.SystemDirectories.AppPlugins}/{dir.Name}{Constants.SystemDirectories.AppPluginIcons}"); + iconPathExists = Directory.Exists(iconPath); + } + + if (iconPathExists) { var dirIcons = new DirectoryInfo(iconPath).EnumerateFiles("*.svg", SearchOption.TopDirectoryOnly); icons.UnionWith(dirIcons); diff --git a/src/Umbraco.Web.BackOffice/Trees/ApplicationTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/ApplicationTreeController.cs index db714bb675..ee7d480731 100644 --- a/src/Umbraco.Web.BackOffice/Trees/ApplicationTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/ApplicationTreeController.cs @@ -349,7 +349,7 @@ namespace Umbraco.Cms.Web.BackOffice.Trees var actionContext = new ActionContext(HttpContext, routeData, actionDescriptor); var proxyControllerContext = new ControllerContext(actionContext); - var controller = (TreeController)_controllerFactory.CreateController(proxyControllerContext); + var controller = (TreeControllerBase)_controllerFactory.CreateController(proxyControllerContext); // TODO: What about other filters? Will they execute? var isAllowed = await controller.ControllerContext.InvokeAuthorizationFiltersForRequest(actionContext); diff --git a/src/Umbraco.Web.BackOffice/Trees/ContentBlueprintTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/ContentBlueprintTreeController.cs index efbc71f6cb..72bf6c299c 100644 --- a/src/Umbraco.Web.BackOffice/Trees/ContentBlueprintTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/ContentBlueprintTreeController.cs @@ -80,13 +80,13 @@ namespace Umbraco.Cms.Web.BackOffice.Trees if (id == Constants.System.RootString) { //get all blueprint content types - var contentTypeAliases = entities.Select(x => ((IContentEntitySlim) x).ContentTypeAlias).Distinct(); + var contentTypeAliases = entities.Select(x => ((IContentEntitySlim)x).ContentTypeAlias).Distinct(); //get the ids var contentTypeIds = _contentTypeService.GetAllContentTypeIds(contentTypeAliases.ToArray()).ToArray(); //now get the entities ... it's a bit round about but still smaller queries than getting all document types var docTypeEntities = contentTypeIds.Length == 0 - ? new IUmbracoEntity[0] + ? new IEntitySlim[0] : _entityService.GetAll(UmbracoObjectTypes.DocumentType, contentTypeIds).ToArray(); nodes.AddRange(docTypeEntities @@ -112,11 +112,11 @@ namespace Umbraco.Cms.Web.BackOffice.Trees var ct = _contentTypeService.Get(intId); if (ct == null) return nodes; - var blueprintsForDocType = entities.Where(x => ct.Alias == ((IContentEntitySlim) x).ContentTypeAlias); + var blueprintsForDocType = entities.Where(x => ct.Alias == ((IContentEntitySlim)x).ContentTypeAlias); nodes.AddRange(blueprintsForDocType .Select(entity => { - var treeNode = CreateTreeNode(entity, Constants.ObjectTypes.DocumentBlueprint, id, queryStrings, "icon-blueprint", false); + var treeNode = CreateTreeNode(entity, Constants.ObjectTypes.DocumentBlueprint, id, queryStrings, Constants.Icons.Blueprint, false); treeNode.Path = $"-1,{ct.Id},{entity.Id}"; return treeNode; })); @@ -135,6 +135,7 @@ namespace Umbraco.Cms.Web.BackOffice.Trees menu.Items.Add(new RefreshNode(LocalizedTextService, true)); return menu; } + var cte = _entityService.Get(int.Parse(id, CultureInfo.InvariantCulture), UmbracoObjectTypes.DocumentType); //only refresh & create if it's a content type if (cte != null) diff --git a/src/Umbraco.Web.BackOffice/Trees/ContentTreeControllerBase.cs b/src/Umbraco.Web.BackOffice/Trees/ContentTreeControllerBase.cs index 8bf8435703..aa2f4194f4 100644 --- a/src/Umbraco.Web.BackOffice/Trees/ContentTreeControllerBase.cs +++ b/src/Umbraco.Web.BackOffice/Trees/ContentTreeControllerBase.cs @@ -470,7 +470,7 @@ namespace Umbraco.Cms.Web.BackOffice.Trees // only add empty recycle bin if the current user is allowed to delete by default if (deleteAllowed) { - menu.Items.Add(new MenuItem("emptyRecycleBin", LocalizedTextService) + menu.Items.Add(new MenuItem("emptyrecyclebin", LocalizedTextService) { Icon = "trash", OpensDialog = true diff --git a/src/Umbraco.Web.BackOffice/Trees/ContentTypeTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/ContentTypeTreeController.cs index 27eb5b7f6e..9fd4f98875 100644 --- a/src/Umbraco.Web.BackOffice/Trees/ContentTypeTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/ContentTypeTreeController.cs @@ -67,7 +67,7 @@ namespace Umbraco.Cms.Web.BackOffice.Trees .OrderBy(entity => entity.Name) .Select(dt => { - var node = CreateTreeNode(dt.Id.ToString(), id, queryStrings, dt.Name, "icon-folder", dt.HasChildren, ""); + var node = CreateTreeNode(dt.Id.ToString(), id, queryStrings, dt.Name, Constants.Icons.Folder, dt.HasChildren, ""); node.Path = dt.Path; node.NodeType = "container"; // TODO: This isn't the best way to ensure a no operation process for clicking a node but it works for now. diff --git a/src/Umbraco.Web.BackOffice/Trees/MediaTypeTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/MediaTypeTreeController.cs index c19c9a800a..a631165613 100644 --- a/src/Umbraco.Web.BackOffice/Trees/MediaTypeTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/MediaTypeTreeController.cs @@ -54,7 +54,7 @@ namespace Umbraco.Cms.Web.BackOffice.Trees .OrderBy(entity => entity.Name) .Select(dt => { - var node = CreateTreeNode(dt.Id.ToString(), id, queryStrings, dt.Name, "icon-folder", dt.HasChildren, ""); + var node = CreateTreeNode(dt.Id.ToString(), id, queryStrings, dt.Name, Constants.Icons.Folder, dt.HasChildren, ""); node.Path = dt.Path; node.NodeType = "container"; // TODO: This isn't the best way to ensure a no operation process for clicking a node but it works for now. diff --git a/src/Umbraco.Web.BackOffice/Trees/TreeCollectionBuilder.cs b/src/Umbraco.Web.BackOffice/Trees/TreeCollectionBuilder.cs index 08f6d7b400..42a15cccc8 100644 --- a/src/Umbraco.Web.BackOffice/Trees/TreeCollectionBuilder.cs +++ b/src/Umbraco.Web.BackOffice/Trees/TreeCollectionBuilder.cs @@ -64,15 +64,34 @@ namespace Umbraco.Cms.Web.BackOffice.Trees AddTreeController(controllerType); } - public void RemoveTreeController() => RemoveTreeController(typeof(T)); + public void RemoveTree(Tree treeDefinition) + { + if (treeDefinition == null) + throw new ArgumentNullException(nameof(treeDefinition)); + _trees.Remove(treeDefinition); + } + public void RemoveTreeController() + where T : TreeControllerBase + => RemoveTreeController(typeof(T)); + + // TODO: Change parameter name to "controllerType" in a major version to make it consistent with AddTreeController method. public void RemoveTreeController(Type type) { - var tree = _trees.FirstOrDefault(it => it.TreeControllerType == type); + if (!typeof(TreeControllerBase).IsAssignableFrom(type)) + throw new ArgumentException($"Type {type} does not inherit from {typeof(TreeControllerBase).FullName}."); + + var tree = _trees.FirstOrDefault(x => x.TreeControllerType == type); if (tree != null) { _trees.Remove(tree); } } + + public void RemoveTreeControllers(IEnumerable controllerTypes) + { + foreach (var controllerType in controllerTypes) + RemoveTreeController(controllerType); + } } } diff --git a/src/Umbraco.Web.BackOffice/Trees/TreeControllerBase.cs b/src/Umbraco.Web.BackOffice/Trees/TreeControllerBase.cs index b6f2948965..20d0e1a305 100644 --- a/src/Umbraco.Web.BackOffice/Trees/TreeControllerBase.cs +++ b/src/Umbraco.Web.BackOffice/Trees/TreeControllerBase.cs @@ -47,6 +47,7 @@ namespace Umbraco.Cms.Web.BackOffice.Trees /// We are allowing an arbitrary number of query strings to be passed in so that developers are able to persist custom data from the front-end /// to the back end to be used in the query for model data. /// + [Obsolete("See GetTreeNodesAsync")] protected abstract ActionResult GetTreeNodes(string id, [ModelBinder(typeof(HttpQueryStringModelBinder))]FormCollection queryStrings); /// @@ -55,8 +56,40 @@ namespace Umbraco.Cms.Web.BackOffice.Trees /// /// /// + [Obsolete("See GetMenuForNodeAsync")] protected abstract ActionResult GetMenuForNode(string id, [ModelBinder(typeof(HttpQueryStringModelBinder))]FormCollection queryStrings); + /// + /// The method called to render the contents of the tree structure + /// + /// + /// + /// All of the query string parameters passed from jsTree + /// + /// + /// If overriden, GetTreeNodes will not be called + /// We are allowing an arbitrary number of query strings to be passed in so that developers are able to persist custom data from the front-end + /// to the back end to be used in the query for model data. + /// + protected virtual async Task> GetTreeNodesAsync(string id, [ModelBinder(typeof(HttpQueryStringModelBinder))] FormCollection queryStrings) + { + return GetTreeNodes(id, queryStrings); + } + + /// + /// Returns the menu structure for the node + /// + /// + /// + /// + /// + /// If overriden, GetMenuForNode will not be called + /// + protected virtual async Task> GetMenuForNodeAsync(string id, [ModelBinder(typeof(HttpQueryStringModelBinder))] FormCollection queryStrings) + { + return GetMenuForNode(id, queryStrings); + } + /// /// The name to display on the root node /// @@ -132,7 +165,7 @@ namespace Umbraco.Cms.Web.BackOffice.Trees public async Task> GetNodes(string id, [ModelBinder(typeof(HttpQueryStringModelBinder))]FormCollection queryStrings) { if (queryStrings == null) queryStrings = FormCollection.Empty; - var nodesResult = GetTreeNodes(id, queryStrings); + var nodesResult = await GetTreeNodesAsync(id, queryStrings); if (!(nodesResult.Result is null)) { @@ -150,7 +183,7 @@ namespace Umbraco.Cms.Web.BackOffice.Trees node.RoutePath = "#"; //raise the event - await _eventAggregator.PublishAsync(new TreeNodesRenderingNotification(nodes, queryStrings, TreeAlias)); + await _eventAggregator.PublishAsync(new TreeNodesRenderingNotification(nodes, queryStrings, TreeAlias, id)); return nodes; } @@ -164,7 +197,7 @@ namespace Umbraco.Cms.Web.BackOffice.Trees public async Task> GetMenu(string id, [ModelBinder(typeof(HttpQueryStringModelBinder))]FormCollection queryStrings) { if (queryStrings == null) queryStrings = FormCollection.Empty; - var menuResult = GetMenuForNode(id, queryStrings); + var menuResult = await GetMenuForNodeAsync(id, queryStrings); if (!(menuResult.Result is null)) { return menuResult.Result; diff --git a/src/Umbraco.Web.BackOffice/Trees/TreeNodesRenderingNotification.cs b/src/Umbraco.Web.BackOffice/Trees/TreeNodesRenderingNotification.cs index f022b23a20..b7334c0e5b 100644 --- a/src/Umbraco.Web.BackOffice/Trees/TreeNodesRenderingNotification.cs +++ b/src/Umbraco.Web.BackOffice/Trees/TreeNodesRenderingNotification.cs @@ -1,3 +1,4 @@ +using System; using Microsoft.AspNetCore.Http; using Umbraco.Cms.Core.Trees; @@ -11,26 +12,57 @@ namespace Umbraco.Cms.Core.Notifications /// public class TreeNodesRenderingNotification : INotification { - /// - /// The tree nodes being rendered - /// - public TreeNodeCollection Nodes { get; } /// - /// The query string of the current request + /// Initializes a new instance of the class. /// - public FormCollection QueryString { get; } + /// The tree nodes being rendered + /// The query string of the current request + /// The alias of the tree rendered + /// The id of the node rendered + public TreeNodesRenderingNotification(TreeNodeCollection nodes, FormCollection queryString, string treeAlias, string id) + { + Nodes = nodes; + QueryString = queryString; + TreeAlias = treeAlias; + Id = id; + } /// - /// The alias of the tree rendered + /// Initializes a new instance of the class. + /// Constructor /// - public string TreeAlias { get; } - + /// The tree nodes being rendered + /// The query string of the current request + /// The alias of the tree rendered + [Obsolete("Use ctor with all parameters")] public TreeNodesRenderingNotification(TreeNodeCollection nodes, FormCollection queryString, string treeAlias) { Nodes = nodes; QueryString = queryString; TreeAlias = treeAlias; + Id = default; } + + /// + /// Gets the tree nodes being rendered + /// + public TreeNodeCollection Nodes { get; } + + /// + /// Gets the query string of the current request + /// + public FormCollection QueryString { get; } + + /// + /// Gets the alias of the tree rendered + /// + public string TreeAlias { get; } + + /// + /// Gets the id of the node rendered + /// + public string Id { get; } + } } diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs index 172a093c3c..9f999e7167 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs @@ -2,6 +2,7 @@ using System; using System.Data.Common; using System.IO; using System.Linq; +using System.Net.Http; using System.Reflection; using Dazinator.Extensions.FileProviders.GlobPatternFilter; using Microsoft.AspNetCore.Builder; @@ -144,10 +145,8 @@ namespace Umbraco.Extensions builder.Services.AddUnique(); builder.Services.AddHostedService(factory => factory.GetRequiredService()); - // Add supported databases - builder.AddUmbracoSqlServerSupport(); - builder.AddUmbracoSqlCeSupport(); builder.Services.AddSingleton(); + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); // Must be added here because DbProviderFactories is netstandard 2.1 so cannot exist in Infra for now builder.Services.AddSingleton(factory => new DbProviderFactoryCreator( @@ -155,7 +154,8 @@ namespace Umbraco.Extensions factory.GetServices(), factory.GetServices(), factory.GetServices(), - factory.GetServices() + factory.GetServices(), + factory.GetServices() )); builder.AddCoreInitialServices(); @@ -191,6 +191,11 @@ namespace Umbraco.Extensions private static IUmbracoBuilder AddHttpClients(this IUmbracoBuilder builder) { builder.Services.AddHttpClient(); + builder.Services.AddHttpClient(Constants.HttpClients.IgnoreCertificateErrors) + .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler + { + ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator + }); return builder; } @@ -382,66 +387,6 @@ namespace Umbraco.Extensions return builder; } - /// - /// Adds SqlCe support for Umbraco - /// - private static IUmbracoBuilder AddUmbracoSqlCeSupport(this IUmbracoBuilder builder) - { - try - { - var binFolder = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); - if (binFolder != null) - { - var dllPath = Path.Combine(binFolder, "Umbraco.Persistence.SqlCe.dll"); - var umbSqlCeAssembly = Assembly.LoadFrom(dllPath); - - Type sqlCeSyntaxProviderType = umbSqlCeAssembly.GetType("Umbraco.Cms.Persistence.SqlCe.SqlCeSyntaxProvider"); - Type sqlCeBulkSqlInsertProviderType = umbSqlCeAssembly.GetType("Umbraco.Cms.Persistence.SqlCe.SqlCeBulkSqlInsertProvider"); - Type sqlCeDatabaseCreatorType = umbSqlCeAssembly.GetType("Umbraco.Cms.Persistence.SqlCe.SqlCeDatabaseCreator"); - Type sqlCeSpecificMapperFactory = umbSqlCeAssembly.GetType("Umbraco.Cms.Persistence.SqlCe.SqlCeSpecificMapperFactory"); - - if (!(sqlCeSyntaxProviderType is null - || sqlCeBulkSqlInsertProviderType is null - || sqlCeDatabaseCreatorType is null - || sqlCeSpecificMapperFactory is null)) - { - builder.Services.AddSingleton(typeof(ISqlSyntaxProvider), sqlCeSyntaxProviderType); - builder.Services.AddSingleton(typeof(IBulkSqlInsertProvider), sqlCeBulkSqlInsertProviderType); - builder.Services.AddSingleton(typeof(IDatabaseCreator), sqlCeDatabaseCreatorType); - builder.Services.AddSingleton(typeof(IProviderSpecificMapperFactory), sqlCeSpecificMapperFactory); - } - - var sqlCeAssembly = Assembly.LoadFrom(Path.Combine(binFolder, "System.Data.SqlServerCe.dll")); - - var sqlCe = sqlCeAssembly.GetType("System.Data.SqlServerCe.SqlCeProviderFactory"); - if (!(sqlCe is null)) - { - DbProviderFactories.RegisterFactory(Cms.Core.Constants.DbProviderNames.SqlCe, sqlCe); - } - } - } - catch - { - // Ignore if SqlCE is not available - } - - return builder; - } - - /// - /// Adds Sql Server support for Umbraco - /// - private static IUmbracoBuilder AddUmbracoSqlServerSupport(this IUmbracoBuilder builder) - { - DbProviderFactories.RegisterFactory(Cms.Core.Constants.DbProviderNames.SqlServer, SqlClientFactory.Instance); - - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - - return builder; - } - private static IProfiler GetWebProfiler(IConfiguration config) { var isDebug = config.GetValue($"{Cms.Core.Constants.Configuration.ConfigHosting}:Debug"); diff --git a/src/Umbraco.Web.Common/Extensions/ControllerExtensions.cs b/src/Umbraco.Web.Common/Extensions/ControllerExtensions.cs index 6ae94ab57f..911ecee8e5 100644 --- a/src/Umbraco.Web.Common/Extensions/ControllerExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/ControllerExtensions.cs @@ -24,7 +24,7 @@ namespace Umbraco.Extensions /// public static string GetControllerName(Type controllerType) { - if (!controllerType.Name.EndsWith("Controller")) + if (!controllerType.Name.EndsWith("Controller") && !controllerType.Name.EndsWith("Controller`1")) { throw new InvalidOperationException("The controller type " + controllerType + " does not follow conventions, MVC Controller class names must be suffixed with the term 'Controller'"); } diff --git a/src/Umbraco.Web.Common/Extensions/FriendlyUrlHelperExtensions.cs b/src/Umbraco.Web.Common/Extensions/FriendlyUrlHelperExtensions.cs index 0340ce7a50..c812e77c01 100644 --- a/src/Umbraco.Web.Common/Extensions/FriendlyUrlHelperExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/FriendlyUrlHelperExtensions.cs @@ -9,7 +9,7 @@ namespace Umbraco.Extensions public static class FriendlyUrlHelperExtensions { - private static IUmbracoContext UmbracoContext { get; } = + private static IUmbracoContext UmbracoContext => StaticServiceProvider.Instance.GetRequiredService().GetRequiredUmbracoContext(); private static IDataProtectionProvider DataProtectionProvider { get; } = diff --git a/src/Umbraco.Web.Common/Extensions/HttpRequestExtensions.cs b/src/Umbraco.Web.Common/Extensions/HttpRequestExtensions.cs index 2aeb2555eb..7d9ce136ef 100644 --- a/src/Umbraco.Web.Common/Extensions/HttpRequestExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/HttpRequestExtensions.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Primitives; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Routing; @@ -136,5 +137,25 @@ namespace Umbraco.Extensions return new Uri(routingSettings.UmbracoApplicationUrl); } + + /// + /// Gets the Umbraco `ufprt` encrypted string from the current request + /// + /// The current request + /// The extracted `ufprt` token. + public static string GetUfprt(this HttpRequest request) + { + if (request.HasFormContentType && request.Form.TryGetValue("ufprt", out StringValues formVal) && formVal != StringValues.Empty) + { + return formVal.ToString(); + } + + if (request.Query.TryGetValue("ufprt", out StringValues queryVal) && queryVal != StringValues.Empty) + { + return queryVal.ToString(); + } + + return null; + } } } diff --git a/src/Umbraco.Web.Common/Filters/ValidateUmbracoFormRouteStringAttribute.cs b/src/Umbraco.Web.Common/Filters/ValidateUmbracoFormRouteStringAttribute.cs index ed86d7c783..0c51edd4e1 100644 --- a/src/Umbraco.Web.Common/Filters/ValidateUmbracoFormRouteStringAttribute.cs +++ b/src/Umbraco.Web.Common/Filters/ValidateUmbracoFormRouteStringAttribute.cs @@ -42,7 +42,7 @@ namespace Umbraco.Cms.Web.Common.Filters { if (context == null) throw new ArgumentNullException(nameof(context)); - var ufprt = context.HttpContext.Request.Form["ufprt"]; + var ufprt = context.HttpContext.Request.GetUfprt(); if (context.ActionDescriptor is ControllerActionDescriptor controllerActionDescriptor) { diff --git a/src/Umbraco.Web.Common/Routing/RoutableDocumentFilter.cs b/src/Umbraco.Web.Common/Routing/RoutableDocumentFilter.cs index a4311b988c..125a11ef1c 100644 --- a/src/Umbraco.Web.Common/Routing/RoutableDocumentFilter.cs +++ b/src/Umbraco.Web.Common/Routing/RoutableDocumentFilter.cs @@ -195,7 +195,8 @@ namespace Umbraco.Cms.Web.Common.Routing var routeValues = new RouteValueDictionary(); // To get the matchedEndpoint of the provide url - RouteEndpoint matchedEndpoint = routeEndpoints + RouteEndpoint matchedEndpoint = routeEndpoints? + .Where(e => e.RoutePattern.RawText != null) .Where(e => new TemplateMatcher( TemplateParser.Parse(e.RoutePattern.RawText), new RouteValueDictionary()) diff --git a/src/Umbraco.Web.Common/Security/ConfigureSecurityStampOptions.cs b/src/Umbraco.Web.Common/Security/ConfigureSecurityStampOptions.cs index 03bdf8f4dd..66cf97fd4c 100644 --- a/src/Umbraco.Web.Common/Security/ConfigureSecurityStampOptions.cs +++ b/src/Umbraco.Web.Common/Security/ConfigureSecurityStampOptions.cs @@ -30,7 +30,8 @@ namespace Umbraco.Cms.Web.Common.Security ClaimsIdentity newIdentity = refreshingPrincipal.NewPrincipal.Identities.First(); ClaimsIdentity currentIdentity = refreshingPrincipal.CurrentPrincipal.Identities.First(); - newIdentity.MergeClaimsFromCookieIdentity(currentIdentity); + // Since this is refreshing an existing principal, we want to merge all claims. + newIdentity.MergeAllClaims(currentIdentity); return Task.CompletedTask; }; diff --git a/src/Umbraco.Web.Common/Security/MemberManager.cs b/src/Umbraco.Web.Common/Security/MemberManager.cs index 79c4220ed9..dad9fddad2 100644 --- a/src/Umbraco.Web.Common/Security/MemberManager.cs +++ b/src/Umbraco.Web.Common/Security/MemberManager.cs @@ -76,8 +76,6 @@ namespace Umbraco.Cms.Web.Common.Security } else { - string username; - MemberIdentityUser currentMember = await GetCurrentMemberAsync(); // If a member could not be resolved from the provider, we are clearly not authorized and can break right here @@ -87,7 +85,6 @@ namespace Umbraco.Cms.Web.Common.Security } int memberId = int.Parse(currentMember.Id, CultureInfo.InvariantCulture); - username = currentMember.UserName; // If types defined, check member is of one of those types IList allowTypesList = allowTypes as IList ?? allowTypes.ToList(); @@ -98,10 +95,11 @@ namespace Umbraco.Cms.Web.Common.Security } // If specific members defined, check member is of one of those - if (allowAction && allowMembers.Any()) + var allowMembersList = allowMembers.ToList(); + if (allowAction && allowMembersList.Any()) { // Allow only if member's Id is in the list - allowAction = allowMembers.Contains(memberId); + allowAction = allowMembersList.Contains(memberId); } // If groups defined, check member is of one of those groups @@ -121,7 +119,7 @@ namespace Umbraco.Cms.Web.Common.Security public bool IsLoggedIn() { HttpContext httpContext = _httpContextAccessor.HttpContext; - return httpContext?.User != null && httpContext.User.Identity.IsAuthenticated; + return httpContext?.User.Identity?.IsAuthenticated ?? false; } /// diff --git a/src/Umbraco.Web.UI.Client/.babelrc b/src/Umbraco.Web.UI.Client/.babelrc index 748cd6a810..5d148e346a 100644 --- a/src/Umbraco.Web.UI.Client/.babelrc +++ b/src/Umbraco.Web.UI.Client/.babelrc @@ -8,6 +8,8 @@ ] ], "plugins": [ + "@babel/plugin-proposal-nullish-coalescing-operator", + "@babel/plugin-proposal-optional-chaining", [ "@babel/plugin-proposal-object-rest-spread", { diff --git a/src/Umbraco.Web.UI.Client/.eslintrc b/src/Umbraco.Web.UI.Client/.eslintrc index b3e410109e..f727714466 100644 --- a/src/Umbraco.Web.UI.Client/.eslintrc +++ b/src/Umbraco.Web.UI.Client/.eslintrc @@ -8,7 +8,7 @@ }, "parserOptions": { - "ecmaVersion": 2018 + "ecmaVersion": 2020 }, "globals": { diff --git a/src/Umbraco.Web.UI.Client/package-lock.json b/src/Umbraco.Web.UI.Client/package-lock.json index 0145421fa9..6aa1f42bbe 100644 --- a/src/Umbraco.Web.UI.Client/package-lock.json +++ b/src/Umbraco.Web.UI.Client/package-lock.json @@ -3,6 +3,15 @@ "requires": true, "lockfileVersion": 1, "dependencies": { + "@ampproject/remapping": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.1.2.tgz", + "integrity": "sha512-hoyByceqwKirw7w3Z7gnIIZC3Wx3J484Y3L/cMpXFbr7d9ZQj2mODrirNzcJa+SM3UlpWXYvKV4RlRpFXlWgXg==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "^0.3.0" + } + }, "@babel/code-frame": { "version": "7.15.8", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.15.8.tgz", @@ -13,198 +22,351 @@ } }, "@babel/compat-data": { - "version": "7.15.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.15.0.tgz", - "integrity": "sha512-0NqAC1IJE0S0+lL1SWFMxMkz1pKCNCjI4tr2Zx4LJSXxCLAdr6KyArnY+sno5m3yH9g737ygOyPABDsnXkpxiA==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.17.0.tgz", + "integrity": "sha512-392byTlpGWXMv4FbyWw3sAZ/FrW/DrwqLGXpy0mbyNe9Taqv1mg9yON5/o0cnr8XYCkFTZbC1eV+c+LAROgrng==", "dev": true }, "@babel/core": { - "version": "7.6.4", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.6.4.tgz", - "integrity": "sha512-Rm0HGw101GY8FTzpWSyRbki/jzq+/PkNQJ+nSulrdY6gFGOsNseCqD6KHRYe2E+EdzuBdr2pxCp6s4Uk6eJ+XQ==", + "version": "7.17.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.17.5.tgz", + "integrity": "sha512-/BBMw4EvjmyquN5O+t5eh0+YqB3XXJkYD2cjKpYtWOfFy4lQ4UozNSmxAcWT8r2XtZs0ewG+zrfsqeR15i1ajA==", "dev": true, "requires": { - "@babel/code-frame": "^7.5.5", - "@babel/generator": "^7.6.4", - "@babel/helpers": "^7.6.2", - "@babel/parser": "^7.6.4", - "@babel/template": "^7.6.0", - "@babel/traverse": "^7.6.3", - "@babel/types": "^7.6.3", - "convert-source-map": "^1.1.0", + "@ampproject/remapping": "^2.1.0", + "@babel/code-frame": "^7.16.7", + "@babel/generator": "^7.17.3", + "@babel/helper-compilation-targets": "^7.16.7", + "@babel/helper-module-transforms": "^7.16.7", + "@babel/helpers": "^7.17.2", + "@babel/parser": "^7.17.3", + "@babel/template": "^7.16.7", + "@babel/traverse": "^7.17.3", + "@babel/types": "^7.17.0", + "convert-source-map": "^1.7.0", "debug": "^4.1.0", - "json5": "^2.1.0", - "lodash": "^4.17.13", - "resolve": "^1.3.2", - "semver": "^5.4.1", - "source-map": "^0.5.0" + "gensync": "^1.0.0-beta.2", + "json5": "^2.1.2", + "semver": "^6.3.0" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", + "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", + "dev": true, + "requires": { + "@babel/highlight": "^7.16.7" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", + "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", + "dev": true + }, + "@babel/highlight": { + "version": "7.16.10", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.16.10.tgz", + "integrity": "sha512-5FnTQLSLswEj6IkgVw5KusNUUFY9ZGqe/TRFnP/BKYHYgfh7tc+C7mwiy95/yNP7Dh9x580Vv8r7u7ZfTBFxdw==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.16.7", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } } }, "@babel/generator": { - "version": "7.15.8", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.15.8.tgz", - "integrity": "sha512-ECmAKstXbp1cvpTTZciZCgfOt6iN64lR0d+euv3UZisU5awfRawOvg07Utn/qBGuH4bRIEZKrA/4LzZyXhZr8g==", + "version": "7.17.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.3.tgz", + "integrity": "sha512-+R6Dctil/MgUsZsZAkYgK+ADNSZzJRRy0TvY65T71z/CR854xHQ1EweBYXdfT+HNeN7w0cSJJEzgxZMv40pxsg==", "dev": true, "requires": { - "@babel/types": "^7.15.6", + "@babel/types": "^7.17.0", "jsesc": "^2.5.1", "source-map": "^0.5.0" } }, "@babel/helper-annotate-as-pure": { - "version": "7.15.4", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.15.4.tgz", - "integrity": "sha512-QwrtdNvUNsPCj2lfNQacsGSQvGX8ee1ttrBrcozUP2Sv/jylewBP/8QFe6ZkBsC8T/GYWonNAWJV4aRR9AL2DA==", + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.16.7.tgz", + "integrity": "sha512-s6t2w/IPQVTAET1HitoowRGXooX8mCgtuP5195wD/QJPV6wYjpujCGF7JuMODVX2ZAJOf1GT6DT9MHEZvLOFSw==", "dev": true, "requires": { - "@babel/types": "^7.15.4" + "@babel/types": "^7.16.7" } }, "@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.15.4", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.15.4.tgz", - "integrity": "sha512-P8o7JP2Mzi0SdC6eWr1zF+AEYvrsZa7GSY1lTayjF5XJhVH0kjLYUZPvTMflP7tBgZoe9gIhTa60QwFpqh/E0Q==", + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.16.7.tgz", + "integrity": "sha512-C6FdbRaxYjwVu/geKW4ZeQ0Q31AftgRcdSnZ5/jsH6BzCJbtvXvhpfkbkThYSuutZA7nCXpPR6AD9zd1dprMkA==", "dev": true, "requires": { - "@babel/helper-explode-assignable-expression": "^7.15.4", - "@babel/types": "^7.15.4" + "@babel/helper-explode-assignable-expression": "^7.16.7", + "@babel/types": "^7.16.7" + } + }, + "@babel/helper-compilation-targets": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.16.7.tgz", + "integrity": "sha512-mGojBwIWcwGD6rfqgRXVlVYmPAv7eOpIemUG3dGnDdCY4Pae70ROij3XmfrH6Fa1h1aiDylpglbZyktfzyo/hA==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.16.4", + "@babel/helper-validator-option": "^7.16.7", + "browserslist": "^4.17.5", + "semver": "^6.3.0" + }, + "dependencies": { + "browserslist": { + "version": "4.19.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.19.3.tgz", + "integrity": "sha512-XK3X4xtKJ+Txj8G5c30B4gsm71s69lqXlkYui4s6EkKxuv49qjYlY6oVd+IFJ73d4YymtM3+djvvt/R/iJwwDg==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30001312", + "electron-to-chromium": "^1.4.71", + "escalade": "^3.1.1", + "node-releases": "^2.0.2", + "picocolors": "^1.0.0" + } + }, + "caniuse-lite": { + "version": "1.0.30001312", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001312.tgz", + "integrity": "sha512-Wiz1Psk2MEK0pX3rUzWaunLTZzqS2JYZFzNKqAiJGiuxIjRPLgV6+VDPOg6lQOUxmDwhTlh198JsTTi8Hzw6aQ==", + "dev": true + }, + "electron-to-chromium": { + "version": "1.4.71", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.71.tgz", + "integrity": "sha512-Hk61vXXKRb2cd3znPE9F+2pLWdIOmP7GjiTj45y6L3W/lO+hSnUSUhq+6lEaERWBdZOHbk2s3YV5c9xVl3boVw==", + "dev": true + }, + "node-releases": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.2.tgz", + "integrity": "sha512-XxYDdcQ6eKqp/YjI+tb2C5WM2LgjnZrfYg4vgQt49EK268b6gYCHsBLrK2qvJo4FmCtqmKezb0WZFK4fkrZNsg==", + "dev": true + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "@babel/helper-create-class-features-plugin": { + "version": "7.17.6", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.17.6.tgz", + "integrity": "sha512-SogLLSxXm2OkBbSsHZMM4tUi8fUzjs63AT/d0YQIzr6GSd8Hxsbk2KYDX0k0DweAzGMj/YWeiCsorIdtdcW8Eg==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.16.7", + "@babel/helper-environment-visitor": "^7.16.7", + "@babel/helper-function-name": "^7.16.7", + "@babel/helper-member-expression-to-functions": "^7.16.7", + "@babel/helper-optimise-call-expression": "^7.16.7", + "@babel/helper-replace-supers": "^7.16.7", + "@babel/helper-split-export-declaration": "^7.16.7" + } + }, + "@babel/helper-create-regexp-features-plugin": { + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.17.0.tgz", + "integrity": "sha512-awO2So99wG6KnlE+TPs6rn83gCz5WlEePJDTnLEqbchMVrBeAujURVphRdigsk094VhvZehFoNOihSlcBjwsXA==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.16.7", + "regexpu-core": "^5.0.1" + } + }, + "@babel/helper-define-polyfill-provider": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.1.tgz", + "integrity": "sha512-J9hGMpJQmtWmj46B3kBHmL38UhJGhYX7eqkcq+2gsstyYt341HmPeWspihX43yVRA0mS+8GGk2Gckc7bY/HCmA==", + "dev": true, + "requires": { + "@babel/helper-compilation-targets": "^7.13.0", + "@babel/helper-module-imports": "^7.12.13", + "@babel/helper-plugin-utils": "^7.13.0", + "@babel/traverse": "^7.13.0", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2", + "semver": "^6.1.2" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "@babel/helper-environment-visitor": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.16.7.tgz", + "integrity": "sha512-SLLb0AAn6PkUeAfKJCCOl9e1R53pQlGAfc4y4XuMRZfqeMYLE0dM1LMhqbGAlGQY0lfw5/ohoYWAe9V1yibRag==", + "dev": true, + "requires": { + "@babel/types": "^7.16.7" } }, "@babel/helper-explode-assignable-expression": { - "version": "7.15.4", - "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.15.4.tgz", - "integrity": "sha512-J14f/vq8+hdC2KoWLIQSsGrC9EFBKE4NFts8pfMpymfApds+fPqR30AOUWc4tyr56h9l/GA1Sxv2q3dLZWbQ/g==", + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.16.7.tgz", + "integrity": "sha512-KyUenhWMC8VrxzkGP0Jizjo4/Zx+1nNZhgocs+gLzyZyB8SHidhoq9KK/8Ato4anhwsivfkBLftky7gvzbZMtQ==", "dev": true, "requires": { - "@babel/types": "^7.15.4" + "@babel/types": "^7.16.7" } }, "@babel/helper-function-name": { - "version": "7.15.4", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.15.4.tgz", - "integrity": "sha512-Z91cOMM4DseLIGOnog+Z8OI6YseR9bua+HpvLAQ2XayUGU+neTtX+97caALaLdyu53I/fjhbeCnWnRH1O3jFOw==", + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.16.7.tgz", + "integrity": "sha512-QfDfEnIUyyBSR3HtrtGECuZ6DAyCkYFp7GHl75vFtTnn6pjKeK0T1DB5lLkFvBea8MdaiUABx3osbgLyInoejA==", "dev": true, "requires": { - "@babel/helper-get-function-arity": "^7.15.4", - "@babel/template": "^7.15.4", - "@babel/types": "^7.15.4" + "@babel/helper-get-function-arity": "^7.16.7", + "@babel/template": "^7.16.7", + "@babel/types": "^7.16.7" } }, "@babel/helper-get-function-arity": { - "version": "7.15.4", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.15.4.tgz", - "integrity": "sha512-1/AlxSF92CmGZzHnC515hm4SirTxtpDnLEJ0UyEMgTMZN+6bxXKg04dKhiRx5Enel+SUA1G1t5Ed/yQia0efrA==", + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.16.7.tgz", + "integrity": "sha512-flc+RLSOBXzNzVhcLu6ujeHUrD6tANAOU5ojrRx/as+tbzf8+stUCj7+IfRRoAbEZqj/ahXEMsjhOhgeZsrnTw==", "dev": true, "requires": { - "@babel/types": "^7.15.4" + "@babel/types": "^7.16.7" } }, "@babel/helper-hoist-variables": { - "version": "7.15.4", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.15.4.tgz", - "integrity": "sha512-VTy085egb3jUGVK9ycIxQiPbquesq0HUQ+tPO0uv5mPEBZipk+5FkRKiWq5apuyTE9FUrjENB0rCf8y+n+UuhA==", + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz", + "integrity": "sha512-m04d/0Op34H5v7pbZw6pSKP7weA6lsMvfiIAMeIvkY/R4xQtBSMFEigu9QTZ2qB/9l22vsxtM8a+Q8CzD255fg==", "dev": true, "requires": { - "@babel/types": "^7.15.4" + "@babel/types": "^7.16.7" } }, "@babel/helper-member-expression-to-functions": { - "version": "7.15.4", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.15.4.tgz", - "integrity": "sha512-cokOMkxC/BTyNP1AlY25HuBWM32iCEsLPI4BHDpJCHHm1FU2E7dKWWIXJgQgSFiu4lp8q3bL1BIKwqkSUviqtA==", + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.16.7.tgz", + "integrity": "sha512-VtJ/65tYiU/6AbMTDwyoXGPKHgTsfRarivm+YbB5uAzKUyuPjgZSgAFeG87FCigc7KNHu2Pegh1XIT3lXjvz3Q==", "dev": true, "requires": { - "@babel/types": "^7.15.4" + "@babel/types": "^7.16.7" } }, "@babel/helper-module-imports": { - "version": "7.15.4", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.15.4.tgz", - "integrity": "sha512-jeAHZbzUwdW/xHgHQ3QmWR4Jg6j15q4w/gCfwZvtqOxoo5DKtLHk8Bsf4c5RZRC7NmLEs+ohkdq8jFefuvIxAA==", + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz", + "integrity": "sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg==", "dev": true, "requires": { - "@babel/types": "^7.15.4" + "@babel/types": "^7.16.7" } }, "@babel/helper-module-transforms": { - "version": "7.15.8", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.15.8.tgz", - "integrity": "sha512-DfAfA6PfpG8t4S6npwzLvTUpp0sS7JrcuaMiy1Y5645laRJIp/LiLGIBbQKaXSInK8tiGNI7FL7L8UvB8gdUZg==", + "version": "7.17.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.17.6.tgz", + "integrity": "sha512-2ULmRdqoOMpdvkbT8jONrZML/XALfzxlb052bldftkicAUy8AxSCkD5trDPQcwHNmolcl7wP6ehNqMlyUw6AaA==", "dev": true, "requires": { - "@babel/helper-module-imports": "^7.15.4", - "@babel/helper-replace-supers": "^7.15.4", - "@babel/helper-simple-access": "^7.15.4", - "@babel/helper-split-export-declaration": "^7.15.4", - "@babel/helper-validator-identifier": "^7.15.7", - "@babel/template": "^7.15.4", - "@babel/traverse": "^7.15.4", - "@babel/types": "^7.15.6" + "@babel/helper-environment-visitor": "^7.16.7", + "@babel/helper-module-imports": "^7.16.7", + "@babel/helper-simple-access": "^7.16.7", + "@babel/helper-split-export-declaration": "^7.16.7", + "@babel/helper-validator-identifier": "^7.16.7", + "@babel/template": "^7.16.7", + "@babel/traverse": "^7.17.3", + "@babel/types": "^7.17.0" + }, + "dependencies": { + "@babel/helper-validator-identifier": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", + "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", + "dev": true + } } }, "@babel/helper-optimise-call-expression": { - "version": "7.15.4", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.15.4.tgz", - "integrity": "sha512-E/z9rfbAOt1vDW1DR7k4SzhzotVV5+qMciWV6LaG1g4jeFrkDlJedjtV4h0i4Q/ITnUu+Pk08M7fczsB9GXBDw==", + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.16.7.tgz", + "integrity": "sha512-EtgBhg7rd/JcnpZFXpBy0ze1YRfdm7BnBX4uKMBd3ixa3RGAE002JZB66FJyNH7g0F38U05pXmA5P8cBh7z+1w==", "dev": true, "requires": { - "@babel/types": "^7.15.4" + "@babel/types": "^7.16.7" } }, "@babel/helper-plugin-utils": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.14.5.tgz", - "integrity": "sha512-/37qQCE3K0vvZKwoK4XU/irIJQdIfCJuhU5eKnNxpFDsOkgFaUAwbv+RYw6eYgsC0E4hS7r5KqGULUogqui0fQ==", + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.16.7.tgz", + "integrity": "sha512-Qg3Nk7ZxpgMrsox6HreY1ZNKdBq7K72tDSliA6dCl5f007jR4ne8iD5UzuNnCJH2xBf2BEEVGr+/OL6Gdp7RxA==", "dev": true }, "@babel/helper-remap-async-to-generator": { - "version": "7.15.4", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.15.4.tgz", - "integrity": "sha512-v53MxgvMK/HCwckJ1bZrq6dNKlmwlyRNYM6ypaRTdXWGOE2c1/SCa6dL/HimhPulGhZKw9W0QhREM583F/t0vQ==", + "version": "7.16.8", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.16.8.tgz", + "integrity": "sha512-fm0gH7Flb8H51LqJHy3HJ3wnE1+qtYR2A99K06ahwrawLdOFsCEWjZOrYricXJHoPSudNKxrMBUPEIPxiIIvBw==", "dev": true, "requires": { - "@babel/helper-annotate-as-pure": "^7.15.4", - "@babel/helper-wrap-function": "^7.15.4", - "@babel/types": "^7.15.4" + "@babel/helper-annotate-as-pure": "^7.16.7", + "@babel/helper-wrap-function": "^7.16.8", + "@babel/types": "^7.16.8" } }, "@babel/helper-replace-supers": { - "version": "7.15.4", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.15.4.tgz", - "integrity": "sha512-/ztT6khaXF37MS47fufrKvIsiQkx1LBRvSJNzRqmbyeZnTwU9qBxXYLaaT/6KaxfKhjs2Wy8kG8ZdsFUuWBjzw==", + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.16.7.tgz", + "integrity": "sha512-y9vsWilTNaVnVh6xiJfABzsNpgDPKev9HnAgz6Gb1p6UUwf9NepdlsV7VXGCftJM+jqD5f7JIEubcpLjZj5dBw==", "dev": true, "requires": { - "@babel/helper-member-expression-to-functions": "^7.15.4", - "@babel/helper-optimise-call-expression": "^7.15.4", - "@babel/traverse": "^7.15.4", - "@babel/types": "^7.15.4" + "@babel/helper-environment-visitor": "^7.16.7", + "@babel/helper-member-expression-to-functions": "^7.16.7", + "@babel/helper-optimise-call-expression": "^7.16.7", + "@babel/traverse": "^7.16.7", + "@babel/types": "^7.16.7" } }, "@babel/helper-simple-access": { - "version": "7.15.4", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.15.4.tgz", - "integrity": "sha512-UzazrDoIVOZZcTeHHEPYrr1MvTR/K+wgLg6MY6e1CJyaRhbibftF6fR2KU2sFRtI/nERUZR9fBd6aKgBlIBaPg==", + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.16.7.tgz", + "integrity": "sha512-ZIzHVyoeLMvXMN/vok/a4LWRy8G2v205mNP0XOuf9XRLyX5/u9CnVulUtDgUTama3lT+bf/UqucuZjqiGuTS1g==", "dev": true, "requires": { - "@babel/types": "^7.15.4" + "@babel/types": "^7.16.7" } }, "@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.15.4", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.15.4.tgz", - "integrity": "sha512-BMRLsdh+D1/aap19TycS4eD1qELGrCBJwzaY9IE8LrpJtJb+H7rQkPIdsfgnMtLBA6DJls7X9z93Z4U8h7xw0A==", + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.16.0.tgz", + "integrity": "sha512-+il1gTy0oHwUsBQZyJvukbB4vPMdcYBrFHa0Uc4AizLxbq6BOYC51Rv4tWocX9BLBDLZ4kc6qUFpQ6HRgL+3zw==", "dev": true, "requires": { - "@babel/types": "^7.15.4" + "@babel/types": "^7.16.0" } }, "@babel/helper-split-export-declaration": { - "version": "7.15.4", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.15.4.tgz", - "integrity": "sha512-HsFqhLDZ08DxCpBdEVtKmywj6PQbwnF6HHybur0MAnkAKnlS6uHkwnmRIkElB2Owpfb4xL4NwDmDLFubueDXsw==", + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.7.tgz", + "integrity": "sha512-xbWoy/PFoxSWazIToT9Sif+jJTlrMcndIsaOKvTA6u7QEo7ilkRZpjew18/W3c7nm8fXdUDXh02VXTbZ0pGDNw==", "dev": true, "requires": { - "@babel/types": "^7.15.4" + "@babel/types": "^7.16.7" } }, "@babel/helper-validator-identifier": { @@ -214,32 +376,32 @@ "dev": true }, "@babel/helper-validator-option": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.14.5.tgz", - "integrity": "sha512-OX8D5eeX4XwcroVW45NMvoYaIuFI+GQpA2a8Gi+X/U/cDUIRsV37qQfF905F0htTRCREQIB4KqPeaveRJUl3Ow==", + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.16.7.tgz", + "integrity": "sha512-TRtenOuRUVo9oIQGPC5G9DgK4743cdxvtOw0weQNpZXaS16SCBi5MNjZF8vba3ETURjZpTbVn7Vvcf2eAwFozQ==", "dev": true }, "@babel/helper-wrap-function": { - "version": "7.15.4", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.15.4.tgz", - "integrity": "sha512-Y2o+H/hRV5W8QhIfTpRIBwl57y8PrZt6JM3V8FOo5qarjshHItyH5lXlpMfBfmBefOqSCpKZs/6Dxqp0E/U+uw==", + "version": "7.16.8", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.16.8.tgz", + "integrity": "sha512-8RpyRVIAW1RcDDGTA+GpPAwV22wXCfKOoM9bet6TLkGIFTkRQSkH1nMQ5Yet4MpoXe1ZwHPVtNasc2w0uZMqnw==", "dev": true, "requires": { - "@babel/helper-function-name": "^7.15.4", - "@babel/template": "^7.15.4", - "@babel/traverse": "^7.15.4", - "@babel/types": "^7.15.4" + "@babel/helper-function-name": "^7.16.7", + "@babel/template": "^7.16.7", + "@babel/traverse": "^7.16.8", + "@babel/types": "^7.16.8" } }, "@babel/helpers": { - "version": "7.15.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.15.4.tgz", - "integrity": "sha512-V45u6dqEJ3w2rlryYYXf6i9rQ5YMNu4FLS6ngs8ikblhu2VdR1AqAd6aJjBzmf2Qzh6KOLqKHxEN9+TFbAkAVQ==", + "version": "7.17.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.17.2.tgz", + "integrity": "sha512-0Qu7RLR1dILozr/6M0xgj+DFPmi6Bnulgm9M8BVa9ZCWxDqlSnqt3cf8IDPB5m45sVXUZ0kuQAgUrdSFFH79fQ==", "dev": true, "requires": { - "@babel/template": "^7.15.4", - "@babel/traverse": "^7.15.4", - "@babel/types": "^7.15.4" + "@babel/template": "^7.16.7", + "@babel/traverse": "^7.17.0", + "@babel/types": "^7.17.0" } }, "@babel/highlight": { @@ -254,54 +416,719 @@ } }, "@babel/parser": { - "version": "7.15.8", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.15.8.tgz", - "integrity": "sha512-BRYa3wcQnjS/nqI8Ac94pYYpJfojHVvVXJ97+IDCImX4Jc8W8Xv1+47enbruk+q1etOpsQNwnfFcNGw+gtPGxA==", + "version": "7.17.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.17.3.tgz", + "integrity": "sha512-7yJPvPV+ESz2IUTPbOL+YkIGyCqOyNIzdguKQuJGnH7bg1WTIifuM21YqokFt/THWh1AkCRn9IgoykTRCBVpzA==", "dev": true }, - "@babel/plugin-proposal-object-rest-spread": { - "version": "7.13.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.13.8.tgz", - "integrity": "sha512-DhB2EuB1Ih7S3/IRX5AFVgZ16k3EzfRbq97CxAVI1KSYcW+lexV8VZb7G7L8zuPVSdQMRn0kiBpf/Yzu9ZKH0g==", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.16.7.tgz", + "integrity": "sha512-anv/DObl7waiGEnC24O9zqL0pSuI9hljihqiDuFHC8d7/bjr/4RLGPWuc8rYOff/QPzbEPSkzG8wGG9aDuhHRg==", "dev": true, "requires": { - "@babel/compat-data": "^7.13.8", - "@babel/helper-compilation-targets": "^7.13.8", - "@babel/helper-plugin-utils": "^7.13.0", + "@babel/helper-plugin-utils": "^7.16.7" + } + }, + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.16.7.tgz", + "integrity": "sha512-di8vUHRdf+4aJ7ltXhaDbPoszdkh59AQtJM5soLsuHpQJdFQZOA4uGj0V2u/CZ8bJ/u8ULDL5yq6FO/bCXnKHw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.16.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.16.0", + "@babel/plugin-proposal-optional-chaining": "^7.16.7" + } + }, + "@babel/plugin-proposal-async-generator-functions": { + "version": "7.16.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.16.8.tgz", + "integrity": "sha512-71YHIvMuiuqWJQkebWJtdhQTfd4Q4mF76q2IX37uZPkG9+olBxsX+rH1vkhFto4UeJZ9dPY2s+mDvhDm1u2BGQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.16.7", + "@babel/helper-remap-async-to-generator": "^7.16.8", + "@babel/plugin-syntax-async-generators": "^7.8.4" + } + }, + "@babel/plugin-proposal-class-properties": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.16.7.tgz", + "integrity": "sha512-IobU0Xme31ewjYOShSIqd/ZGM/r/cuOz2z0MDbNrhF5FW+ZVgi0f2lyeoj9KFPDOAqsYxmLWZte1WOwlvY9aww==", + "dev": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.16.7", + "@babel/helper-plugin-utils": "^7.16.7" + } + }, + "@babel/plugin-proposal-class-static-block": { + "version": "7.17.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.17.6.tgz", + "integrity": "sha512-X/tididvL2zbs7jZCeeRJ8167U/+Ac135AM6jCAx6gYXDUviZV5Ku9UDvWS2NCuWlFjIRXklYhwo6HhAC7ETnA==", + "dev": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.17.6", + "@babel/helper-plugin-utils": "^7.16.7", + "@babel/plugin-syntax-class-static-block": "^7.14.5" + } + }, + "@babel/plugin-proposal-dynamic-import": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.16.7.tgz", + "integrity": "sha512-I8SW9Ho3/8DRSdmDdH3gORdyUuYnk1m4cMxUAdu5oy4n3OfN8flDEH+d60iG7dUfi0KkYwSvoalHzzdRzpWHTg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.16.7", + "@babel/plugin-syntax-dynamic-import": "^7.8.3" + } + }, + "@babel/plugin-proposal-export-namespace-from": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.16.7.tgz", + "integrity": "sha512-ZxdtqDXLRGBL64ocZcs7ovt71L3jhC1RGSyR996svrCi3PYqHNkb3SwPJCs8RIzD86s+WPpt2S73+EHCGO+NUA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.16.7", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + } + }, + "@babel/plugin-proposal-json-strings": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.16.7.tgz", + "integrity": "sha512-lNZ3EEggsGY78JavgbHsK9u5P3pQaW7k4axlgFLYkMd7UBsiNahCITShLjNQschPyjtO6dADrL24757IdhBrsQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.16.7", + "@babel/plugin-syntax-json-strings": "^7.8.3" + } + }, + "@babel/plugin-proposal-logical-assignment-operators": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.16.7.tgz", + "integrity": "sha512-K3XzyZJGQCr00+EtYtrDjmwX7o7PLK6U9bi1nCwkQioRFVUv6dJoxbQjtWVtP+bCPy82bONBKG8NPyQ4+i6yjg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.16.7", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + } + }, + "@babel/plugin-proposal-nullish-coalescing-operator": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.16.7.tgz", + "integrity": "sha512-aUOrYU3EVtjf62jQrCj63pYZ7k6vns2h/DQvHPWGmsJRYzWXZ6/AsfgpiRy6XiuIDADhJzP2Q9MwSMKauBQ+UQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.16.7", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + } + }, + "@babel/plugin-proposal-numeric-separator": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.16.7.tgz", + "integrity": "sha512-vQgPMknOIgiuVqbokToyXbkY/OmmjAzr/0lhSIbG/KmnzXPGwW/AdhdKpi+O4X/VkWiWjnkKOBiqJrTaC98VKw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.16.7", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + } + }, + "@babel/plugin-proposal-object-rest-spread": { + "version": "7.17.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.17.3.tgz", + "integrity": "sha512-yuL5iQA/TbZn+RGAfxQXfi7CNLmKi1f8zInn4IgobuCWcAb7i+zj4TYzQ9l8cEzVyJ89PDGuqxK1xZpUDISesw==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.17.0", + "@babel/helper-compilation-targets": "^7.16.7", + "@babel/helper-plugin-utils": "^7.16.7", "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-transform-parameters": "^7.13.0" + "@babel/plugin-transform-parameters": "^7.16.7" + } + }, + "@babel/plugin-proposal-optional-catch-binding": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.16.7.tgz", + "integrity": "sha512-eMOH/L4OvWSZAE1VkHbr1vckLG1WUcHGJSLqqQwl2GaUqG6QjddvrOaTUMNYiv77H5IKPMZ9U9P7EaHwvAShfA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.16.7", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + } + }, + "@babel/plugin-proposal-optional-chaining": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.16.7.tgz", + "integrity": "sha512-eC3xy+ZrUcBtP7x+sq62Q/HYd674pPTb/77XZMb5wbDPGWIdUbSr4Agr052+zaUPSb+gGRnjxXfKFvx5iMJ+DA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.16.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.16.0", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + } + }, + "@babel/plugin-proposal-private-methods": { + "version": "7.16.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.16.11.tgz", + "integrity": "sha512-F/2uAkPlXDr8+BHpZvo19w3hLFKge+k75XUprE6jaqKxjGkSYcK+4c+bup5PdW/7W/Rpjwql7FTVEDW+fRAQsw==", + "dev": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.16.10", + "@babel/helper-plugin-utils": "^7.16.7" + } + }, + "@babel/plugin-proposal-private-property-in-object": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.16.7.tgz", + "integrity": "sha512-rMQkjcOFbm+ufe3bTZLyOfsOUOxyvLXZJCTARhJr+8UMSoZmqTe1K1BgkFcrW37rAchWg57yI69ORxiWvUINuQ==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.16.7", + "@babel/helper-create-class-features-plugin": "^7.16.7", + "@babel/helper-plugin-utils": "^7.16.7", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + } + }, + "@babel/plugin-proposal-unicode-property-regex": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.16.7.tgz", + "integrity": "sha512-QRK0YI/40VLhNVGIjRNAAQkEHws0cswSdFFjpFyt943YmJIU1da9uW63Iu6NFV6CxTZW5eTDCrwZUstBWgp/Rg==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.16.7", + "@babel/helper-plugin-utils": "^7.16.7" + } + }, + "@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.12.13" + } + }, + "@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-export-namespace-from": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", + "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.3" + } + }, + "@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-arrow-functions": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.16.7.tgz", + "integrity": "sha512-9ffkFFMbvzTvv+7dTp/66xvZAWASuPD5Tl9LK3Z9vhOmANo6j94rik+5YMBt4CwHVMWLWpMsriIc2zsa3WW3xQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.16.7" + } + }, + "@babel/plugin-transform-async-to-generator": { + "version": "7.16.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.16.8.tgz", + "integrity": "sha512-MtmUmTJQHCnyJVrScNzNlofQJ3dLFuobYn3mwOTKHnSCMtbNsqvF71GQmJfFjdrXSsAA7iysFmYWw4bXZ20hOg==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/helper-plugin-utils": "^7.16.7", + "@babel/helper-remap-async-to-generator": "^7.16.8" + } + }, + "@babel/plugin-transform-block-scoped-functions": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.16.7.tgz", + "integrity": "sha512-JUuzlzmF40Z9cXyytcbZEZKckgrQzChbQJw/5PuEHYeqzCsvebDx0K0jWnIIVcmmDOAVctCgnYs0pMcrYj2zJg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.16.7" + } + }, + "@babel/plugin-transform-block-scoping": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.16.7.tgz", + "integrity": "sha512-ObZev2nxVAYA4bhyusELdo9hb3H+A56bxH3FZMbEImZFiEDYVHXQSJ1hQKFlDnlt8G9bBrCZ5ZpURZUrV4G5qQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.16.7" + } + }, + "@babel/plugin-transform-classes": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.16.7.tgz", + "integrity": "sha512-WY7og38SFAGYRe64BrjKf8OrE6ulEHtr5jEYaZMwox9KebgqPi67Zqz8K53EKk1fFEJgm96r32rkKZ3qA2nCWQ==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.16.7", + "@babel/helper-environment-visitor": "^7.16.7", + "@babel/helper-function-name": "^7.16.7", + "@babel/helper-optimise-call-expression": "^7.16.7", + "@babel/helper-plugin-utils": "^7.16.7", + "@babel/helper-replace-supers": "^7.16.7", + "@babel/helper-split-export-declaration": "^7.16.7", + "globals": "^11.1.0" + } + }, + "@babel/plugin-transform-computed-properties": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.16.7.tgz", + "integrity": "sha512-gN72G9bcmenVILj//sv1zLNaPyYcOzUho2lIJBMh/iakJ9ygCo/hEF9cpGb61SCMEDxbbyBoVQxrt+bWKu5KGw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.16.7" + } + }, + "@babel/plugin-transform-destructuring": { + "version": "7.17.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.17.3.tgz", + "integrity": "sha512-dDFzegDYKlPqa72xIlbmSkly5MluLoaC1JswABGktyt6NTXSBcUuse/kWE/wvKFWJHPETpi158qJZFS3JmykJg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.16.7" + } + }, + "@babel/plugin-transform-dotall-regex": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.16.7.tgz", + "integrity": "sha512-Lyttaao2SjZF6Pf4vk1dVKv8YypMpomAbygW+mU5cYP3S5cWTfCJjG8xV6CFdzGFlfWK81IjL9viiTvpb6G7gQ==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.16.7", + "@babel/helper-plugin-utils": "^7.16.7" + } + }, + "@babel/plugin-transform-duplicate-keys": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.16.7.tgz", + "integrity": "sha512-03DvpbRfvWIXyK0/6QiR1KMTWeT6OcQ7tbhjrXyFS02kjuX/mu5Bvnh5SDSWHxyawit2g5aWhKwI86EE7GUnTw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.16.7" + } + }, + "@babel/plugin-transform-exponentiation-operator": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.16.7.tgz", + "integrity": "sha512-8UYLSlyLgRixQvlYH3J2ekXFHDFLQutdy7FfFAMm3CPZ6q9wHCwnUyiXpQCe3gVVnQlHc5nsuiEVziteRNTXEA==", + "dev": true, + "requires": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.16.7", + "@babel/helper-plugin-utils": "^7.16.7" + } + }, + "@babel/plugin-transform-for-of": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.16.7.tgz", + "integrity": "sha512-/QZm9W92Ptpw7sjI9Nx1mbcsWz33+l8kuMIQnDwgQBG5s3fAfQvkRjQ7NqXhtNcKOnPkdICmUHyCaWW06HCsqg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.16.7" + } + }, + "@babel/plugin-transform-function-name": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.16.7.tgz", + "integrity": "sha512-SU/C68YVwTRxqWj5kgsbKINakGag0KTgq9f2iZEXdStoAbOzLHEBRYzImmA6yFo8YZhJVflvXmIHUO7GWHmxxA==", + "dev": true, + "requires": { + "@babel/helper-compilation-targets": "^7.16.7", + "@babel/helper-function-name": "^7.16.7", + "@babel/helper-plugin-utils": "^7.16.7" + } + }, + "@babel/plugin-transform-literals": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.16.7.tgz", + "integrity": "sha512-6tH8RTpTWI0s2sV6uq3e/C9wPo4PTqqZps4uF0kzQ9/xPLFQtipynvmT1g/dOfEJ+0EQsHhkQ/zyRId8J2b8zQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.16.7" + } + }, + "@babel/plugin-transform-member-expression-literals": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.16.7.tgz", + "integrity": "sha512-mBruRMbktKQwbxaJof32LT9KLy2f3gH+27a5XSuXo6h7R3vqltl0PgZ80C8ZMKw98Bf8bqt6BEVi3svOh2PzMw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.16.7" + } + }, + "@babel/plugin-transform-modules-amd": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.16.7.tgz", + "integrity": "sha512-KaaEtgBL7FKYwjJ/teH63oAmE3lP34N3kshz8mm4VMAw7U3PxjVwwUmxEFksbgsNUaO3wId9R2AVQYSEGRa2+g==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.16.7", + "@babel/helper-plugin-utils": "^7.16.7", + "babel-plugin-dynamic-import-node": "^2.3.3" + } + }, + "@babel/plugin-transform-modules-commonjs": { + "version": "7.16.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.16.8.tgz", + "integrity": "sha512-oflKPvsLT2+uKQopesJt3ApiaIS2HW+hzHFcwRNtyDGieAeC/dIHZX8buJQ2J2X1rxGPy4eRcUijm3qcSPjYcA==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.16.7", + "@babel/helper-plugin-utils": "^7.16.7", + "@babel/helper-simple-access": "^7.16.7", + "babel-plugin-dynamic-import-node": "^2.3.3" + } + }, + "@babel/plugin-transform-modules-systemjs": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.16.7.tgz", + "integrity": "sha512-DuK5E3k+QQmnOqBR9UkusByy5WZWGRxfzV529s9nPra1GE7olmxfqO2FHobEOYSPIjPBTr4p66YDcjQnt8cBmw==", + "dev": true, + "requires": { + "@babel/helper-hoist-variables": "^7.16.7", + "@babel/helper-module-transforms": "^7.16.7", + "@babel/helper-plugin-utils": "^7.16.7", + "@babel/helper-validator-identifier": "^7.16.7", + "babel-plugin-dynamic-import-node": "^2.3.3" + }, + "dependencies": { + "@babel/helper-validator-identifier": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", + "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", + "dev": true + } + } + }, + "@babel/plugin-transform-modules-umd": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.16.7.tgz", + "integrity": "sha512-EMh7uolsC8O4xhudF2F6wedbSHm1HHZ0C6aJ7K67zcDNidMzVcxWdGr+htW9n21klm+bOn+Rx4CBsAntZd3rEQ==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.16.7", + "@babel/helper-plugin-utils": "^7.16.7" + } + }, + "@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.16.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.16.8.tgz", + "integrity": "sha512-j3Jw+n5PvpmhRR+mrgIh04puSANCk/T/UA3m3P1MjJkhlK906+ApHhDIqBQDdOgL/r1UYpz4GNclTXxyZrYGSw==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.16.7" + } + }, + "@babel/plugin-transform-new-target": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.16.7.tgz", + "integrity": "sha512-xiLDzWNMfKoGOpc6t3U+etCE2yRnn3SM09BXqWPIZOBpL2gvVrBWUKnsJx0K/ADi5F5YC5f8APFfWrz25TdlGg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.16.7" + } + }, + "@babel/plugin-transform-object-super": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.16.7.tgz", + "integrity": "sha512-14J1feiQVWaGvRxj2WjyMuXS2jsBkgB3MdSN5HuC2G5nRspa5RK9COcs82Pwy5BuGcjb+fYaUj94mYcOj7rCvw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.16.7", + "@babel/helper-replace-supers": "^7.16.7" + } + }, + "@babel/plugin-transform-parameters": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.16.7.tgz", + "integrity": "sha512-AT3MufQ7zZEhU2hwOA11axBnExW0Lszu4RL/tAlUJBuNoRak+wehQW8h6KcXOcgjY42fHtDxswuMhMjFEuv/aw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.16.7" + } + }, + "@babel/plugin-transform-property-literals": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.16.7.tgz", + "integrity": "sha512-z4FGr9NMGdoIl1RqavCqGG+ZuYjfZ/hkCIeuH6Do7tXmSm0ls11nYVSJqFEUOSJbDab5wC6lRE/w6YjVcr6Hqw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.16.7" + } + }, + "@babel/plugin-transform-regenerator": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.16.7.tgz", + "integrity": "sha512-mF7jOgGYCkSJagJ6XCujSQg+6xC1M77/03K2oBmVJWoFGNUtnVJO4WHKJk3dnPC8HCcj4xBQP1Egm8DWh3Pb3Q==", + "dev": true, + "requires": { + "regenerator-transform": "^0.14.2" + } + }, + "@babel/plugin-transform-reserved-words": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.16.7.tgz", + "integrity": "sha512-KQzzDnZ9hWQBjwi5lpY5v9shmm6IVG0U9pB18zvMu2i4H90xpT4gmqwPYsn8rObiadYe2M0gmgsiOIF5A/2rtg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.16.7" + } + }, + "@babel/plugin-transform-shorthand-properties": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.16.7.tgz", + "integrity": "sha512-hah2+FEnoRoATdIb05IOXf+4GzXYTq75TVhIn1PewihbpyrNWUt2JbudKQOETWw6QpLe+AIUpJ5MVLYTQbeeUg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.16.7" + } + }, + "@babel/plugin-transform-spread": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.16.7.tgz", + "integrity": "sha512-+pjJpgAngb53L0iaA5gU/1MLXJIfXcYepLgXB3esVRf4fqmj8f2cxM3/FKaHsZms08hFQJkFccEWuIpm429TXg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.16.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.16.0" + } + }, + "@babel/plugin-transform-sticky-regex": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.16.7.tgz", + "integrity": "sha512-NJa0Bd/87QV5NZZzTuZG5BPJjLYadeSZ9fO6oOUoL4iQx+9EEuw/eEM92SrsT19Yc2jgB1u1hsjqDtH02c3Drw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.16.7" + } + }, + "@babel/plugin-transform-template-literals": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.16.7.tgz", + "integrity": "sha512-VwbkDDUeenlIjmfNeDX/V0aWrQH2QiVyJtwymVQSzItFDTpxfyJh3EVaQiS0rIN/CqbLGr0VcGmuwyTdZtdIsA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.16.7" + } + }, + "@babel/plugin-transform-typeof-symbol": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.16.7.tgz", + "integrity": "sha512-p2rOixCKRJzpg9JB4gjnG4gjWkWa89ZoYUnl9snJ1cWIcTH/hvxZqfO+WjG6T8DRBpctEol5jw1O5rA8gkCokQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.16.7" + } + }, + "@babel/plugin-transform-unicode-escapes": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.16.7.tgz", + "integrity": "sha512-TAV5IGahIz3yZ9/Hfv35TV2xEm+kaBDaZQCn2S/hG9/CZ0DktxJv9eKfPc7yYCvOYR4JGx1h8C+jcSOvgaaI/Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.16.7" + } + }, + "@babel/plugin-transform-unicode-regex": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.16.7.tgz", + "integrity": "sha512-oC5tYYKw56HO75KZVLQ+R/Nl3Hro9kf8iG0hXoaHP7tjAyCpvqBiSNe6vGrZni1Z6MggmUOC6A7VP7AVmw225Q==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.16.7", + "@babel/helper-plugin-utils": "^7.16.7" + } + }, + "@babel/preset-env": { + "version": "7.16.11", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.16.11.tgz", + "integrity": "sha512-qcmWG8R7ZW6WBRPZK//y+E3Cli151B20W1Rv7ln27vuPaXU/8TKms6jFdiJtF7UDTxcrb7mZd88tAeK9LjdT8g==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.16.8", + "@babel/helper-compilation-targets": "^7.16.7", + "@babel/helper-plugin-utils": "^7.16.7", + "@babel/helper-validator-option": "^7.16.7", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.16.7", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.16.7", + "@babel/plugin-proposal-async-generator-functions": "^7.16.8", + "@babel/plugin-proposal-class-properties": "^7.16.7", + "@babel/plugin-proposal-class-static-block": "^7.16.7", + "@babel/plugin-proposal-dynamic-import": "^7.16.7", + "@babel/plugin-proposal-export-namespace-from": "^7.16.7", + "@babel/plugin-proposal-json-strings": "^7.16.7", + "@babel/plugin-proposal-logical-assignment-operators": "^7.16.7", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.16.7", + "@babel/plugin-proposal-numeric-separator": "^7.16.7", + "@babel/plugin-proposal-object-rest-spread": "^7.16.7", + "@babel/plugin-proposal-optional-catch-binding": "^7.16.7", + "@babel/plugin-proposal-optional-chaining": "^7.16.7", + "@babel/plugin-proposal-private-methods": "^7.16.11", + "@babel/plugin-proposal-private-property-in-object": "^7.16.7", + "@babel/plugin-proposal-unicode-property-regex": "^7.16.7", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5", + "@babel/plugin-transform-arrow-functions": "^7.16.7", + "@babel/plugin-transform-async-to-generator": "^7.16.8", + "@babel/plugin-transform-block-scoped-functions": "^7.16.7", + "@babel/plugin-transform-block-scoping": "^7.16.7", + "@babel/plugin-transform-classes": "^7.16.7", + "@babel/plugin-transform-computed-properties": "^7.16.7", + "@babel/plugin-transform-destructuring": "^7.16.7", + "@babel/plugin-transform-dotall-regex": "^7.16.7", + "@babel/plugin-transform-duplicate-keys": "^7.16.7", + "@babel/plugin-transform-exponentiation-operator": "^7.16.7", + "@babel/plugin-transform-for-of": "^7.16.7", + "@babel/plugin-transform-function-name": "^7.16.7", + "@babel/plugin-transform-literals": "^7.16.7", + "@babel/plugin-transform-member-expression-literals": "^7.16.7", + "@babel/plugin-transform-modules-amd": "^7.16.7", + "@babel/plugin-transform-modules-commonjs": "^7.16.8", + "@babel/plugin-transform-modules-systemjs": "^7.16.7", + "@babel/plugin-transform-modules-umd": "^7.16.7", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.16.8", + "@babel/plugin-transform-new-target": "^7.16.7", + "@babel/plugin-transform-object-super": "^7.16.7", + "@babel/plugin-transform-parameters": "^7.16.7", + "@babel/plugin-transform-property-literals": "^7.16.7", + "@babel/plugin-transform-regenerator": "^7.16.7", + "@babel/plugin-transform-reserved-words": "^7.16.7", + "@babel/plugin-transform-shorthand-properties": "^7.16.7", + "@babel/plugin-transform-spread": "^7.16.7", + "@babel/plugin-transform-sticky-regex": "^7.16.7", + "@babel/plugin-transform-template-literals": "^7.16.7", + "@babel/plugin-transform-typeof-symbol": "^7.16.7", + "@babel/plugin-transform-unicode-escapes": "^7.16.7", + "@babel/plugin-transform-unicode-regex": "^7.16.7", + "@babel/preset-modules": "^0.1.5", + "@babel/types": "^7.16.8", + "babel-plugin-polyfill-corejs2": "^0.3.0", + "babel-plugin-polyfill-corejs3": "^0.5.0", + "babel-plugin-polyfill-regenerator": "^0.3.0", + "core-js-compat": "^3.20.2", + "semver": "^6.3.0" }, "dependencies": { - "@babel/helper-compilation-targets": { - "version": "7.15.4", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.15.4.tgz", - "integrity": "sha512-rMWPCirulnPSe4d+gwdWXLfAXTTBj8M3guAf5xFQJ0nvFY7tfNAFnWdqaHegHlgDZOCT4qvhF3BYlSJag8yhqQ==", - "dev": true, - "requires": { - "@babel/compat-data": "^7.15.0", - "@babel/helper-validator-option": "^7.14.5", - "browserslist": "^4.16.6", - "semver": "^6.3.0" - } - }, - "@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-transform-parameters": { - "version": "7.15.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.15.4.tgz", - "integrity": "sha512-9WB/GUTO6lvJU3XQsSr6J/WKvBC2hcs4Pew8YxZagi6GkTdniyqp8On5kqdK8MN0LMeu0mGbhPN+O049NV/9FQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5" - } - }, "semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", @@ -310,558 +1137,129 @@ } } }, - "@babel/preset-env": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.6.3.tgz", - "integrity": "sha512-CWQkn7EVnwzlOdR5NOm2+pfgSNEZmvGjOhlCHBDq0J8/EStr+G+FvPEiz9B56dR6MoiUFjXhfE4hjLoAKKJtIQ==", + "@babel/preset-modules": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.5.tgz", + "integrity": "sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA==", "dev": true, "requires": { - "@babel/helper-module-imports": "^7.0.0", "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-proposal-async-generator-functions": "^7.2.0", - "@babel/plugin-proposal-dynamic-import": "^7.5.0", - "@babel/plugin-proposal-json-strings": "^7.2.0", - "@babel/plugin-proposal-object-rest-spread": "^7.6.2", - "@babel/plugin-proposal-optional-catch-binding": "^7.2.0", - "@babel/plugin-proposal-unicode-property-regex": "^7.6.2", - "@babel/plugin-syntax-async-generators": "^7.2.0", - "@babel/plugin-syntax-dynamic-import": "^7.2.0", - "@babel/plugin-syntax-json-strings": "^7.2.0", - "@babel/plugin-syntax-object-rest-spread": "^7.2.0", - "@babel/plugin-syntax-optional-catch-binding": "^7.2.0", - "@babel/plugin-transform-arrow-functions": "^7.2.0", - "@babel/plugin-transform-async-to-generator": "^7.5.0", - "@babel/plugin-transform-block-scoped-functions": "^7.2.0", - "@babel/plugin-transform-block-scoping": "^7.6.3", - "@babel/plugin-transform-classes": "^7.5.5", - "@babel/plugin-transform-computed-properties": "^7.2.0", - "@babel/plugin-transform-destructuring": "^7.6.0", - "@babel/plugin-transform-dotall-regex": "^7.6.2", - "@babel/plugin-transform-duplicate-keys": "^7.5.0", - "@babel/plugin-transform-exponentiation-operator": "^7.2.0", - "@babel/plugin-transform-for-of": "^7.4.4", - "@babel/plugin-transform-function-name": "^7.4.4", - "@babel/plugin-transform-literals": "^7.2.0", - "@babel/plugin-transform-member-expression-literals": "^7.2.0", - "@babel/plugin-transform-modules-amd": "^7.5.0", - "@babel/plugin-transform-modules-commonjs": "^7.6.0", - "@babel/plugin-transform-modules-systemjs": "^7.5.0", - "@babel/plugin-transform-modules-umd": "^7.2.0", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.6.3", - "@babel/plugin-transform-new-target": "^7.4.4", - "@babel/plugin-transform-object-super": "^7.5.5", - "@babel/plugin-transform-parameters": "^7.4.4", - "@babel/plugin-transform-property-literals": "^7.2.0", - "@babel/plugin-transform-regenerator": "^7.4.5", - "@babel/plugin-transform-reserved-words": "^7.2.0", - "@babel/plugin-transform-shorthand-properties": "^7.2.0", - "@babel/plugin-transform-spread": "^7.6.2", - "@babel/plugin-transform-sticky-regex": "^7.2.0", - "@babel/plugin-transform-template-literals": "^7.4.4", - "@babel/plugin-transform-typeof-symbol": "^7.2.0", - "@babel/plugin-transform-unicode-regex": "^7.6.2", - "@babel/types": "^7.6.3", - "browserslist": "^4.6.0", - "core-js-compat": "^3.1.1", - "invariant": "^2.2.2", - "js-levenshtein": "^1.1.3", - "semver": "^5.5.0" - }, - "dependencies": { - "@babel/plugin-proposal-async-generator-functions": { - "version": "7.15.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.15.8.tgz", - "integrity": "sha512-2Z5F2R2ibINTc63mY7FLqGfEbmofrHU9FitJW1Q7aPaKFhiPvSq6QEt/BoWN5oME3GVyjcRuNNSRbb9LC0CSWA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5", - "@babel/helper-remap-async-to-generator": "^7.15.4", - "@babel/plugin-syntax-async-generators": "^7.8.4" - } - }, - "@babel/plugin-proposal-dynamic-import": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.14.5.tgz", - "integrity": "sha512-ExjiNYc3HDN5PXJx+bwC50GIx/KKanX2HiggnIUAYedbARdImiCU4RhhHfdf0Kd7JNXGpsBBBCOm+bBVy3Gb0g==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5", - "@babel/plugin-syntax-dynamic-import": "^7.8.3" - } - }, - "@babel/plugin-proposal-json-strings": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.14.5.tgz", - "integrity": "sha512-NSq2fczJYKVRIsUJyNxrVUMhB27zb7N7pOFGQOhBKJrChbGcgEAqyZrmZswkPk18VMurEeJAaICbfm57vUeTbQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5", - "@babel/plugin-syntax-json-strings": "^7.8.3" - } - }, - "@babel/plugin-proposal-optional-catch-binding": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.14.5.tgz", - "integrity": "sha512-3Oyiixm0ur7bzO5ybNcZFlmVsygSIQgdOa7cTfOYCMY+wEPAYhZAJxi3mixKFCTCKUhQXuCTtQ1MzrpL3WT8ZQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" - } - }, - "@babel/plugin-proposal-unicode-property-regex": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.14.5.tgz", - "integrity": "sha512-6axIeOU5LnY471KenAB9vI8I5j7NQ2d652hIYwVyRfgaZT5UpiqFKCuVXCDMSrU+3VFafnu2c5m3lrWIlr6A5Q==", - "dev": true, - "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.14.5", - "@babel/helper-plugin-utils": "^7.14.5" - }, - "dependencies": { - "@babel/helper-create-regexp-features-plugin": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.14.5.tgz", - "integrity": "sha512-TLawwqpOErY2HhWbGJ2nZT5wSkR192QpN+nBg1THfBfftrlvOh+WbhrxXCH4q4xJ9Gl16BGPR/48JA+Ryiho/A==", - "dev": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.14.5", - "regexpu-core": "^4.7.1" - } - } - } - }, - "@babel/plugin-syntax-async-generators": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", - "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-dynamic-import": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", - "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-json-strings": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", - "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-optional-catch-binding": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", - "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-transform-arrow-functions": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.14.5.tgz", - "integrity": "sha512-KOnO0l4+tD5IfOdi4x8C1XmEIRWUjNRV8wc6K2vz/3e8yAOoZZvsRXRRIF/yo/MAOFb4QjtAw9xSxMXbSMRy8A==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5" - } - }, - "@babel/plugin-transform-async-to-generator": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.14.5.tgz", - "integrity": "sha512-szkbzQ0mNk0rpu76fzDdqSyPu0MuvpXgC+6rz5rpMb5OIRxdmHfQxrktL8CYolL2d8luMCZTR0DpIMIdL27IjA==", - "dev": true, - "requires": { - "@babel/helper-module-imports": "^7.14.5", - "@babel/helper-plugin-utils": "^7.14.5", - "@babel/helper-remap-async-to-generator": "^7.14.5" - } - }, - "@babel/plugin-transform-block-scoped-functions": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.14.5.tgz", - "integrity": "sha512-dtqWqdWZ5NqBX3KzsVCWfQI3A53Ft5pWFCT2eCVUftWZgjc5DpDponbIF1+c+7cSGk2wN0YK7HGL/ezfRbpKBQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5" - } - }, - "@babel/plugin-transform-block-scoping": { - "version": "7.15.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.15.3.tgz", - "integrity": "sha512-nBAzfZwZb4DkaGtOes1Up1nOAp9TDRRFw4XBzBBSG9QK7KVFmYzgj9o9sbPv7TX5ofL4Auq4wZnxCoPnI/lz2Q==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5" - } - }, - "@babel/plugin-transform-classes": { - "version": "7.15.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.15.4.tgz", - "integrity": "sha512-Yjvhex8GzBmmPQUvpXRPWQ9WnxXgAFuZSrqOK/eJlOGIXwvv8H3UEdUigl1gb/bnjTrln+e8bkZUYCBt/xYlBg==", - "dev": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.15.4", - "@babel/helper-function-name": "^7.15.4", - "@babel/helper-optimise-call-expression": "^7.15.4", - "@babel/helper-plugin-utils": "^7.14.5", - "@babel/helper-replace-supers": "^7.15.4", - "@babel/helper-split-export-declaration": "^7.15.4", - "globals": "^11.1.0" - } - }, - "@babel/plugin-transform-computed-properties": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.14.5.tgz", - "integrity": "sha512-pWM+E4283UxaVzLb8UBXv4EIxMovU4zxT1OPnpHJcmnvyY9QbPPTKZfEj31EUvG3/EQRbYAGaYEUZ4yWOBC2xg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5" - } - }, - "@babel/plugin-transform-destructuring": { - "version": "7.14.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.14.7.tgz", - "integrity": "sha512-0mDE99nK+kVh3xlc5vKwB6wnP9ecuSj+zQCa/n0voENtP/zymdT4HH6QEb65wjjcbqr1Jb/7z9Qp7TF5FtwYGw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5" - } - }, - "@babel/plugin-transform-dotall-regex": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.14.5.tgz", - "integrity": "sha512-loGlnBdj02MDsFaHhAIJzh7euK89lBrGIdM9EAtHFo6xKygCUGuuWe07o1oZVk287amtW1n0808sQM99aZt3gw==", - "dev": true, - "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.14.5", - "@babel/helper-plugin-utils": "^7.14.5" - }, - "dependencies": { - "@babel/helper-create-regexp-features-plugin": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.14.5.tgz", - "integrity": "sha512-TLawwqpOErY2HhWbGJ2nZT5wSkR192QpN+nBg1THfBfftrlvOh+WbhrxXCH4q4xJ9Gl16BGPR/48JA+Ryiho/A==", - "dev": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.14.5", - "regexpu-core": "^4.7.1" - } - } - } - }, - "@babel/plugin-transform-duplicate-keys": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.14.5.tgz", - "integrity": "sha512-iJjbI53huKbPDAsJ8EmVmvCKeeq21bAze4fu9GBQtSLqfvzj2oRuHVx4ZkDwEhg1htQ+5OBZh/Ab0XDf5iBZ7A==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5" - } - }, - "@babel/plugin-transform-exponentiation-operator": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.14.5.tgz", - "integrity": "sha512-jFazJhMBc9D27o9jDnIE5ZErI0R0m7PbKXVq77FFvqFbzvTMuv8jaAwLZ5PviOLSFttqKIW0/wxNSDbjLk0tYA==", - "dev": true, - "requires": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.14.5", - "@babel/helper-plugin-utils": "^7.14.5" - } - }, - "@babel/plugin-transform-for-of": { - "version": "7.15.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.15.4.tgz", - "integrity": "sha512-DRTY9fA751AFBDh2oxydvVm4SYevs5ILTWLs6xKXps4Re/KG5nfUkr+TdHCrRWB8C69TlzVgA9b3RmGWmgN9LA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5" - } - }, - "@babel/plugin-transform-function-name": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.14.5.tgz", - "integrity": "sha512-vbO6kv0fIzZ1GpmGQuvbwwm+O4Cbm2NrPzwlup9+/3fdkuzo1YqOZcXw26+YUJB84Ja7j9yURWposEHLYwxUfQ==", - "dev": true, - "requires": { - "@babel/helper-function-name": "^7.14.5", - "@babel/helper-plugin-utils": "^7.14.5" - } - }, - "@babel/plugin-transform-literals": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.14.5.tgz", - "integrity": "sha512-ql33+epql2F49bi8aHXxvLURHkxJbSmMKl9J5yHqg4PLtdE6Uc48CH1GS6TQvZ86eoB/ApZXwm7jlA+B3kra7A==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5" - } - }, - "@babel/plugin-transform-member-expression-literals": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.14.5.tgz", - "integrity": "sha512-WkNXxH1VXVTKarWFqmso83xl+2V3Eo28YY5utIkbsmXoItO8Q3aZxN4BTS2k0hz9dGUloHK26mJMyQEYfkn/+Q==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5" - } - }, - "@babel/plugin-transform-modules-amd": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.14.5.tgz", - "integrity": "sha512-3lpOU8Vxmp3roC4vzFpSdEpGUWSMsHFreTWOMMLzel2gNGfHE5UWIh/LN6ghHs2xurUp4jRFYMUIZhuFbody1g==", - "dev": true, - "requires": { - "@babel/helper-module-transforms": "^7.14.5", - "@babel/helper-plugin-utils": "^7.14.5", - "babel-plugin-dynamic-import-node": "^2.3.3" - } - }, - "@babel/plugin-transform-modules-commonjs": { - "version": "7.15.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.15.4.tgz", - "integrity": "sha512-qg4DPhwG8hKp4BbVDvX1s8cohM8a6Bvptu4l6Iingq5rW+yRUAhe/YRup/YcW2zCOlrysEWVhftIcKzrEZv3sA==", - "dev": true, - "requires": { - "@babel/helper-module-transforms": "^7.15.4", - "@babel/helper-plugin-utils": "^7.14.5", - "@babel/helper-simple-access": "^7.15.4", - "babel-plugin-dynamic-import-node": "^2.3.3" - } - }, - "@babel/plugin-transform-modules-systemjs": { - "version": "7.15.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.15.4.tgz", - "integrity": "sha512-fJUnlQrl/mezMneR72CKCgtOoahqGJNVKpompKwzv3BrEXdlPspTcyxrZ1XmDTIr9PpULrgEQo3qNKp6dW7ssw==", - "dev": true, - "requires": { - "@babel/helper-hoist-variables": "^7.15.4", - "@babel/helper-module-transforms": "^7.15.4", - "@babel/helper-plugin-utils": "^7.14.5", - "@babel/helper-validator-identifier": "^7.14.9", - "babel-plugin-dynamic-import-node": "^2.3.3" - } - }, - "@babel/plugin-transform-modules-umd": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.14.5.tgz", - "integrity": "sha512-RfPGoagSngC06LsGUYyM9QWSXZ8MysEjDJTAea1lqRjNECE3y0qIJF/qbvJxc4oA4s99HumIMdXOrd+TdKaAAA==", - "dev": true, - "requires": { - "@babel/helper-module-transforms": "^7.14.5", - "@babel/helper-plugin-utils": "^7.14.5" - } - }, - "@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.14.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.14.9.tgz", - "integrity": "sha512-l666wCVYO75mlAtGFfyFwnWmIXQm3kSH0C3IRnJqWcZbWkoihyAdDhFm2ZWaxWTqvBvhVFfJjMRQ0ez4oN1yYA==", - "dev": true, - "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.14.5" - }, - "dependencies": { - "@babel/helper-create-regexp-features-plugin": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.14.5.tgz", - "integrity": "sha512-TLawwqpOErY2HhWbGJ2nZT5wSkR192QpN+nBg1THfBfftrlvOh+WbhrxXCH4q4xJ9Gl16BGPR/48JA+Ryiho/A==", - "dev": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.14.5", - "regexpu-core": "^4.7.1" - } - } - } - }, - "@babel/plugin-transform-new-target": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.14.5.tgz", - "integrity": "sha512-Nx054zovz6IIRWEB49RDRuXGI4Gy0GMgqG0cII9L3MxqgXz/+rgII+RU58qpo4g7tNEx1jG7rRVH4ihZoP4esQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5" - } - }, - "@babel/plugin-transform-object-super": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.14.5.tgz", - "integrity": "sha512-MKfOBWzK0pZIrav9z/hkRqIk/2bTv9qvxHzPQc12RcVkMOzpIKnFCNYJip00ssKWYkd8Sf5g0Wr7pqJ+cmtuFg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5", - "@babel/helper-replace-supers": "^7.14.5" - } - }, - "@babel/plugin-transform-parameters": { - "version": "7.15.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.15.4.tgz", - "integrity": "sha512-9WB/GUTO6lvJU3XQsSr6J/WKvBC2hcs4Pew8YxZagi6GkTdniyqp8On5kqdK8MN0LMeu0mGbhPN+O049NV/9FQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5" - } - }, - "@babel/plugin-transform-property-literals": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.14.5.tgz", - "integrity": "sha512-r1uilDthkgXW8Z1vJz2dKYLV1tuw2xsbrp3MrZmD99Wh9vsfKoob+JTgri5VUb/JqyKRXotlOtwgu4stIYCmnw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5" - } - }, - "@babel/plugin-transform-regenerator": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.14.5.tgz", - "integrity": "sha512-NVIY1W3ITDP5xQl50NgTKlZ0GrotKtLna08/uGY6ErQt6VEQZXla86x/CTddm5gZdcr+5GSsvMeTmWA5Ii6pkg==", - "dev": true, - "requires": { - "regenerator-transform": "^0.14.2" - } - }, - "@babel/plugin-transform-reserved-words": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.14.5.tgz", - "integrity": "sha512-cv4F2rv1nD4qdexOGsRQXJrOcyb5CrgjUH9PKrrtyhSDBNWGxd0UIitjyJiWagS+EbUGjG++22mGH1Pub8D6Vg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5" - } - }, - "@babel/plugin-transform-shorthand-properties": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.14.5.tgz", - "integrity": "sha512-xLucks6T1VmGsTB+GWK5Pl9Jl5+nRXD1uoFdA5TSO6xtiNjtXTjKkmPdFXVLGlK5A2/or/wQMKfmQ2Y0XJfn5g==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5" - } - }, - "@babel/plugin-transform-spread": { - "version": "7.15.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.15.8.tgz", - "integrity": "sha512-/daZ8s2tNaRekl9YJa9X4bzjpeRZLt122cpgFnQPLGUe61PH8zMEBmYqKkW5xF5JUEh5buEGXJoQpqBmIbpmEQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5", - "@babel/helper-skip-transparent-expression-wrappers": "^7.15.4" - } - }, - "@babel/plugin-transform-sticky-regex": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.14.5.tgz", - "integrity": "sha512-Z7F7GyvEMzIIbwnziAZmnSNpdijdr4dWt+FJNBnBLz5mwDFkqIXU9wmBcWWad3QeJF5hMTkRe4dAq2sUZiG+8A==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5" - } - }, - "@babel/plugin-transform-template-literals": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.14.5.tgz", - "integrity": "sha512-22btZeURqiepOfuy/VkFr+zStqlujWaarpMErvay7goJS6BWwdd6BY9zQyDLDa4x2S3VugxFb162IZ4m/S/+Gg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5" - } - }, - "@babel/plugin-transform-typeof-symbol": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.14.5.tgz", - "integrity": "sha512-lXzLD30ffCWseTbMQzrvDWqljvZlHkXU+CnseMhkMNqU1sASnCsz3tSzAaH3vCUXb9PHeUb90ZT1BdFTm1xxJw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5" - } - }, - "@babel/plugin-transform-unicode-regex": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.14.5.tgz", - "integrity": "sha512-UygduJpC5kHeCiRw/xDVzC+wj8VaYSoKl5JNVmbP7MadpNinAm3SvZCxZ42H37KZBKztz46YC73i9yV34d0Tzw==", - "dev": true, - "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.14.5", - "@babel/helper-plugin-utils": "^7.14.5" - }, - "dependencies": { - "@babel/helper-create-regexp-features-plugin": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.14.5.tgz", - "integrity": "sha512-TLawwqpOErY2HhWbGJ2nZT5wSkR192QpN+nBg1THfBfftrlvOh+WbhrxXCH4q4xJ9Gl16BGPR/48JA+Ryiho/A==", - "dev": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.14.5", - "regexpu-core": "^4.7.1" - } - } - } - } + "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", + "@babel/plugin-transform-dotall-regex": "^7.4.4", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" } }, "@babel/runtime": { - "version": "7.15.4", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.15.4.tgz", - "integrity": "sha512-99catp6bHCaxr4sJ/DbTGgHS4+Rs2RVd2g7iOap6SLGPDknRK9ztKNsE/Fg6QhSeh1FGE5f6gHGQmvvn3I3xhw==", + "version": "7.17.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.2.tgz", + "integrity": "sha512-hzeyJyMA1YGdJTuWU0e/j4wKXrU4OMFvY2MSlaI9B7VQb0r5cxTE3EAIS2Q7Tn2RIcDkRvTA/v2JsAEhxe99uw==", "dev": true, "requires": { "regenerator-runtime": "^0.13.4" } }, "@babel/template": { - "version": "7.15.4", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.15.4.tgz", - "integrity": "sha512-UgBAfEa1oGuYgDIPM2G+aHa4Nlo9Lh6mGD2bDBGMTbYnc38vulXPuC1MGjYILIEmlwl6Rd+BPR9ee3gm20CBtg==", + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.16.7.tgz", + "integrity": "sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w==", "dev": true, "requires": { - "@babel/code-frame": "^7.14.5", - "@babel/parser": "^7.15.4", - "@babel/types": "^7.15.4" + "@babel/code-frame": "^7.16.7", + "@babel/parser": "^7.16.7", + "@babel/types": "^7.16.7" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", + "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", + "dev": true, + "requires": { + "@babel/highlight": "^7.16.7" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", + "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", + "dev": true + }, + "@babel/highlight": { + "version": "7.16.10", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.16.10.tgz", + "integrity": "sha512-5FnTQLSLswEj6IkgVw5KusNUUFY9ZGqe/TRFnP/BKYHYgfh7tc+C7mwiy95/yNP7Dh9x580Vv8r7u7ZfTBFxdw==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.16.7", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + } } }, "@babel/traverse": { - "version": "7.15.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.15.4.tgz", - "integrity": "sha512-W6lQD8l4rUbQR/vYgSuCAE75ADyyQvOpFVsvPPdkhf6lATXAsQIG9YdtOcu8BB1dZ0LKu+Zo3c1wEcbKeuhdlA==", + "version": "7.17.3", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.17.3.tgz", + "integrity": "sha512-5irClVky7TxRWIRtxlh2WPUUOLhcPN06AGgaQSB8AEwuyEBgJVuJ5imdHm5zxk8w0QS5T+tDfnDxAlhWjpb7cw==", "dev": true, "requires": { - "@babel/code-frame": "^7.14.5", - "@babel/generator": "^7.15.4", - "@babel/helper-function-name": "^7.15.4", - "@babel/helper-hoist-variables": "^7.15.4", - "@babel/helper-split-export-declaration": "^7.15.4", - "@babel/parser": "^7.15.4", - "@babel/types": "^7.15.4", + "@babel/code-frame": "^7.16.7", + "@babel/generator": "^7.17.3", + "@babel/helper-environment-visitor": "^7.16.7", + "@babel/helper-function-name": "^7.16.7", + "@babel/helper-hoist-variables": "^7.16.7", + "@babel/helper-split-export-declaration": "^7.16.7", + "@babel/parser": "^7.17.3", + "@babel/types": "^7.17.0", "debug": "^4.1.0", "globals": "^11.1.0" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", + "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", + "dev": true, + "requires": { + "@babel/highlight": "^7.16.7" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", + "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", + "dev": true + }, + "@babel/highlight": { + "version": "7.16.10", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.16.10.tgz", + "integrity": "sha512-5FnTQLSLswEj6IkgVw5KusNUUFY9ZGqe/TRFnP/BKYHYgfh7tc+C7mwiy95/yNP7Dh9x580Vv8r7u7ZfTBFxdw==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.16.7", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + } } }, "@babel/types": { - "version": "7.15.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.15.6.tgz", - "integrity": "sha512-BPU+7QhqNjmWyDO0/vitH/CuhpV8ZmK1wpKva8nuyNF5MJfuRNWMc+hc14+u9xT93kvykMdncrJT19h74uB1Ig==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz", + "integrity": "sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.14.9", + "@babel/helper-validator-identifier": "^7.16.7", "to-fast-properties": "^2.0.0" + }, + "dependencies": { + "@babel/helper-validator-identifier": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", + "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", + "dev": true + } } }, "@gulp-sourcemaps/identity-map": { @@ -921,6 +1319,28 @@ } } }, + "@jridgewell/resolve-uri": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.0.5.tgz", + "integrity": "sha512-VPeQ7+wH0itvQxnG+lIzWgkysKIr3L9sslimFW55rHMdGu/qCQ5z5h9zq4gI8uBtqkpHhsF4Z/OwExufUCThew==", + "dev": true + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.11", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.11.tgz", + "integrity": "sha512-Fg32GrJo61m+VqYSdRSjRXMjQ06j8YIYfcTqndLYVAaHmroZHLJZCydsWBOTDqXS2v+mjxohBWEMfg97GXmYQg==", + "dev": true + }, + "@jridgewell/trace-mapping": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.4.tgz", + "integrity": "sha512-vFv9ttIedivx0ux3QSjhgtCVjPZd5l46ZOMDSCwnH1yUO2e964gO8LZGyv2QkqcgR6TnBU1v+1IFqmeoG+0UJQ==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "@microsoft/signalr": { "version": "3.1.20", "resolved": "https://registry.npmjs.org/@microsoft/signalr/-/signalr-3.1.20.tgz", @@ -964,11 +1384,35 @@ "dev": true, "optional": true }, + "@socket.io/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@socket.io/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-dOlCBKnDw4iShaIsH/bxujKTM18+2TOAsYz+KSc11Am38H4q5Xw8Bbz97ZYdrVNM+um3p7w86Bvvmcn9q+5+eQ==", + "dev": true + }, "@types/angular": { "version": "1.8.3", "resolved": "https://registry.npmjs.org/@types/angular/-/angular-1.8.3.tgz", "integrity": "sha512-vgc5Z+TD07DT7NEUjFm6XMp0kEbGXIa95XmOL5IiHXR9LdrJpcdDh3jl1nCuZbWyzFn5/1OqtMfomcnA1sUFXQ==" }, + "@types/component-emitter": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@types/component-emitter/-/component-emitter-1.2.11.tgz", + "integrity": "sha512-SRXjM+tfsSlA9VuG8hGO2nft2p8zjXCK1VcC6N4NXbBbYbSia9kzCChYQajIjzIqOOOuh5Ock6MmV2oux4jDZQ==", + "dev": true + }, + "@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==", + "dev": true + }, + "@types/cors": { + "version": "2.8.12", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.12.tgz", + "integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==", + "dev": true + }, "@types/glob": { "version": "7.1.4", "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.4.tgz", @@ -1004,13 +1448,30 @@ "dev": true }, "accepts": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", - "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", "dev": true, "requires": { - "mime-types": "~2.1.24", - "negotiator": "0.6.2" + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "dependencies": { + "mime-db": { + "version": "1.51.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz", + "integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==", + "dev": true + }, + "mime-types": { + "version": "2.1.34", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz", + "integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==", + "dev": true, + "requires": { + "mime-db": "1.51.0" + } + } } }, "accord": { @@ -1068,12 +1529,6 @@ "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", "dev": true }, - "after": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz", - "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=", - "dev": true - }, "ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -1485,12 +1940,6 @@ "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", "dev": true }, - "arraybuffer.slice": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz", - "integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==", - "dev": true - }, "asap": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", @@ -1609,6 +2058,44 @@ "object.assign": "^4.1.0" } }, + "babel-plugin-polyfill-corejs2": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.1.tgz", + "integrity": "sha512-v7/T6EQcNfVLfcN2X8Lulb7DjprieyLWJK/zOWH5DUYcAgex9sP3h25Q+DLsX9TloXe3y1O8l2q2Jv9q8UVB9w==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.13.11", + "@babel/helper-define-polyfill-provider": "^0.3.1", + "semver": "^6.1.1" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "babel-plugin-polyfill-corejs3": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.5.2.tgz", + "integrity": "sha512-G3uJih0XWiID451fpeFaYGVuxHEjzKTHtc9uGFEjR6hHrvNzeS/PX+LLLcetJcytsB5m4j+K3o/EpXJNb/5IEQ==", + "dev": true, + "requires": { + "@babel/helper-define-polyfill-provider": "^0.3.1", + "core-js-compat": "^3.21.0" + } + }, + "babel-plugin-polyfill-regenerator": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.3.1.tgz", + "integrity": "sha512-Y2B06tvgHYt1x0yz17jGkGeeMr5FeKUu+ASJ+N6nB5lQ8Dapfg42i0OVrf8PNGJ3zKL4A23snMi1IRwrqqND7A==", + "dev": true, + "requires": { + "@babel/helper-define-polyfill-provider": "^0.3.1" + } + }, "bach": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/bach/-/bach-1.2.0.tgz", @@ -1626,12 +2113,6 @@ "now-and-later": "^2.0.0" } }, - "backo2": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", - "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=", - "dev": true - }, "balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1664,12 +2145,6 @@ } } }, - "base64-arraybuffer": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz", - "integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg=", - "dev": true - }, "base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -1678,9 +2153,9 @@ "optional": true }, "base64id": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/base64id/-/base64id-1.0.0.tgz", - "integrity": "sha1-R2iMuZu2gE8OBtPnY7HDLlfY5rY=", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", "dev": true }, "bcrypt-pbkdf": { @@ -1697,15 +2172,6 @@ "integrity": "sha1-5tXqjF2tABMEpwsiY4RH9pyy+Ak=", "dev": true }, - "better-assert": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz", - "integrity": "sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=", - "dev": true, - "requires": { - "callsite": "1.0.0" - } - }, "bin-build": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/bin-build/-/bin-build-3.0.0.tgz", @@ -2014,12 +2480,6 @@ "safe-buffer": "^5.1.1" } }, - "blob": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz", - "integrity": "sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig==", - "dev": true - }, "bluebird": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", @@ -2027,21 +2487,21 @@ "dev": true }, "body-parser": { - "version": "1.19.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", - "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.2.tgz", + "integrity": "sha512-SAAwOxgoCKMGs9uUAUFHygfLAyaniaoun6I8mFY9pRAJL9+Kec34aU+oIjDhTycub1jozEfEwx1W1IuOYxVSFw==", "dev": true, "requires": { - "bytes": "3.1.0", + "bytes": "3.1.2", "content-type": "~1.0.4", "debug": "2.6.9", "depd": "~1.1.2", - "http-errors": "1.7.2", + "http-errors": "1.8.1", "iconv-lite": "0.4.24", "on-finished": "~2.3.0", - "qs": "6.7.0", - "raw-body": "2.4.0", - "type-is": "~1.6.17" + "qs": "6.9.7", + "raw-body": "2.4.3", + "type-is": "~1.6.18" }, "dependencies": { "debug": { @@ -2144,6 +2604,7 @@ "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", "dev": true, + "optional": true, "requires": { "buffer-alloc-unsafe": "^1.1.0", "buffer-fill": "^1.0.0" @@ -2153,7 +2614,8 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==", - "dev": true + "dev": true, + "optional": true }, "buffer-crc32": { "version": "0.2.13", @@ -2172,7 +2634,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", "integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw=", - "dev": true + "dev": true, + "optional": true }, "buffer-from": { "version": "1.1.2", @@ -2216,9 +2679,9 @@ } }, "bytes": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", - "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", "dev": true }, "cache-base": { @@ -2327,12 +2790,6 @@ "caller-callsite": "^2.0.0" } }, - "callsite": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz", - "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA=", - "dev": true - }, "callsites": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", @@ -2370,9 +2827,9 @@ } }, "caniuse-lite": { - "version": "1.0.30001267", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001267.tgz", - "integrity": "sha512-r1mjTzAuJ9W8cPBGbbus8E0SKcUP7gn03R14Wk8FlAlqhH9hroy9nLqmpuXlfKEw/oILW+FGz47ipXV2O7x8lg==", + "version": "1.0.30001312", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001312.tgz", + "integrity": "sha512-Wiz1Psk2MEK0pX3rUzWaunLTZzqS2JYZFzNKqAiJGiuxIjRPLgV6+VDPOg6lQOUxmDwhTlh198JsTTi8Hzw6aQ==", "dev": true }, "caseless": { @@ -2797,24 +3254,12 @@ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true }, - "component-bind": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz", - "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=", - "dev": true - }, "component-emitter": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", "dev": true }, - "component-inherit": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz", - "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=", - "dev": true - }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2932,9 +3377,9 @@ } }, "cookie": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", - "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=", + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", "dev": true }, "copy-descriptor": { @@ -2954,15 +3399,46 @@ } }, "core-js-compat": { - "version": "3.18.3", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.18.3.tgz", - "integrity": "sha512-4zP6/y0a2RTHN5bRGT7PTq9lVt3WzvffTNjqnTKsXhkAYNDTkdCLOIfAdOLcQ/7TDdyRj3c+NeHe1NmF1eDScw==", + "version": "3.21.1", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.21.1.tgz", + "integrity": "sha512-gbgX5AUvMb8gwxC7FLVWYT7Kkgu/y7+h/h1X43yJkNqhlK2fuYyQimqvKGNZFAY6CKii/GFKJ2cp/1/42TN36g==", "dev": true, "requires": { - "browserslist": "^4.17.3", + "browserslist": "^4.19.1", "semver": "7.0.0" }, "dependencies": { + "browserslist": { + "version": "4.19.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.19.3.tgz", + "integrity": "sha512-XK3X4xtKJ+Txj8G5c30B4gsm71s69lqXlkYui4s6EkKxuv49qjYlY6oVd+IFJ73d4YymtM3+djvvt/R/iJwwDg==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30001312", + "electron-to-chromium": "^1.4.71", + "escalade": "^3.1.1", + "node-releases": "^2.0.2", + "picocolors": "^1.0.0" + } + }, + "caniuse-lite": { + "version": "1.0.30001312", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001312.tgz", + "integrity": "sha512-Wiz1Psk2MEK0pX3rUzWaunLTZzqS2JYZFzNKqAiJGiuxIjRPLgV6+VDPOg6lQOUxmDwhTlh198JsTTi8Hzw6aQ==", + "dev": true + }, + "electron-to-chromium": { + "version": "1.4.71", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.71.tgz", + "integrity": "sha512-Hk61vXXKRb2cd3znPE9F+2pLWdIOmP7GjiTj45y6L3W/lO+hSnUSUhq+6lEaERWBdZOHbk2s3YV5c9xVl3boVw==", + "dev": true + }, + "node-releases": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.2.tgz", + "integrity": "sha512-XxYDdcQ6eKqp/YjI+tb2C5WM2LgjnZrfYg4vgQt49EK268b6gYCHsBLrK2qvJo4FmCtqmKezb0WZFK4fkrZNsg==", + "dev": true + }, "semver": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz", @@ -2977,6 +3453,16 @@ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "dev": true }, + "cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dev": true, + "requires": { + "object-assign": "^4", + "vary": "^1" + } + }, "cosmiconfig": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz", @@ -3265,9 +3751,9 @@ } }, "date-format": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/date-format/-/date-format-2.1.0.tgz", - "integrity": "sha512-bYQuGLeFxhkxNOF3rcMtiZxvCBAquGzZm6oWA1oZ0g2THUzivaRhv8uOhdr19LmoobSOLoIAxeUK2RdbM8IFTA==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.3.tgz", + "integrity": "sha512-7P3FyqDcfeznLZp2b+OMitV9Sz2lUnsT87WaTat9nVwqsBkTzPG3lPLNwW3en6F4pHUiWzr6vb8CLhjdK9bcxQ==", "dev": true }, "dateformat": { @@ -3866,111 +4352,38 @@ } }, "engine.io": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.2.1.tgz", - "integrity": "sha512-+VlKzHzMhaU+GsCIg4AoXF1UdDFjHHwMmMKqMJNDNLlUlejz58FCy4LBqB2YVJskHGYl06BatYWKP2TVdVXE5w==", + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.1.3.tgz", + "integrity": "sha512-rqs60YwkvWTLLnfazqgZqLa/aKo+9cueVfEi/dZ8PyGyaf8TLOxj++4QMIgeG3Gn0AhrWiFXvghsoY9L9h25GA==", "dev": true, "requires": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", "accepts": "~1.3.4", - "base64id": "1.0.0", - "cookie": "0.3.1", - "debug": "~3.1.0", - "engine.io-parser": "~2.1.0", - "ws": "~3.3.1" + "base64id": "2.0.0", + "cookie": "~0.4.1", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.0.3", + "ws": "~8.2.3" }, "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - }, "ws": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-3.3.3.tgz", - "integrity": "sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA==", - "dev": true, - "requires": { - "async-limiter": "~1.0.0", - "safe-buffer": "~5.1.0", - "ultron": "~1.1.0" - } - } - } - }, - "engine.io-client": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.2.1.tgz", - "integrity": "sha512-y5AbkytWeM4jQr7m/koQLc5AxpRKC1hEVUb/s1FUAWEJq5AzJJ4NLvzuKPuxtDi5Mq755WuDvZ6Iv2rXj4PTzw==", - "dev": true, - "requires": { - "component-emitter": "1.2.1", - "component-inherit": "0.0.3", - "debug": "~3.1.0", - "engine.io-parser": "~2.1.1", - "has-cors": "1.1.0", - "indexof": "0.0.1", - "parseqs": "0.0.5", - "parseuri": "0.0.5", - "ws": "~3.3.1", - "xmlhttprequest-ssl": "~1.5.4", - "yeast": "0.1.2" - }, - "dependencies": { - "component-emitter": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", - "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz", + "integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==", "dev": true - }, - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - }, - "ws": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-3.3.3.tgz", - "integrity": "sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA==", - "dev": true, - "requires": { - "async-limiter": "~1.0.0", - "safe-buffer": "~5.1.0", - "ultron": "~1.1.0" - } } } }, "engine.io-parser": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.1.3.tgz", - "integrity": "sha512-6HXPre2O4Houl7c4g7Ic/XzPnHBvaEmN90vtRO9uLmwtRqQmTOw0QMevL1TOfL2Cpu1VzsaTmMotQgMdkzGkVA==", + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.3.tgz", + "integrity": "sha512-BtQxwF27XUNnSafQLvDi0dQ8s3i6VgzSoQMJacpIcGNrlUdfHSKbgm3jmjCVvQluGzqwujQMPAoMai3oYSTurg==", "dev": true, "requires": { - "after": "0.8.2", - "arraybuffer.slice": "~0.0.7", - "base64-arraybuffer": "0.1.5", - "blob": "0.0.5", - "has-binary2": "~1.0.2" + "@socket.io/base64-arraybuffer": "~1.0.2" } }, "ent": { @@ -5036,9 +5449,9 @@ } }, "follow-redirects": { - "version": "1.14.4", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.4.tgz", - "integrity": "sha512-zwGkiSXC1MUJG/qmeIFH2HBJx9u0V46QGUe3YR1fXG8bXQxq7fLj0RjLZQ5nubr9qNJUZrH+xUcwXEoXNpfS+g==", + "version": "1.14.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz", + "integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==", "dev": true }, "font-awesome": { @@ -5110,14 +5523,14 @@ "optional": true }, "fs-extra": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", - "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.0.1.tgz", + "integrity": "sha512-NbdoVMZso2Lsrn/QwLXOy6rm0ufY2zEOKCDzJR/0kBsb0E6qed0P3iYK+Ath3BfvXEeu4JhEtXLgILx5psUfag==", "dev": true, "requires": { - "graceful-fs": "^4.1.2", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" } }, "fs-mkdirp-stream": { @@ -5168,6 +5581,12 @@ "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", "dev": true }, + "gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true + }, "get-caller-file": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", @@ -6372,29 +6791,6 @@ "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==", "dev": true }, - "has-binary2": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.3.tgz", - "integrity": "sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==", - "dev": true, - "requires": { - "isarray": "2.0.1" - }, - "dependencies": { - "isarray": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", - "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=", - "dev": true - } - } - }, - "has-cors": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz", - "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=", - "dev": true - }, "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -6538,24 +6934,16 @@ "optional": true }, "http-errors": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", - "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", "dev": true, "requires": { "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.1", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.0" - }, - "dependencies": { - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - } + "toidentifier": "1.0.1" } }, "http-proxy": { @@ -6734,12 +7122,6 @@ "integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc=", "dev": true }, - "indexof": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", - "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=", - "dev": true - }, "indx": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/indx/-/indx-0.2.3.tgz", @@ -6877,15 +7259,6 @@ "p-is-promise": "^1.1.0" } }, - "invariant": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", - "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", - "dev": true, - "requires": { - "loose-envify": "^1.0.0" - } - }, "invert-kv": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", @@ -7370,13 +7743,10 @@ "dev": true }, "isbinaryfile": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-3.0.3.tgz", - "integrity": "sha512-8cJBL5tTd2OS0dM4jz07wQd5g0dCCqIhUxPIGtZfa5L6hWlvV5MHTITy/DBAsF+Oe2LS1X3krBUhNwaGUWpWxw==", - "dev": true, - "requires": { - "buffer-alloc": "^1.2.0" - } + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.8.tgz", + "integrity": "sha512-53h6XFniq77YdW+spoRrebh0mnmTxRPTlcuIArO57lmMdq4uBKFKaeTjnb92oYWrSn/LVL+LT+Hap2tFQj8V+w==", + "dev": true }, "isexe": { "version": "2.0.0", @@ -7427,12 +7797,6 @@ "resolved": "https://registry.npmjs.org/jquery-ui-touch-punch/-/jquery-ui-touch-punch-0.2.3.tgz", "integrity": "sha1-7tgiQnM7okP0az6HwYQbMIGR2mg=" }, - "js-levenshtein": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz", - "integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==", - "dev": true - }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -7554,12 +7918,13 @@ } }, "jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", "dev": true, "requires": { - "graceful-fs": "^4.1.6" + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" } }, "jsprim": { @@ -7586,39 +7951,52 @@ "dev": true }, "karma": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/karma/-/karma-4.4.1.tgz", - "integrity": "sha512-L5SIaXEYqzrh6b1wqYC42tNsFMx2PWuxky84pK9coK09MvmL7mxii3G3bZBh/0rvD27lqDd0le9jyhzvwif73A==", + "version": "6.3.16", + "resolved": "https://registry.npmjs.org/karma/-/karma-6.3.16.tgz", + "integrity": "sha512-nEU50jLvDe5yvXqkEJRf8IuvddUkOY2x5Xc4WXHz6dxINgGDrgD2uqQWeVrJs4hbfNaotn+HQ1LZJ4yOXrL7xQ==", "dev": true, "requires": { - "bluebird": "^3.3.0", - "body-parser": "^1.16.1", + "body-parser": "^1.19.0", "braces": "^3.0.2", - "chokidar": "^3.0.0", - "colors": "^1.1.0", - "connect": "^3.6.0", + "chokidar": "^3.5.1", + "colors": "1.4.0", + "connect": "^3.7.0", "di": "^0.0.1", - "dom-serialize": "^2.2.0", - "flatted": "^2.0.0", - "glob": "^7.1.1", - "graceful-fs": "^4.1.2", - "http-proxy": "^1.13.0", - "isbinaryfile": "^3.0.0", - "lodash": "^4.17.14", - "log4js": "^4.0.0", - "mime": "^2.3.1", - "minimatch": "^3.0.2", - "optimist": "^0.6.1", - "qjobs": "^1.1.4", - "range-parser": "^1.2.0", - "rimraf": "^2.6.0", - "safe-buffer": "^5.0.1", - "socket.io": "2.1.1", + "dom-serialize": "^2.2.1", + "glob": "^7.1.7", + "graceful-fs": "^4.2.6", + "http-proxy": "^1.18.1", + "isbinaryfile": "^4.0.8", + "lodash": "^4.17.21", + "log4js": "^6.4.1", + "mime": "^2.5.2", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.5", + "qjobs": "^1.2.0", + "range-parser": "^1.2.1", + "rimraf": "^3.0.2", + "socket.io": "^4.2.0", "source-map": "^0.6.1", - "tmp": "0.0.33", - "useragent": "2.3.0" + "tmp": "^0.2.1", + "ua-parser-js": "^0.7.30", + "yargs": "^16.1.1" }, "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, "anymatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", @@ -7645,9 +8023,9 @@ } }, "chokidar": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz", - "integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==", + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", "dev": true, "requires": { "anymatch": "~3.1.2", @@ -7660,6 +8038,26 @@ "readdirp": "~3.6.0" } }, + "cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, "fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -7676,6 +8074,12 @@ "dev": true, "optional": true }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, "glob-parent": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", @@ -7709,12 +8113,39 @@ "picomatch": "^2.2.1" } }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "tmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "dev": true, + "requires": { + "rimraf": "^3.0.0" + } + }, "to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -7723,6 +8154,44 @@ "requires": { "is-number": "^7.0.0" } + }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true + }, + "yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "requires": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + } + }, + "yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true } } }, @@ -7999,6 +8468,12 @@ "integrity": "sha1-GVhwRQ9aExkkeN9Lw9I9LeoZB7Y=", "dev": true }, + "lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=", + "dev": true + }, "lodash.defaults": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", @@ -8099,16 +8574,33 @@ "dev": true }, "log4js": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/log4js/-/log4js-4.5.1.tgz", - "integrity": "sha512-EEEgFcE9bLgaYUKuozyFfytQM2wDHtXn4tAN41pkaxpNjAykv11GVdeI4tHtmPWW4Xrgh9R/2d7XYghDVjbKKw==", + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/log4js/-/log4js-6.4.1.tgz", + "integrity": "sha512-iUiYnXqAmNKiIZ1XSAitQ4TmNs8CdZYTAWINARF3LjnsLN8tY5m0vRwd6uuWj/yNY0YHxeZodnbmxKFUOM2rMg==", "dev": true, "requires": { - "date-format": "^2.0.0", - "debug": "^4.1.1", - "flatted": "^2.0.0", - "rfdc": "^1.1.4", - "streamroller": "^1.0.6" + "date-format": "^4.0.3", + "debug": "^4.3.3", + "flatted": "^3.2.4", + "rfdc": "^1.3.0", + "streamroller": "^3.0.2" + }, + "dependencies": { + "debug": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "flatted": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.5.tgz", + "integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==", + "dev": true + } } }, "logalot": { @@ -8141,15 +8633,6 @@ "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=", "dev": true }, - "loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, - "requires": { - "js-tokens": "^3.0.0 || ^4.0.0" - } - }, "loud-rejection": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", @@ -8186,6 +8669,7 @@ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", "dev": true, + "optional": true, "requires": { "pseudomap": "^1.0.2", "yallist": "^2.1.2" @@ -8414,9 +8898,9 @@ } }, "mime": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.5.2.tgz", - "integrity": "sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", "dev": true }, "mime-db": { @@ -8627,9 +9111,9 @@ "dev": true }, "negotiator": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", - "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==", + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", "dev": true }, "next-tick": { @@ -11892,12 +12376,6 @@ "integrity": "sha1-ejs9DpgGPUP0wD8uiubNUahog6A=", "dev": true }, - "object-component": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/object-component/-/object-component-0.0.3.tgz", - "integrity": "sha1-8MaapQ78lbhmwYb0AKM3acsvEpE=", - "dev": true - }, "object-copy": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", @@ -12110,24 +12588,6 @@ "mimic-fn": "^2.1.0" } }, - "optimist": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", - "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", - "dev": true, - "requires": { - "minimist": "~0.0.1", - "wordwrap": "~0.0.2" - }, - "dependencies": { - "minimist": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", - "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=", - "dev": true - } - } - }, "optionator": { "version": "0.8.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", @@ -12345,24 +12805,6 @@ "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==", "dev": true }, - "parseqs": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz", - "integrity": "sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0=", - "dev": true, - "requires": { - "better-assert": "~1.0.0" - } - }, - "parseuri": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.5.tgz", - "integrity": "sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo=", - "dev": true, - "requires": { - "better-assert": "~1.0.0" - } - }, "parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -13115,7 +13557,8 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", - "dev": true + "dev": true, + "optional": true }, "psl": { "version": "1.8.0", @@ -13174,9 +13617,9 @@ "dev": true }, "qs": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", - "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.7.tgz", + "integrity": "sha512-IhMFgUmuNpyRfxA90umL7ByLlgRXu6tIfKPpF5TmcfRLlLCckfP/g3IQmju6jjpu+Hh8rA+2p6A27ZSPOOHdKw==", "dev": true }, "query-string": { @@ -13234,13 +13677,13 @@ "dev": true }, "raw-body": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", - "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.3.tgz", + "integrity": "sha512-UlTNLIcu0uzb4D2f4WltY6cVjLi+/jEN4lgEUj3E04tpMDpUlkBo/eSn6zou9hum2VMNpCCUone0O0WeJim07g==", "dev": true, "requires": { - "bytes": "3.1.0", - "http-errors": "1.7.2", + "bytes": "3.1.2", + "http-errors": "1.8.1", "iconv-lite": "0.4.24", "unpipe": "1.0.0" } @@ -13338,9 +13781,9 @@ "dev": true }, "regenerate-unicode-properties": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-9.0.0.tgz", - "integrity": "sha512-3E12UeNSPfjrgwjkR81m5J7Aw/T55Tu7nUyZVQYCKEOs+2dkxEY+DpPtZzO4YruuiPb7NkYLVcyJC4+zCbk5pA==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.0.1.tgz", + "integrity": "sha512-vn5DU6yg6h8hP/2OkQo3K7uVILvY4iu0oI4t3HFa81UPkhGJwkRwM10JEc3upjdhHjs/k8GJY1sRBhk5sr69Bw==", "dev": true, "requires": { "regenerate": "^1.4.2" @@ -13417,29 +13860,29 @@ "dev": true }, "regexpu-core": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.8.0.tgz", - "integrity": "sha512-1F6bYsoYiz6is+oz70NWur2Vlh9KWtswuRuzJOfeYUrfPX2o8n74AnUVaOGDbUqVGO9fNHu48/pjJO4sNVwsOg==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.0.1.tgz", + "integrity": "sha512-CriEZlrKK9VJw/xQGJpQM5rY88BtuL8DM+AEwvcThHilbxiTAy8vq4iJnd2tqq8wLmjbGZzP7ZcKFjbGkmEFrw==", "dev": true, "requires": { "regenerate": "^1.4.2", - "regenerate-unicode-properties": "^9.0.0", - "regjsgen": "^0.5.2", - "regjsparser": "^0.7.0", + "regenerate-unicode-properties": "^10.0.1", + "regjsgen": "^0.6.0", + "regjsparser": "^0.8.2", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.0.0" } }, "regjsgen": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.5.2.tgz", - "integrity": "sha512-OFFT3MfrH90xIW8OOSyUrk6QHD5E9JOTeGodiJeBS3J6IwlgzJMNE/1bZklWz5oTg+9dCMyEetclvCVXOPoN3A==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.6.0.tgz", + "integrity": "sha512-ozE883Uigtqj3bx7OhL1KNbCzGyW2NQZPl6Hs09WTvCuZD5sTI4JY58bkbQWa/Y9hxIsvJ3M8Nbf7j54IqeZbA==", "dev": true }, "regjsparser": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.7.0.tgz", - "integrity": "sha512-A4pcaORqmNMDVwUjWoTzuhwMGpP+NykpfqAsEgI1FSH/EzC7lrN5TMd+kN8YCovX+jMpu8eaqXgXPCa0g8FQNQ==", + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.8.4.tgz", + "integrity": "sha512-J3LABycON/VNEu3abOviqGHuB/LOtOQj8SKmfP9anY5GfAVw/SPjwzSjxGjbZXIxbGfqTHtJw58C2Li/WkStmA==", "dev": true, "requires": { "jsesc": "~0.5.0" @@ -13952,9 +14395,9 @@ } }, "setprototypeof": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", - "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "dev": true }, "shebang-command": { @@ -14173,125 +14616,34 @@ } }, "socket.io": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.1.1.tgz", - "integrity": "sha512-rORqq9c+7W0DAK3cleWNSyfv/qKXV99hV4tZe+gGLfBECw3XEhBy7x85F3wypA9688LKjtwO9pX9L33/xQI8yA==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.4.1.tgz", + "integrity": "sha512-s04vrBswdQBUmuWJuuNTmXUVJhP0cVky8bBDhdkf8y0Ptsu7fKU2LuLbts9g+pdmAdyMMn8F/9Mf1/wbtUN0fg==", "dev": true, "requires": { - "debug": "~3.1.0", - "engine.io": "~3.2.0", - "has-binary2": "~1.0.2", - "socket.io-adapter": "~1.1.0", - "socket.io-client": "2.1.1", - "socket.io-parser": "~3.2.0" - }, - "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - } + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "debug": "~4.3.2", + "engine.io": "~6.1.0", + "socket.io-adapter": "~2.3.3", + "socket.io-parser": "~4.0.4" } }, "socket.io-adapter": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-1.1.2.tgz", - "integrity": "sha512-WzZRUj1kUjrTIrUKpZLEzFZ1OLj5FwLlAFQs9kuZJzJi5DKdU7FsWc36SNmA8iDOtwBQyT8FkrriRM8vXLYz8g==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.3.3.tgz", + "integrity": "sha512-Qd/iwn3VskrpNO60BeRyCyr8ZWw9CPZyitW4AQwmRZ8zCiyDiL+znRnWX6tDHXnWn1sJrM1+b6Mn6wEDJJ4aYQ==", "dev": true }, - "socket.io-client": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.1.1.tgz", - "integrity": "sha512-jxnFyhAuFxYfjqIgduQlhzqTcOEQSn+OHKVfAxWaNWa7ecP7xSNk2Dx/3UEsDcY7NcFafxvNvKPmmO7HTwTxGQ==", - "dev": true, - "requires": { - "backo2": "1.0.2", - "base64-arraybuffer": "0.1.5", - "component-bind": "1.0.0", - "component-emitter": "1.2.1", - "debug": "~3.1.0", - "engine.io-client": "~3.2.0", - "has-binary2": "~1.0.2", - "has-cors": "1.1.0", - "indexof": "0.0.1", - "object-component": "0.0.3", - "parseqs": "0.0.5", - "parseuri": "0.0.5", - "socket.io-parser": "~3.2.0", - "to-array": "0.1.4" - }, - "dependencies": { - "component-emitter": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", - "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", - "dev": true - }, - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - } - } - }, "socket.io-parser": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.2.0.tgz", - "integrity": "sha512-FYiBx7rc/KORMJlgsXysflWx/RIvtqZbyGLlHZvjfmPTPeuD/I8MaW7cfFrj5tRltICJdgwflhfZ3NVVbVLFQA==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.0.4.tgz", + "integrity": "sha512-t+b0SS+IxG7Rxzda2EVvyBZbvFPBCjJoyHuE0P//7OAsN23GItzDRdWa6ALxZI/8R5ygK7jAR6t028/z+7295g==", "dev": true, "requires": { - "component-emitter": "1.2.1", - "debug": "~3.1.0", - "isarray": "2.0.1" - }, - "dependencies": { - "component-emitter": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", - "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", - "dev": true - }, - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "isarray": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", - "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=", - "dev": true - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - } + "@types/component-emitter": "^1.2.10", + "component-emitter": "~1.3.0", + "debug": "~4.3.1" } }, "sort-keys": { @@ -14622,27 +14974,14 @@ "dev": true }, "streamroller": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-1.0.6.tgz", - "integrity": "sha512-3QC47Mhv3/aZNFpDDVO44qQb9gwB9QggMEE0sQmkTAwBVYdBRWISdsywlkfm5II1Q5y/pmrHflti/IgmIzdDBg==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.0.2.tgz", + "integrity": "sha512-ur6y5S5dopOaRXBuRIZ1u6GC5bcEXHRZKgfBjfCglMhmIf+roVCECjvkEYzNQOXIN2/JPnkMPW/8B3CZoKaEPA==", "dev": true, "requires": { - "async": "^2.6.2", - "date-format": "^2.0.0", - "debug": "^3.2.6", - "fs-extra": "^7.0.1", - "lodash": "^4.17.14" - }, - "dependencies": { - "debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - } + "date-format": "^4.0.3", + "debug": "^4.1.1", + "fs-extra": "^10.0.0" } }, "strict-uri-encode": { @@ -15073,12 +15412,6 @@ "is-negated-glob": "^1.0.0" } }, - "to-array": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz", - "integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA=", - "dev": true - }, "to-buffer": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz", @@ -15174,9 +15507,9 @@ } }, "toidentifier": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", - "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", "dev": true }, "tough-cookie": { @@ -15286,6 +15619,12 @@ "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", "dev": true }, + "ua-parser-js": { + "version": "0.7.31", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.31.tgz", + "integrity": "sha512-qLK/Xe9E2uzmYI3qLeOmI0tEOt+TBBQyUIAh4aAgU05FVYzeZrKUdkAZfBNVGRaHVgV0TDkdEngJSw/SyQchkQ==", + "dev": true + }, "uglify-js": { "version": "2.8.29", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", @@ -15341,12 +15680,6 @@ "dev": true, "optional": true }, - "ultron": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz", - "integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==", - "dev": true - }, "unbox-primitive": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz", @@ -15476,9 +15809,9 @@ } }, "universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", "dev": true }, "unpipe": { @@ -15554,9 +15887,9 @@ "dev": true }, "url-parse": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.3.tgz", - "integrity": "sha512-IIORyIQD9rvj0A4CLWsHkBBJuNqWpFQe224b6j9t/ABmquIS0qDU2pY6kl6AuOrL5OkCXHMCFNe1jBcuAggjvQ==", + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", "requires": { "querystringify": "^2.1.1", "requires-port": "^1.0.0" @@ -15585,16 +15918,6 @@ "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", "dev": true }, - "useragent": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/useragent/-/useragent-2.3.0.tgz", - "integrity": "sha512-4AoH4pxuSvHCjqLO04sU6U/uE65BYza8l/KKBS0b0hnUPWi+cQ2BpeTEwejCSx9SPV5/U03nniDTrWx5NrmKdw==", - "dev": true, - "requires": { - "lru-cache": "4.1.x", - "tmp": "0.0.x" - } - }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -15655,6 +15978,12 @@ "integrity": "sha1-HCQ6ULWVwb5Up1S/7OhWO5/42BM=", "dev": true }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", + "dev": true + }, "vendors": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/vendors/-/vendors-1.0.4.tgz", @@ -15919,12 +16248,6 @@ "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", "dev": true }, - "wordwrap": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", - "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=", - "dev": true - }, "wrap-ansi": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", @@ -16007,12 +16330,6 @@ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "dev": true }, - "xmlhttprequest-ssl": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz", - "integrity": "sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4=", - "dev": true - }, "xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", @@ -16029,7 +16346,8 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", - "dev": true + "dev": true, + "optional": true }, "yargs": { "version": "7.1.2", @@ -16117,12 +16435,6 @@ "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } - }, - "yeast": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", - "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=", - "dev": true } } } diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index ef8135487a..fbbfd95981 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -54,11 +54,10 @@ "wicg-inert": "^3.0.2" }, "devDependencies": { - "@babel/core": "7.6.4", - "@babel/plugin-proposal-object-rest-spread": "7.13.8", - "@babel/preset-env": "7.6.3", + "@babel/core": "7.17.5", + "@babel/preset-env": "7.16.11", "autoprefixer": "9.6.5", - "caniuse-lite": "^1.0.30001237", + "caniuse-lite": "^1.0.30001312", "cssnano": "4.1.10", "fs": "0.0.2", "gulp": "4.0.2", @@ -81,7 +80,7 @@ "gulp-wrap-js": "0.4.1", "jasmine-core": "3.5.0", "jsdom": "16.4.0", - "karma": "4.4.1", + "karma": "6.3.16", "karma-jasmine": "2.0.1", "karma-jsdom-launcher": "^8.0.2", "karma-junit-reporter": "2.0.1", diff --git a/src/Umbraco.Web.UI.Client/src/assets/img/application/umbraco_logomark_white.svg b/src/Umbraco.Web.UI.Client/src/assets/img/application/umbraco_logomark_white.svg index b27ae89e91..08c2264337 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/img/application/umbraco_logomark_white.svg +++ b/src/Umbraco.Web.UI.Client/src/assets/img/application/umbraco_logomark_white.svg @@ -1,3 +1,3 @@ - - - + + + diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbappheader.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbappheader.directive.js index 6cf6dd85f3..01e199c572 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbappheader.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbappheader.directive.js @@ -16,6 +16,7 @@ { value: "assets/img/application/logo@2x.png" }, { value: "assets/img/application/logo@3x.png" } ]; + scope.hideBackofficeLogo = Umbraco.Sys.ServerVariables.umbracoSettings.hideBackofficeLogo; // when a user logs out or timesout evts.push(eventsService.on("app.notAuthenticated", function () { @@ -104,15 +105,26 @@ $timeout.cancel(scope.logoModal.timer); }; scope.hideLogoModal = function() { - $timeout.cancel(scope.logoModal.timer); - scope.logoModal.timer = $timeout(function () { - scope.logoModal.show = false; - }, 100); + if(scope.logoModal.show === true) { + $timeout.cancel(scope.logoModal.timer); + scope.logoModal.timer = $timeout(function () { + scope.logoModal.show = false; + }, 100); + } }; scope.stopClickEvent = function($event) { $event.stopPropagation(); }; + scope.toggleLogoModal = function() { + if(scope.logoModal.show) { + $timeout.cancel(scope.logoModal.timer); + scope.logoModal.show = false; + } else { + scope.showLogoModal(); + } + }; + } var directive = { diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/contenttype/umbcontenttypegroup.component.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/contenttype/umbcontenttypegroup.component.js index 82112012c0..3445b4039c 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/contenttype/umbcontenttypegroup.component.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/contenttype/umbcontenttypegroup.component.js @@ -45,9 +45,9 @@ vm.onChangeSortOrderValue( {group: vm.group}); } } - function clickComposition (documentTypeId) { + function clickComposition (contentTypeId) { if (vm.onClickComposition) { - vm.onClickComposition({documentTypeId: documentTypeId}); + vm.onClickComposition({contentTypeId: contentTypeId}); } } } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/contenttype/umbcontenttypeproperty.component.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/contenttype/umbcontenttypeproperty.component.js index 8f27332ec2..c96944dbd5 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/contenttype/umbcontenttypeproperty.component.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/contenttype/umbcontenttypeproperty.component.js @@ -12,6 +12,7 @@ vm.edit = edit; vm.remove = remove; vm.changeSortOrderValue = changeSortOrderValue; + vm.clickComposition = clickComposition; function edit () { if (vm.onEdit) { @@ -31,6 +32,12 @@ } } + function clickComposition(contentTypeId) { + if (vm.onClickComposition) { + vm.onClickComposition({ contentTypeId: contentTypeId }); + } + } + } const umbContentTypePropertyComponent = { @@ -41,6 +48,7 @@ onEdit: '&', onRemove: '&', onChangeSortOrderValue: '&', + onClickComposition: '&?', valServerFieldAlias: '@', valServerFieldLabel: '@', valTabAlias: '@' diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/contenttype/umbcontenttypetab.component.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/contenttype/umbcontenttypetab.component.js index 6c0e5c8baf..5fb8297847 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/contenttype/umbcontenttypetab.component.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/contenttype/umbcontenttypetab.component.js @@ -59,9 +59,9 @@ } } - function clickComposition (documentTypeId) { + function clickComposition(contentTypeId) { if (vm.onClickComposition) { - vm.onClickComposition({documentTypeId: documentTypeId}); + vm.onClickComposition({ contentTypeId: contentTypeId}); } } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorheader.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorheader.directive.js index 75df00c596..364190cf72 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorheader.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorheader.directive.js @@ -161,12 +161,14 @@ Use this directive to construct a header inside the main editor window. name: "", navigation: [ { + "alias": "section1", "name": "Section 1", "icon": "icon-document-dashed-line", "view": "/App_Plugins/path/to/html.html", "active": true }, { + "alias": "section2", "name": "Section 2", "icon": "icon-list", "view": "/App_Plugins/path/to/html.html", diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/media/umbmedianodeinfo.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/media/umbmedianodeinfo.directive.js index c07777ca60..2a65c67a8d 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/media/umbmedianodeinfo.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/media/umbmedianodeinfo.directive.js @@ -8,22 +8,6 @@ var evts = []; scope.allowChangeMediaType = false; - scope.loading = true; - - scope.changeContentPageNumber = changeContentPageNumber; - scope.contentOptions = {}; - scope.contentOptions.entityType = "DOCUMENT"; - scope.hasContentReferences = false; - - scope.changeMediaPageNumber = changeMediaPageNumber; - scope.mediaOptions = {}; - scope.mediaOptions.entityType = "MEDIA"; - scope.hasMediaReferences = false; - - scope.changeMemberPageNumber = changeMemberPageNumber; - scope.memberOptions = {}; - scope.memberOptions.entityType = "MEMBER"; - scope.hasMemberReferences = false; function onInit() { @@ -110,45 +94,6 @@ setMediaExtension(); }); - function changeContentPageNumber(pageNumber) { - scope.contentOptions.pageNumber = pageNumber; - loadContentRelations(); - } - - function changeMediaPageNumber(pageNumber) { - scope.mediaOptions.pageNumber = pageNumber; - loadMediaRelations(); - } - - function changeMemberPageNumber(pageNumber) { - scope.memberOptions.pageNumber = pageNumber; - loadMemberRelations(); - } - - function loadContentRelations() { - return mediaResource.getPagedReferences(scope.node.id, scope.contentOptions) - .then(function (data) { - scope.contentReferences = data; - scope.hasContentReferences = data.items.length > 0; - }); - } - - function loadMediaRelations() { - return mediaResource.getPagedReferences(scope.node.id, scope.mediaOptions) - .then(data => { - scope.mediaReferences = data; - scope.hasMediaReferences = data.items.length > 0; - }); - } - - function loadMemberRelations() { - return mediaResource.getPagedReferences(scope.node.id, scope.memberOptions) - .then(data => { - scope.memberReferences = data; - scope.hasMemberReferences = data.items.length > 0; - }); - } - //ensure to unregister from all events! scope.$on('$destroy', function () { for (var e in evts) { @@ -157,18 +102,6 @@ }); onInit(); - - // load media type references when the 'info' tab is first activated/switched to - evts.push(eventsService.on("app.tabChange", function (event, args) { - $timeout(function () { - if (args.alias === "umbInfo") { - - $q.all([loadContentRelations(), loadMediaRelations(), loadMemberRelations()]).then(function () { - scope.loading = false; - }); - } - }); - })); } var directive = { diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/references/umbtrackedreferences.component.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/references/umbtrackedreferences.component.js new file mode 100644 index 0000000000..000e87146c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/references/umbtrackedreferences.component.js @@ -0,0 +1,125 @@ +(function () { + 'use strict'; + + /** + * A component to render the tracked references of an item + */ + + function umbTrackedReferencesController($q, trackedReferencesResource, localizationService) { + + var vm = this; + + vm.changeReferencesPageNumber = changeReferencesPageNumber; + vm.changeDescendantsPageNumber = changeDescendantsPageNumber; + + vm.$onInit = onInit; + + function onInit() { + + vm.referencesTitle = this.hideNoneDependencies ? "The following items depends on this" : "Referenced by the following items"; + vm.referencedDescendantsTitle = this.hideNoneDependencies ? "The following descending items has dependencies" : "The following descending items are referenced"; + + localizationService.localize(this.hideNoneDependencies ? "references_labelDependsOnThis" : "references_labelUsedByItems").then(function (value) { + vm.referencesTitle = value; + }); + + localizationService.localize(this.hideNoneDependencies ? "references_labelDependentDescendants" : "references_labelUsedDescendants").then(function (value) { + vm.referencedDescendantsTitle = value; + }); + + vm.descendantsOptions = {}; + vm.descendantsOptions.filterMustBeIsDependency = this.hideNoneDependencies; + vm.hasReferencesInDescendants = false; + + vm.referencesOptions = {}; + vm.referencesOptions.filterMustBeIsDependency = this.hideNoneDependencies; + vm.hasReferences = false; + + this.loading = true; + this.hideNoResult = this.hideNoResult || false; + + // when vm.id == 0 it means that this is a new item, so it has no references yet + if (vm.id === 0) { + vm.loading = false; + if(vm.onLoadingComplete) { + vm.onLoadingComplete(); + } + return; + } + + // Make array of promises to load: + var promises = [loadReferencesRelations()]; + + // only load descendants if we want to show them. + if (vm.showDescendants) { + promises.push(loadDescendantsUsage()); + } + + $q.all(promises).then(function () { + vm.loading = false; + if(vm.onLoadingComplete) { + vm.onLoadingComplete(); + } + }); + } + + function changeReferencesPageNumber(pageNumber) { + vm.referencesOptions.pageNumber = pageNumber; + loadReferencesRelations(); + } + + function changeDescendantsPageNumber(pageNumber) { + vm.descendantsOptions.pageNumber = pageNumber; + loadDescendantsUsage(); + } + + function loadReferencesRelations() { + return trackedReferencesResource.getPagedReferences(vm.id, vm.referencesOptions) + .then(function (data) { + vm.references = data; + + if (data.items.length > 0) { + vm.hasReferences = data.items.length > 0; + activateWarning(); + } + }); + } + + function loadDescendantsUsage() { + return trackedReferencesResource.getPagedDescendantsInReferences(vm.id, vm.descendantsOptions) + .then(function (data) { + vm.referencedDescendants = data; + + if (data.items.length > 0) { + vm.hasReferencesInDescendants = data.items.length > 0; + activateWarning(); + } + }); + } + + function activateWarning() { + if (vm.onWarning) { + vm.onWarning(); + } + } + } + + var umbTrackedReferencesComponent = { + templateUrl: 'views/components/references/umb-tracked-references.html', + transclude: true, + bindings: { + id: "<", + hideNoResult: " s.id); + + return trackedReferencesResource.getPagedReferencedItems(ids, vm.referencesOptions) + .then(function (data) { + vm.referencedItems = data; + + if (data.items.length > 0) { + vm.hasReferences = data.items.length > 0; + activateWarning(); + } + }); + } + + function activateWarning() { + if (vm.onWarning) { + vm.onWarning(); + } + } + } + + var umbTrackedReferencesBulkActionComponent = { + templateUrl: 'views/components/references/umb-tracked-references-bulk-action.html', + transclude: true, + bindings: { + selection: "<", + hideNoResult: "attribute): The size of the avatar (xs, s, m, l, xl). @param {string} img-src (attribute): The image source to the avatar. @param {string} img-srcset (atribute): Reponsive support for the image source. +@param {string=} name (attribute): Name initials will be used if no image source. +@param {string=} color (attribute): Color will be used if no image source (primary, secondary, success, warning, danger). **/ (function() { diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js index 79da9e3ac6..9a116beb5a 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js @@ -484,9 +484,9 @@ }; - scope.openDocumentType = (documentTypeId) => { + scope.openContentType = (contentTypeId) => { const editor = { - id: documentTypeId, + id: contentTypeId, submit: () => { const args = { node: scope.model }; eventsService.emit("editors.documentType.reload", args); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbimagelazyload.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbimagelazyload.directive.js index 988f8fab24..81c2cf2533 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbimagelazyload.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbimagelazyload.directive.js @@ -36,19 +36,23 @@ Use this directive to lazy-load an image only when it is scrolled into view. function ImageLazyLoadDirective() { - const placeholder = "assets/img/transparent.png"; + const placeholder = "assets/img/transparent.png"; - function link(scope, element, attrs) { + function link(scope, element, attrs) { const observer = new IntersectionObserver(loadImg); const img = element[0]; img.src = placeholder; + img.classList.add("lazy"); observer.observe(img); - function loadImg(changes) { - changes.forEach(change => { - if (change.intersectionRatio > 0 && change.target.src.indexOf(placeholder) > 0) { - change.target.src = attrs.umbImageLazyLoad; + function loadImg(entries) { + entries.forEach(entry => { + if (entry.intersectionRatio > 0 && entry.target.src.indexOf(placeholder) > 0) { + let lazyImage = entry.target; + lazyImage.src = attrs.umbImageLazyLoad; + lazyImage.classList.add("loaded"); + observer.unobserve(lazyImage); } }); } diff --git a/src/Umbraco.Web.UI.Client/src/common/filters/simpleMarkdown.filter.js b/src/Umbraco.Web.UI.Client/src/common/filters/simpleMarkdown.filter.js new file mode 100644 index 0000000000..58d5b0ed91 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/filters/simpleMarkdown.filter.js @@ -0,0 +1,20 @@ +/** +* @ngdoc filter +* @name umbraco.filters.simpleMarkdown +* @description +* Used when rendering a string as Markdown as HTML (i.e. with ng-bind-html). Allows use of **bold**, *italics*, ![images](url) and [links](url) +**/ +angular.module("umbraco.filters").filter('simpleMarkdown', function () { + return function (text) { + if (!text) { + return ''; + } + + return text + .replace(/\*\*(.*)\*\*/gim, '$1') + .replace(/\*(.*)\*/gim, '$1') + .replace(/!\[(.*?)\]\((.*?)\)/gim, "$1") + .replace(/\[(.*?)\]\((.*?)\)/gim, "$1") + .replace(/\n/g, '
').trim(); + }; +}); diff --git a/src/Umbraco.Web.UI.Client/src/common/filters/umbCmsBlockCard.filter.js b/src/Umbraco.Web.UI.Client/src/common/filters/umbCmsBlockCard.filter.js new file mode 100644 index 0000000000..b461315fc7 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/filters/umbCmsBlockCard.filter.js @@ -0,0 +1,49 @@ +/** + * @ngdoc filter + * @name umbraco.filters.filter:umbCmsBlockCard + * @namespace umbCmsBlockCard + * + * @description + * Filter block cards based on specific properties. + * + */ +angular.module("umbraco.filters").filter('umbCmsBlockCard', function () { + return function (array, searchTerm) { + // If no array is given, exit. + if (!array) { + return; + } + // If no search term exists, return the array unfiltered. + else if (!searchTerm) { + return array; + } + // Otherwise, continue. + else { + // Convert filter text to lower case. + const term = searchTerm.toLowerCase(); + + // Return the filtered array + return array.filter((block, i) => { + const props = ['id', 'key', 'udi', 'alias', 'name', 'description']; + + let found = false; + + for (let i = 0; i < props.length; i++) { + + if (!block.elementTypeModel.hasOwnProperty(props[i])) { + continue; + } + + if (block.elementTypeModel[props[i]] != null && + block.elementTypeModel[props[i]] !== '' && + block.elementTypeModel[props[i]].toString().toLowerCase().includes(term)) { + found = true; + } + } + + return found; + + }) + } + } +}); diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js index ff05656a4e..3b2618a82b 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js @@ -36,6 +36,27 @@ function entityResource($q, $http, umbRequestHelper) { //the factory object returned return { + /** + * @ngdoc method + * @name umbraco.resources.entityResource#getSafeAlias + * @methodOf umbraco.resources.entityResource + * + * @description + * Converts the given string to a safe alias + * + * ##usage + *
+         * entityResource.getSafeAlias(value, camelCase)
+         *    .then(function(safeAlias) {
+         *        Do stuff...
+         *    });
+         * 
+ * + * @param {string} value the value to convert to a safe alias + * @param {boolean} camelCase if camel casing should be used + * @returns {Promise} resourcePromise object containing the safe alias. + * + */ getSafeAlias: function (value, camelCase) { if (!value) { diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/media.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/media.resource.js index 87ba054675..d426dcd516 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/media.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/media.resource.js @@ -3,7 +3,7 @@ * @name umbraco.resources.mediaResource * @description Loads in data for media **/ -function mediaResource($q, $http, umbDataFormatter, umbRequestHelper) { +function mediaResource($q, $http, umbDataFormatter, umbRequestHelper, trackedReferencesResource) { /** internal method process the saving of data and post processing the result */ function saveMediaItem(content, action, files) { @@ -552,36 +552,41 @@ function mediaResource($q, $http, umbDataFormatter, umbRequestHelper) { 'Failed to retrieve media items for search: ' + query); }, + /** + * @ngdoc method + * @name umbraco.resources.mediaResource#getPagedReferences + * @methodOf umbraco.resources.mediaResource + * + * @description + * Gets a page list of tracked references for the current item, so you can see where an item is being used + * + * @deprecated + * Use umbraco.resources.trackedReferencesResource#getPagedReferences instead + * + * ##usage + *
+         * var options = {
+         *      pageSize : 25,
+         *      pageNumber : 1,
+         *      entityType : 'DOCUMENT'
+         *  };
+         * trackedReferencesResource.getPagedReferences(1, options)
+         *    .then(function(data) {
+         *        console.log(data);
+         *    });
+         * 
+ * + * @param {int} id Id of the item to query for tracked references + * @param {Object} args optional args object + * @param {Int} args.pageSize the pagesize of the returned list (default 25) + * @param {Int} args.pageNumber the current page index (default 1) + * @param {Int} args.entityType the type of tracked entity (default : DOCUMENT). Possible values DOCUMENT, MEDIA + * @returns {Promise} resourcePromise object. + * + */ getPagedReferences: function (id, options) { - - var defaults = { - pageSize: 25, - pageNumber: 1, - entityType: "DOCUMENT" - }; - if (options === undefined) { - options = {}; - } - //overwrite the defaults if there are any specified - Utilities.extend(defaults, options); - //now copy back to the options we will use - options = defaults; - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "mediaApiBaseUrl", - "GetPagedReferences", - { - id: id, - entityType: options.entityType, - pageNumber: options.pageNumber, - pageSize: options.pageSize - } - )), - "Failed to retrieve usages for media of id " + id); + return trackedReferencesResource.getPagedReferences(id, options); } - }; } diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/ourpackagerrepository.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/ourpackagerrepository.resource.js index f803d7edce..be13e6d0ec 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/ourpackagerrepository.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/ourpackagerrepository.resource.js @@ -36,7 +36,21 @@ function ourPackageRepositoryResource($q, $http, umbDataFormatter, umbRequestHel $http.get(baseurl + "?pageIndex=0&pageSize=" + maxResults + "&category=" + category + "&order=Popular&version=" + Umbraco.Sys.ServerVariables.application.version), 'Failed to query packages'); }, - + + getPromoted: function (maxResults, category) { + + if (maxResults === undefined) { + maxResults = 20; + } + if (category === undefined) { + category = ""; + } + + return umbRequestHelper.resourcePromise( + $http.get(baseurl + "?pageIndex=0&pageSize=" + maxResults + "&category=" + category + "&order=Popular&version=" + Umbraco.Sys.ServerVariables.application.version + "&onlyPromoted=true"), + 'Failed to query packages'); + }, + search: function (pageIndex, pageSize, orderBy, category, query, canceler) { var httpConfig = {}; diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/trackedreferences.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/trackedreferences.resource.js new file mode 100644 index 0000000000..cd64c89589 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/resources/trackedreferences.resource.js @@ -0,0 +1,178 @@ +/** + * @ngdoc service + * @name umbraco.resources.trackedReferencesResource + * @description Loads in data for tracked references + **/ +function trackedReferencesResource($q, $http, umbRequestHelper) { + + return { + + /** + * @ngdoc method + * @name umbraco.resources.trackedReferencesResource#getPagedReferences + * @methodOf umbraco.resources.trackedReferencesResource + * + * @description + * Gets a page list of tracked references for the current item, so you can see where an item is being used + * + * ##usage + *
+         * var options = {
+         *      pageSize : 25,
+         *      pageNumber : 1,
+         *      entityType : 'DOCUMENT'
+         *  };
+         * trackedReferencesResource.getPagedReferences(1, options)
+         *    .then(function(data) {
+         *        console.log(data);
+         *    });
+         * 
+ * + * @param {int} id Id of the item to query for tracked references + * @param {Object} args optional args object + * @param {Int} args.pageSize the pagesize of the returned list (default 25) + * @param {Int} args.pageNumber the current page index (default 1) + * @param {String} args.entityType the type of tracked entity (default : DOCUMENT). Possible values DOCUMENT, MEDIA + * @returns {Promise} resourcePromise object. + * + */ + getPagedReferences: function (id, args) { + + var defaults = { + pageSize: 10, + pageNumber: 1, + entityType: "DOCUMENT" + }; + if (args === undefined) { + args = {}; + } + + //overwrite the defaults if there are any specified + var options = Utilities.extend(defaults, args); + + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "trackedReferencesApiBaseUrl", + "GetPagedReferences", + { + id: id, + entityType: options.entityType, + pageNumber: options.pageNumber, + pageSize: options.pageSize, + filterMustBeIsDependency: options.filterMustBeIsDependency + } + )), + "Failed to retrieve usages for entity of id " + id); + }, + + /** + * @ngdoc method + * @name umbraco.resources.trackedReferencesResource#getPagedDescendantsInReferences + * @methodOf umbraco.resources.trackedReferencesResource + * + * @description + * Gets a page list of the child nodes of the current item used in any kind of relation + * + * ##usage + *
+         * var options = {
+         *      pageSize : 25,
+         *      pageNumber : 1,
+         *      entityType : 'DOCUMENT'
+         *  };
+         * trackedReferencesResource.getPagedDescendantsInReferences(1, options)
+         *    .then(function(data) {
+         *        console.log(data);
+         *    });
+         * 
+ * + * @param {int} id Id of the item to query for child nodes used in relation + * @param {Object} args optional args object + * @param {Int} args.pageSize the pagesize of the returned list (default 25) + * @param {Int} args.pageNumber the current page index (default 1) + * @param {String} args.entityType the type of tracked entity (default : DOCUMENT). Possible values DOCUMENT, MEDIA + * @returns {Promise} resourcePromise object. + * + */ + getPagedDescendantsInReferences: function (id, args) { + + var defaults = { + pageSize: 10, + pageNumber: 1, + entityType: "DOCUMENT" + }; + if (args === undefined) { + args = {}; + } + + //overwrite the defaults if there are any specified + var options = Utilities.extend(defaults, args); + + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "trackedReferencesApiBaseUrl", + "GetPagedDescendantsInReferences", + { + parentId: id, + entityType: options.entityType, + pageNumber: options.pageNumber, + pageSize: options.pageSize + } + )), + "Failed to retrieve usages for descendants of parent with id " + id); + }, + + /** + * @ngdoc method + * @name umbraco.resources.trackedReferencesResource#getPagedReferencedItems + * @methodOf umbraco.resources.trackedReferencesResource + * + * @description + * Checks if any of the items are used in a relation and returns a page list, so you can see which items are being used + * + * ##usage + *
+         * var ids = [123,3453,2334,2343];
+         * var options = {
+         *      pageSize : 25,
+         *      pageNumber : 1,
+         *      entityType : 'DOCUMENT'
+         *  };
+         *
+         * trackedReferencesResource.getPagedReferencedItems(ids, options)
+         *    .then(function(data) {
+         *        console.log(data);
+         *    });
+         * 
+ * + * @param {Array} ids array of the selected items ids to query for references + * @param {Object} options optional options object + * @param {Int} options.pageSize the pagesize of the returned list (default 25) + * @param {Int} options.pageNumber the current page index (default 1) + * @param {String} options.entityType the type of tracked entity (default : DOCUMENT). Possible values DOCUMENT, MEDIA + * @returns {Promise} resourcePromise object. + * + */ + getPagedReferencedItems: function (ids, options) { + var query = `entityType=${options.entityType}&pageNumber=${options.pageNumber}&pageSize=${options.pageSize}`; + + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "trackedReferencesApiBaseUrl", + "getPagedReferencedItems", + query), + { + ids: ids, + entityType: options.entityType, + pageNumber: options.pageNumber, + pageSize: options.pageSize + }), + "Failed to check for references of nodes with ids " + ids); + } + } +} + +angular.module('umbraco.resources').factory('trackedReferencesResource', trackedReferencesResource); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js b/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js index 09c1659775..24432ca261 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js @@ -30,9 +30,8 @@ for (var p = 0; p < tab.properties.length; p++) { var prop = tab.properties[p]; - if (dataModel[prop.alias]) { - prop.value = dataModel[prop.alias]; - } + + prop.value = dataModel[prop.alias]; } } @@ -53,9 +52,8 @@ for (var p = 0; p < tab.properties.length; p++) { var prop = tab.properties[p]; - if (prop.value) { - dataModel[prop.alias] = prop.value; - } + + dataModel[prop.alias] = prop.value; } } diff --git a/src/Umbraco.Web.UI.Client/src/common/services/mediapreview.service.js b/src/Umbraco.Web.UI.Client/src/common/services/mediapreview.service.js index b922e07c9c..5e5aeed872 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/mediapreview.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/mediapreview.service.js @@ -1,4 +1,4 @@ -/** +/** * @ngdoc service * @name umbraco.services.mediaPreview * @description A service providing views used for dealing with previewing files. @@ -56,12 +56,15 @@ function mediaPreview() { */ getMediaPreview: function (fileExtension) { - fileExtension = fileExtension.toLowerCase(); + if (fileExtension) + { + fileExtension = fileExtension.toLowerCase(); - var previewObject = _mediaPreviews.find((preview) => preview.fileExtensions.indexOf(fileExtension) !== -1); + var previewObject = _mediaPreviews.find(preview => preview.fileExtensions.indexOf(fileExtension) !== -1); - if(previewObject !== undefined) { + if (previewObject !== undefined) { return previewObject.view; + } } return DEFAULT_FILE_PREVIEW; diff --git a/src/Umbraco.Web.UI.Client/src/common/services/navigation.service.js b/src/Umbraco.Web.UI.Client/src/common/services/navigation.service.js index a932888aa4..81d1818448 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/navigation.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/navigation.service.js @@ -259,8 +259,8 @@ function navigationService($routeParams, $location, $q, $injector, eventsService var updated = false; retainedQueryStrings.forEach(r => { - // if mculture is set to null in nextRouteParams, the value will be undefined and we will not retain any query string that has a value of "null" - if (currRouteParams[r] && nextRouteParams[r] !== undefined && !nextRouteParams[r]) { + // testing explicitly for undefined in nextRouteParams here, as it must be possible to "unset" e.g. mculture by specifying a null value + if (currRouteParams[r] && nextRouteParams[r] === undefined) { toRetain[r] = currRouteParams[r]; updated = true; } diff --git a/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js b/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js index e6eb430201..98543a7c68 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js @@ -11,7 +11,7 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s //These are absolutely required in order for the macros to render inline //we put these as extended elements because they get merged on top of the normal allowed elements by tiny mce - var extendedValidElements = "@[id|class|style],-div[id|dir|class|align|style],ins[datetime|cite],-ul[class|style],-li[class|style],-h1[id|dir|class|align|style],-h2[id|dir|class|align|style],-h3[id|dir|class|align|style],-h4[id|dir|class|align|style],-h5[id|dir|class|align|style],-h6[id|style|dir|class|align],span[id|class|style|lang]"; + var extendedValidElements = "@[id|class|style],-div[id|dir|class|align|style],ins[datetime|cite],-ul[class|style],-li[class|style],-h1[id|dir|class|align|style],-h2[id|dir|class|align|style],-h3[id|dir|class|align|style],-h4[id|dir|class|align|style],-h5[id|dir|class|align|style],-h6[id|style|dir|class|align],span[id|class|style|lang],figure,figcaption"; var fallbackStyles = [{ title: "Page header", block: "h2" }, { title: "Section header", block: "h3" }, { title: "Paragraph header", block: "h4" }, { title: "Normal", block: "p" }, { title: "Quote", block: "blockquote" }, { title: "Code", block: "code" }]; // these languages are available for localization var availableLanguages = [ @@ -292,7 +292,6 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s } function sizeImageInEditor(editor, imageDomElement, imgUrl) { - var size = editor.dom.getSize(imageDomElement); if (editor.settings.maxImageSize && editor.settings.maxImageSize !== 0) { @@ -656,21 +655,19 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s stateSelector: 'img[data-udi]', onclick: function () { - - var selectedElm = editor.selection.getNode(), - currentTarget, - imgDomElement; + var selectedElm = editor.selection.getNode(), + currentTarget; if (selectedElm.nodeName === 'IMG') { var img = $(selectedElm); - imgDomElement = selectedElm; var hasUdi = img.attr("data-udi") ? true : false; var hasDataTmpImg = img.attr("data-tmpimg") ? true : false; currentTarget = { altText: img.attr("alt"), - url: img.attr("src") + url: img.attr("src"), + caption: img.attr('data-caption') }; if (hasUdi) { @@ -687,85 +684,80 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s userService.getCurrentUser().then(function (userData) { if (callback) { angularHelper.safeApply($rootScope, function() { - callback(currentTarget, userData, imgDomElement); + callback(currentTarget, userData); }); } }); } }); }, - - insertMediaInEditor: function (editor, img, imgDomElement) { + /** + * @ngdoc method + * @name umbraco.services.tinyMceService#insetMediaInEditor + * @methodOf umbraco.services.tinyMceService + * + * @description + * Inserts the image element in tinymce plugin + * + * @param {Object} editor the TinyMCE editor instance + */ + insertMediaInEditor: function (editor, img) { if (img) { - // imgElement is only definied if updating an image - // if null/undefinied then its a BRAND new image - if(imgDomElement){ - // Check if the img src has changed - // If it has we will need to do some resizing/recalc again - var hasImageSrcChanged = false; - - if(img.url !== editor.dom.getAttrib(imgDomElement, "src")){ - hasImageSrcChanged = true; + // We need to create a NEW DOM element to insert + // setting an attribute of ID to __mcenew, so we can gather a reference to the node, to be able to update its size accordingly to the size of the image. + var data = { + alt: img.altText || "", + src: (img.url) ? img.url : "nothing.jpg", + id: "__mcenew", + "data-udi": img.udi, + "data-caption": img.caption + }; + var newImage = editor.dom.createHTML('img', data); + var parentElement = editor.selection.getNode().parentElement; + + if (img.caption) { + var figCaption = editor.dom.createHTML('figcaption', {}, img.caption); + var combined = newImage + figCaption; + + if (parentElement.nodeName !== 'FIGURE') { + var fragment = editor.dom.createHTML('figure', {}, combined); + editor.selection.setContent(fragment); } - - // If null/undefinied it will remove the attribute - editor.dom.setAttrib(imgDomElement, "alt", img.altText); - - // It's possible to pick a NEW image - so need to ensure this gets updated - if(img.udi){ - editor.dom.setAttrib(imgDomElement, "data-udi", img.udi); + else { + parentElement.innerHTML = combined; } - - // It's possible to pick a NEW image - so need to ensure this gets updated - if(img.url){ - editor.dom.setAttrib(imgDomElement, "src", img.url); - } - - // Remove width & height attributes (ONLY if imgSrc changed) - // So native image size is used as this needed to re-calc width & height - // For the function sizeImageInEditor() & apply the image resizing querystrings etc.. - if(hasImageSrcChanged){ - editor.dom.setAttrib(imgDomElement, "width", null); - editor.dom.setAttrib(imgDomElement, "height", null); - - //Re-calc the image dimensions - sizeImageInEditor(editor, imgDomElement, img.url); - } - - } else{ - // We need to create a NEW DOM element to insert - // setting an attribute of ID to __mcenew, so we can gather a reference to the node, to be able to update its size accordingly to the size of the image. - var data = { - alt: img.altText || "", - src: (img.url) ? img.url : "nothing.jpg", - id: "__mcenew", - "data-udi": img.udi - }; - - editor.selection.setContent(editor.dom.createHTML('img', data)); - - // Using settimeout to wait for a DoM-render, so we can find the new element by ID. - $timeout(function () { - - var imgElm = editor.dom.get("__mcenew"); - editor.dom.setAttrib(imgElm, "id", null); - - // When image is loaded we are ready to call sizeImageInEditor. - var onImageLoaded = function() { - sizeImageInEditor(editor, imgElm, img.url); - editor.fire("Change"); - } - - // Check if image already is loaded. - if(imgElm.complete === true) { - onImageLoaded(); - } else { - imgElm.onload = onImageLoaded; - } - - }); - } + else { + //if caption is removed, remove the figure element + if (parentElement.nodeName === 'FIGURE') { + parentElement.parentElement.innerHTML = newImage; + } + else { + editor.selection.setContent(newImage); + } + } + + // Using settimeout to wait for a DoM-render, so we can find the new element by ID. + $timeout(function () { + + var imgElm = editor.dom.get("__mcenew"); + editor.dom.setAttrib(imgElm, "id", null); + + // When image is loaded we are ready to call sizeImageInEditor. + var onImageLoaded = function() { + sizeImageInEditor(editor, imgElm, img.url); + editor.fire("Change"); + } + + // Check if image already is loaded. + if(imgElm.complete === true) { + onImageLoaded(); + } else { + imgElm.onload = onImageLoaded; + } + + }); + } }, diff --git a/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js b/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js index 7e4d7eaa4a..38b56b7661 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js @@ -154,7 +154,7 @@ formatUserPostData: function (displayModel) { //create the save model from the display model - var saveModel = _.pick(displayModel, 'id', 'parentId', 'name', 'username', 'culture', 'email', 'startContentIds', 'startMediaIds', 'userGroups', 'message'); + var saveModel = _.pick(displayModel, 'id', 'parentId', 'name', 'username', 'culture', 'email', 'startContentIds', 'startMediaIds', 'userGroups', 'message', 'key'); //make sure the userGroups are just a string array var currGroups = saveModel.userGroups; @@ -461,6 +461,7 @@ alias: relationType.alias, key: relationType.key, isBidirectional: relationType.isBidirectional, + isDependency: relationType.isDependency, parentObjectType: relationType.parentObjectType, childObjectType: relationType.childObjectType }; diff --git a/src/Umbraco.Web.UI.Client/src/init.js b/src/Umbraco.Web.UI.Client/src/init.js index b31ea0e699..92ed2c4944 100644 --- a/src/Umbraco.Web.UI.Client/src/init.js +++ b/src/Umbraco.Web.UI.Client/src/init.js @@ -1,6 +1,6 @@ /** Executed when the application starts, binds to events and set global state */ -app.run(['$rootScope', '$route', '$location', 'urlHelper', 'navigationService', 'appState', 'assetsService', 'eventsService', '$cookies', 'tourService', 'localStorageService', - function ($rootScope, $route, $location, urlHelper, navigationService, appState, assetsService, eventsService, $cookies, tourService, localStorageService) { +app.run(['$rootScope', '$route', '$location', '$cookies', 'urlHelper', 'appState', 'assetsService', 'eventsService', 'tourService', 'localStorageService', 'navigationService', 'localizationService', + function ($rootScope, $route, $location, $cookies, urlHelper, appState, assetsService, eventsService, tourService, localStorageService, navigationService, localizationService) { //This sets the default jquery ajax headers to include our csrf token, we // need to user the beforeSend method because our token changes per user/login so @@ -33,9 +33,7 @@ app.run(['$rootScope', '$route', '$location', 'urlHelper', 'navigationService', if (introTour && introTour.disabled !== true && introTour.completed !== true) { tourService.startTour(introTour); localStorageService.set("introTourShown", true); - } - else { - + } else { const introTourShown = localStorageService.get("introTourShown"); if (!introTourShown) { // Go & show email marketing tour (ONLY when intro tour is completed or been dismissed) @@ -44,7 +42,6 @@ app.run(['$rootScope', '$route', '$location', 'urlHelper', 'navigationService', // Unless invoked from tourService JS Client code explicitly. // Accepted mails = Completed and Declicned mails = Disabled if (emailMarketingTour && emailMarketingTour.disabled !== true && emailMarketingTour.completed !== true) { - // Only show the email tour once per logged in session // The localstorage key is removed on logout or user session timeout const emailMarketingTourShown = localStorageService.get("emailMarketingTourShown"); @@ -71,31 +68,24 @@ app.run(['$rootScope', '$route', '$location', 'urlHelper', 'navigationService', var currentRouteParams = null; - var originalTitle = ""; - $rootScope.$on('$changeTitle', function (event, titlePrefix) { if (titlePrefix) { - $rootScope.locationTitle = titlePrefix + " - " + originalTitle; - } else { - $rootScope.locationTitle = originalTitle; + $rootScope.locationTitle = titlePrefix + " - " + $rootScope.locationTitle; } }); /** execute code on each successful route */ $rootScope.$on('$routeChangeSuccess', function (event, current, previous) { - var toRetain = currentRouteParams ? navigationService.retainQueryStrings(currentRouteParams, current.params) : null; //if toRetain is not null it means that there are missing query strings and we need to update the current params if (toRetain) { $route.updateParams(toRetain); currentRouteParams = toRetain; - } - else { + } else { currentRouteParams = Utilities.copy(current.params); } - var deployConfig = Umbraco.Sys.ServerVariables.deploy; var deployEnv, deployEnvTitle; if (deployConfig) { @@ -104,51 +94,39 @@ app.run(['$rootScope', '$route', '$location', 'urlHelper', 'navigationService', } if (current.params.section) { - - //Uppercase the current section, content, media, settings, developer, forms - var currentSection = current.params.section.charAt(0).toUpperCase() + current.params.section.slice(1); - - var baseTitle = currentSection + " - " + $location.$$host; - - //Check deploy for Global Umbraco.Sys obj workspace - if (deployEnv) { - $rootScope.locationTitle = deployEnvTitle + baseTitle; - } - else { - $rootScope.locationTitle = baseTitle; - } - - } - else { - + localizationService.localize("sections_" + current.params.section) + .then(function (currentSection) { + var baseTitle = currentSection + " - " + $location.$$host; + //Check deploy for Global Umbraco.Sys obj workspace + if (deployEnv) { + $rootScope.locationTitle = deployEnvTitle + baseTitle; + } else { + $rootScope.locationTitle = baseTitle; + } + }); + } else { if (deployEnv) { $rootScope.locationTitle = deployEnvTitle + "Umbraco - " + $location.$$host; } - $rootScope.locationTitle = "Umbraco - " + $location.$$host; } - originalTitle = $rootScope.locationTitle; }); /** When the route change is rejected - based on checkAuth - we'll prevent the rejected route from executing including wiring up it's controller, etc... and then redirect to the rejected URL. */ $rootScope.$on('$routeChangeError', function (event, current, previous, rejection) { - if (rejection.path) { event.preventDefault(); - var returnPath = null; if (rejection.path == "/login" || rejection.path.startsWith("/login/")) { //Set the current path before redirecting so we know where to redirect back to returnPath = encodeURIComponent($location.url()); } - $location.path(rejection.path) if (returnPath) { $location.search("returnPath", returnPath); } } - }); //Bind to $routeUpdate which will execute anytime a location changes but the route is not triggered. @@ -156,45 +134,32 @@ app.run(['$rootScope', '$route', '$location', 'urlHelper', 'navigationService', //global state query strings without force re-loading views. //We can then detect if it's a location change that should force a route or not programatically. $rootScope.$on('$routeUpdate', function (event, next) { - if (!currentRouteParams) { //if there is no current route then always route which is done with reload $route.reload(); - } - else { - + } else { var toRetain = navigationService.retainQueryStrings(currentRouteParams, next.params); - //if toRetain is not null it means that there are missing query strings and we need to update the current params. if (toRetain) { $route.updateParams(toRetain); } - //check if the location being changed is only due to global/state query strings which means the location change //isn't actually going to cause a route change. if (navigationService.isRouteChangingNavigation(currentRouteParams, next.params)) { - //The location change will cause a route change, continue the route if the query strings haven't been updated. $route.reload(); - - } - else { - + } else { //navigation is not changing but we should update the currentRouteParams to include all current parameters - if (toRetain) { currentRouteParams = toRetain; - } - else { + } else { currentRouteParams = Utilities.copy(next.params); } - //always clear the 'sr' query string (soft redirect) if it exists if (currentRouteParams.sr) { currentRouteParams.sr = null; $route.updateParams(currentRouteParams); } - } } }); @@ -202,6 +167,7 @@ app.run(['$rootScope', '$route', '$location', 'urlHelper', 'navigationService', //check for touch device, add to global appState //var touchDevice = ("ontouchstart" in window || window.touch || window.navigator.msMaxTouchPoints === 5 || window.DocumentTouch && document instanceof DocumentTouch); var touchDevice = /android|webos|iphone|ipad|ipod|blackberry|iemobile|touch/i.test(navigator.userAgent.toLowerCase()); + appState.setGlobalState("touchDevice", touchDevice); }]); diff --git a/src/Umbraco.Web.UI.Client/src/installer/steps/database.controller.js b/src/Umbraco.Web.UI.Client/src/installer/steps/database.controller.js index d236d45568..33c0ffca12 100644 --- a/src/Umbraco.Web.UI.Client/src/installer/steps/database.controller.js +++ b/src/Umbraco.Web.UI.Client/src/installer/steps/database.controller.js @@ -1,38 +1,57 @@ angular.module("umbraco.install").controller("Umbraco.Installer.DataBaseController", function($scope, $http, installerService){ - $scope.checking = false; - $scope.invalidDbDns = false; + $scope.checking = false; + $scope.invalidDbDns = false; - $scope.dbs = $scope.installer.current.model.databases; + $scope.dbs = $scope.installer.current.model.databases; + window.dbs = $scope.dbs; + + $scope.providerNames = _.chain(dbs) + .map('providerName') + .filter(x => x) + .uniq() + .value(); + + if (!$scope.selectedDbMeta) { + $scope.selectedDbMeta = $scope.dbs[0]; + } + + $scope.$watch('selectedDbMeta', function(newValue, oldValue) { + $scope.installer.current.model.integratedAuth = false; + $scope.installer.current.model.databaseProviderMetadataId = newValue.id; + $scope.installer.current.model.providerName = newValue.providerName; + $scope.installer.current.model.databaseName = newValue.defaultDatabaseName; + }); + + $scope.isCustom = function() { + return $scope.selectedDbMeta.displayName === 'Custom'; + } - if (angular.isUndefined(installerService.status.current.model.dbType) || installerService.status.current.model.dbType === null) { - installerService.status.current.model.dbType = $scope.dbs[0].id; - } $scope.validateAndForward = function() { if (!$scope.checking && this.myForm.$valid) { - $scope.checking = true; - $scope.invalidDbDns = false; + $scope.checking = true; + $scope.invalidDbDns = false; - var model = installerService.status.current.model; + var model = installerService.status.current.model; - $http.post( - Umbraco.Sys.ServerVariables.installApiBaseUrl + "PostValidateDatabaseConnection", - model).then(function(response) { + $http.post( + Umbraco.Sys.ServerVariables.installApiBaseUrl + "PostValidateDatabaseConnection", + model).then(function(response) { - if (response.data === true) { - installerService.forward(); - } - else { - $scope.invalidDbDns = true; - } + if (response.data === true) { + installerService.forward(); + } + else { + $scope.invalidDbDns = true; + } - $scope.checking = false; - }, function(){ - $scope.invalidDbDns = true; - $scope.checking = false; - }); - } - }; + $scope.checking = false; + }, function(){ + $scope.invalidDbDns = true; + $scope.checking = false; + }); + } + }; }); diff --git a/src/Umbraco.Web.UI.Client/src/installer/steps/database.html b/src/Umbraco.Web.UI.Client/src/installer/steps/database.html index cf367b2ff2..dc3b972718 100644 --- a/src/Umbraco.Web.UI.Client/src/installer/steps/database.html +++ b/src/Umbraco.Web.UI.Client/src/installer/steps/database.html @@ -11,22 +11,14 @@
-
-

Great! No need to configure anything, you can simply click the continue button below to continue to the next step

-
- -
-

Great! No need to configure anything, you can simply click the continue button below to continue to the next step

-
- -
+
What is the exact connection string we should use?
@@ -39,20 +31,36 @@ Enter a valid database connection string.
+ +
+ +
+ +
+
-
+ + +
Where do we find your database? -
-
- -
- - Enter server domain or IP + +
+
+
+ +
+ + Enter server domain or IP +
@@ -71,49 +79,52 @@
-
- What credentials are used to access the database? -
-
- -
- - Enter the database user name +
+
+ What credentials are used to access the database? +
+
+ +
+ + Enter the database user name +
-
-
-
- -
- - Enter the database password +
+
+ +
+ + Enter the database password +
-
-
-
- +
+
+ +
-
+
+
- diff --git a/src/Umbraco.Web.UI.Client/src/installer/steps/user.controller.js b/src/Umbraco.Web.UI.Client/src/installer/steps/user.controller.js index 896a7a54f5..b03282c62e 100644 --- a/src/Umbraco.Web.UI.Client/src/installer/steps/user.controller.js +++ b/src/Umbraco.Web.UI.Client/src/installer/steps/user.controller.js @@ -1,16 +1,16 @@ angular.module("umbraco.install").controller("Umbraco.Install.UserController", function($scope, installerService) { - + $scope.majorVersion = Umbraco.Sys.ServerVariables.application.version; $scope.passwordPattern = /.*/; $scope.installer.current.model.subscribeToNewsLetter = false; - + if ($scope.installer.current.model.minNonAlphaNumericLength > 0) { var exp = ""; for (var i = 0; i < $scope.installer.current.model.minNonAlphaNumericLength; i++) { exp += ".*[\\W].*"; } //replace duplicates - exp = exp.replace(".*.*", ".*"); + exp = exp.replace(".*.*", ".*"); $scope.passwordPattern = new RegExp(exp); } @@ -23,5 +23,5 @@ angular.module("umbraco.install").controller("Umbraco.Install.UserController", f installerService.forward(); } }; - + }); diff --git a/src/Umbraco.Web.UI.Client/src/installer/steps/user.html b/src/Umbraco.Web.UI.Client/src/installer/steps/user.html index e314a16319..44ee16c840 100644 --- a/src/Umbraco.Web.UI.Client/src/installer/steps/user.html +++ b/src/Umbraco.Web.UI.Client/src/installer/steps/user.html @@ -1,73 +1,85 @@
-

Install Umbraco

- -

Enter your name, email and password to install Umbraco with its default settings, alternatively you can customize your installation

- -
- -
-
-
-
- -
- -
-
- -
- -
- - Your email will be used as your login -
-
- -
- -
- - - At least {{installer.current.model.minCharLength}} characters long - - - At least {{installer.current.model.minNonAlphaNumericLength}} symbol{{installer.current.model.minNonAlphaNumericLength > 1 ? 's' : ''}} - -
-
- - -
-
- -
-
- -
-
- - - - -
-
- -
+

Install Umbraco

+

Enter your name, email and password for this Umbraco installation.

+ +
+
+
+
+ +
+
+
+
+ +
+ + Your email will be used as your login +
+
+
+ +
+ + + At least {{installer.current.model.minCharLength}} characters long + + At least {{installer.current.model.minNonAlphaNumericLength}} symbol{{installer.current.model.minNonAlphaNumericLength > 1 ? 's' : ''}} + +
+
+
+
+ +
+
- - +
+
+
+ Database Configuration +
+
+
+
+ +
+
{{installer.current.model.quickInstallSettings.displayName}}
+
+
+
+ +
+
{{installer.current.model.quickInstallSettings.defaultDatabaseName}}
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+ + +
+
+
+
+
+
diff --git a/src/Umbraco.Web.UI.Client/src/less/components/application/umb-app-header.less b/src/Umbraco.Web.UI.Client/src/less/components/application/umb-app-header.less index bb346fc402..6e1fa29eab 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/application/umb-app-header.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/application/umb-app-header.less @@ -9,11 +9,18 @@ .umb-app-header__logo { margin-right: 30px; + flex-shrink: 0; button { img { height: 30px; } } + +} +@media (max-width: 1279px) { + .umb-app-header__logo { + display: none; + } } .umb-app-header__logo-modal { diff --git a/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-editor.less b/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-editor.less index 274f74bbac..85dbb05a1b 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-editor.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-editor.less @@ -69,7 +69,8 @@ // and also use a loop to build editor sizes - easily extended with new sizes by adding to the map @editorSizes: small 500px, - medium 800px; + medium 800px, + large 1600px; .create-editor-sizes(@iterator:1) when(@iterator <= length(@editorSizes)) { .umb-editor { @@ -92,6 +93,11 @@ .create-editor-sizes(); +.umb-editor--large { + max-width: 1600px; + width: calc(100% - 50px); +} + .umb-editor__overlay { .absolute(); background: rgba(0,0,0,0.4); diff --git a/src/Umbraco.Web.UI.Client/src/less/components/notifications/umb-notifications.less b/src/Umbraco.Web.UI.Client/src/less/components/notifications/umb-notifications.less index 050f907acc..07eb8bfc18 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/notifications/umb-notifications.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/notifications/umb-notifications.less @@ -1,5 +1,5 @@ .umb-notifications { - z-index: 999999; + z-index: @zindexNotification; position: absolute; bottom: @editorFooterHeight; left: 0; @@ -22,7 +22,7 @@ font-size: 14px; border: none; position: relative; - border-radius: 10px; + border-radius: 3px; margin: 10px; .close { diff --git a/src/Umbraco.Web.UI.Client/src/less/components/overlays.less b/src/Umbraco.Web.UI.Client/src/less/components/overlays.less index 12cce286d6..60020066c0 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/overlays.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/overlays.less @@ -10,6 +10,10 @@ .scoped-view{ display: none; } + + .abstract { + margin-bottom : 20px; + } } .umb-overlay__form { @@ -51,7 +55,7 @@ } .umb-overlay__title { - font-size: 16px; + font-size: 20px; color: @black; line-height: 16px; font-weight: bold; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree.less b/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree.less index a39a38fbde..1f61b7cfc2 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree.less @@ -252,6 +252,7 @@ body.touch .umb-tree { // Tree item states // ------------------------- .not-published { + > .umb-tree-item__inner > .umb-icon, > .umb-tree-item__inner > i.icon, > .umb-tree-item__inner > a { opacity: 0.6; @@ -259,6 +260,7 @@ body.touch .umb-tree { } .not-allowed { + > .umb-tree-item__inner > .umb-icon, > .umb-tree-item__inner > i.icon, > .umb-tree-item__inner > a { cursor: not-allowed; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-group-builder.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-group-builder.less index 1122558b05..486c0f1336 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-group-builder.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-group-builder.less @@ -458,7 +458,7 @@ input.umb-group-builder__group-title-input:disabled:hover { } .umb-group-builder__group-inherited-label { - font-size: 13px; + font-size: 0.9rem; display: inline-flex; align-items: center; margin-right: 10px; @@ -644,7 +644,7 @@ input.umb-group-builder__group-title-input:disabled:hover { } .umb-group-builder__property-preview-label { - font-size: 12px; + font-size: 0.75rem; position: absolute; top: 0; left: 0; @@ -714,7 +714,7 @@ input.umb-group-builder__group-title-input:disabled:hover { } .umb-group-builder__property-tags.-right { - right: 0; + right: 4px; left: auto; } @@ -740,6 +740,7 @@ input.umb-group-builder__group-title-input:disabled:hover { margin-right: 3px; display: flex; align-items: center; + line-height: 1; } /* ---------- SORTABLE ---------- */ diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-package-local-install.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-package-local-install.less index ead54ac49f..43f3c5e353 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-package-local-install.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-package-local-install.less @@ -9,44 +9,51 @@ color: @gray-5; } -.umb-upload-local__dropzone { - position: relative; - width: 500px; - height: 300px; - border: 2px dashed @ui-action-border; - border-radius: 3px; - background: @white; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - margin-bottom: 30px; - transition: 100ms box-shadow ease, 100ms border ease; +.umb-upload-local { - &.drag-over { - border-color: @ui-action-border-hover; - border-style: solid; - box-shadow: 0 3px 8px rgba(0,0,0, .1); + &__dropzone { + position: relative; + width: 500px; + height: 300px; + border: 2px dashed @ui-action-border; + border-radius: 3px; + background: @white; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + margin-bottom: 30px; transition: 100ms box-shadow ease, 100ms border ease; + + &.drag-over { + border-color: @ui-action-border-hover; + border-style: solid; + box-shadow: 0 3px 8px rgba(0,0,0, .1); + transition: 100ms box-shadow ease, 100ms border ease; + } + + .umb-icon { + display: block; + color: @ui-action-type; + font-size: 6.75rem; + line-height: 1; + margin: 0 auto; + } + + .umb-info-local-item { + margin: 20px; + } } - .umb-icon { - display: block; + &__select-file { + font-weight: bold; color: @ui-action-type; - font-size: 6.75rem; - line-height: 1; - margin: 0 auto; - } -} + cursor: pointer; -.umb-upload-local__select-file { - font-weight: bold; - color: @ui-action-type; - cursor: pointer; - - &:hover { - text-decoration: underline; - color: @ui-action-type-hover; + &:hover { + text-decoration: underline; + color: @ui-action-type-hover; + } } } @@ -117,7 +124,3 @@ .umb-info-local-item { margin-bottom: 20px; } - -.umb-upload-local__dropzone .umb-info-local-item { - margin:20px; -} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-table.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-table.less index 6c1e5058d2..73ef47133c 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-table.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-table.less @@ -162,6 +162,7 @@ input.umb-table__input { font-size: 14px; font-weight: bold; text-decoration: none; + cursor: pointer; &:hover { color: @ui-option-type-hover; diff --git a/src/Umbraco.Web.UI.Client/src/less/installer.less b/src/Umbraco.Web.UI.Client/src/less/installer.less index 9ce519186a..46c15fcc89 100644 --- a/src/Umbraco.Web.UI.Client/src/less/installer.less +++ b/src/Umbraco.Web.UI.Client/src/less/installer.less @@ -53,17 +53,17 @@ body { opacity: 0.8; z-index: 777; } - #installer { margin: auto; background: @white; width: 80%; max-width: 750px; - height: 600px; + height: 640px; text-align: left; padding: 30px; - overflow: hidden; z-index: 667; + border-radius: 6px; + box-shadow: 0 2px 4px rgba(0,0,0,0.16); } #overlay { @@ -159,6 +159,14 @@ input.ng-dirty.ng-invalid { } } +#installer .input-readonly-text { + padding: 4px 6px; +} + +#installer legend { + clear: both; +} + .absolute-center { margin: auto; position: absolute; diff --git a/src/Umbraco.Web.UI.Client/src/less/main.less b/src/Umbraco.Web.UI.Client/src/less/main.less index 43911fccb1..d46a1751b2 100644 --- a/src/Umbraco.Web.UI.Client/src/less/main.less +++ b/src/Umbraco.Web.UI.Client/src/less/main.less @@ -14,6 +14,20 @@ box-shadow: 3px 0px 7px rgba(0,0,0,0.16); } +img:not([src]):not([srcset]) { + visibility: hidden; +} + +img.lazy { + -webkit-transition: opacity 1.2s cubic-bezier(0.16, 1.08, 0.38, 0.98); + transition: opacity 1.2s cubic-bezier(0.16, 1.08, 0.38, 0.98); + opacity: 0; +} + +img.lazy.loaded { + opacity: 1; +} + .umb-scrollable, .umb-auto-overflow { overflow: auto; } diff --git a/src/Umbraco.Web.UI.Client/src/less/pages/login.less b/src/Umbraco.Web.UI.Client/src/less/pages/login.less index 015c291564..66fe2e0ae4 100644 --- a/src/Umbraco.Web.UI.Client/src/less/pages/login.less +++ b/src/Umbraco.Web.UI.Client/src/less/pages/login.less @@ -31,6 +31,7 @@ top: 12.5px; left: 20px; right: 25px; + min-width: 112px; height: 30px; z-index: 1; } diff --git a/src/Umbraco.Web.UI.Client/src/less/rte.less b/src/Umbraco.Web.UI.Client/src/less/rte.less index 0eab4b346b..f9406c72b8 100644 --- a/src/Umbraco.Web.UI.Client/src/less/rte.less +++ b/src/Umbraco.Web.UI.Client/src/less/rte.less @@ -38,6 +38,10 @@ .umb-rte .mce-top-part { position: sticky; top: 0; + + umb-editor-tab-bar ~ div & { + top: 50px; + } } /* make sure the menu wraps */ diff --git a/src/Umbraco.Web.UI.Client/src/less/variables.less b/src/Umbraco.Web.UI.Client/src/less/variables.less index 6877938574..52085b59ab 100644 --- a/src/Umbraco.Web.UI.Client/src/less/variables.less +++ b/src/Umbraco.Web.UI.Client/src/less/variables.less @@ -362,6 +362,8 @@ @zindexUmbOverlay: 7500; @zindexOverlayBackdrop: 2000; +@zindexNotification: 8000; + // these are used for the tour which should be on top of everything else @zindexTourBackdrop: 9999; @zindexTourModal: 10000; diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.html index 273599343d..8ffb0f42a1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.html @@ -27,17 +27,23 @@
-
+
+ + + Sorry, we can not find what you are looking for. + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediaentryeditor/mediaentryeditor.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediaentryeditor/mediaentryeditor.controller.js index eea8e87034..732127ffa0 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediaentryeditor/mediaentryeditor.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediaentryeditor/mediaentryeditor.controller.js @@ -3,25 +3,35 @@ angular.module("umbraco") function ($scope, localizationService, entityResource, editorService, overlayService, eventsService, mediaHelper) { var unsubscribe = []; - var vm = this; - + + const vm = this; + vm.loading = true; vm.model = $scope.model; vm.mediaEntry = vm.model.mediaEntry; vm.currentCrop = null; - - localizationService.localizeMany([ - vm.model.createFlow ? "general_cancel" : "general_close", - vm.model.createFlow ? "general_create" : "buttons_submitChanges" - ]).then(function (data) { - vm.closeLabel = data[0]; - vm.submitLabel = data[1]; - }); - vm.title = ""; + + vm.focalPointChanged = focalPointChanged; + vm.onImageLoaded = onImageLoaded; + vm.openMedia = openMedia; + vm.repickMedia = repickMedia; + vm.selectCrop = selectCrop; + vm.deselectCrop = deselectCrop; + vm.resetCrop = resetCrop; + vm.submitAndClose = submitAndClose; + vm.close = close; function init() { + localizationService.localizeMany([ + vm.model.createFlow ? "general_cancel" : "general_close", + vm.model.createFlow ? "general_create" : "buttons_submitChanges" + ]).then(data => { + vm.closeLabel = data[0]; + vm.submitLabel = data[1]; + }); + updateMedia(); unsubscribe.push(eventsService.on("editors.media.saved", function(name, args) { @@ -35,6 +45,7 @@ angular.module("umbraco") function updateMedia() { vm.loading = true; + entityResource.getById(vm.mediaEntry.mediaKey, "Media").then(function (mediaEntity) { vm.media = mediaEntity; vm.imageSrc = mediaHelper.resolveFileFromEntity(mediaEntity, true); @@ -44,11 +55,11 @@ angular.module("umbraco") vm.hasDimensions = false; vm.isCroppable = false; - localizationService.localize("mediaPicker_editMediaEntryLabel", [vm.media.name, vm.model.documentName]).then(function (data) { + localizationService.localize("mediaPicker_editMediaEntryLabel", [vm.media.name, vm.model.documentName]).then(data => { vm.title = data; }); }, function () { - localizationService.localize("mediaPicker_deletedItem").then(function (localized) { + localizationService.localize("mediaPicker_deletedItem").then(localized => { vm.media = { name: localized, icon: "icon-picture", @@ -60,15 +71,12 @@ angular.module("umbraco") }); }); } - - vm.onImageLoaded = onImageLoaded; + function onImageLoaded(isCroppable, hasDimensions) { vm.isCroppable = isCroppable; vm.hasDimensions = hasDimensions; - }; - - - vm.repickMedia = repickMedia; + } + function repickMedia() { vm.model.propertyEditor.changeMediaFor(vm.model.mediaEntry, onMediaReplaced); } @@ -84,24 +92,23 @@ angular.module("umbraco") // updateMedia(); } - - vm.openMedia = openMedia; + function openMedia() { - var mediaEditor = { + const mediaEditor = { id: vm.mediaEntry.mediaKey, - submit: function () { + submit: () => { editorService.close(); }, - close: function () { + close: () => { editorService.close(); } }; + editorService.mediaEditor(mediaEditor); } - - vm.focalPointChanged = function(left, top) { + function focalPointChanged(left, top) { //update the model focalpoint value vm.mediaEntry.focalPoint = { left: left, @@ -111,22 +118,17 @@ angular.module("umbraco") //set form to dirty to track changes setDirty(); } - - - - vm.selectCrop = selectCrop; + function selectCrop(targetCrop) { vm.currentCrop = targetCrop; setDirty(); // TODO: start watchin values of crop, first when changed set to dirty. - }; - - vm.deselectCrop = deselectCrop; + } + function deselectCrop() { vm.currentCrop = null; - }; - - vm.resetCrop = resetCrop; + } + function resetCrop() { if (vm.currentCrop) { $scope.$evalAsync( () => { @@ -140,18 +142,22 @@ angular.module("umbraco") vm.imageCropperForm.$setDirty(); } - - vm.submitAndClose = function () { + function submitAndClose() { if (vm.model && vm.model.submit) { vm.model.submit(vm.model); } } - - vm.close = function () { - if (vm.model && vm.model.close) { - if (vm.model.createFlow === true || vm.imageCropperForm.$dirty === true) { - var labels = vm.model.createFlow === true ? ["mediaPicker_confirmCancelMediaEntryCreationHeadline", "mediaPicker_confirmCancelMediaEntryCreationMessage"] : ["prompt_discardChanges", "mediaPicker_confirmCancelMediaEntryHasChanges"]; - localizationService.localizeMany(labels).then(function (localizations) { + + function close() { + if (vm.model && vm.model.close) + { + if (vm.model.createFlow === true || vm.imageCropperForm.$dirty === true) + { + const labelKeys = vm.model.createFlow === true + ? ["mediaPicker_confirmCancelMediaEntryCreationHeadline", "mediaPicker_confirmCancelMediaEntryCreationMessage"] + : ["prompt_discardChanges", "mediaPicker_confirmCancelMediaEntryHasChanges"]; + + localizationService.localizeMany(labelKeys).then(localizations => { const confirm = { title: localizations[0], view: "default", @@ -159,11 +165,11 @@ angular.module("umbraco") submitButtonLabelKey: "general_discard", submitButtonStyle: "danger", closeButtonLabelKey: "prompt_stay", - submit: function () { + submit: () => { overlayService.close(); vm.model.close(vm.model); }, - close: function () { + close: () => { overlayService.close(); } }; @@ -177,6 +183,7 @@ angular.module("umbraco") } init(); + $scope.$on("$destroy", function () { unsubscribe.forEach(x => x()); }); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.controller.js index 405556e2ae..b19f35034b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.controller.js @@ -103,40 +103,43 @@ angular.module("umbraco") $scope.target = dialogOptions.currentTarget; } - function setTitle() { - if (!$scope.model.title) { - localizationService.localizeMany(["defaultdialogs_selectMedia", "mediaPicker_tabClipboard"]) - .then(function (data) { - $scope.model.title = data[0]; + function setTitle(data) { + if (!$scope.model.title) + $scope.model.title = data[0]; + } - - vm.navigation = [{ - "alias": "empty", - "name": data[0], - "icon": "icon-umb-media", - "active": true, - "view": "" - }]; - - if(vm.clipboardItems) { - vm.navigation.push({ - "alias": "clipboard", - "name": data[1], - "icon": "icon-paste-in", - "view": "", - "disabled": vm.clipboardItems.length === 0 - }); - } - - vm.activeTab = vm.navigation[0]; - }); - - } + function setNavigation(data) { + if (!vm.navigation.length) { + vm.navigation = [{ + "alias": "empty", + "name": data[0], + "icon": "icon-umb-media", + "active": true, + "view": "" + }]; + if (vm.clipboardItems) { + vm.navigation.push({ + "alias": "clipboard", + "name": data[1], + "icon": "icon-paste-in", + "view": "", + "disabled": vm.clipboardItems.length === 0 + }); } + vm.activeTab = vm.navigation[0]; + } + } - function onInit() { - setTitle(); + function onInit() { + + + localizationService.localizeMany(["defaultdialogs_selectMedia", "mediaPicker_tabClipboard"]) + .then(function (localizationResult) { + setTitle(localizationResult); + setNavigation(localizationResult); + }); + userService.getCurrentUser().then(function (userData) { userStartNodes = userData.startMediaIds; @@ -425,9 +428,9 @@ angular.module("umbraco") }; function onNavigationChanged(tab) { - vm.activeTab.active = false; - vm.activeTab = tab; - vm.activeTab.active = true; + vm.activeTab.active = false; + vm.activeTab = tab; + vm.activeTab.active = true; }; function clickClearClipboard() { diff --git a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-app-header.html b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-app-header.html index 98b8d88869..ce3bf06853 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-app-header.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-app-header.html @@ -1,7 +1,11 @@
- + +
+
+ + Inherited from + +
+ +
+ + Locked +
+
+ + + + + + +
-
- - -
- -
- - -
- -
+
+ +
+
+ +
+ +
+ +
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editors.html b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editors.html index 4d6ceb2ffc..8443465e25 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editors.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editors.html @@ -6,6 +6,7 @@ ng-attr-inert="{{$last ? undefined : true}}" ng-class="{'umb-editor--small': model.size === 'small', 'umb-editor--medium': model.size === 'medium', + 'umb-editor--large': model.size === 'large', 'umb-editor--animating': model.animating, 'umb-editor--notInFront': model.inFront !== true, 'umb-editor--infiniteMode': model.infiniteMode, diff --git a/src/Umbraco.Web.UI.Client/src/views/components/media/umb-media-node-info.html b/src/Umbraco.Web.UI.Client/src/views/components/media/umb-media-node-info.html index 1e72950410..010102b920 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/media/umb-media-node-info.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/media/umb-media-node-info.html @@ -3,7 +3,7 @@ -
+
@@ -27,131 +27,11 @@ - - - - - - This Media item has no references. - - - - - -
- -
- -
- Used in Documents -
- -
-
-
-
-
Name
-
Alias
-
Open
-
-
-
-
-
-
{{::reference.name}}
-
{{::reference.alias}}
- -
-
-
- - -
- - -
-
- - -
- -
- Used in Members -
- -
-
-
-
-
Name
-
Alias
-
Open
-
-
-
-
-
-
{{::reference.name}}
-
{{::reference.alias}}
- -
-
-
- - -
- - -
-
- - -
- -
- Used in Media -
- -
-
-
-
-
Name
-
Alias
-
Open
-
-
-
-
-
-
{{::reference.name}}
-
{{::reference.alias}}
- -
-
-
- - -
- - -
-
- -
+
-
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/notifications/umb-notifications.html b/src/Umbraco.Web.UI.Client/src/views/components/notifications/umb-notifications.html index 37cd736830..065f2bd28c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/notifications/umb-notifications.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/notifications/umb-notifications.html @@ -1,5 +1,5 @@
-
    +
    • diff --git a/src/Umbraco.Web.UI.Client/src/views/components/references/umb-tracked-references-bulk-action.html b/src/Umbraco.Web.UI.Client/src/views/components/references/umb-tracked-references-bulk-action.html new file mode 100644 index 0000000000..8d3c36f196 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/references/umb-tracked-references-bulk-action.html @@ -0,0 +1,13 @@ + + +
      + +
      + + +
      +
      diff --git a/src/Umbraco.Web.UI.Client/src/views/components/references/umb-tracked-references-table.html b/src/Umbraco.Web.UI.Client/src/views/components/references/umb-tracked-references-table.html new file mode 100644 index 0000000000..afc8f9a3e6 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/references/umb-tracked-references-table.html @@ -0,0 +1,35 @@ +
      +
      + {{vm.headline}} +
      + +
      +
      +
      +
      +
      Node Name
      +
      Type Name
      +
      Type
      +
      Relation
      +
      +
      +
      +
      +
      + +
      {{::reference.contentTypeName}}
      +
      {{::reference.type}}
      +
      {{::reference.relationTypeName}}
      +
      +
      +
      + + +
      + + +
      +
      diff --git a/src/Umbraco.Web.UI.Client/src/views/components/references/umb-tracked-references.html b/src/Umbraco.Web.UI.Client/src/views/components/references/umb-tracked-references.html new file mode 100644 index 0000000000..fd788cc598 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/references/umb-tracked-references.html @@ -0,0 +1,36 @@ + + + + + + + This item is not referenced + + + + +
      + + +
      + + +
      + +
      + + +
      +
      diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-groups-builder.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-groups-builder.html index 3575453bd8..c35e1ee45a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-groups-builder.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-groups-builder.html @@ -22,7 +22,7 @@ tab="tab" is-open="tab.alias === openTabAlias" on-click="changeTab(tab)" - on-click-composition="openDocumentType(documentTypeId)" + on-click-composition="openContentType(contentTypeId)" sorting="sortingMode" allow-remove="canRemoveTab(tab) && !sortingMode" on-remove="removeTab(tab, tabIndex)" @@ -105,6 +105,7 @@ sortable="sortingMode" on-edit="editPropertyTypeSettings(property)" on-remove="deleteProperty(tab.properties, property)" + on-click-composition="openContentType(contentTypeId)" val-server-field-alias="{{'Groups[' + tab.serverValidationIndex + '].Properties[' + propertyIndex + '].Alias'}}" val-server-field-label="{{'Groups[' + tab.serverValidationIndex + '].Properties[' + propertyIndex + '].Label'}}" on-change-sort-order-value="onChangePropertySortOrderValue(tab)" @@ -143,7 +144,7 @@ allow-name="true" group="group" allow-remove="canRemoveGroup(group) && !sortingMode" - on-click-composition="openDocumentType(documentTypeId)" + on-click-composition="openContentType(contentTypeId)" on-remove="removeGroup(group)" on-update-name="onChangeGroupName(group)" sorting="sortingMode" @@ -159,6 +160,7 @@ sortable="sortingMode" on-edit="editPropertyTypeSettings(property)" on-remove="deleteProperty(group.properties, property)" + on-click-composition="openContentType(contentTypeId)" on-change-sort-order-value="onChangePropertySortOrderValue(group)" val-server-field-alias="{{'Groups[' + group.serverValidationIndex + '].Properties[' + propertyIndex + '].Alias'}}" val-server-field-label="{{'Groups[' + group.serverValidationIndex + '].Properties[' + propertyIndex + '].Label'}}" diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-keyboard-shortcuts-overview.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-keyboard-shortcuts-overview.html index c1f6985257..b5b6524ce3 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-keyboard-shortcuts-overview.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-keyboard-shortcuts-overview.html @@ -1,6 +1,8 @@ -
      +
      -
      show shortcuts
      +
      + show shortcuts +
      diff --git a/src/Umbraco.Web.UI.Client/src/views/content/content.delete.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/content.delete.controller.js index 060c0ec7d9..771d1b9280 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/content.delete.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/content.delete.controller.js @@ -6,7 +6,7 @@ * @description * The controller for deleting content */ -function ContentDeleteController($scope, $timeout, contentResource, treeService, navigationService, editorState, $location, overlayService, languageResource) { +function ContentDeleteController($scope, $timeout, contentResource, treeService, navigationService, editorState, $location, overlayService, languageResource, localizationService) { /** * Used to toggle UI elements during delete operations @@ -16,6 +16,10 @@ function ContentDeleteController($scope, $timeout, contentResource, treeService, $scope.currentNode.loading = isDeleting; $scope.busy = isDeleting; } + + $scope.checkingReferences = true; + $scope.warningText = null; + $scope.disableDelete = false; $scope.performDelete = function() { @@ -77,6 +81,27 @@ function ContentDeleteController($scope, $timeout, contentResource, treeService, }; + $scope.checkingReferencesComplete = () => { + $scope.checkingReferences = false; + }; + + $scope.onReferencesWarning = () => { + // check if the deletion of items that have references has been disabled + if (Umbraco.Sys.ServerVariables.umbracoSettings.disableDeleteWhenReferenced) { + // this will only be set to true if we have a warning, indicating that this item or its descendants have reference + $scope.disableDelete = true; + + localizationService.localize("references_deleteDisabledWarning").then((value) => { + $scope.warningText = value; + }); + } + else { + localizationService.localize("references_deleteWarning").then((value) => { + $scope.warningText = value; + }); + } + }; + $scope.cancel = function() { toggleDeleting(false); $scope.close(); diff --git a/src/Umbraco.Web.UI.Client/src/views/content/content.protect.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/content.protect.controller.js index 01ba2567a7..92efb24e63 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/content.protect.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/content.protect.controller.js @@ -1,261 +1,261 @@ (function () { - "use strict"; + "use strict"; - function ContentProtectController($scope, $q, publicAccessResource, memberResource, memberGroupResource, navigationService, localizationService, editorService) { + function ContentProtectController($scope, $q, contentResource, memberResource, memberGroupResource, navigationService, localizationService, editorService) { - var vm = this; - var id = $scope.currentNode.id; + var vm = this; + var id = $scope.currentNode.id; - vm.loading = false; - vm.buttonState = "init"; - - vm.isValid = isValid; - vm.next = next; - vm.save = save; - vm.close = close; - vm.toggle = toggle; - vm.pickLoginPage = pickLoginPage; - vm.pickErrorPage = pickErrorPage; - vm.pickGroup = pickGroup; - vm.removeGroup = removeGroup; - vm.pickMember = pickMember; - vm.removeMember = removeMember; - vm.removeProtection = removeProtection; - vm.removeProtectionConfirm = removeProtectionConfirm; - - vm.type = null; - vm.step = null; - - function onInit() { - vm.loading = true; - - // get the current public access protection - publicAccessResource.getPublicAccess(id).then(function (publicAccess) { vm.loading = false; + vm.buttonState = "init"; - // init the current settings for public access (if any) - vm.loginPage = publicAccess.loginPage; - vm.errorPage = publicAccess.errorPage; - vm.groups = publicAccess.groups || []; - vm.members = publicAccess.members || []; - vm.canRemove = true; + vm.isValid = isValid; + vm.next = next; + vm.save = save; + vm.close = close; + vm.toggle = toggle; + vm.pickLoginPage = pickLoginPage; + vm.pickErrorPage = pickErrorPage; + vm.pickGroup = pickGroup; + vm.removeGroup = removeGroup; + vm.pickMember = pickMember; + vm.removeMember = removeMember; + vm.removeProtection = removeProtection; + vm.removeProtectionConfirm = removeProtectionConfirm; - if (vm.members.length) { - vm.type = "member"; - next(); - } - else if (vm.groups.length) { - vm.type = "group"; - next(); - } - else { - vm.canRemove = false; - } - }); - } + vm.type = null; + vm.step = null; - function next() { - if (vm.type === "group") { - vm.loading = true; - // get all existing member groups for lookup upon selection - // NOTE: if/when member groups support infinite editing, we can't rely on using a cached lookup list of valid groups anymore - memberGroupResource.getGroups().then(function (groups) { - vm.step = vm.type; - vm.allGroups = groups; - vm.hasGroups = groups.length > 0; - vm.loading = false; - }); - } - else { - vm.step = vm.type; - } - } - - function isValid() { - if (!vm.type) { - return false; - } - if (!vm.protectForm.$valid) { - return false; - } - if (!vm.loginPage || !vm.errorPage) { - return false; - } - if (vm.type === "group") { - return vm.groups && vm.groups.length > 0; - } - if (vm.type === "member") { - return vm.members && vm.members.length > 0; - } - return true; - } - - function save() { - vm.buttonState = "busy"; - var groups = _.map(vm.groups, function (group) { return group.name; }); - var usernames = _.map(vm.members, function (member) { return member.username; }); - publicAccessResource.updatePublicAccess(id, groups, usernames, vm.loginPage.id, vm.errorPage.id).then( - function () { - localizationService.localize("publicAccess_paIsProtected", [$scope.currentNode.name]).then(function (value) { - vm.success = { - message: value - }; - }); - navigationService.syncTree({ tree: "content", path: $scope.currentNode.path, forceReload: true }); - $scope.dialog.confirmDiscardChanges = true; - }, function (error) { - vm.error = error; - vm.buttonState = "error"; - } - ); - } - - function close() { - // ensure that we haven't set a locked state on the dialog before closing it - navigationService.allowHideDialog(true); - navigationService.hideDialog(); - } - - function toggle(group) { - group.selected = !group.selected; - $scope.dialog.confirmDiscardChanges = true; - } - - function pickGroup() { - navigationService.allowHideDialog(false); - editorService.memberGroupPicker({ - multiPicker: true, - submit: function (model) { - var selectedGroupIds = model.selectedMemberGroups - ? model.selectedMemberGroups - : [model.selectedMemberGroup]; - _.each(selectedGroupIds, - function (groupId) { - // find the group in the lookup list and add it if it isn't already - var group = _.find(vm.allGroups, function (g) { return g.id === parseInt(groupId); }); - if (group && !_.find(vm.groups, function (g) { return g.id === group.id })) { - vm.groups.push(group); - } - }); - editorService.close(); - navigationService.allowHideDialog(true); - $scope.dialog.confirmDiscardChanges = true; - }, - close: function () { - editorService.close(); - navigationService.allowHideDialog(true); - } - }); - } - - function removeGroup(group) { - vm.groups = _.reject(vm.groups, function (g) { return g.id === group.id }); - $scope.dialog.confirmDiscardChanges = true; - } - - function pickMember() { - navigationService.allowHideDialog(false); - // TODO: once editorService has a memberPicker method, use that instead - editorService.treePicker({ - multiPicker: true, - entityType: "Member", - section: "member", - treeAlias: "member", - filter: function (i) { - return i.metaData.isContainer; - }, - filterCssClass: "not-allowed", - submit: function (model) { - if (model.selection && model.selection.length) { - var promises = []; - // get the selected member usernames - _.each(model.selection, - function (member) { - // TODO: - // as-is we need to fetch all the picked members one at a time to get their usernames. - // when editorService has a memberPicker method, see if this can't be avoided - otherwise - // add a memberResource.getByKeys() method to do all this in one request - promises.push( - memberResource.getByKey(member.key).then(function (newMember) { - if (!_.find(vm.members, function (currentMember) { return currentMember.username === newMember.username })) { - vm.members.push(newMember); - } - }) - ); - }); - editorService.close(); - navigationService.allowHideDialog(true); - // wait for all the member lookups to complete + function onInit() { vm.loading = true; - $q.all(promises).then(function () { - vm.loading = false; + + // get the current public access protection + contentResource.getPublicAccess(id).then(function (publicAccess) { + vm.loading = false; + + // init the current settings for public access (if any) + vm.loginPage = publicAccess.loginPage; + vm.errorPage = publicAccess.errorPage; + vm.groups = publicAccess.groups || []; + vm.members = publicAccess.members || []; + vm.canRemove = true; + + if (vm.members.length) { + vm.type = "member"; + next(); + } + else if (vm.groups.length) { + vm.type = "group"; + next(); + } + else { + vm.canRemove = false; + } }); + } + + function next() { + if (vm.type === "group") { + vm.loading = true; + // get all existing member groups for lookup upon selection + // NOTE: if/when member groups support infinite editing, we can't rely on using a cached lookup list of valid groups anymore + memberGroupResource.getGroups().then(function (groups) { + vm.step = vm.type; + vm.allGroups = groups; + vm.hasGroups = groups.length > 0; + vm.loading = false; + }); + } + else { + vm.step = vm.type; + } + } + + function isValid() { + if (!vm.type) { + return false; + } + if (!vm.protectForm.$valid) { + return false; + } + if (!vm.loginPage || !vm.errorPage) { + return false; + } + if (vm.type === "group") { + return vm.groups && vm.groups.length > 0; + } + if (vm.type === "member") { + return vm.members && vm.members.length > 0; + } + return true; + } + + function save() { + vm.buttonState = "busy"; + var groups = _.map(vm.groups, function (group) { return encodeURIComponent(group.name); }); + var usernames = _.map(vm.members, function (member) { return member.username; }); + contentResource.updatePublicAccess(id, groups, usernames, vm.loginPage.id, vm.errorPage.id).then( + function () { + localizationService.localize("publicAccess_paIsProtected", [$scope.currentNode.name]).then(function (value) { + vm.success = { + message: value + }; + }); + navigationService.syncTree({ tree: "content", path: $scope.currentNode.path, forceReload: true }); + $scope.dialog.confirmDiscardChanges = true; + }, function (error) { + vm.error = error; + vm.buttonState = "error"; + } + ); + } + + function close() { + // ensure that we haven't set a locked state on the dialog before closing it + navigationService.allowHideDialog(true); + navigationService.hideDialog(); + } + + function toggle(group) { + group.selected = !group.selected; $scope.dialog.confirmDiscardChanges = true; - } - }, - close: function () { - editorService.close(); - navigationService.allowHideDialog(true); } - }); - } - function removeMember(member) { - vm.members = _.without(vm.members, member); - } - - function pickLoginPage() { - pickPage(vm.loginPage); - } - - function pickErrorPage() { - pickPage(vm.errorPage); - } - - function pickPage(page) { - navigationService.allowHideDialog(false); - editorService.contentPicker({ - submit: function (model) { - if (page === vm.loginPage) { - vm.loginPage = model.selection[0]; - } - else { - vm.errorPage = model.selection[0]; - } - editorService.close(); - navigationService.allowHideDialog(true); - $scope.dialog.confirmDiscardChanges = true; - }, - close: function () { - editorService.close(); - navigationService.allowHideDialog(true); + function pickGroup() { + navigationService.allowHideDialog(false); + editorService.memberGroupPicker({ + multiPicker: true, + submit: function(model) { + var selectedGroupIds = model.selectedMemberGroups + ? model.selectedMemberGroups + : [model.selectedMemberGroup]; + _.each(selectedGroupIds, + function (groupId) { + // find the group in the lookup list and add it if it isn't already + var group = _.find(vm.allGroups, function(g) { return g.id === parseInt(groupId); }); + if (group && !_.find(vm.groups, function (g) { return g.id === group.id })) { + vm.groups.push(group); + } + }); + editorService.close(); + navigationService.allowHideDialog(true); + $scope.dialog.confirmDiscardChanges = true; + }, + close: function() { + editorService.close(); + navigationService.allowHideDialog(true); + } + }); } - }); - } - function removeProtection() { - vm.removing = true; - } - - function removeProtectionConfirm() { - vm.buttonState = "busy"; - publicAccessResource.removePublicAccess(id).then( - function () { - localizationService.localize("publicAccess_paIsRemoved", [$scope.currentNode.name]).then(function (value) { - vm.success = { - message: value - }; - }); - navigationService.syncTree({ tree: "content", path: $scope.currentNode.path, forceReload: true }); - }, function (error) { - vm.error = error; - vm.buttonState = "error"; + function removeGroup(group) { + vm.groups = _.reject(vm.groups, function(g) { return g.id === group.id }); + $scope.dialog.confirmDiscardChanges = true; } - ); + + function pickMember() { + navigationService.allowHideDialog(false); + // TODO: once editorService has a memberPicker method, use that instead + editorService.treePicker({ + multiPicker: true, + entityType: "Member", + section: "member", + treeAlias: "member", + filter: function (i) { + return i.metaData.isContainer; + }, + filterCssClass: "not-allowed", + submit: function (model) { + if (model.selection && model.selection.length) { + var promises = []; + // get the selected member usernames + _.each(model.selection, + function (member) { + // TODO: + // as-is we need to fetch all the picked members one at a time to get their usernames. + // when editorService has a memberPicker method, see if this can't be avoided - otherwise + // add a memberResource.getByKeys() method to do all this in one request + promises.push( + memberResource.getByKey(member.key).then(function(newMember) { + if (!_.find(vm.members, function (currentMember) { return currentMember.username === newMember.username })) { + vm.members.push(newMember); + } + }) + ); + }); + editorService.close(); + navigationService.allowHideDialog(true); + // wait for all the member lookups to complete + vm.loading = true; + $q.all(promises).then(function() { + vm.loading = false; + }); + $scope.dialog.confirmDiscardChanges = true; + } + }, + close: function () { + editorService.close(); + navigationService.allowHideDialog(true); + } + }); + } + + function removeMember(member) { + vm.members = _.without(vm.members, member); + } + + function pickLoginPage() { + pickPage(vm.loginPage); + } + + function pickErrorPage() { + pickPage(vm.errorPage); + } + + function pickPage(page) { + navigationService.allowHideDialog(false); + editorService.contentPicker({ + submit: function (model) { + if (page === vm.loginPage) { + vm.loginPage = model.selection[0]; + } + else { + vm.errorPage = model.selection[0]; + } + editorService.close(); + navigationService.allowHideDialog(true); + $scope.dialog.confirmDiscardChanges = true; + }, + close: function () { + editorService.close(); + navigationService.allowHideDialog(true); + } + }); + } + + function removeProtection() { + vm.removing = true; + } + + function removeProtectionConfirm() { + vm.buttonState = "busy"; + contentResource.removePublicAccess(id).then( + function () { + localizationService.localize("publicAccess_paIsRemoved", [$scope.currentNode.name]).then(function(value) { + vm.success = { + message: value + }; + }); + navigationService.syncTree({ tree: "content", path: $scope.currentNode.path, forceReload: true }); + }, function (error) { + vm.error = error; + vm.buttonState = "error"; + } + ); + } + + onInit(); } - onInit(); - } - - angular.module("umbraco").controller("Umbraco.Editors.Content.ProtectController", ContentProtectController); + angular.module("umbraco").controller("Umbraco.Editors.Content.ProtectController", ContentProtectController); })(); diff --git a/src/Umbraco.Web.UI.Client/src/views/content/delete.html b/src/Umbraco.Web.UI.Client/src/views/content/delete.html index 316d0669c4..765dbda531 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/delete.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/delete.html @@ -1,7 +1,7 @@ -
      -
      +
      +
      -
      +
      {{currentNode.name}} was deleted @@ -9,20 +9,28 @@
      -
      -

      +

      +

      Are you sure you want to delete {{currentNode.name}}?

      -
      +
      This will delete the node and all its languages. If you only want to delete one language go and unpublish it instead.
      + + +
      + {{warningText}} +
      +
      When items are deleted from the recycle bin, they will be gone forever.
      - + + +
      diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/unpublish.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/overlays/unpublish.controller.js index 936ab3b104..f6c0ea67f9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/unpublish.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/unpublish.controller.js @@ -6,10 +6,13 @@ var vm = this; var autoSelectedVariants = []; + vm.id = $scope.content.id; + vm.warningText = null; vm.changeSelection = changeSelection; function onInit() { + $scope.model.hideSubmitButton = true; vm.variants = $scope.model.variants; vm.unpublishableVariants = vm.variants.filter(publishedVariantFilter) @@ -108,6 +111,24 @@ }); }); + vm.checkingReferencesComplete = () => { + $scope.model.hideSubmitButton = false; + }; + + vm.onReferencesWarning = () => { + $scope.model.submitButtonStyle = "danger"; + + // check if the unpublishing of items that have references has been disabled + if (Umbraco.Sys.ServerVariables.umbracoSettings.disableUnpublishWhenReferenced) { + // this will only be disabled if we have a warning, indicating that this item or its descendants have reference + $scope.model.disableSubmitButton = true; + } + + localizationService.localize("references_unpublishWarning").then((value) => { + vm.warningText = value; + }); + }; + onInit(); } diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/unpublish.html b/src/Umbraco.Web.UI.Client/src/views/content/overlays/unpublish.html index a994fe38ed..b938065bc2 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/unpublish.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/unpublish.html @@ -49,7 +49,14 @@
      -
      + +
      + +
      + +
      + {{vm.warningText}} +
      diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/settingsdashboardintro.html b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/settingsdashboardintro.html index 4216d0a57a..436155de72 100644 --- a/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/settingsdashboardintro.html +++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/settingsdashboardintro.html @@ -17,7 +17,7 @@ Ask a question in the Community Forum
    • - Watch our tutorial videos (some are free, some require a subscription) + Watch our free tutorial videos on the Umbraco Learning Base
    • Find out about our productivity boosting tools and commercial support diff --git a/src/Umbraco.Web.UI.Client/src/views/dictionary/dictionary.edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/dictionary/dictionary.edit.controller.js index 416d2c3a65..2f6fb2d3a8 100644 --- a/src/Umbraco.Web.UI.Client/src/views/dictionary/dictionary.edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/dictionary/dictionary.edit.controller.js @@ -1,4 +1,4 @@ -/** +/** * @ngdoc controller * @name Umbraco.Editors.Dictionary.EditController * @function @@ -7,128 +7,131 @@ * The controller for editing dictionary items */ function DictionaryEditController($scope, $routeParams, $location, dictionaryResource, navigationService, appState, editorState, contentEditingHelper, formHelper, notificationsService, localizationService) { - - var vm = this; - //setup scope vars - vm.nameDirty = false; - vm.header = {}; - vm.header.editorfor = "template_insertDictionaryItem"; - vm.header.setPageTitle = true; + var vm = this; - vm.page = {}; - vm.page.loading = false; - vm.page.nameLocked = false; - vm.page.menu = {}; - vm.page.menu.currentSection = appState.getSectionState("currentSection"); - vm.page.menu.currentNode = null; - vm.description = ""; - vm.showBackButton = true; - vm.maxlength = 1000; - - vm.save = saveDictionary; - vm.back = back; - vm.change = change; - - function loadDictionary() { + //setup scope vars + vm.nameDirty = false; + vm.header = {}; + vm.header.editorfor = "template_insertDictionaryItem"; + vm.header.setPageTitle = true; - vm.page.loading = true; + vm.page = {}; + vm.page.navigation = {}; + vm.page.loading = false; + vm.page.nameLocked = false; + vm.page.menu = {}; + vm.page.menu.currentSection = appState.getSectionState("currentSection"); + vm.page.menu.currentNode = null; + vm.description = ""; + vm.showBackButton = true; + vm.maxlength = 1000; - //we are editing so get the content item from the server - dictionaryResource.getById($routeParams.id) - .then(function (data) { - bindDictionary(data); - vm.page.loading = false; - }); + vm.save = saveDictionary; + vm.back = back; + vm.change = change; + + function loadDictionary() { + + vm.page.loading = true; + + //we are editing so get the content item from the server + dictionaryResource.getById($routeParams.id) + .then(function (data) { + bindDictionary(data); + vm.page.navigation = data.apps; + data.apps[0].active = true; + vm.page.loading = false; + }); + } + + function createTranslationProperty(translation) { + return { + alias: translation.isoCode, + label: translation.displayName, + hideLabel: false } + } - function createTranslationProperty(translation) { - return { - alias: translation.isoCode, - label: translation.displayName, - hideLabel : false - } - } - - function bindDictionary(data) { - localizationService.localize("dictionaryItem_description").then(function (value) { - vm.description = value.replace("%0%", data.name); - }); - - // create data for umb-property displaying - for (var i = 0; i < data.translations.length; i++) { - data.translations[i].property = createTranslationProperty(data.translations[i]); - change(data.translations[i]); - } - - contentEditingHelper.handleSuccessfulSave({ - scope: $scope, - savedContent: data - }); - - // set content - vm.content = data; - - //share state - editorState.set(vm.content); - - navigationService.syncTree({ tree: "dictionary", path: data.path, forceReload: true }).then(function (syncArgs) { - vm.page.menu.currentNode = syncArgs.node; - }); - } - - function onInit() { - loadDictionary(); - } - - function saveDictionary() { - if (formHelper.submitForm({ scope: $scope, statusMessage: "Saving..." })) { - - vm.page.saveButtonState = "busy"; - - dictionaryResource.save(vm.content, vm.nameDirty) - .then(function (data) { - - formHelper.resetForm({ scope: $scope }); - - bindDictionary(data); - - vm.page.saveButtonState = "success"; - }, - function (err) { - - formHelper.resetForm({ scope: $scope, hasErrors: true }); - - contentEditingHelper.handleSaveError({ - err: err - }); - - notificationsService.error(err.data.message); - - vm.page.saveButtonState = "error"; - }); - } - } - - function back() { - $location.path(vm.page.menu.currentSection + "/dictionary/list"); - } - - function change(translation) { - if (translation.translation) { - var charsCount = translation.translation.length; - translation.nearMaxLimit = charsCount > Math.max(vm.maxlength * .8, vm.maxlength - 50); - } - } - - $scope.$watch("vm.content.name", function (newVal, oldVal) { - //when the value changes, we need to set the name dirty - if (newVal && (newVal !== oldVal) && typeof(oldVal) !== "undefined") { - vm.nameDirty = true; - } + function bindDictionary(data) { + localizationService.localize("dictionaryItem_description").then(function (value) { + vm.description = value.replace("%0%", data.name); }); - onInit(); + // create data for umb-property displaying + for (var i = 0; i < data.translations.length; i++) { + data.translations[i].property = createTranslationProperty(data.translations[i]); + change(data.translations[i]); + } + + contentEditingHelper.handleSuccessfulSave({ + scope: $scope, + savedContent: data + }); + + // set content + vm.content = data; + + //share state + editorState.set(vm.content); + + navigationService.syncTree({ tree: "dictionary", path: data.path, forceReload: true }).then(function (syncArgs) { + vm.page.menu.currentNode = syncArgs.node; + }); + } + + function onInit() { + loadDictionary(); + } + + function saveDictionary() { + if (formHelper.submitForm({ scope: $scope, statusMessage: "Saving..." })) { + + vm.page.saveButtonState = "busy"; + + dictionaryResource.save(vm.content, vm.nameDirty) + .then(function (data) { + + formHelper.resetForm({ scope: $scope }); + + bindDictionary(data); + + vm.page.saveButtonState = "success"; + }, + function (err) { + + formHelper.resetForm({ scope: $scope, hasErrors: true }); + + contentEditingHelper.handleSaveError({ + err: err + }); + + notificationsService.error(err.data.message); + + vm.page.saveButtonState = "error"; + }); + } + } + + function back() { + $location.path(vm.page.menu.currentSection + "/dictionary/list"); + } + + function change(translation) { + if (translation.translation) { + var charsCount = translation.translation.length; + translation.nearMaxLimit = charsCount > Math.max(vm.maxlength * .8, vm.maxlength - 50); + } + } + + $scope.$watch("vm.content.name", function (newVal, oldVal) { + //when the value changes, we need to set the name dirty + if (newVal && (newVal !== oldVal) && typeof (oldVal) !== "undefined") { + vm.nameDirty = true; + } + }); + + onInit(); } angular.module("umbraco").controller("Umbraco.Editors.Dictionary.EditController", DictionaryEditController); diff --git a/src/Umbraco.Web.UI.Client/src/views/dictionary/edit.html b/src/Umbraco.Web.UI.Client/src/views/dictionary/edit.html index f59491c693..01c3a628e9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/dictionary/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/dictionary/edit.html @@ -1,63 +1,45 @@ -
      - - -
      +
      + - - - - - - - -

      - + - -
      -

      - {{ translation.displayName }} - %0% characters left. -

      -
      -
      -
      -
      -
      - - + + + - + + + + - - + - + - -
      - + + + + + + + +
      diff --git a/src/Umbraco.Web.UI.Client/src/views/dictionary/views/content/content.html b/src/Umbraco.Web.UI.Client/src/views/dictionary/views/content/content.html new file mode 100644 index 0000000000..4e97626c4f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/dictionary/views/content/content.html @@ -0,0 +1,20 @@ + + +

      + + + +
      +

      + {{ translation.displayName }} + %0% characters left. +

      +
      +
      +
      +
      diff --git a/src/Umbraco.Web.UI.Client/src/views/media/delete.html b/src/Umbraco.Web.UI.Client/src/views/media/delete.html index 7231ccf2c4..894cf5ef77 100644 --- a/src/Umbraco.Web.UI.Client/src/views/media/delete.html +++ b/src/Umbraco.Web.UI.Client/src/views/media/delete.html @@ -10,15 +10,23 @@
      -

      +

      Are you sure you want to delete {{currentNode.name}}?

      + + +
      + {{warningText}} +
      +
      When items are deleted from the recycle bin, they will be gone forever.
      - + + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/media/media.delete.controller.js b/src/Umbraco.Web.UI.Client/src/views/media/media.delete.controller.js index 8792571377..0761504b6b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/media/media.delete.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/media/media.delete.controller.js @@ -6,7 +6,11 @@ * @description * The controller for deleting content */ -function MediaDeleteController($scope, mediaResource, treeService, navigationService, editorState, $location, overlayService) { +function MediaDeleteController($scope, mediaResource, treeService, navigationService, editorState, $location, overlayService, localizationService) { + + $scope.checkingReferences = true; + $scope.warningText = null; + $scope.disableDelete = false; $scope.performDelete = function() { @@ -26,7 +30,7 @@ function MediaDeleteController($scope, mediaResource, treeService, navigationSer treeService.removeNode($scope.currentNode); if (rootNode) { - //ensure the recycle bin has child nodes now + //ensure the recycle bin has child nodes now var recycleBin = treeService.getDescendantNode(rootNode, -21); if (recycleBin) { recycleBin.hasChildren = true; @@ -65,6 +69,27 @@ function MediaDeleteController($scope, mediaResource, treeService, navigationSer }); }; + $scope.checkingReferencesComplete = () => { + $scope.checkingReferences = false; + }; + + $scope.onReferencesWarning = () => { + // check if the deletion of items that have references has been disabled + if (Umbraco.Sys.ServerVariables.umbracoSettings.disableDeleteWhenReferenced) { + // this will only be set to true if we have a warning, indicating that this item or its descendants have reference + $scope.disableDelete = true; + + localizationService.localize("references_deleteDisabledWarning").then((value) => { + $scope.warningText = value; + }); + } + else { + localizationService.localize("references_deleteWarning").then((value) => { + $scope.warningText = value; + }); + } + }; + $scope.close = function() { navigationService.hideDialog(); }; diff --git a/src/Umbraco.Web.UI.Client/src/views/packages/views/install-local.controller.js b/src/Umbraco.Web.UI.Client/src/views/packages/views/install-local.controller.js new file mode 100644 index 0000000000..5996ae105c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/packages/views/install-local.controller.js @@ -0,0 +1,212 @@ +(function () { + "use strict"; + + function PackagesInstallLocalController($scope, Upload, umbRequestHelper, packageResource, localStorageService, $timeout, $window, localizationService, $q) { + + var vm = this; + vm.state = "upload"; + + vm.localPackage = {}; + vm.installPackage = installPackage; + vm.installState = { + status: "", + progress: 0 + }; + vm.installCompleted = false; + vm.zipFile = { + uploadStatus: "idle", + uploadProgress: 0, + serverErrorMessage: null + }; + + $scope.handleFiles = function (files, event, invalidFiles) { + if (files) { + for (var i = 0; i < files.length; i++) { + upload(files[i]); + } + } + }; + + var labels = {}; + var labelKeys = [ + "packager_installStateImporting", + "packager_installStateInstalling", + "packager_installStateRestarting", + "packager_installStateComplete", + "packager_installStateCompleted" + ]; + + localizationService.localizeMany(labelKeys).then(function (values) { + labels.installStateImporting = values[0]; + labels.installStateInstalling = values[1]; + labels.installStateRestarting = values[2]; + labels.installStateComplete = values[3]; + labels.installStateCompleted = values[4]; + }); + + function upload(file) { + + Upload.upload({ + url: umbRequestHelper.getApiUrl("packageInstallApiBaseUrl", "UploadLocalPackage"), + fields: {}, + file: file + }).progress(function (evt) { + + // hack: in some browsers the progress event is called after success + // this prevents the UI from going back to a uploading state + if (vm.zipFile.uploadStatus !== "done" && vm.zipFile.uploadStatus !== "error") { + + // set view state to uploading + vm.state = 'uploading'; + + // calculate progress in percentage + var progressPercentage = parseInt(100.0 * evt.loaded / evt.total, 10); + + // set percentage property on file + vm.zipFile.uploadProgress = progressPercentage; + + // set uploading status on file + vm.zipFile.uploadStatus = "uploading"; + + } + + }).success(function (data, status, headers, config) { + + if (data.notifications && data.notifications.length > 0) { + + // set error status on file + vm.zipFile.uploadStatus = "error"; + + // Throw message back to user with the cause of the error + vm.zipFile.serverErrorMessage = data.notifications[0].message; + + } else { + + // set done status on file + vm.zipFile.uploadStatus = "done"; + loadPackage(); + vm.zipFile.uploadProgress = 100; + vm.localPackage = data; + } + + }).error(function (evt, status, headers, config) { + + // set status done + vm.zipFile.uploadStatus = "error"; + + // If file not found, server will return a 404 and display this message + if (status === 404) { + vm.zipFile.serverErrorMessage = "File not found"; + } + else if (status == 400) { + //it's a validation error + vm.zipFile.serverErrorMessage = evt.message; + } + else { + //it's an unhandled error + //if the service returns a detailed error + if (evt.InnerException) { + vm.zipFile.serverErrorMessage = evt.InnerException.ExceptionMessage; + + //Check if its the common "too large file" exception + if (evt.InnerException.StackTrace && evt.InnerException.StackTrace.indexOf("ValidateRequestEntityLength") > 0) { + vm.zipFile.serverErrorMessage = "File too large to upload"; + } + + } else if (evt.Message) { + vm.zipFile.serverErrorMessage = evt.Message; + } + } + }); + } + + function loadPackage() { + if (vm.zipFile.uploadStatus === "done") { + vm.state = "packageDetails"; + } + } + + function installPackage() { + + vm.installState.status = labels.installStateImporting; + vm.installState.progress = "0"; + + packageResource + .import(vm.localPackage) + .then(function (pack) { + vm.installState.progress = "25"; + vm.installState.status = labels.installStateInstalling; + return packageResource.installFiles(pack); + }, + installError) + .then(function (pack) { + vm.installState.status = labels.installStateRestarting; + vm.installState.progress = "50"; + var deferred = $q.defer(); + + //check if the app domain is restarted ever 2 seconds + var count = 0; + + function checkRestart() { + $timeout(function () { + packageResource.checkRestart(pack).then(function (d) { + count++; + //if there is an id it means it's not restarted yet but we'll limit it to only check 10 times + if (d.isRestarting && count < 10) { + checkRestart(); + } + else { + //it's restarted! + deferred.resolve(d); + } + }, + installError); + }, + 2000); + } + + checkRestart(); + + return deferred.promise; + }, + installError) + .then(function (pack) { + vm.installState.status = labels.installStateInstalling; + vm.installState.progress = "75"; + return packageResource.installData(pack); + }, + installError) + .then(function (pack) { + vm.installState.status = labels.installStateComplete; + vm.installState.progress = "100"; + return packageResource.cleanUp(pack); + }, + installError) + .then(function (result) { + + //Put the package data in local storage so we can use after reloading + localStorageService.set("packageInstallData", result); + + vm.installState.status = labels.installStateCompleted; + vm.installCompleted = true; + + + }, installError); + } + + function installError() { + //This will return a rejection meaning that the promise change above will stop + return $q.reject(); + } + + vm.reloadPage = function () { + //reload on next digest (after cookie) + $timeout(function () { + $window.location.reload(true); + }); + } + } + + angular.module("umbraco").controller("Umbraco.Editors.Packages.InstallLocalController", PackagesInstallLocalController); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/packages/views/install-local.html b/src/Umbraco.Web.UI.Client/src/views/packages/views/install-local.html new file mode 100644 index 0000000000..4d44af7d6e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/packages/views/install-local.html @@ -0,0 +1,194 @@ +
+ +
+ + +
+ + + +
+ +
+ + + + + Drop to upload + + + +
+ - or click here to choose files +
+ +
+ +
+ +

Upload package

+

+ Install a local package by selecting it from your machine. Only install packages from sources you know and trust. +

+ +
+
+
+ +
+ + + + + + + +
+ +
+
+
+ +
+
+ +

Uploading package...

+ + + + +
+ {{ vm.zipFile.serverErrorMessage }} +
+ +
+
+
+ +
+ +
+ +
+ + + + + + + +
+ + +
+
+ +
+
+ + +
+ +
+

{{ vm.localPackage.name }}

+ + + +
+ Contributors
+ {{ vm.localPackage.contributors.join(', ')}} +
+ +
+ Version
+ {{ vm.localPackage.version }} +

+ + + Upgrading from version + {{ vm.localPackage.originalVersion }} + + +

+
+ + + +
+ Read me
+ +
+ +
+ + + I accept terms of use + + + + +
+ +
+ + +
+ +
+ This package cannot be installed, it requires a minimum Umbraco version of {{vm.localPackage.umbracoVersion}} +
+
+

{{vm.installState.status}}

+
+ +
+ + +
+ +
+
+ +
+
+
+
+ +
diff --git a/src/Umbraco.Web.UI.Client/src/views/packages/views/repo.controller.js b/src/Umbraco.Web.UI.Client/src/views/packages/views/repo.controller.js index 05b09e8abd..a3405bc2bf 100644 --- a/src/Umbraco.Web.UI.Client/src/views/packages/views/repo.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/packages/views/repo.controller.js @@ -1,7 +1,7 @@ (function () { "use strict"; - function PackagesRepoController($scope, $timeout, ourPackageRepositoryResource, $q, packageResource, localStorageService, localizationService) { + function PackagesRepoController($scope, $timeout, ourPackageRepositoryResource, $q, localizationService) { var vm = this; @@ -24,6 +24,8 @@ vm.closeLightbox = closeLightbox; vm.search = search; vm.installCompleted = false; + vm.highlightedPackageCollections = []; + vm.labels = {}; var defaultSort = "Latest"; var currSort = defaultSort; @@ -46,28 +48,38 @@ function init() { vm.loading = true; + localizationService.localizeMany(["packager_packagesPopular", "packager_packagesPromoted"]) + .then(function (labels) { + vm.labels.popularPackages = labels[0]; + vm.labels.promotedPackages = labels[1]; - $q.all([ - ourPackageRepositoryResource.getCategories() - .then(function (cats) { - vm.categories = cats.filter(function (cat) { - return cat.name !== "Umbraco Pro"; + var popularPackages, promotedPackages; + $q.all([ + ourPackageRepositoryResource.getCategories() + .then(function (cats) { + vm.categories = cats.filter(function (cat) { + return cat.name !== "Umbraco Pro"; + }); + }), + ourPackageRepositoryResource.getPopular(10) + .then(function (pack) { + popularPackages = { title: vm.labels.popularPackages, packages: pack.packages }; + }), + ourPackageRepositoryResource.getPromoted(20) + .then(function (pack) { + promotedPackages = { title: vm.labels.promotedPackages, packages: pack.packages }; + }), + ourPackageRepositoryResource.search(vm.pagination.pageNumber - 1, vm.pagination.pageSize, currSort) + .then(function (pack) { + vm.packages = pack.packages; + vm.pagination.totalPages = Math.ceil(pack.total / vm.pagination.pageSize); + }) + ]) + .then(function () { + vm.highlightedPackageCollections = [popularPackages, promotedPackages]; + vm.loading = false; }); - }), - ourPackageRepositoryResource.getPopular(8) - .then(function (pack) { - vm.popular = pack.packages; - }), - ourPackageRepositoryResource.search(vm.pagination.pageNumber - 1, vm.pagination.pageSize, currSort) - .then(function (pack) { - vm.packages = pack.packages; - vm.pagination.totalPages = Math.ceil(pack.total / vm.pagination.pageSize); - }) - ]) - .then(function () { - vm.loading = false; }); - } function selectCategory(selectedCategory, categories) { @@ -96,10 +108,15 @@ currSort = defaultSort; + var popularPackages, promotedPackages; $q.all([ - ourPackageRepositoryResource.getPopular(8, searchCategory) + ourPackageRepositoryResource.getPopular(10, searchCategory) .then(function (pack) { - vm.popular = pack.packages; + popularPackages = { title: vm.labels.popularPackages, packages: pack.packages }; + }), + ourPackageRepositoryResource.getPromoted(20, searchCategory) + .then(function (pack) { + promotedPackages = { title: vm.labels.promotedPackages, packages: pack.packages }; }), ourPackageRepositoryResource.search(vm.pagination.pageNumber - 1, vm.pagination.pageSize, currSort, searchCategory, vm.searchQuery) .then(function (pack) { @@ -109,6 +126,7 @@ }) ]) .then(function () { + vm.highlightedPackageCollections = [popularPackages, promotedPackages]; vm.loading = false; }); } diff --git a/src/Umbraco.Web.UI.Client/src/views/packages/views/repo.html b/src/Umbraco.Web.UI.Client/src/views/packages/views/repo.html index 262cf3eda6..7dc3e499fe 100644 --- a/src/Umbraco.Web.UI.Client/src/views/packages/views/repo.html +++ b/src/Umbraco.Web.UI.Client/src/views/packages/views/repo.html @@ -36,46 +36,48 @@
-
-

Popular

-
+
+
+

{{highlightedPackageCollection.title}}

+
-
- -
+ +
-
+
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js index 21f6354c62..4e2bdf6ce4 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js @@ -667,6 +667,7 @@ angular.module("umbraco") $scope.removeControl = function (cell, $index) { $scope.currentControl = null; cell.controls.splice($index, 1); + currentForm.$setDirty(); }; $scope.percentage = function (spans) { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js index c66ff1a461..c3c5dff69b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js @@ -146,6 +146,7 @@ function listViewController($scope, $interpolate, $routeParams, $injector, $time } var listParamsForCurrent = $routeParams.id == $routeParams.list; + $scope.options = { useInfiniteEditor: $scope.model.config.useInfiniteEditor === true, pageSize: $scope.model.config.pageSize ? $scope.model.config.pageSize : 10, @@ -169,6 +170,7 @@ function listViewController($scope, $interpolate, $routeParams, $injector, $time allowBulkDelete: $scope.model.config.bulkActionPermissions.allowBulkDelete, cultureName: $routeParams.cculture ? $routeParams.cculture : $routeParams.mculture }; + _.each($scope.options.includeProperties, function (property) { property.nameExp = !!property.nameTemplate ? $interpolate(property.nameTemplate) @@ -268,12 +270,13 @@ function listViewController($scope, $interpolate, $routeParams, $injector, $time $scope.getContent = function (contentId) { $scope.reloadView($scope.contentId, true); - } + }; $scope.reloadView = function (id, reloadActiveNode) { if (!id) { return; } + $scope.viewLoaded = false; $scope.folders = []; @@ -327,7 +330,7 @@ function listViewController($scope, $interpolate, $routeParams, $injector, $time $scope.options.pageNumber = 1; $scope.reloadView($scope.contentId); } - } + }; $scope.onSearchStartTyping = function() { $scope.viewLoaded = false; @@ -386,6 +389,7 @@ function listViewController($scope, $interpolate, $routeParams, $injector, $time view: "views/propertyeditors/listview/overlays/delete.html", deletesVariants: selectionHasVariants(), isTrashed: $scope.isTrashed, + selection: $scope.selection, submitButtonLabelKey: "contentTypeEditor_yesDelete", submitButtonStyle: "danger", submit: function (model) { @@ -495,8 +499,8 @@ function listViewController($scope, $interpolate, $routeParams, $injector, $time view: "views/propertyeditors/listview/overlays/listviewunpublish.html", submitButtonLabelKey: "actions_unpublish", submitButtonStyle: "warning", + selection: $scope.selection, submit: function (model) { - // create a comma separated array of selected cultures let selectedCultures = []; if (model.languages && model.languages.length > 0) { @@ -506,7 +510,6 @@ function listViewController($scope, $interpolate, $routeParams, $injector, $time } }); } - performUnpublish(selectedCultures); overlayService.close(); }, @@ -708,13 +711,15 @@ function listViewController($scope, $interpolate, $routeParams, $injector, $time } function initView() { + var id = $routeParams.id; if (id === undefined) { // no ID found in route params - don't list anything as we don't know for sure where we are return; } - - $scope.contentId = id; + + // Get current id for node to load it's children + $scope.contentId = editorState.current ? editorState.current.id : id; $scope.isTrashed = editorState.current ? editorState.current.trashed : id === "-20" || id === "-21"; $scope.options.allowBulkPublish = $scope.options.allowBulkPublish && !$scope.isTrashed; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/overlays/delete.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/overlays/delete.controller.js new file mode 100644 index 0000000000..85c8b82620 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/overlays/delete.controller.js @@ -0,0 +1,40 @@ +(function () { + "use strict"; + + function ListViewDeleteController($scope, localizationService) { + + var vm = this; + vm.loading = true; + vm.disableDelete = false; + + function onInit() { + $scope.model.hideSubmitButton = true; + } + + vm.checkingReferencesComplete = () => { + $scope.model.hideSubmitButton = false; + }; + + vm.onReferencesWarning = () => { + $scope.model.submitButtonStyle = "danger"; + + // check if the deletion of items that have references has been disabled + if (Umbraco.Sys.ServerVariables.umbracoSettings.disableDeleteWhenReferenced) { + // this will only be set to true if we have a warning, indicating that this item or its descendants have reference + vm.disableDelete = true; + $scope.model.disableSubmitButton = true; + } + + localizationService.localize("general_delete").then(function (action) { + localizationService.localize("references_listViewDialogWarning", [action.toLowerCase()]).then((value) => { + vm.warningText = value; + }); + }); + }; + + onInit(); + } + + angular.module("umbraco").controller("Umbraco.Overlays.ListViewDeleteController", ListViewDeleteController); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/overlays/delete.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/overlays/delete.html index f8b2b008d3..b137c74260 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/overlays/delete.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/overlays/delete.html @@ -1,9 +1,15 @@ -
+
-

+

Are you sure you want to delete?

+ + +
+ {{vm.warningText}} +
+
This will delete the node and all its languages. If you only want to delete one language go and unpublish it instead.
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/overlays/listviewunpublish.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/overlays/listviewunpublish.controller.js index 650b1b8438..cf432c0318 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/overlays/listviewunpublish.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/overlays/listviewunpublish.controller.js @@ -5,6 +5,7 @@ var vm = this; vm.loading = true; + vm.warningText = null; vm.changeSelection = changeSelection; @@ -29,7 +30,9 @@ function onInit() { + vm.selection = $scope.model.selection; vm.languages = $scope.model.languages; + $scope.model.hideSubmitButton = true; if (!$scope.model.title) { localizationService.localize("content_unpublish").then(function (value) { @@ -65,6 +68,26 @@ vm.loading = false; } + vm.checkingReferencesComplete = () => { + $scope.model.hideSubmitButton = false; + }; + + vm.onReferencesWarning = () => { + $scope.model.submitButtonStyle = "danger"; + + // check if the unpublishing of items that have references has been disabled + if (Umbraco.Sys.ServerVariables.umbracoSettings.disableUnpublishWhenReferenced) { + // this will only be disabled if we have a warning, indicating that this item or its descendants have reference + $scope.model.disableSubmitButton = true; + } + + localizationService.localize("content_unpublish").then(function (action) { + localizationService.localize("references_listViewDialogWarning", [action.toLowerCase()]).then((value) => { + vm.warningText = value; + }); + }); + }; + onInit(); //when this dialog is closed, reset all 'publish' flags diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/overlays/listviewunpublish.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/overlays/listviewunpublish.html index 9a6af50f3e..2d255cbb25 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/overlays/listviewunpublish.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/overlays/listviewunpublish.html @@ -5,6 +5,14 @@

Unpublishing will remove the selected items and all their descendants from the site.

+
+ +
+ +
+ {{vm.warningText}} +
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/membergrouppicker/membergrouppicker.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/membergrouppicker/membergrouppicker.html index e644fac29d..029596a8f6 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/membergrouppicker/membergrouppicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/membergrouppicker/membergrouppicker.html @@ -2,7 +2,7 @@
+
@@ -10,7 +10,11 @@ - diff --git a/src/Umbraco.Web.UI.Client/src/views/relationTypes/create.html b/src/Umbraco.Web.UI.Client/src/views/relationTypes/create.html index 4e4711cc09..e910dd31ce 100644 --- a/src/Umbraco.Web.UI.Client/src/views/relationTypes/create.html +++ b/src/Umbraco.Web.UI.Client/src/views/relationTypes/create.html @@ -55,6 +55,34 @@ + + + + +

+ +

+
    +
  • + +
  • +
  • + +
  • +
+
- + diff --git a/src/Umbraco.Web.UI.Client/src/views/relationTypes/views/relationType.settings.controller.js b/src/Umbraco.Web.UI.Client/src/views/relationTypes/views/relationType.settings.controller.js index c8e0af0aa9..01b5367eab 100644 --- a/src/Umbraco.Web.UI.Client/src/views/relationTypes/views/relationType.settings.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/relationTypes/views/relationType.settings.controller.js @@ -16,12 +16,17 @@ function RelationTypeSettingsController($scope, localizationService) { var labelKeys = [ "relationType_parentToChild", - "relationType_bidirectional" + "relationType_bidirectional", + "relationType_dependency", + "relationType_noDependency" + ]; localizationService.localizeMany(labelKeys).then(function (data) { vm.labels.parentToChild = data[0]; vm.labels.bidirectional = data[1]; + vm.labels.dependency = data[2]; + vm.labels.noDependency = data[3]; }); } diff --git a/src/Umbraco.Web.UI.Client/src/views/templates/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/templates/edit.controller.js index 856886a870..99f11f9913 100644 --- a/src/Umbraco.Web.UI.Client/src/views/templates/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/templates/edit.controller.js @@ -558,24 +558,32 @@ } } }); + + const editor = { + filterCssClass: 'not-allowed', + filter: item => !availableMasterTemplates.some(template => template.id == item.id), + submit: model => { + const template = model.selection[0]; + if (template && template.alias) { + vm.template.masterTemplateAlias = template.alias; + setLayout(template.alias + ".cshtml"); + } else { + vm.template.masterTemplateAlias = null; + setLayout(null); + } + editorService.close(); + }, + close: () => editorService.close() + } localizationService.localize("template_mastertemplate").then(title => { - const editor = { - title, - filterCssClass: 'not-allowed', - filter: item => !availableMasterTemplates.some(template => template.id == item.id), - submit: model => { - var template = model.selection[0]; - if (template && template.alias) { - vm.template.masterTemplateAlias = template.alias; - setLayout(template.alias + ".cshtml"); - } else { - vm.template.masterTemplateAlias = null; - setLayout(null); - } - editorService.close(); - }, - close: () => editorService.close() + editor.title = title; + + const currentTemplate = vm.templates.find(template => template.alias == vm.template.masterTemplateAlias); + if (currentTemplate) { + editor.currentNode = { + path: currentTemplate.path + }; } editorService.templatePicker(editor); diff --git a/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js index 684ce6d2f0..79164a2457 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js @@ -374,7 +374,7 @@ } function enableUser() { - vm.enableUserButtonState = "busy"; + vm.enableUserButtonState = "busfy"; usersResource.enableUsers([vm.user.id]).then(function (data) { vm.user.userState = "Active"; setUserDisplayState(); diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index c055651efc..d400f787cb 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -16,16 +16,19 @@ + + + + + + - + - + @@ -39,6 +42,7 @@ + @@ -46,6 +50,7 @@ + @@ -55,6 +60,7 @@ + @@ -72,6 +78,7 @@ + diff --git a/src/Umbraco.Web.UI/Views/Partials/blocklist/default.cshtml b/src/Umbraco.Web.UI/Views/Partials/blocklist/default.cshtml index d5944b93c3..accca2ef37 100644 --- a/src/Umbraco.Web.UI/Views/Partials/blocklist/default.cshtml +++ b/src/Umbraco.Web.UI/Views/Partials/blocklist/default.cshtml @@ -1,6 +1,6 @@ @inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage @{ - if (!Model.Any()) { return; } + if (Model?.Any() != true) { return; } }
@foreach (var block in Model) diff --git a/src/Umbraco.Web.UI/Views/Partials/grid/bootstrap3-fluid.cshtml b/src/Umbraco.Web.UI/Views/Partials/grid/bootstrap3-fluid.cshtml index 84004929bd..b92734e761 100644 --- a/src/Umbraco.Web.UI/Views/Partials/grid/bootstrap3-fluid.cshtml +++ b/src/Umbraco.Web.UI/Views/Partials/grid/bootstrap3-fluid.cshtml @@ -7,7 +7,7 @@ Razor helpers located at the bottom of this file *@ -@if (Model != null && Model.GetType() == typeof(JObject) && Model.sections != null) +@if (Model is JObject && Model?.sections is not null) { var oneColumn = ((System.Collections.ICollection)Model.sections).Count == 1; @@ -55,7 +55,7 @@
@foreach (var control in area.controls) { - if (control != null && control.editor != null && control.editor.view != null) + if (control?.editor?.view != null) { @await Html.PartialAsync("grid/editors/base", (object)control) } diff --git a/src/Umbraco.Web.UI/Views/Partials/grid/bootstrap3.cshtml b/src/Umbraco.Web.UI/Views/Partials/grid/bootstrap3.cshtml index ebe1cf725f..886378848b 100644 --- a/src/Umbraco.Web.UI/Views/Partials/grid/bootstrap3.cshtml +++ b/src/Umbraco.Web.UI/Views/Partials/grid/bootstrap3.cshtml @@ -3,9 +3,9 @@ @using Newtonsoft.Json.Linq @inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage -@if (Model != null && Model.GetType() == typeof(JObject) && Model.sections != null) +@if (Model is JObject && Model?.sections is not null) { - var oneColumn = ((System.Collections.ICollection)Model.sections).Count == 1; + var oneColumn = ((System.Collections.ICollection)Model.sections).Count == 1;
@if (oneColumn) @@ -56,7 +56,7 @@
@foreach (var control in area.controls) { - if (control != null && control.editor != null && control.editor.view != null) + if (control?.editor?.view != null) { @await Html.PartialAsync("grid/editors/base", (object)control) } diff --git a/src/Umbraco.Web.UI/Views/Partials/grid/editors/base.cshtml b/src/Umbraco.Web.UI/Views/Partials/grid/editors/base.cshtml index eca6381fd0..05e27e18e5 100644 --- a/src/Umbraco.Web.UI/Views/Partials/grid/editors/base.cshtml +++ b/src/Umbraco.Web.UI/Views/Partials/grid/editors/base.cshtml @@ -3,7 +3,7 @@ @try { string editor = EditorView(Model); - @await Html.PartialAsync(editor, (object)Model) + @await Html.PartialAsync(editor, Model as object) } catch (Exception ex) { diff --git a/src/Umbraco.Web.UI/Views/Partials/grid/editors/embed.cshtml b/src/Umbraco.Web.UI/Views/Partials/grid/editors/embed.cshtml index a383046420..74c8fe2753 100644 --- a/src/Umbraco.Web.UI/Views/Partials/grid/editors/embed.cshtml +++ b/src/Umbraco.Web.UI/Views/Partials/grid/editors/embed.cshtml @@ -1,10 +1,11 @@ -@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage +@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage -@{ - string embedValue = Convert.ToString(Model.value); - embedValue = embedValue.DetectIsJson() ? Model.value.preview : Model.value; +@if (Model is not null) +{ + string embedValue = Convert.ToString(Model.value); + embedValue = embedValue.DetectIsJson() ? Model.value.preview : Model.value; + +
+ @Html.Raw(embedValue) +
} - -
- @Html.Raw(embedValue) -
diff --git a/src/Umbraco.Web.UI/Views/Partials/grid/editors/macro.cshtml b/src/Umbraco.Web.UI/Views/Partials/grid/editors/macro.cshtml index 0e9661edd9..a4450d1c03 100644 --- a/src/Umbraco.Web.UI/Views/Partials/grid/editors/macro.cshtml +++ b/src/Umbraco.Web.UI/Views/Partials/grid/editors/macro.cshtml @@ -1,6 +1,6 @@ @inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage -@if (Model.value != null) +@if (Model?.value is not null) { string macroAlias = Model.value.macroAlias.ToString(); var parameters = new Dictionary(); diff --git a/src/Umbraco.Web.UI/Views/Partials/grid/editors/media.cshtml b/src/Umbraco.Web.UI/Views/Partials/grid/editors/media.cshtml index 4cc31d0754..bc3b111332 100644 --- a/src/Umbraco.Web.UI/Views/Partials/grid/editors/media.cshtml +++ b/src/Umbraco.Web.UI/Views/Partials/grid/editors/media.cshtml @@ -2,7 +2,8 @@ @using Umbraco.Cms.Core.Media @using Umbraco.Cms.Core.PropertyEditors.ValueConverters @inject IImageUrlGenerator ImageUrlGenerator -@if (Model.value != null) + +@if (Model?.value is not null) { var url = Model.value.image; diff --git a/src/Umbraco.Web.UI/Views/Partials/grid/editors/rte.cshtml b/src/Umbraco.Web.UI/Views/Partials/grid/editors/rte.cshtml index e14c6e1a97..944566688a 100644 --- a/src/Umbraco.Web.UI/Views/Partials/grid/editors/rte.cshtml +++ b/src/Umbraco.Web.UI/Views/Partials/grid/editors/rte.cshtml @@ -5,7 +5,7 @@ @inject HtmlImageSourceParser HtmlImageSourceParser; @{ - var value = HtmlLocalLinkParser.EnsureInternalLinks(Model.value.ToString()); + var value = HtmlLocalLinkParser.EnsureInternalLinks(Model?.value.ToString()); value = HtmlUrlParser.EnsureUrls(value); value = HtmlImageSourceParser.EnsureImageSources(value); } diff --git a/src/Umbraco.Web.UI/Views/Partials/grid/editors/textstring.cshtml b/src/Umbraco.Web.UI/Views/Partials/grid/editors/textstring.cshtml index 42972f64d8..d4152a59b1 100644 --- a/src/Umbraco.Web.UI/Views/Partials/grid/editors/textstring.cshtml +++ b/src/Umbraco.Web.UI/Views/Partials/grid/editors/textstring.cshtml @@ -1,7 +1,7 @@ -@using System.Web +@using System.Web @model dynamic -@if (Model.editor.config.markup != null) +@if (Model?.editor.config.markup is not null) { string markup = Model.editor.config.markup.ToString(); markup = markup.Replace("#value#", Html.ReplaceLineBreaks((string)Model.value.ToString()).ToString()); @@ -18,6 +18,6 @@ else { -
@Model.value
+
@Model?.value
} diff --git a/src/Umbraco.Web.UI/umbraco/PartialViewMacros/Templates/Breadcrumb.cshtml b/src/Umbraco.Web.UI/umbraco/PartialViewMacros/Templates/Breadcrumb.cshtml index 30c3feb5c5..6a807e8379 100644 --- a/src/Umbraco.Web.UI/umbraco/PartialViewMacros/Templates/Breadcrumb.cshtml +++ b/src/Umbraco.Web.UI/umbraco/PartialViewMacros/Templates/Breadcrumb.cshtml @@ -1,4 +1,4 @@ -@using Umbraco.Cms.Core.Routing +@using Umbraco.Cms.Core.Routing @using Umbraco.Extensions @inherits Umbraco.Cms.Web.Common.Macros.PartialViewMacroPage @inject IPublishedUrlProvider PublishedUrlProvider @@ -10,9 +10,9 @@ - Finally it outputs the name of the current page (without a link) *@ -@{ var selection = Model.Content.Ancestors().ToArray(); } +@{ var selection = Model?.Content.Ancestors().ToArray(); } -@if (selection.Length > 0) +@if (selection?.Length > 0) { } diff --git a/src/Umbraco.Web.UI/umbraco/PartialViewMacros/Templates/Gallery.cshtml b/src/Umbraco.Web.UI/umbraco/PartialViewMacros/Templates/Gallery.cshtml index fec760b5cb..83ac245500 100644 --- a/src/Umbraco.Web.UI/umbraco/PartialViewMacros/Templates/Gallery.cshtml +++ b/src/Umbraco.Web.UI/umbraco/PartialViewMacros/Templates/Gallery.cshtml @@ -24,7 +24,7 @@ Type: (note: You can use a Single Media Picker if that's more appropriate to your needs) *@ -@{ var mediaIds = Model.MacroParameters["mediaIds"] as string; } +@{ var mediaIds = Model?.MacroParameters["mediaIds"] as string; } @if (mediaIds != null) { diff --git a/src/Umbraco.Web.UI/umbraco/PartialViewMacros/Templates/ListAncestorsFromCurrentPage.cshtml b/src/Umbraco.Web.UI/umbraco/PartialViewMacros/Templates/ListAncestorsFromCurrentPage.cshtml index c44965ec85..ead17cfe59 100644 --- a/src/Umbraco.Web.UI/umbraco/PartialViewMacros/Templates/ListAncestorsFromCurrentPage.cshtml +++ b/src/Umbraco.Web.UI/umbraco/PartialViewMacros/Templates/ListAncestorsFromCurrentPage.cshtml @@ -10,9 +10,9 @@ - Finally it outputs the name of the current page (without a link) *@ -@{ var selection = Model.Content.Ancestors().ToArray(); } +@{ var selection = Model?.Content.Ancestors().ToArray(); } -@if (selection.Length > 0) +@if (selection?.Length > 0) {
    @* For each page in the ancestors collection which have been ordered by Level (so we start with the highest top node first) *@ @@ -22,6 +22,6 @@ } @* Display the current page as the last item in the list *@ -
  • @Model.Content.Name
  • +
  • @Model?.Content.Name
} diff --git a/src/Umbraco.Web.UI/umbraco/PartialViewMacros/Templates/ListChildPagesFromChangeableSource.cshtml b/src/Umbraco.Web.UI/umbraco/PartialViewMacros/Templates/ListChildPagesFromChangeableSource.cshtml index 464c05fb78..a53c658364 100644 --- a/src/Umbraco.Web.UI/umbraco/PartialViewMacros/Templates/ListChildPagesFromChangeableSource.cshtml +++ b/src/Umbraco.Web.UI/umbraco/PartialViewMacros/Templates/ListChildPagesFromChangeableSource.cshtml @@ -18,7 +18,7 @@ Alias:startNodeId Name:Select starting page Type:Content Picker *@ -@{ var startNodeId = Model.MacroParameters["startNodeId"]; } +@{ var startNodeId = Model?.MacroParameters["startNodeId"]; } @if (startNodeId != null) { diff --git a/src/Umbraco.Web.UI/umbraco/PartialViewMacros/Templates/ListChildPagesFromCurrentPage.cshtml b/src/Umbraco.Web.UI/umbraco/PartialViewMacros/Templates/ListChildPagesFromCurrentPage.cshtml index 491f90238a..7b055e8424 100644 --- a/src/Umbraco.Web.UI/umbraco/PartialViewMacros/Templates/ListChildPagesFromCurrentPage.cshtml +++ b/src/Umbraco.Web.UI/umbraco/PartialViewMacros/Templates/ListChildPagesFromCurrentPage.cshtml @@ -13,9 +13,9 @@ - It then generates links so the visitor can go to each page *@ -@{ var selection = Model.Content.Children.Where(x => x.IsVisible(PublishedValueFallback)).ToArray(); } +@{ var selection = Model?.Content.Children.Where(x => x.IsVisible(PublishedValueFallback)).ToArray(); } -@if (selection.Length > 0) +@if (selection?.Length > 0) {
    @foreach (var item in selection) diff --git a/src/Umbraco.Web.UI/umbraco/PartialViewMacros/Templates/ListChildPagesOrderedByDate.cshtml b/src/Umbraco.Web.UI/umbraco/PartialViewMacros/Templates/ListChildPagesOrderedByDate.cshtml index b998d917a1..4b07c6209f 100644 --- a/src/Umbraco.Web.UI/umbraco/PartialViewMacros/Templates/ListChildPagesOrderedByDate.cshtml +++ b/src/Umbraco.Web.UI/umbraco/PartialViewMacros/Templates/ListChildPagesOrderedByDate.cshtml @@ -14,9 +14,9 @@ - It then generates links so the visitor can go to each page *@ -@{ var selection = Model.Content.Children.Where(x => x.IsVisible(PublishedValueFallback)).OrderByDescending(x => x.CreateDate).ToArray(); } +@{ var selection = Model?.Content.Children.Where(x => x.IsVisible(PublishedValueFallback)).OrderByDescending(x => x.CreateDate).ToArray(); } -@if (selection.Length > 0) +@if (selection?.Length > 0) {
      @foreach (var item in selection) diff --git a/src/Umbraco.Web.UI/umbraco/PartialViewMacros/Templates/ListChildPagesOrderedByName.cshtml b/src/Umbraco.Web.UI/umbraco/PartialViewMacros/Templates/ListChildPagesOrderedByName.cshtml index ac66ece1ea..f46bbcda80 100644 --- a/src/Umbraco.Web.UI/umbraco/PartialViewMacros/Templates/ListChildPagesOrderedByName.cshtml +++ b/src/Umbraco.Web.UI/umbraco/PartialViewMacros/Templates/ListChildPagesOrderedByName.cshtml @@ -14,9 +14,9 @@ - It then generates links so the visitor can go to each page *@ -@{ var selection = Model.Content.Children.Where(x => x.IsVisible(PublishedValueFallback)).OrderBy(x => x.Name).ToArray(); } +@{ var selection = Model?.Content.Children.Where(x => x.IsVisible(PublishedValueFallback)).OrderBy(x => x.Name).ToArray(); } -@if (selection.Length > 0) +@if (selection?.Length > 0) {
        @foreach (var item in selection) diff --git a/src/Umbraco.Web.UI/umbraco/PartialViewMacros/Templates/ListChildPagesOrderedByProperty.cshtml b/src/Umbraco.Web.UI/umbraco/PartialViewMacros/Templates/ListChildPagesOrderedByProperty.cshtml index f20253bc7f..82b34ca55c 100644 --- a/src/Umbraco.Web.UI/umbraco/PartialViewMacros/Templates/ListChildPagesOrderedByProperty.cshtml +++ b/src/Umbraco.Web.UI/umbraco/PartialViewMacros/Templates/ListChildPagesOrderedByProperty.cshtml @@ -17,13 +17,13 @@ Alias:propertyAlias Name:Property Alias Type:Textbox *@ -@{ var propertyAlias = Model.MacroParameters["propertyAlias"]; } +@{ var propertyAlias = Model?.MacroParameters["propertyAlias"]; } @if (propertyAlias != null) { - var selection = Model.Content.Children.Where(x => x.IsVisible(PublishedValueFallback)).OrderBy(x => x.Value(PublishedValueFallback, propertyAlias.ToString())).ToArray(); + var selection = Model?.Content.Children.Where(x => x.IsVisible(PublishedValueFallback)).OrderBy(x => x.Value(PublishedValueFallback, propertyAlias.ToString())).ToArray(); - if (selection.Length > 0) + if (selection?.Length > 0) {
          @foreach (var item in selection) diff --git a/src/Umbraco.Web.UI/umbraco/PartialViewMacros/Templates/ListChildPagesWithDoctype.cshtml b/src/Umbraco.Web.UI/umbraco/PartialViewMacros/Templates/ListChildPagesWithDoctype.cshtml index 1f78ae43a0..466aeacaa3 100644 --- a/src/Umbraco.Web.UI/umbraco/PartialViewMacros/Templates/ListChildPagesWithDoctype.cshtml +++ b/src/Umbraco.Web.UI/umbraco/PartialViewMacros/Templates/ListChildPagesWithDoctype.cshtml @@ -13,9 +13,9 @@ (You can find the alias of your Document Type by editing it in the Settings section) *@ -@{ var selection = Model.Content.Children(VariationContextAccessor).Where(x => x.IsVisible(PublishedValueFallback)).ToArray(); } +@{ var selection = Model?.Content.Children(VariationContextAccessor).Where(x => x.IsVisible(PublishedValueFallback)).ToArray(); } -@if (selection.Length > 0) +@if (selection?.Length > 0) {
            @foreach (var item in selection) diff --git a/src/Umbraco.Web.UI/umbraco/PartialViewMacros/Templates/ListDescendantsFromCurrentPage.cshtml b/src/Umbraco.Web.UI/umbraco/PartialViewMacros/Templates/ListDescendantsFromCurrentPage.cshtml index 92caf9906d..339c0bc42d 100644 --- a/src/Umbraco.Web.UI/umbraco/PartialViewMacros/Templates/ListDescendantsFromCurrentPage.cshtml +++ b/src/Umbraco.Web.UI/umbraco/PartialViewMacros/Templates/ListDescendantsFromCurrentPage.cshtml @@ -10,10 +10,10 @@ the page currently being viewed by the website visitor, displayed as nested unordered HTML lists. *@ -@{ var selection = Model.Content.Children.Where(x => x.IsVisible(PublishedValueFallback)).ToArray(); } +@{ var selection = Model?.Content.Children.Where(x => x.IsVisible(PublishedValueFallback)).ToArray(); } @* Ensure that the Current Page has children *@ -@if (selection.Length > 0) +@if (selection?.Length > 0) { @* Get the first page in the children, where the property umbracoNaviHide is not True *@ var naviLevel = selection[0].Level; diff --git a/src/Umbraco.Web.UI/umbraco/PartialViewMacros/Templates/ListImagesFromMediaFolder.cshtml b/src/Umbraco.Web.UI/umbraco/PartialViewMacros/Templates/ListImagesFromMediaFolder.cshtml index 2ef595992d..8148612409 100644 --- a/src/Umbraco.Web.UI/umbraco/PartialViewMacros/Templates/ListImagesFromMediaFolder.cshtml +++ b/src/Umbraco.Web.UI/umbraco/PartialViewMacros/Templates/ListImagesFromMediaFolder.cshtml @@ -16,7 +16,7 @@ Alias:mediaId Name:Select folder with images Type:Single Media Picker *@ -@{ var mediaId = Model.MacroParameters["mediaId"]; } +@{ var mediaId = Model?.MacroParameters["mediaId"]; } @if (mediaId != null) { diff --git a/src/Umbraco.Web.UI/umbraco/PartialViewMacros/Templates/LoginStatus.cshtml b/src/Umbraco.Web.UI/umbraco/PartialViewMacros/Templates/LoginStatus.cshtml index bca0a23c43..8f5477bca4 100644 --- a/src/Umbraco.Web.UI/umbraco/PartialViewMacros/Templates/LoginStatus.cshtml +++ b/src/Umbraco.Web.UI/umbraco/PartialViewMacros/Templates/LoginStatus.cshtml @@ -15,7 +15,7 @@ { + +
            - @if (!string.IsNullOrWhiteSpace(Model.PreviewExtendedHeaderView)) + @if (!string.IsNullOrWhiteSpace(Model?.PreviewExtendedHeaderView)) { @await Html.PartialAsync(Model.PreviewExtendedHeaderView) } @@ -77,7 +77,7 @@
- @if (Model.Languages != null && Model.Languages.Count() > 1) + @if (Model?.Languages.Any() == true) {
diff --git a/src/Umbraco.Web.UI/umbraco/UmbracoWebsite/NoNodes.cshtml b/src/Umbraco.Web.UI/umbraco/UmbracoWebsite/NoNodes.cshtml index a19390f8ec..69d8318a1f 100644 --- a/src/Umbraco.Web.UI/umbraco/UmbracoWebsite/NoNodes.cshtml +++ b/src/Umbraco.Web.UI/umbraco/UmbracoWebsite/NoNodes.cshtml @@ -1,4 +1,4 @@ -@using Microsoft.Extensions.Options +@using Microsoft.Extensions.Options @using Umbraco.Cms.Core.Configuration.Models @using Umbraco.Cms.Core.Hosting @using Umbraco.Cms.Core.Routing @@ -31,7 +31,7 @@

You're seeing this wonderful page because your website doesn't contain any published content yet.

diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/cs.xml b/src/Umbraco.Web.UI/umbraco/config/lang/cs.xml index fbbf9a495a..939b515eeb 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/cs.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/cs.xml @@ -16,7 +16,7 @@ Vytvořit skupinu Odstranit Deaktivovat - Vyprázdnit koš + Vyprázdnit koš Aktivovat Exportovat typ dokumentu Importovat typ dokumentu diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/cy.xml b/src/Umbraco.Web.UI/umbraco/config/lang/cy.xml index 94fde5ebfe..d0c6a45d27 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/cy.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/cy.xml @@ -18,7 +18,7 @@ Dileu Analluogi Golygu gosodiadau - Gwagu bin ailgylchu + Gwagu bin ailgylchu Galluogi Allforio Math o Ddogfen Mewnforio Math o Ddogfen diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/da.xml b/src/Umbraco.Web.UI/umbraco/config/lang/da.xml index 5b90b7511d..5ec919874f 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/da.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/da.xml @@ -18,7 +18,7 @@ Slet Deaktivér Edit settings - Tøm papirkurv + Tøm papirkurv Aktivér Eksportér dokumenttype Importér dokumenttype @@ -1323,6 +1323,8 @@ Mange hilsner fra Umbraco robotten %0% dokumenter udgivet og synlige på hjemmesiden %0% udgivet og synligt på hjemmesiden %0% dokumenter udgivet for sprogene %1% og synlige på hjemmesiden + Indholdsskabelon gemt + Rettelser er blevet gemt Indhold gemt Husk at publicere for at gøre det synligt for besøgende En planlægning for udgivelse er blevet opdateret diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/de.xml b/src/Umbraco.Web.UI/umbraco/config/lang/de.xml index 0366fe6853..24999a1187 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/de.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/de.xml @@ -16,7 +16,7 @@ Neue Gruppe Entfernen Deaktivieren - Papierkorb leeren + Papierkorb leeren Aktivieren Dokumenttyp exportieren Dokumenttyp importieren diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml index 4fc52fc0a7..6b5d301c5f 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml @@ -17,7 +17,7 @@ Delete Disable Edit settings - Empty recycle bin + Empty recycle bin Enable Export Document Type Import Document Type @@ -332,6 +332,7 @@ Click to upload or click here to choose files Cannot upload this file, it does not have an approved file type + Cannot upload this file, the media type with alias '%0%' is not allowed here Cannot upload this file, it does not have a valid file name Max file size is Media root @@ -1292,6 +1293,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Please try searching for another package or browse through the categories Popular + Promoted New releases has karma points @@ -1506,6 +1508,8 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Insufficient user permissions, could not complete the operation Cancelled Operation was cancelled by a 3rd party add-in + This file is being uploaded as part of a folder, but creating a new folder is not allowed here + Creating a new folder is not allowed here Publishing was cancelled by a 3rd party add-in Property type already exists Property type created @@ -1523,6 +1527,8 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Publishing failed because the parent page isn't published Content published and visible on the website + Content Template saved + Changes have been successfully saved Content saved Remember to publish to make changes visible Sent For Approval @@ -2389,6 +2395,9 @@ To manage your website, simply open the Umbraco backoffice and start adding cont No relations for this Relation Type Relation Type Relations + Is Dependency + Yes + No Getting Started @@ -2466,6 +2475,15 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Used in Documents Used in Members Used in Media + Items in use + Descendants in use + One or more of this item's descendants is being used in a media item. + One or more of this item's descendants is being used in a content item. + One or more of this item's descendants is being used in a member. + This item or its descendants is being used. Deletion can lead to broken links on your website. + This item or its descendants is being used. Unpublishing can lead to broken links on your website. Please take the appropriate actions. + This item or its descendants is being used. Therefore, deletion has been disabled. + The following items you are trying to %0% are used by other content. Delete Saved Search @@ -2619,9 +2637,9 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Ask a question in the Community Forum ]]> - + tutorial videos (some are free, some require a subscription) + Watch our free tutorial videos on the Umbraco Learning Base ]]> diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml index 2d575ba77f..e056647ebc 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml @@ -18,7 +18,7 @@ Delete Disable Edit settings - Empty recycle bin + Empty recycle bin Enable Export Document Type Import Document Type @@ -335,6 +335,7 @@ Click to upload or click here to choose files Cannot upload this file, it does not have an approved file type + Cannot upload this file, the media type with alias '%0%' is not allowed here Cannot upload this file, it does not have a valid file name Max file size is Media root @@ -808,6 +809,7 @@ New Next No + Node Name of Off OK @@ -848,6 +850,7 @@ Submit Success Type + Type Name Type to search... under Up @@ -1310,6 +1313,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Please try searching for another package or browse through the categories Popular + Promoted New releases has karma points @@ -1531,6 +1535,8 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Insufficient user permissions, could not complete the operation Cancelled Operation was cancelled by a 3rd party add-in + This file is being uploaded as part of a folder, but creating a new folder is not allowed here + Creating a new folder is not allowed here Property type already exists Property type created DataType: %1%]]> @@ -1548,8 +1554,9 @@ To manage your website, simply open the Umbraco backoffice and start adding cont and is visible on the website %0% documents published and visible on the website %0% published and visible on the website - %0% documents published for languages %1% and visible on the website - + %0% documents published for languages %1% and visible on the website + Content Template saved + Changes have been successfully saved Content saved Remember to publish to make changes visible A schedule for publishing has been updated @@ -2465,6 +2472,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Parent Child Count + Relation Relations Created Comment @@ -2472,6 +2480,9 @@ To manage your website, simply open the Umbraco backoffice and start adding cont No relations for this Relation Type Relation Type Relations + Is Dependency + Yes + No Getting Started @@ -2542,13 +2553,26 @@ To manage your website, simply open the Umbraco backoffice and start adding cont References This Data Type has no references. - Used in Document Types - Used in Media Types - Used in Member Types - Used by - Used in Documents - Used in Members - Used in Media + This item has no references. + Referenced by the following Document Types + Referenced by the following Media Types + Referenced by the following Member Types + Referenced by + Referenced by the following items + The following items depends on this + Referenced by the following Documents + Referenced by the following Members + Referenced by the following Media + The following items are referenced + The following descending items are referenced + The following descending items has dependencies + One or more of this item's descendants is being referenced in a media item. + One or more of this item's descendants is being referenced in a content item. + One or more of this item's descendants is being referenced in a member. + This item or its descendants is being referenced. Deletion can lead to broken links on your website. + This item or its descendants is being referenced. Unpublishing can lead to broken links on your website. Please take the appropriate actions. + This item or its descendants is being referenced. Therefore, deletion has been disabled. + The following items you are trying to %0% are referenced by other content. Delete Saved Search @@ -2702,9 +2726,9 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Ask a question in the Community Forum ]]> - + tutorial videos (some are free, some require a subscription) + Watch our free tutorial videos on the Umbraco Learning Base ]]> diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/es.xml b/src/Umbraco.Web.UI/umbraco/config/lang/es.xml index ec8dfbc895..2c64f3f9a6 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/es.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/es.xml @@ -15,7 +15,7 @@ Crear grupo Borrar Deshabilitar - Vaciar Papelera + Vaciar Papelera Activar Exportar Documento (tipo) Importar Documento (tipo) diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/fr.xml b/src/Umbraco.Web.UI/umbraco/config/lang/fr.xml index 4f292c0889..9013a4473f 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/fr.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/fr.xml @@ -16,7 +16,7 @@ Créer un groupe Supprimer Désactiver - Vider la corbeille + Vider la corbeille Activer Exporter le type de document Importer un type de document diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/he.xml b/src/Umbraco.Web.UI/umbraco/config/lang/he.xml index 403ea645fc..0996c81ba0 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/he.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/he.xml @@ -13,7 +13,7 @@ צור חבילה מחק נטרל - רוקן סל מיחזור + רוקן סל מיחזור ייצא סוג קובץ ייבא סוג מסמך ייבא חבילה diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/it.xml b/src/Umbraco.Web.UI/umbraco/config/lang/it.xml index 357d63e84b..23bff095a3 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/it.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/it.xml @@ -18,7 +18,7 @@ Cancella Disabilita Modifica impostazioni - Svuota il cestino + Svuota il cestino Abilita Esporta il tipo di documento Importa il tipo di documento diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/ja.xml b/src/Umbraco.Web.UI/umbraco/config/lang/ja.xml index 1107181cab..2d4c80570b 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/ja.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/ja.xml @@ -14,7 +14,7 @@ パッケージの作成 削除 無効 - ごみ箱を空にする + ごみ箱を空にする ドキュメントタイプの書出 ドキュメントタイプの読込 パッケージの読み込み diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/ko.xml b/src/Umbraco.Web.UI/umbraco/config/lang/ko.xml index 68cff8a318..852d8765aa 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/ko.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/ko.xml @@ -13,7 +13,7 @@ 패키지 새로 만들기 삭제 비활성 - 휴지통 비우기 + 휴지통 비우기 추출 문서 유형 등록 문서 유형 패키지 등록 diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/nb.xml b/src/Umbraco.Web.UI/umbraco/config/lang/nb.xml index 341adad4bf..30d2da3e4f 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/nb.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/nb.xml @@ -14,7 +14,7 @@ Opprett pakke Slett Deaktiver - Tøm papirkurv + Tøm papirkurv Eksporter dokumenttype Importer dokumenttype Importer pakke diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/nl.xml b/src/Umbraco.Web.UI/umbraco/config/lang/nl.xml index fb6167520b..d9ecc89673 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/nl.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/nl.xml @@ -18,7 +18,7 @@ Verwijderen Uitschakelen Instellingen wijzigen - Prullenbak leegmaken + Prullenbak leegmaken Inschakelen Documenttype exporteren Documenttype importeren @@ -533,6 +533,7 @@ Er zijn geen woordenboekitems. + Woordenboekitem aanmaken Stwórz grupę Usuń Deaktywuj - Opróżnij kosz + Opróżnij kosz Aktywuj Eksportuj typ dokumentu Importuj typ dokumentu diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/pt.xml b/src/Umbraco.Web.UI/umbraco/config/lang/pt.xml index c69912099c..39d0cfc4a1 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/pt.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/pt.xml @@ -13,7 +13,7 @@ Criar Pacote Remover Desabilitar - Esvaziar Lixeira + Esvaziar Lixeira Exportar Tipo de Documento Importar Tipo de Documento Importar Pacote diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/ru.xml b/src/Umbraco.Web.UI/umbraco/config/lang/ru.xml index 24910c2e6f..d5b31e47d2 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/ru.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/ru.xml @@ -17,7 +17,7 @@ Значение по умолчанию Удалить Отключить - Очистить корзину + Очистить корзину Включить Экспорт Экспортировать diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/sv.xml b/src/Umbraco.Web.UI/umbraco/config/lang/sv.xml index 0efdedfff3..ad6f18b4cd 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/sv.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/sv.xml @@ -20,7 +20,7 @@ Standardvärde Ta bort Avaktivera - Töm papperskorgen + Töm papperskorgen Exportera dokumenttyp Importera dokumenttyp Importera paket diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/tr.xml b/src/Umbraco.Web.UI/umbraco/config/lang/tr.xml index f54fc31076..3ef3db0ad6 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/tr.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/tr.xml @@ -17,7 +17,7 @@ Sil Devre Dışı Bırak Ayarları düzenle - Geri dönüşüm kutusunu boşalt + Geri dönüşüm kutusunu boşalt Etkinleştir Belge Türünü Dışa Aktar Belge Türünü İçe Aktar diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/zh.xml b/src/Umbraco.Web.UI/umbraco/config/lang/zh.xml index 8e7ca24f32..d8132c151b 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/zh.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/zh.xml @@ -14,7 +14,7 @@ 创建扩展包 删除 禁用 - 清空回收站 + 清空回收站 导出文档类型 导入文档类型 导入扩展包 diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/zh_tw.xml b/src/Umbraco.Web.UI/umbraco/config/lang/zh_tw.xml index 0102971ae9..216dc3d0fe 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/zh_tw.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/zh_tw.xml @@ -14,7 +14,7 @@ 創建擴展包 刪除 禁用 - 清空回收站 + 清空回收站 匯出文檔類型 導入文檔類型 導入擴展包 diff --git a/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs b/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs index 9106c3ed09..60384de752 100644 --- a/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs +++ b/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs @@ -201,23 +201,23 @@ namespace Umbraco.Cms.Web.Website.Routing throw new ArgumentNullException(nameof(httpContext)); } - // if it is a POST/GET then a value must be in the request - if ((!httpContext.Request.HasFormContentType || !httpContext.Request.Form.TryGetValue("ufprt", out StringValues encodedVal)) - && !httpContext.Request.Query.TryGetValue("ufprt", out encodedVal)) + // if it is a POST/GET then a `ufprt` value must be in the request + var ufprt = httpContext.Request.GetUfprt(); + if (string.IsNullOrWhiteSpace(ufprt)) { return null; } if (!EncryptionHelper.DecryptAndValidateEncryptedRouteString( _dataProtectionProvider, - encodedVal, - out IDictionary decodedParts)) + ufprt, + out IDictionary decodedUfprt)) { return null; } // Get all route values that are not the default ones and add them separately so they eventually get to action parameters - foreach (KeyValuePair item in decodedParts.Where(x => ReservedAdditionalKeys.AllKeys.Contains(x.Key) == false)) + foreach (KeyValuePair item in decodedUfprt.Where(x => ReservedAdditionalKeys.AllKeys.Contains(x.Key) == false)) { values[item.Key] = item.Value; } @@ -225,9 +225,9 @@ namespace Umbraco.Cms.Web.Website.Routing // return the proxy info without the surface id... could be a local controller. return new PostedDataProxyInfo { - ControllerName = WebUtility.UrlDecode(decodedParts.First(x => x.Key == ReservedAdditionalKeys.Controller).Value), - ActionName = WebUtility.UrlDecode(decodedParts.First(x => x.Key == ReservedAdditionalKeys.Action).Value), - Area = WebUtility.UrlDecode(decodedParts.First(x => x.Key == ReservedAdditionalKeys.Area).Value), + ControllerName = WebUtility.UrlDecode(decodedUfprt.First(x => x.Key == ReservedAdditionalKeys.Controller).Value), + ActionName = WebUtility.UrlDecode(decodedUfprt.First(x => x.Key == ReservedAdditionalKeys.Action).Value), + Area = WebUtility.UrlDecode(decodedUfprt.First(x => x.Key == ReservedAdditionalKeys.Area).Value), }; } diff --git a/src/Umbraco.Web.Website/Security/MemberAuthenticationBuilder.cs b/src/Umbraco.Web.Website/Security/MemberAuthenticationBuilder.cs index d58abfc871..8308abafff 100644 --- a/src/Umbraco.Web.Website/Security/MemberAuthenticationBuilder.cs +++ b/src/Umbraco.Web.Website/Security/MemberAuthenticationBuilder.cs @@ -41,7 +41,7 @@ namespace Umbraco.Cms.Web.Website.Security // Validate that the prefix is set if (!authenticationScheme.StartsWith(Constants.Security.MemberExternalAuthenticationTypePrefix)) { - throw new InvalidOperationException($"The {nameof(authenticationScheme)} is not prefixed with {Constants.Security.BackOfficeExternalAuthenticationTypePrefix}. The scheme must be created with a call to the method {nameof(SchemeForMembers)}"); + throw new InvalidOperationException($"The {nameof(authenticationScheme)} is not prefixed with {Constants.Security.MemberExternalAuthenticationTypePrefix}. The scheme must be created with a call to the method {nameof(SchemeForMembers)}"); } // add our login provider to the container along with a custom options configuration diff --git a/tests/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/relationTypes.ts b/tests/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/relationTypes.ts index 4773f37c37..1b903d4b76 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/relationTypes.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/relationTypes.ts @@ -25,6 +25,8 @@ context('Relation Types', () => { cy.get('select[name="relationType-parent"]').select('Document'); cy.get('select[name="relationType-child"]').select('Media'); + + cy.get('[name="relationType-isdependency"]').last().click({force: true}) cy.get(".btn-primary").click(); }); diff --git a/tests/Umbraco.Tests.AcceptanceTest/cypress/integration/Users/userGroups.js b/tests/Umbraco.Tests.AcceptanceTest/cypress/integration/Users/userGroups.js deleted file mode 100644 index ce2e366f2c..0000000000 --- a/tests/Umbraco.Tests.AcceptanceTest/cypress/integration/Users/userGroups.js +++ /dev/null @@ -1,35 +0,0 @@ -context('User Groups', () => { - - beforeEach(() => { - cy.umbracoLogin(Cypress.env('username'), Cypress.env('password')); - }); - - it('Create user group', () => { - const name = "Test Group"; - - cy.umbracoEnsureUserGroupNameNotExists(name); - - cy.umbracoSection('users'); - cy.get('[data-element="sub-view-userGroups"]').click(); - - cy.umbracoButtonByLabelKey("actions_createGroup").click(); - - //Type name - cy.umbracoEditorHeaderName(name); - - // Assign sections - cy.get('.umb-box:nth-child(1) .umb-property:nth-child(1) localize').click(); - cy.get('.umb-tree-item__inner').click({multiple:true, timeout: 10000}); - cy.get('.btn-success').last().click(); - - // Save - cy.get('.btn-success').click(); - - //Assert - cy.umbracoSuccessNotification().should('be.visible'); - - //Clean up - cy.umbracoEnsureUserGroupNameNotExists(name); - }); - -}); diff --git a/tests/Umbraco.Tests.AcceptanceTest/cypress/integration/Users/userGroups.ts b/tests/Umbraco.Tests.AcceptanceTest/cypress/integration/Users/userGroups.ts new file mode 100644 index 0000000000..cd4b022544 --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/cypress/integration/Users/userGroups.ts @@ -0,0 +1,85 @@ +/// +import { UserGroupBuilder } from 'umbraco-cypress-testhelpers'; + +context('User Groups', () => { + + function navigateToUserGroups() { + cy.umbracoSection('users'); + cy.get('[data-element="sub-view-userGroups"]').click(); + } + + beforeEach(() => { + cy.umbracoLogin(Cypress.env('username'), Cypress.env('password')); + }); + + it('Create user group', () => { + const name = "Test Group"; + + cy.umbracoEnsureUserGroupNameNotExists(name); + + navigateToUserGroups(); + cy.umbracoButtonByLabelKey("actions_createGroup").click(); + + //Type name + cy.umbracoEditorHeaderName(name); + + // Assign sections + cy.get('.umb-box:nth-child(1) .umb-property:nth-child(1) localize').click(); + cy.get('.umb-tree-item__inner').click({multiple:true, timeout: 10000}); + cy.get('.btn-success').last().click(); + + // Save + cy.get('.btn-success').click(); + + //Assert + cy.umbracoSuccessNotification().should('be.visible'); + + //Clean up + cy.umbracoEnsureUserGroupNameNotExists(name); + }); + + it('Can delete user group', () => { + // Create user group + const groupName = "Delete user group test" + cy.umbracoEnsureUserGroupNameNotExists(groupName); + + const userGroup = new UserGroupBuilder() + .withName(groupName) + .build(); + + cy.saveUserGroup(userGroup); + navigateToUserGroups(); + + // Delete the user group + cy.get('.umb-table-body > :nth-child(2)').click(); + cy.umbracoButtonByLabelKey("general_delete").click(); + cy.get('umb-button[alias="overlaySubmit"]').click(); + + cy.umbracoSuccessNotification().should('be.visible'); + cy.get('.umb-table-body').contains(groupName).should('not.exist'); + + cy.umbracoEnsureUserGroupNameNotExists(groupName); + }); + + it('Cannot delete required groups', () => { + navigateToUserGroups(); + + // There's not really a good way to be 100% sure we'll get the admin group, it should be first, but who knows + // so double check that we actually got the correct one + const administrators = cy.get('.umb-table-body > :nth-child(1)'); + administrators.should('contain', 'Administrators'); + administrators.click({force: true}); + + const sensitive = cy.get('.umb-table-body > :nth-child(3)'); + sensitive.should('contain', 'Sensitive data'); + sensitive.click({force: true}); + + const translators = cy.get('.umb-table-body > :nth-child(4)'); + translators.should('contain', 'Translators'); + translators.click({force: true}); + + // Now that we've clicked all that we shouldn't be able to delete, ensure that the delete button does not show up + cy.get('.umb-editor-sub-header').should('not.contain', 'Delete'); + }); +}); + diff --git a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json index dcead8cc07..4beacd8e3c 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json +++ b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json @@ -1,7 +1,2343 @@ { "name": "acceptancetest", + "lockfileVersion": 2, "requires": true, - "lockfileVersion": 1, + "packages": { + "": { + "name": "acceptancetest", + "hasInstallScript": true, + "dependencies": { + "typescript": "^3.9.2" + }, + "devDependencies": { + "cross-env": "^7.0.2", + "cy-verify-downloads": "0.0.5", + "cypress": "8.4.1", + "del": "^6.0.0", + "ncp": "^2.0.0", + "prompt": "^1.2.0", + "umbraco-cypress-testhelpers": "^1.0.0-beta-66" + } + }, + "node_modules/@cypress/request": { + "version": "2.88.7", + "resolved": "https://registry.npmjs.org/@cypress/request/-/request-2.88.7.tgz", + "integrity": "sha512-FTULIP2rnDJvZDT9t6B4nSfYR40ue19tVmv3wUcY05R9/FPCoMl1nAPJkzWzBCo7ltVn5ThQTbxiMoGBN7k0ig==", + "dev": true, + "dependencies": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^8.3.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@cypress/xvfb": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@cypress/xvfb/-/xvfb-1.2.4.tgz", + "integrity": "sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q==", + "dev": true, + "dependencies": { + "debug": "^3.1.0", + "lodash.once": "^4.1.1" + } + }, + "node_modules/@cypress/xvfb/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@types/node": { + "version": "14.17.33", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.33.tgz", + "integrity": "sha512-noEeJ06zbn3lOh4gqe2v7NMGS33jrulfNqYFDjjEbhpDEHR5VTxgYNQSBqBlJIsBJW3uEYDgD6kvMnrrhGzq8g==", + "dev": true + }, + "node_modules/@types/sinonjs__fake-timers": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.4.tgz", + "integrity": "sha512-IFQTJARgMUBF+xVd2b+hIgXWrZEjND3vJtRCvIelcFB5SIXfjV4bOHbHJ0eXKh+0COrBRc8MqteKAz/j88rE0A==", + "dev": true + }, + "node_modules/@types/sizzle": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.3.tgz", + "integrity": "sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==", + "dev": true + }, + "node_modules/@types/yauzl": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.9.2.tgz", + "integrity": "sha512-8uALY5LTvSuHgloDVUvWP3pIauILm+8/0pDMokuDYIoNsOkSwd5AiHBTSEJjKTDcZr5z8UpgOWZkxBF4iJftoA==", + "dev": true, + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/aggregate-error/node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/arch": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", + "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "dev": true, + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/async": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.2.tgz", + "integrity": "sha512-H0E+qZaDEfx/FY4t7iLRv1W2fFI6+pyCeTw1uN20AQPiwqwM6ojPxHxdLv4z8hi2DtnW9BOckSspLucW7pIE5g==", + "dev": true + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/aws4": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", + "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==", + "dev": true + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "dev": true, + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/blob-util": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/blob-util/-/blob-util-2.0.2.tgz", + "integrity": "sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ==", + "dev": true + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/cachedir": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.3.0.tgz", + "integrity": "sha512-A+Fezp4zxnit6FanDmv9EqXNAi3vt9DWp51/71UEhXukb7QUuvtv9344h91dyAxuTLoSYJFU299qzR3tzwPAhw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelize": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.0.tgz", + "integrity": "sha1-FkpUg+Yw+kMh5a8HAg5TGDGyYJs=", + "dev": true + }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", + "dev": true + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/check-more-types": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/check-more-types/-/check-more-types-2.24.0.tgz", + "integrity": "sha1-FCD/sQ/URNz8ebQ4kbv//TKoRgA=", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ci-info": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.2.0.tgz", + "integrity": "sha512-dVqRX7fLUm8J6FgHJ418XuIgDLZDkYcDFTeL6TA2gt5WlIZUQrrH6EZrNClwT/H0FateUsZkGIOPRrLbP+PR9A==", + "dev": true + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-table3": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.0.tgz", + "integrity": "sha512-gnB85c3MGC7Nm9I/FkiasNBOKjOiO1RNuXXarQms37q4QMpWdlbBgD/VnOStA2faG1dpXMv31RFApjX1/QdgWQ==", + "dev": true, + "dependencies": { + "object-assign": "^4.1.0", + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "colors": "^1.1.2" + } + }, + "node_modules/cli-truncate": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", + "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", + "dev": true, + "dependencies": { + "slice-ansi": "^3.0.0", + "string-width": "^4.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/colorette": { + "version": "2.0.16", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.16.tgz", + "integrity": "sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g==", + "dev": true + }, + "node_modules/colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", + "dev": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/common-tags": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.1.tgz", + "integrity": "sha512-uOZd85rJqrdEIE/JjhW5YAeatX8iqjjvVzIyfx7JL7G5r9Tep6YpYT9gEJWhWpVyDQEyzukWd6p2qULpJ8tmBw==", + "dev": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true + }, + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cy-verify-downloads": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/cy-verify-downloads/-/cy-verify-downloads-0.0.5.tgz", + "integrity": "sha512-aRK7VvKG5rmDJK4hjZ27KM2oOOz0cMO7z/j4zX8qCc4ffXZS1XRJkofUY0w5u6MCB/wUsNMs03VuvkeR2tNPoQ==", + "dev": true + }, + "node_modules/cycle": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cycle/-/cycle-1.0.3.tgz", + "integrity": "sha1-IegLK+hYD5i0aPN5QwZisEbDStI=", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/cypress": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-8.4.1.tgz", + "integrity": "sha512-itJXq0Vx3sXCUrDyBi2IUrkxVu/gTTp1VhjB5tzGgkeCR8Ae+/T8WV63rsZ7fS8Tpq7LPPXiyoM/sEdOX7cR6A==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@cypress/request": "^2.88.6", + "@cypress/xvfb": "^1.2.4", + "@types/node": "^14.14.31", + "@types/sinonjs__fake-timers": "^6.0.2", + "@types/sizzle": "^2.3.2", + "arch": "^2.2.0", + "blob-util": "^2.0.2", + "bluebird": "^3.7.2", + "cachedir": "^2.3.0", + "chalk": "^4.1.0", + "check-more-types": "^2.24.0", + "cli-cursor": "^3.1.0", + "cli-table3": "~0.6.0", + "commander": "^5.1.0", + "common-tags": "^1.8.0", + "dayjs": "^1.10.4", + "debug": "^4.3.2", + "enquirer": "^2.3.6", + "eventemitter2": "^6.4.3", + "execa": "4.1.0", + "executable": "^4.1.1", + "extract-zip": "2.0.1", + "figures": "^3.2.0", + "fs-extra": "^9.1.0", + "getos": "^3.2.1", + "is-ci": "^3.0.0", + "is-installed-globally": "~0.4.0", + "lazy-ass": "^1.6.0", + "listr2": "^3.8.3", + "lodash": "^4.17.21", + "log-symbols": "^4.0.0", + "minimist": "^1.2.5", + "ospath": "^1.2.2", + "pretty-bytes": "^5.6.0", + "ramda": "~0.27.1", + "request-progress": "^3.0.0", + "supports-color": "^8.1.1", + "tmp": "~0.2.1", + "untildify": "^4.0.0", + "url": "^0.11.0", + "yauzl": "^2.10.0" + }, + "bin": { + "cypress": "bin/cypress" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "dev": true, + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/dayjs": { + "version": "1.10.7", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.10.7.tgz", + "integrity": "sha512-P6twpd70BcPK34K26uJ1KT3wlhpuOAPoMwJzpsIWUxHZ7wpmbdZL/hQqBDfz7hGurYSa5PhzdhDHtt319hL3ig==", + "dev": true + }, + "node_modules/debug": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/debug/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/del": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/del/-/del-6.0.0.tgz", + "integrity": "sha512-1shh9DQ23L16oXSZKB2JxpL7iMy2E0S9d517ptA1P8iw0alkPtQcrKH7ru31rYtKwF499HkTu+DRzq3TCKDFRQ==", + "dev": true, + "dependencies": { + "globby": "^11.0.1", + "graceful-fs": "^4.2.4", + "is-glob": "^4.0.1", + "is-path-cwd": "^2.2.0", + "is-path-inside": "^3.0.2", + "p-map": "^4.0.0", + "rimraf": "^3.0.2", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "dev": true, + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/enquirer": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", + "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", + "dev": true, + "dependencies": { + "ansi-colors": "^4.1.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eventemitter2": { + "version": "6.4.5", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.5.tgz", + "integrity": "sha512-bXE7Dyc1i6oQElDG0jMRZJrRAn9QR2xyyFGmBdZleNmyQX0FqGYmhZIrIrpPfm/w//LTo4tVQGOGQcGCb5q9uw==", + "dev": true + }, + "node_modules/execa": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz", + "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "get-stream": "^5.0.0", + "human-signals": "^1.1.1", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.0", + "onetime": "^5.1.0", + "signal-exit": "^3.0.2", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/executable": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/executable/-/executable-4.1.1.tgz", + "integrity": "sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==", + "dev": true, + "dependencies": { + "pify": "^2.2.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", + "dev": true, + "engines": [ + "node >=0.6.0" + ] + }, + "node_modules/eyes": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", + "integrity": "sha1-Ys8SAjTGg3hdkCNIqADvPgzCC8A=", + "dev": true, + "engines": { + "node": "> 0.1.90" + } + }, + "node_modules/faker": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/faker/-/faker-4.1.0.tgz", + "integrity": "sha1-HkW7vsxndLPBlfrSg1EJxtdIzD8=", + "dev": true + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.7.tgz", + "integrity": "sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", + "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=", + "dev": true, + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/getos": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/getos/-/getos-3.2.1.tgz", + "integrity": "sha512-U56CfOK17OKgTVqozZjUKNdkfEv6jk5WISBJ8SHoagjE6L69zOwl3Z+O8myjY9MEW3i2HPWQBt/LTbCgcC973Q==", + "dev": true, + "dependencies": { + "async": "^3.2.0" + } + }, + "node_modules/getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "dev": true, + "dependencies": { + "assert-plus": "^1.0.0" + } + }, + "node_modules/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/global-dirs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.0.tgz", + "integrity": "sha512-v8ho2DS5RiCjftj1nD9NmnfaOzTdud7RRnVd9kFNOjqZbISlx5DQ+OrTkywgd0dIt7oFCvKetZSHoHcP3sDdiA==", + "dev": true, + "dependencies": { + "ini": "2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.4.tgz", + "integrity": "sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.1.1", + "ignore": "^5.1.4", + "merge2": "^1.3.0", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz", + "integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==", + "dev": true + }, + "node_modules/har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/har-validator": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", + "deprecated": "this library is no longer supported", + "dev": true, + "dependencies": { + "ajv": "^6.12.3", + "har-schema": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "dev": true, + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + }, + "engines": { + "node": ">=0.8", + "npm": ">=1.3.7" + } + }, + "node_modules/human-signals": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", + "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", + "dev": true, + "engines": { + "node": ">=8.12.0" + } + }, + "node_modules/ignore": { + "version": "5.1.8", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", + "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/ini": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", + "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/is-ci": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", + "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", + "dev": true, + "dependencies": { + "ci-info": "^3.2.0" + }, + "bin": { + "is-ci": "bin.js" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-installed-globally": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz", + "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==", + "dev": true, + "dependencies": { + "global-dirs": "^3.0.0", + "is-path-inside": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-cwd": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", + "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "dev": true + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", + "dev": true + }, + "node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "dev": true + }, + "node_modules/json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "dev": true + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "node_modules/lazy-ass": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/lazy-ass/-/lazy-ass-1.6.0.tgz", + "integrity": "sha1-eZllXoZGwX8In90YfRUNMyTVRRM=", + "dev": true, + "engines": { + "node": "> 0.8" + } + }, + "node_modules/listr2": { + "version": "3.13.3", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-3.13.3.tgz", + "integrity": "sha512-VqAgN+XVfyaEjSaFewGPcDs5/3hBbWVaX1VgWv2f52MF7US45JuARlArULctiB44IIcEk3JF7GtoFCLqEdeuPA==", + "dev": true, + "dependencies": { + "cli-truncate": "^2.1.0", + "clone": "^2.1.2", + "colorette": "^2.0.16", + "log-update": "^4.0.0", + "p-map": "^4.0.0", + "rxjs": "^7.4.0", + "through": "^2.3.8", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "enquirer": ">= 2.3.0 < 3" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=", + "dev": true + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz", + "integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==", + "dev": true, + "dependencies": { + "ansi-escapes": "^4.3.0", + "cli-cursor": "^3.1.0", + "slice-ansi": "^4.0.0", + "wrap-ansi": "^6.2.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", + "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", + "dev": true, + "dependencies": { + "braces": "^3.0.1", + "picomatch": "^2.2.3" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.51.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz", + "integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.34", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz", + "integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==", + "dev": true, + "dependencies": { + "mime-db": "1.51.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true + }, + "node_modules/ncp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", + "integrity": "sha1-GVoh1sRuNh0vsSgbo4uR6d9727M=", + "dev": true, + "bin": { + "ncp": "bin/ncp" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ospath": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/ospath/-/ospath-1.2.2.tgz", + "integrity": "sha1-EnZjl3Sj+O8lcvf+QoDg6kVQwHs=", + "dev": true + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=", + "dev": true + }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", + "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pretty-bytes": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", + "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/prompt": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/prompt/-/prompt-1.2.0.tgz", + "integrity": "sha512-iGerYRpRUg5ZyC+FJ/25G5PUKuWAGRjW1uOlhX7Pi3O5YygdK6R+KEaBjRbHSkU5vfS5PZCltSPZdDtUYwRCZA==", + "dev": true, + "dependencies": { + "async": "~0.9.0", + "colors": "^1.1.2", + "read": "1.0.x", + "revalidator": "0.1.x", + "winston": "2.x" + }, + "engines": { + "node": ">= 0.6.6" + } + }, + "node_modules/prompt/node_modules/async": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz", + "integrity": "sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0=", + "dev": true + }, + "node_modules/psl": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", + "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==", + "dev": true + }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", + "dev": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", + "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", + "dev": true, + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/ramda": { + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.27.1.tgz", + "integrity": "sha512-PgIdVpn5y5Yns8vqb8FzBUEYn98V3xcPgawAkkgj0YJ0qDsnHCiNmZYfOGMgOvoB0eWFLpYbhxUR3mxfDIMvpw==", + "dev": true + }, + "node_modules/read": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", + "integrity": "sha1-s9oZvQUkMal2cdRKQmNK33ELQMQ=", + "dev": true, + "dependencies": { + "mute-stream": "~0.0.4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/request-progress": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-3.0.0.tgz", + "integrity": "sha1-TKdUCBx/7GP1BeT6qCWqBs1mnb4=", + "dev": true, + "dependencies": { + "throttleit": "^1.0.0" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/revalidator": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/revalidator/-/revalidator-0.1.8.tgz", + "integrity": "sha1-/s5hv6DBtSoga9axgZgYS91SOjs=", + "dev": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rxjs": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.4.0.tgz", + "integrity": "sha512-7SQDi7xeTMCJpqViXh8gL/lebcwlp3d831F05+9B44A4B0WfsEwUQHR64gsH1kvJ+Ep/J9K2+n1hVl1CsGN23w==", + "dev": true, + "dependencies": { + "tslib": "~2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.5.tgz", + "integrity": "sha512-KWcOiKeQj6ZyXx7zq4YxSMgHRlod4czeBQZrPb8OKcohcqAXShm7E20kEMle9WBt26hFcAf0qLOcp5zmY7kOqQ==", + "dev": true + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/slice-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", + "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/sshpk": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", + "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", + "dev": true, + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/throttleit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.0.tgz", + "integrity": "sha1-nnhYNtr0Z0MUWlmEtiaNgoUorGw=", + "dev": true + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "node_modules/tmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "dev": true, + "dependencies": { + "rimraf": "^3.0.0" + }, + "engines": { + "node": ">=8.17.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "dev": true, + "dependencies": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tslib": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz", + "integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==", + "dev": true + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "dev": true, + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "dev": true + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "3.9.10", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.10.tgz", + "integrity": "sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q==", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/umbraco-cypress-testhelpers": { + "version": "1.0.0-beta-66", + "resolved": "https://registry.npmjs.org/umbraco-cypress-testhelpers/-/umbraco-cypress-testhelpers-1.0.0-beta-66.tgz", + "integrity": "sha512-/Iq0P7rN9LfODO9snoLNqvbd8b432JIYtCVjYOdYZFceMAUIv0v2/6t7+N55Z7h8OpAQzcTLU3VCxfPzZp8wQw==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "camelize": "^1.0.0", + "faker": "^4.1.0" + }, + "peerDependencies": { + "cross-env": "^7.0.2", + "cypress": "^8.0.0", + "ncp": "^2.0.0" + } + }, + "node_modules/universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/untildify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", + "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/url": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", + "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", + "dev": true, + "dependencies": { + "punycode": "1.3.2", + "querystring": "0.2.0" + } + }, + "node_modules/url/node_modules/punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", + "dev": true + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/winston": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/winston/-/winston-2.4.5.tgz", + "integrity": "sha512-TWoamHt5yYvsMarGlGEQE59SbJHqGsZV8/lwC+iCcGeAe0vUaOh+Lv6SYM17ouzC/a/LB1/hz/7sxFBtlu1l4A==", + "dev": true, + "dependencies": { + "async": "~1.0.0", + "colors": "1.0.x", + "cycle": "1.0.x", + "eyes": "0.1.x", + "isstream": "0.1.x", + "stack-trace": "0.0.x" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/winston/node_modules/async": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async/-/async-1.0.0.tgz", + "integrity": "sha1-+PwEyjoTeErenhZBr5hXjPvWR6k=", + "dev": true + }, + "node_modules/winston/node_modules/colors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", + "integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=", + "dev": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=", + "dev": true, + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + } + }, "dependencies": { "@cypress/request": { "version": "2.88.7", @@ -1628,9 +3964,9 @@ "integrity": "sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q==" }, "umbraco-cypress-testhelpers": { - "version": "1.0.0-beta-64", - "resolved": "https://registry.npmjs.org/umbraco-cypress-testhelpers/-/umbraco-cypress-testhelpers-1.0.0-beta-64.tgz", - "integrity": "sha512-hivnoBEQ51i4ileiuDVRX2/clncw8L/wDyTHoZZWddaR6N/6iZS2SGXeV++uA0Zm3woqb3rFqNCiKLRrn18vcA==", + "version": "1.0.0-beta-66", + "resolved": "https://registry.npmjs.org/umbraco-cypress-testhelpers/-/umbraco-cypress-testhelpers-1.0.0-beta-66.tgz", + "integrity": "sha512-/Iq0P7rN9LfODO9snoLNqvbd8b432JIYtCVjYOdYZFceMAUIv0v2/6t7+N55Z7h8OpAQzcTLU3VCxfPzZp8wQw==", "dev": true, "requires": { "camelize": "^1.0.0", diff --git a/tests/Umbraco.Tests.AcceptanceTest/package.json b/tests/Umbraco.Tests.AcceptanceTest/package.json index 0f67fc5049..92141f4b1f 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/package.json +++ b/tests/Umbraco.Tests.AcceptanceTest/package.json @@ -14,7 +14,7 @@ "del": "^6.0.0", "ncp": "^2.0.0", "prompt": "^1.2.0", - "umbraco-cypress-testhelpers": "^1.0.0-beta-64" + "umbraco-cypress-testhelpers": "^1.0.0-beta-65" }, "dependencies": { "typescript": "^3.9.2" diff --git a/tests/Umbraco.Tests.Benchmarks/ModelToSqlExpressionHelperBenchmarks.cs b/tests/Umbraco.Tests.Benchmarks/ModelToSqlExpressionHelperBenchmarks.cs index a75e9db8bc..7fd640ebb6 100644 --- a/tests/Umbraco.Tests.Benchmarks/ModelToSqlExpressionHelperBenchmarks.cs +++ b/tests/Umbraco.Tests.Benchmarks/ModelToSqlExpressionHelperBenchmarks.cs @@ -9,6 +9,7 @@ using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Mappers; using Umbraco.Cms.Infrastructure.Persistence.Querying; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; +using Umbraco.Cms.Persistence.SqlServer.Services; namespace Umbraco.Tests.Benchmarks { diff --git a/tests/Umbraco.Tests.Benchmarks/SqlTemplatesBenchmark.cs b/tests/Umbraco.Tests.Benchmarks/SqlTemplatesBenchmark.cs index bd0c426fec..629da3cb25 100644 --- a/tests/Umbraco.Tests.Benchmarks/SqlTemplatesBenchmark.cs +++ b/tests/Umbraco.Tests.Benchmarks/SqlTemplatesBenchmark.cs @@ -7,6 +7,7 @@ using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Mappers; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; +using Umbraco.Cms.Persistence.SqlServer.Services; using Umbraco.Extensions; namespace Umbraco.Tests.Benchmarks diff --git a/tests/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj b/tests/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj index 52ba3c6a37..ca53cc8ed0 100644 --- a/tests/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj +++ b/tests/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj @@ -12,6 +12,7 @@ + diff --git a/tests/Umbraco.Tests.Common/Builders/RelationTypeBuilder.cs b/tests/Umbraco.Tests.Common/Builders/RelationTypeBuilder.cs index 2bd9dc124d..bb16cadd78 100644 --- a/tests/Umbraco.Tests.Common/Builders/RelationTypeBuilder.cs +++ b/tests/Umbraco.Tests.Common/Builders/RelationTypeBuilder.cs @@ -8,7 +8,7 @@ using Umbraco.Cms.Tests.Common.Builders.Interfaces; namespace Umbraco.Cms.Tests.Common.Builders { public class RelationTypeBuilder - : ChildBuilderBase, + : ChildBuilderBase, IWithIdBuilder, IWithAliasBuilder, IWithNameBuilder, @@ -23,6 +23,7 @@ namespace Umbraco.Cms.Tests.Common.Builders private DateTime? _deleteDate; private int? _id; private bool? _isBidirectional; + private bool? _isDependency; private Guid? _key; private string _name; private Guid? _parentObjectType; @@ -42,6 +43,12 @@ namespace Umbraco.Cms.Tests.Common.Builders { _isBidirectional = isBidirectional; return this; + } + + public RelationTypeBuilder WithIsDependency(bool isDependency) + { + _isDependency = isDependency; + return this; } public RelationTypeBuilder WithChildObjectType(Guid childObjectType) @@ -56,7 +63,7 @@ namespace Umbraco.Cms.Tests.Common.Builders return this; } - public override IRelationType Build() + public override IRelationTypeWithIsDependency Build() { var alias = _alias ?? Guid.NewGuid().ToString(); var name = _name ?? Guid.NewGuid().ToString(); @@ -65,11 +72,12 @@ namespace Umbraco.Cms.Tests.Common.Builders var id = _id ?? 0; Guid key = _key ?? Guid.NewGuid(); var isBidirectional = _isBidirectional ?? false; + var isDependency = _isDependency ?? false; DateTime createDate = _createDate ?? DateTime.Now; DateTime updateDate = _updateDate ?? DateTime.Now; DateTime? deleteDate = _deleteDate ?? null; - return new RelationType(name, alias, isBidirectional, parentObjectType, childObjectType) + return new RelationType(name, alias, isBidirectional, parentObjectType, childObjectType, isDependency) { Id = id, Key = key, diff --git a/tests/Umbraco.Tests.Common/TestHelperBase.cs b/tests/Umbraco.Tests.Common/TestHelperBase.cs index 5801e3291e..da23edc74f 100644 --- a/tests/Umbraco.Tests.Common/TestHelperBase.cs +++ b/tests/Umbraco.Tests.Common/TestHelperBase.cs @@ -86,8 +86,6 @@ namespace Umbraco.Cms.Tests.Common public IVariationContextAccessor VariationContextAccessor { get; } = new TestVariationContextAccessor(); - public abstract IDbProviderFactoryCreator DbProviderFactoryCreator { get; } - public abstract IBulkSqlInsertProvider BulkSqlInsertProvider { get; } public abstract IMarchal Marchal { get; } diff --git a/tests/Umbraco.Tests.Common/TestHelpers/TestDatabase.cs b/tests/Umbraco.Tests.Common/TestHelpers/TestDatabase.cs index 1eec4f5ae7..72f22e9960 100644 --- a/tests/Umbraco.Tests.Common/TestHelpers/TestDatabase.cs +++ b/tests/Umbraco.Tests.Common/TestHelpers/TestDatabase.cs @@ -17,6 +17,7 @@ using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Infrastructure.Migrations.Install; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; +using Umbraco.Cms.Persistence.SqlServer.Services; namespace Umbraco.Cms.Tests.Common.TestHelpers { diff --git a/tests/Umbraco.Tests.Common/Umbraco.Tests.Common.csproj b/tests/Umbraco.Tests.Common/Umbraco.Tests.Common.csproj index fffcad5a32..f03331bb49 100644 --- a/tests/Umbraco.Tests.Common/Umbraco.Tests.Common.csproj +++ b/tests/Umbraco.Tests.Common/Umbraco.Tests.Common.csproj @@ -22,6 +22,7 @@ + diff --git a/tests/Umbraco.Tests.Integration.SqlCe/Umbraco.Infrastructure/Persistence/DatabaseBuilderTests.cs b/tests/Umbraco.Tests.Integration.SqlCe/Umbraco.Infrastructure/Persistence/DatabaseBuilderTests.cs deleted file mode 100644 index 500d71dddc..0000000000 --- a/tests/Umbraco.Tests.Integration.SqlCe/Umbraco.Infrastructure/Persistence/DatabaseBuilderTests.cs +++ /dev/null @@ -1,85 +0,0 @@ -using System; -using System.IO; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using Moq; -using NPoco; -using NUnit.Framework; -using Umbraco.Cms.Core; -using Umbraco.Cms.Core.Configuration; -using Umbraco.Cms.Core.Events; -using Umbraco.Cms.Infrastructure.Migrations.Install; -using Umbraco.Cms.Infrastructure.Persistence; -using Umbraco.Cms.Tests.Common.TestHelpers; -using Umbraco.Cms.Tests.Common.Testing; -using Umbraco.Cms.Tests.Integration.Testing; - -namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence -{ - [TestFixture] - [UmbracoTest] - [Platform("Win")] - public class DatabaseBuilderTests : UmbracoIntegrationTest - { - private IDbProviderFactoryCreator DbProviderFactoryCreator => GetRequiredService(); - private IUmbracoDatabaseFactory UmbracoDatabaseFactory => GetRequiredService(); - private IDatabaseCreator EmbeddedDatabaseCreator => GetRequiredService(); - - public DatabaseBuilderTests() - { - TestOptionAttributeBase.ScanAssemblies.Add(typeof(DatabaseBuilderTests).Assembly); - } - - [Test] - public void CreateDatabase() - { - var path = TestContext.CurrentContext.TestDirectory.Split("bin")[0]; - AppDomain.CurrentDomain.SetData("DataDirectory", path); - const string dbFile = "DatabaseContextTests.sdf"; - // delete database file - // NOTE: using a custom db file for this test since we're re-using the one created with BaseDatabaseFactoryTest - var filePath = string.Concat(path, dbFile); - if (File.Exists(filePath)) - File.Delete(filePath); - - var connectionString = $"Datasource=|DataDirectory|{dbFile};Flush Interval=1"; - - UmbracoDatabaseFactory.Configure(connectionString, Constants.DbProviderNames.SqlCe); - DbProviderFactoryCreator.CreateDatabase(Constants.DbProviderNames.SqlCe, connectionString); - UmbracoDatabaseFactory.CreateDatabase(); - - // test get database type (requires an actual database) - using (var database = UmbracoDatabaseFactory.CreateDatabase()) - { - var databaseType = database.DatabaseType; - Assert.AreEqual(DatabaseType.SQLCe, databaseType); - } - - // create application context - //var appCtx = new ApplicationContext( - // _databaseFactory, - // new ServiceContext(migrationEntryService: Mock.Of()), - // CacheHelper.CreateDisabledCacheHelper(), - // new ProfilingLogger(Mock.Of(), Mock.Of())); - - // create the umbraco database - DatabaseSchemaCreator schemaHelper; - using (var database = UmbracoDatabaseFactory.CreateDatabase()) - using (var transaction = database.GetTransaction()) - { - schemaHelper = new DatabaseSchemaCreator(database, Mock.Of>(), NullLoggerFactory.Instance, new UmbracoVersion(), Mock.Of()); - schemaHelper.InitializeDatabaseSchema(); - transaction.Complete(); - } - - var umbracoNodeTable = schemaHelper.TableExists("umbracoNode"); - var umbracoUserTable = schemaHelper.TableExists("umbracoUser"); - var cmsTagsTable = schemaHelper.TableExists("cmsTags"); - - Assert.That(umbracoNodeTable, Is.True); - Assert.That(umbracoUserTable, Is.True); - Assert.That(cmsTagsTable, Is.True); - } - - } -} diff --git a/tests/Umbraco.Tests.Integration.SqlCe/Umbraco.Tests.Integration.SqlCe.csproj b/tests/Umbraco.Tests.Integration.SqlCe/Umbraco.Tests.Integration.SqlCe.csproj deleted file mode 100644 index 2c05ea6bf9..0000000000 --- a/tests/Umbraco.Tests.Integration.SqlCe/Umbraco.Tests.Integration.SqlCe.csproj +++ /dev/null @@ -1,32 +0,0 @@ - - - - net6.0 - false - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - - - diff --git a/tests/Umbraco.Tests.Integration/GlobalSetupTeardown.cs b/tests/Umbraco.Tests.Integration/GlobalSetupTeardown.cs index c952fcc663..3c5929877b 100644 --- a/tests/Umbraco.Tests.Integration/GlobalSetupTeardown.cs +++ b/tests/Umbraco.Tests.Integration/GlobalSetupTeardown.cs @@ -2,12 +2,12 @@ // See LICENSE for more details. using System; -using System.Collections.Generic; using System.Diagnostics; -using System.Text; +using System.Linq; +using Microsoft.Extensions.Configuration; using NUnit.Framework; +using Umbraco.Cms.Tests.Integration.Implementations; using Umbraco.Cms.Tests.Integration.Testing; - // ReSharper disable once CheckNamespace /// @@ -19,19 +19,39 @@ using Umbraco.Cms.Tests.Integration.Testing; [SetUpFixture] public class GlobalSetupTeardown { + public static IConfiguration TestConfiguration { get; private set; } + private Stopwatch _stopwatch; [OneTimeSetUp] public void SetUp() { + var builder = new ConfigurationBuilder(); + builder.AddJsonFile("appsettings.Tests.json"); + builder.AddJsonFile("appsettings.Tests.Local.json", optional: true); + builder.AddEnvironmentVariables(); + TestConfiguration = builder.Build(); + + var testHelper = new TestHelper(); + var databaseType = TestConfiguration.GetValue("Tests:Database:DatabaseType"); + var version = testHelper.GetUmbracoVersion().SemanticVersion; + + TestContext.Progress.WriteLine($"******************************************************************************"); + TestContext.Progress.WriteLine($"* Umbraco.Tests.Integration"); + TestContext.Progress.WriteLine($"*"); + TestContext.Progress.WriteLine($"* DatabaseType : {databaseType}"); + TestContext.Progress.WriteLine($"* UmbracoVersion : {version.ToString().Split('+').First()}"); + TestContext.Progress.WriteLine($"* WorkingDirectory : {testHelper.WorkingDirectory}"); + TestContext.Progress.WriteLine($"******************************************************************************"); + _stopwatch = Stopwatch.StartNew(); } [OneTimeTearDown] public void TearDown() { - LocalDbTestDatabase.Instance?.Finish(); - SqlDeveloperTestDatabase.Instance?.Finish(); + BaseTestDatabase.Instance?.TearDown(); + Console.WriteLine("TOTAL TESTS DURATION: {0}", _stopwatch.Elapsed); } } diff --git a/tests/Umbraco.Tests.Integration/Implementations/TestHelper.cs b/tests/Umbraco.Tests.Integration/Implementations/TestHelper.cs index f8afe1d6ae..4abace502b 100644 --- a/tests/Umbraco.Tests.Integration/Implementations/TestHelper.cs +++ b/tests/Umbraco.Tests.Integration/Implementations/TestHelper.cs @@ -32,6 +32,7 @@ using Umbraco.Cms.Core.Net; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Runtime; using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Persistence.SqlServer.Services; using Umbraco.Cms.Tests.Common; using Umbraco.Cms.Tests.Common.Testing; using Umbraco.Cms.Web.Common.AspNetCore; @@ -103,9 +104,6 @@ namespace Umbraco.Cms.Tests.Integration.Implementations public IWebHostEnvironment GetWebHostEnvironment() => _hostEnvironment; - public override IDbProviderFactoryCreator DbProviderFactoryCreator => - new SqlServerDbProviderFactoryCreator(DbProviderFactories.GetFactory, Options.Create(new GlobalSettings())); - public override IBulkSqlInsertProvider BulkSqlInsertProvider => new SqlServerBulkSqlInsertProvider(); public override IMarchal Marchal { get; } = new AspNetCoreMarchal(); diff --git a/tests/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs b/tests/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs index 8c7eabadde..3c0e0aeb81 100644 --- a/tests/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs +++ b/tests/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs @@ -19,6 +19,8 @@ using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Web; +using Umbraco.Cms.Persistence.Sqlite; +using Umbraco.Cms.Persistence.SqlServer; using Umbraco.Cms.Tests.Common.Testing; using Umbraco.Cms.Tests.Integration.DependencyInjection; using Umbraco.Cms.Tests.Integration.Testing; @@ -139,6 +141,7 @@ namespace Umbraco.Cms.Tests.Integration.TestServerTest context.HostingEnvironment = TestHelper.GetWebHostEnvironment(); configBuilder.Sources.Clear(); configBuilder.AddInMemoryCollection(InMemoryConfiguration); + configBuilder.AddConfiguration(GlobalSetupTeardown.TestConfiguration); Configuration = configBuilder.Build(); }) @@ -224,6 +227,8 @@ namespace Umbraco.Cms.Tests.Integration.TestServerTest }) .AddWebServer() .AddWebsite() + .AddUmbracoSqlServerSupport() + .AddUmbracoSqliteSupport() .AddTestServices(TestHelper) // This is the important one! .Build(); } diff --git a/tests/Umbraco.Tests.Integration/Testing/BaseTestDatabase.cs b/tests/Umbraco.Tests.Integration/Testing/BaseTestDatabase.cs index 47d81e3c11..1afe45bc90 100644 --- a/tests/Umbraco.Tests.Integration/Testing/BaseTestDatabase.cs +++ b/tests/Umbraco.Tests.Integration/Testing/BaseTestDatabase.cs @@ -5,36 +5,33 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Data; +using System.Data.Common; using System.Diagnostics; -using System.Linq; using System.Threading; -using Microsoft.Data.SqlClient; using Microsoft.Extensions.Logging; -using Moq; -using Umbraco.Cms.Core; -using Umbraco.Cms.Core.Configuration; -using Umbraco.Cms.Core.Events; -using Umbraco.Cms.Infrastructure.Migrations.Install; using Umbraco.Cms.Infrastructure.Persistence; namespace Umbraco.Cms.Tests.Integration.Testing { public abstract class BaseTestDatabase { + public static bool IsSqlite() => BaseTestDatabase.Instance is SqliteTestDatabase; + public static bool IsSqlServer() => BaseTestDatabase.Instance is SqlServerBaseTestDatabase; + protected ILoggerFactory _loggerFactory; protected IUmbracoDatabaseFactory _databaseFactory; protected IList _testDatabases; - - protected UmbracoDatabase.CommandInfo[] _cachedDatabaseInitCommands = new UmbracoDatabase.CommandInfo[0]; - protected BlockingCollection _prepareQueue; protected BlockingCollection _readySchemaQueue; protected BlockingCollection _readyEmptyQueue; + public static BaseTestDatabase Instance { get; private set; } + + public BaseTestDatabase() => Instance = this; protected abstract void Initialize(); - public TestDbMeta AttachEmpty() + public virtual TestDbMeta AttachEmpty() { if (_prepareQueue == null) { @@ -44,7 +41,7 @@ namespace Umbraco.Cms.Tests.Integration.Testing return _readyEmptyQueue.Take(); } - public TestDbMeta AttachSchema() + public virtual TestDbMeta AttachSchema() { if (_prepareQueue == null) { @@ -54,110 +51,47 @@ namespace Umbraco.Cms.Tests.Integration.Testing return _readySchemaQueue.Take(); } - public void Detach(TestDbMeta meta) + public virtual void Detach(TestDbMeta meta) { _prepareQueue.TryAdd(meta); } - protected void PrepareDatabase() => + protected virtual void PrepareDatabase() => Retry(10, () => + { + while (_prepareQueue.IsCompleted == false) { - while (_prepareQueue.IsCompleted == false) + TestDbMeta meta; + try { - TestDbMeta meta; - try - { - meta = _prepareQueue.Take(); - } - catch (InvalidOperationException) - { - continue; - } + meta = _prepareQueue.Take(); + } + catch (InvalidOperationException) + { + continue; + } - using (var conn = new SqlConnection(meta.ConnectionString)) - using (SqlCommand cmd = conn.CreateCommand()) + ResetTestDatabase(meta); + + if (!meta.IsEmpty) + { + using (var conn = GetConnection(meta)) { conn.Open(); - ResetTestDatabase(cmd); - - if (!meta.IsEmpty) + using (var cmd = conn.CreateCommand()) { RebuildSchema(cmd, meta); } } - if (!meta.IsEmpty) - { - _readySchemaQueue.TryAdd(meta); - } - else - { - _readyEmptyQueue.TryAdd(meta); - } + _readySchemaQueue.TryAdd(meta); + } + else + { + _readyEmptyQueue.TryAdd(meta); } - }); - - private void RebuildSchema(IDbCommand command, TestDbMeta meta) - { - lock (_cachedDatabaseInitCommands) - { - if (!_cachedDatabaseInitCommands.Any()) - { - RebuildSchemaFirstTime(meta); - return; } - } - - foreach (UmbracoDatabase.CommandInfo dbCommand in _cachedDatabaseInitCommands) - { - if (dbCommand.Text.StartsWith("SELECT ")) - { - continue; - } - - command.CommandText = dbCommand.Text; - command.Parameters.Clear(); - - foreach (UmbracoDatabase.ParameterInfo parameterInfo in dbCommand.Parameters) - { - AddParameter(command, parameterInfo); - } - - command.ExecuteNonQuery(); - } - } - - private void RebuildSchemaFirstTime(TestDbMeta meta) - { - _databaseFactory.Configure(meta.ConnectionString, Constants.DatabaseProviders.SqlServer); - - using (var database = (UmbracoDatabase)_databaseFactory.CreateDatabase()) - { - database.LogCommands = true; - - using (NPoco.ITransaction transaction = database.GetTransaction()) - { - var schemaCreator = new DatabaseSchemaCreator(database, _loggerFactory.CreateLogger(), _loggerFactory, new UmbracoVersion(), Mock.Of()); - schemaCreator.InitializeDatabaseSchema(); - - transaction.Complete(); - - _cachedDatabaseInitCommands = database.Commands.ToArray(); - } - } - } - - protected static void SetCommand(SqlCommand command, string sql, params object[] args) - { - command.CommandType = CommandType.Text; - command.CommandText = sql; - command.Parameters.Clear(); - - for (int i = 0; i < args.Length; i++) - { - command.Parameters.AddWithValue("@" + i, args[i]); - } - } + }); protected static void AddParameter(IDbCommand cmd, UmbracoDatabase.ParameterInfo parameterInfo) { @@ -169,32 +103,11 @@ namespace Umbraco.Cms.Tests.Integration.Testing cmd.Parameters.Add(p); } - protected static void ResetTestDatabase(IDbCommand cmd) - { - // https://stackoverflow.com/questions/536350 - cmd.CommandType = CommandType.Text; - cmd.CommandText = @" - declare @n char(1); - set @n = char(10); - declare @stmt nvarchar(max); - -- check constraints - select @stmt = isnull( @stmt + @n, '' ) + - 'alter table [' + schema_name(schema_id) + '].[' + object_name( parent_object_id ) + '] drop constraint [' + name + ']' - from sys.check_constraints; - -- foreign keys - select @stmt = isnull( @stmt + @n, '' ) + - 'alter table [' + schema_name(schema_id) + '].[' + object_name( parent_object_id ) + '] drop constraint [' + name + ']' - from sys.foreign_keys; - -- tables - select @stmt = isnull( @stmt + @n, '' ) + - 'drop table [' + schema_name(schema_id) + '].[' + name + ']' - from sys.tables; - exec sp_executesql @stmt; - "; + protected abstract DbConnection GetConnection(TestDbMeta meta); - // rudimentary retry policy since a db can still be in use when we try to drop - Retry(10, () => cmd.ExecuteNonQuery()); - } + protected abstract void RebuildSchema(IDbCommand command, TestDbMeta meta); + + protected abstract void ResetTestDatabase(TestDbMeta meta); protected static void Retry(int maxIterations, Action action) { @@ -205,7 +118,7 @@ namespace Umbraco.Cms.Tests.Integration.Testing action(); return; } - catch (SqlException) + catch (DbException ex) { // Console.Error.WriteLine($"SqlException occured, but we try again {i+1}/{maxIterations}.\n{e}"); // This can occur when there's a transaction deadlock which means (i think) that the database is still in use and hasn't been closed properly yet @@ -223,5 +136,7 @@ namespace Umbraco.Cms.Tests.Integration.Testing } } } + + public abstract void TearDown(); } } diff --git a/tests/Umbraco.Tests.Integration/Testing/LocalDbTestDatabase.cs b/tests/Umbraco.Tests.Integration/Testing/LocalDbTestDatabase.cs index 59ddfe55e8..30c306bbee 100644 --- a/tests/Umbraco.Tests.Integration/Testing/LocalDbTestDatabase.cs +++ b/tests/Umbraco.Tests.Integration/Testing/LocalDbTestDatabase.cs @@ -15,7 +15,7 @@ namespace Umbraco.Cms.Tests.Integration.Testing /// /// Manages a pool of LocalDb databases for integration testing /// - public class LocalDbTestDatabase : BaseTestDatabase, ITestDatabase + public class LocalDbTestDatabase : SqlServerBaseTestDatabase, ITestDatabase { public const string InstanceName = "UmbracoTests"; public const string DatabaseName = "UmbracoTests"; @@ -25,19 +25,15 @@ namespace Umbraco.Cms.Tests.Integration.Testing private static LocalDb.Instance s_localDbInstance; private static string s_filesPath; - public static LocalDbTestDatabase Instance { get; private set; } - // It's internal because `Umbraco.Core.Persistence.LocalDb` is internal - internal LocalDbTestDatabase(TestDatabaseSettings settings, ILoggerFactory loggerFactory, LocalDb localDb, string filesPath, IUmbracoDatabaseFactory dbFactory) + internal LocalDbTestDatabase(TestDatabaseSettings settings, ILoggerFactory loggerFactory, LocalDb localDb, IUmbracoDatabaseFactory dbFactory) { _loggerFactory = loggerFactory; _databaseFactory = dbFactory; _settings = settings; _localDb = localDb; - s_filesPath = filesPath; - - Instance = this; // For GlobalSetupTeardown.cs + s_filesPath = settings.FilesPath; var counter = 0; @@ -90,7 +86,7 @@ namespace Umbraco.Cms.Tests.Integration.Testing } } - public void Finish() + public override void TearDown() { if (_prepareQueue == null) { diff --git a/tests/Umbraco.Tests.Integration/Testing/SqlServerBaseTestDatabase.cs b/tests/Umbraco.Tests.Integration/Testing/SqlServerBaseTestDatabase.cs new file mode 100644 index 0000000000..10d24eb01b --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Testing/SqlServerBaseTestDatabase.cs @@ -0,0 +1,119 @@ +using System; +using System.Data; +using System.Data.Common; +using System.Linq; +using Microsoft.Data.SqlClient; +using Microsoft.Extensions.Logging; +using Moq; +using Umbraco.Cms.Core.Configuration; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Infrastructure.Migrations.Install; +using Umbraco.Cms.Infrastructure.Persistence; + +namespace Umbraco.Cms.Tests.Integration.Testing; + +public abstract class SqlServerBaseTestDatabase : BaseTestDatabase +{ + + protected UmbracoDatabase.CommandInfo[] _cachedDatabaseInitCommands = new UmbracoDatabase.CommandInfo[0]; + + protected override void ResetTestDatabase(TestDbMeta meta) + { + using var connection = GetConnection(meta); + connection.Open(); + + using (var cmd = connection.CreateCommand()) + { + // https://stackoverflow.com/questions/536350 + cmd.CommandType = CommandType.Text; + cmd.CommandText = @" + declare @n char(1); + set @n = char(10); + declare @stmt nvarchar(max); + -- check constraints + select @stmt = isnull( @stmt + @n, '' ) + + 'alter table [' + schema_name(schema_id) + '].[' + object_name( parent_object_id ) + '] drop constraint [' + name + ']' + from sys.check_constraints; + -- foreign keys + select @stmt = isnull( @stmt + @n, '' ) + + 'alter table [' + schema_name(schema_id) + '].[' + object_name( parent_object_id ) + '] drop constraint [' + name + ']' + from sys.foreign_keys; + -- tables + select @stmt = isnull( @stmt + @n, '' ) + + 'drop table [' + schema_name(schema_id) + '].[' + name + ']' + from sys.tables; + exec sp_executesql @stmt; + "; + + // rudimentary retry policy since a db can still be in use when we try to drop + Retry(10, () => cmd.ExecuteNonQuery()); + } + } + + protected static void SetCommand(SqlCommand command, string sql, params object[] args) + { + command.CommandType = CommandType.Text; + command.CommandText = sql; + command.Parameters.Clear(); + + for (int i = 0; i < args.Length; i++) + { + command.Parameters.AddWithValue("@" + i, args[i]); + } + } + + + + protected override DbConnection GetConnection(TestDbMeta meta) => new SqlConnection(meta.ConnectionString); + + protected override void RebuildSchema(IDbCommand command, TestDbMeta meta) + { + lock (_cachedDatabaseInitCommands) + { + if (!_cachedDatabaseInitCommands.Any()) + { + RebuildSchemaFirstTime(meta); + return; + } + } + + foreach (UmbracoDatabase.CommandInfo dbCommand in _cachedDatabaseInitCommands) + { + command.CommandText = dbCommand.Text; + command.Parameters.Clear(); + + foreach (UmbracoDatabase.ParameterInfo parameterInfo in dbCommand.Parameters) + { + AddParameter(command, parameterInfo); + } + + command.ExecuteNonQuery(); + } + } + + private void RebuildSchemaFirstTime(TestDbMeta meta) + { + _databaseFactory.Configure(meta.ToStronglyTypedConnectionString()); + + using (var database = (UmbracoDatabase)_databaseFactory.CreateDatabase()) + { + database.LogCommands = true; + + using (NPoco.ITransaction transaction = database.GetTransaction()) + { + var schemaCreator = new DatabaseSchemaCreator( + database, + _loggerFactory.CreateLogger(), _loggerFactory, + new UmbracoVersion(), + Mock.Of()); + schemaCreator.InitializeDatabaseSchema(); + + transaction.Complete(); + + _cachedDatabaseInitCommands = database.Commands + .Where(x => !x.Text.StartsWith("SELECT ", StringComparison.OrdinalIgnoreCase)) + .ToArray(); + } + } + } +} diff --git a/tests/Umbraco.Tests.Integration/Testing/SqlDeveloperTestDatabase.cs b/tests/Umbraco.Tests.Integration/Testing/SqlServerTestDatabase.cs similarity index 75% rename from tests/Umbraco.Tests.Integration/Testing/SqlDeveloperTestDatabase.cs rename to tests/Umbraco.Tests.Integration/Testing/SqlServerTestDatabase.cs index 9daa55a1cb..378ee28c53 100644 --- a/tests/Umbraco.Tests.Integration/Testing/SqlDeveloperTestDatabase.cs +++ b/tests/Umbraco.Tests.Integration/Testing/SqlServerTestDatabase.cs @@ -16,33 +16,30 @@ namespace Umbraco.Cms.Tests.Integration.Testing /// /// It's not meant to be pretty, rushed port of LocalDb.cs + LocalDbTestDatabase.cs /// - public class SqlDeveloperTestDatabase : BaseTestDatabase, ITestDatabase + public class SqlServerTestDatabase : SqlServerBaseTestDatabase, ITestDatabase { private readonly TestDatabaseSettings _settings; - private readonly string _masterConnectionString; public const string DatabaseName = "UmbracoTests"; - public static SqlDeveloperTestDatabase Instance { get; private set; } - - public SqlDeveloperTestDatabase(TestDatabaseSettings settings, ILoggerFactory loggerFactory, IUmbracoDatabaseFactory databaseFactory, string masterConnectionString) + public SqlServerTestDatabase(TestDatabaseSettings settings, ILoggerFactory loggerFactory, + IUmbracoDatabaseFactory databaseFactory) { _loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); _databaseFactory = databaseFactory ?? throw new ArgumentNullException(nameof(databaseFactory)); _settings = settings; - _masterConnectionString = masterConnectionString; var counter = 0; var schema = Enumerable.Range(0, _settings.SchemaDatabaseCount) - .Select(x => TestDbMeta.CreateWithMasterConnectionString($"{DatabaseName}-{++counter}", false, masterConnectionString)); + .Select(x => TestDbMeta.CreateWithMasterConnectionString($"{DatabaseName}-{++counter}", false, + _settings.SQLServerMasterConnectionString)); var empty = Enumerable.Range(0, _settings.EmptyDatabasesCount) - .Select(x => TestDbMeta.CreateWithMasterConnectionString($"{DatabaseName}-{++counter}", true, masterConnectionString)); + .Select(x => TestDbMeta.CreateWithMasterConnectionString($"{DatabaseName}-{++counter}", true, + _settings.SQLServerMasterConnectionString)); _testDatabases = schema.Concat(empty).ToList(); - - Instance = this; // For GlobalSetupTeardown.cs } protected override void Initialize() @@ -66,7 +63,9 @@ namespace Umbraco.Cms.Tests.Integration.Testing private void CreateDatabase(TestDbMeta meta) { - using (var connection = new SqlConnection(_masterConnectionString)) + Drop(meta); + + using (var connection = new SqlConnection(_settings.SQLServerMasterConnectionString)) { connection.Open(); using (SqlCommand command = connection.CreateCommand()) @@ -79,14 +78,21 @@ namespace Umbraco.Cms.Tests.Integration.Testing private void Drop(TestDbMeta meta) { - using (var connection = new SqlConnection(_masterConnectionString)) + using (var connection = new SqlConnection(_settings.SQLServerMasterConnectionString)) { connection.Open(); using (SqlCommand command = connection.CreateCommand()) { + SetCommand(command, "select count(1) from sys.databases where name = @0", meta.Name); + var records = (int)command.ExecuteScalar(); + if (records == 0) + { + return; + } + string sql = $@" - ALTER DATABASE{LocalDb.QuotedName(meta.Name)} - SET SINGLE_USER + ALTER DATABASE {LocalDb.QuotedName(meta.Name)} + SET SINGLE_USER WITH ROLLBACK IMMEDIATE"; SetCommand(command, sql); command.ExecuteNonQuery(); @@ -97,7 +103,7 @@ namespace Umbraco.Cms.Tests.Integration.Testing } } - public void Finish() + public override void TearDown() { if (_prepareQueue == null) { diff --git a/tests/Umbraco.Tests.Integration/Testing/SqliteTestDatabase.cs b/tests/Umbraco.Tests.Integration/Testing/SqliteTestDatabase.cs new file mode 100644 index 0000000000..d8274c4a53 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Testing/SqliteTestDatabase.cs @@ -0,0 +1,164 @@ +using System; +using System.Collections.Concurrent; +using System.Data; +using System.Data.Common; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Data.Sqlite; +using Microsoft.Extensions.Logging; +using Moq; +using NPoco; +using Umbraco.Cms.Core.Configuration; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Infrastructure.Migrations.Install; +using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Infrastructure.Persistence.Mappers; +using Umbraco.Cms.Persistence.Sqlite.Mappers; +using Umbraco.Cms.Persistence.Sqlite.Services; + +namespace Umbraco.Cms.Tests.Integration.Testing; + +public class SqliteTestDatabase : BaseTestDatabase, ITestDatabase +{ + private readonly TestDatabaseSettings _settings; + private readonly TestUmbracoDatabaseFactoryProvider _dbFactoryProvider; + public const string DatabaseName = "UmbracoTests"; + + protected UmbracoDatabase.CommandInfo[] _cachedDatabaseInitCommands = new UmbracoDatabase.CommandInfo[0]; + + public SqliteTestDatabase(TestDatabaseSettings settings, TestUmbracoDatabaseFactoryProvider dbFactoryProvider, + ILoggerFactory loggerFactory) + { + _settings = settings ?? throw new ArgumentNullException(nameof(settings)); + _dbFactoryProvider = dbFactoryProvider; + _databaseFactory = dbFactoryProvider.Create(); + _loggerFactory = loggerFactory; + + var schema = Enumerable.Range(0, _settings.SchemaDatabaseCount) + .Select(x => CreateSqLiteMeta(false)); + + var empty = Enumerable.Range(0, _settings.EmptyDatabasesCount) + .Select(x => CreateSqLiteMeta(true)); + + _testDatabases = schema.Concat(empty).ToList(); + } + + protected override void Initialize() + { + _prepareQueue = new BlockingCollection(); + _readySchemaQueue = new BlockingCollection(); + _readyEmptyQueue = new BlockingCollection(); + + foreach (TestDbMeta meta in _testDatabases) + { + _prepareQueue.Add(meta); + } + + for (var i = 0; i < _settings.PrepareThreadCount; i++) + { + var thread = new Thread(PrepareDatabase); + thread.Start(); + } + } + + protected override void ResetTestDatabase(TestDbMeta meta) + { + // Database survives in memory until all connections closed. + meta.Connection = GetConnection(meta); + meta.Connection.Open(); + } + + public override void Detach(TestDbMeta meta) + { + meta.Connection.Close(); + _prepareQueue.TryAdd(CreateSqLiteMeta(meta.IsEmpty)); + } + + protected override DbConnection GetConnection(TestDbMeta meta) => new SqliteConnection(meta.ConnectionString); + + protected override void RebuildSchema(IDbCommand command, TestDbMeta meta) + { + using var connection = GetConnection(meta); + connection.Open(); + + lock (_cachedDatabaseInitCommands) + { + if (!_cachedDatabaseInitCommands.Any()) + { + RebuildSchemaFirstTime(meta); + return; + } + } + + // Get NPoco to handle all the type mappings (e.g. dates) for us. + var database = new Database(connection, DatabaseType.SQLite); + database.BeginTransaction(); + + database.Mappers.Add(new NullableDateMapper()); + database.Mappers.Add(new SqlitePocoGuidMapper()); + + foreach (UmbracoDatabase.CommandInfo dbCommand in _cachedDatabaseInitCommands) + { + database.Execute(dbCommand.Text, dbCommand.Parameters.Select(x => x.Value).ToArray()); + } + + database.CompleteTransaction(); + } + + private void RebuildSchemaFirstTime(TestDbMeta meta) + { + var dbFactory = _dbFactoryProvider.Create(); + dbFactory.Configure(meta.ToStronglyTypedConnectionString()); + + using var database = (UmbracoDatabase)dbFactory.CreateDatabase(); + database.LogCommands = true; + + using NPoco.ITransaction transaction = database.GetTransaction(); + + var schemaCreator = new DatabaseSchemaCreator( + database, + _loggerFactory.CreateLogger(), _loggerFactory, + new UmbracoVersion(), + Mock.Of()); + + schemaCreator.InitializeDatabaseSchema(); + transaction.Complete(); + + _cachedDatabaseInitCommands = database.Commands + .Where(x => !x.Text.StartsWith("SELECT ", StringComparison.OrdinalIgnoreCase)) + .ToArray(); + } + + public override void TearDown() + { + if (_prepareQueue == null) + { + return; + } + + _prepareQueue.CompleteAdding(); + while (_prepareQueue.TryTake(out _)) { } + + _readyEmptyQueue.CompleteAdding(); + while (_readyEmptyQueue.TryTake(out _)) { } + + _readySchemaQueue.CompleteAdding(); + while (_readySchemaQueue.TryTake(out _)) { } + } + + private TestDbMeta CreateSqLiteMeta(bool empty) + { + var builder = new SqliteConnectionStringBuilder() + { + DataSource = $"{Guid.NewGuid()}", + Mode = SqliteOpenMode.Memory, + ForeignKeys = true, + Pooling = false, // When pooling true, files kept open after connections closed, bad for cleanup. + Cache = SqliteCacheMode.Shared, + }; + + return new TestDbMeta(builder.DataSource, empty, builder.ConnectionString, Persistence.Sqlite.Constants.ProviderName, "InMemory"); + } +} diff --git a/tests/Umbraco.Tests.Integration/Testing/TestDatabaseFactory.cs b/tests/Umbraco.Tests.Integration/Testing/TestDatabaseFactory.cs index 5f14a4928d..c746e52e6c 100644 --- a/tests/Umbraco.Tests.Integration/Testing/TestDatabaseFactory.cs +++ b/tests/Umbraco.Tests.Integration/Testing/TestDatabaseFactory.cs @@ -8,24 +8,38 @@ using Umbraco.Cms.Infrastructure.Persistence; namespace Umbraco.Cms.Tests.Integration.Testing { - public class TestDatabaseFactory + public static class TestDatabaseFactory { - public static ITestDatabase Create(TestDatabaseSettings settings, string filesPath, ILoggerFactory loggerFactory, TestUmbracoDatabaseFactoryProvider dbFactory) - { - string connectionString = Environment.GetEnvironmentVariable("UmbracoIntegrationTestConnectionString"); - - return string.IsNullOrEmpty(connectionString) - ? CreateLocalDb(settings, filesPath, loggerFactory, dbFactory) - : CreateSqlDeveloper(settings, loggerFactory, dbFactory, connectionString); - } - - private static ITestDatabase CreateLocalDb(TestDatabaseSettings settings, string filesPath, ILoggerFactory loggerFactory, TestUmbracoDatabaseFactoryProvider dbFactory) - { - if (!Directory.Exists(filesPath)) + /// + /// Creates a TestDatabase instance + /// + /// + /// SQL Server setup requires configured master connection string & privileges to create database. + /// + /// + /// + /// # SQL Server Environment variable setup + /// $ export Tests__Database__DatabaseType="SqlServer" + /// $ export Tests__Database__SQLServerMasterConnectionString="Server=localhost,1433; User Id=sa; Password=MySuperSecretPassword123!;" + /// + /// + /// + /// + /// # Docker cheat sheet + /// $ docker run -e 'ACCEPT_EULA=Y' -e "SA_PASSWORD=MySuperSecretPassword123!" -e 'MSSQL_PID=Developer' -p 1433:1433 -d mcr.microsoft.com/mssql/server:2017-latest-ubuntu + /// + /// + public static ITestDatabase Create(TestDatabaseSettings settings, TestUmbracoDatabaseFactoryProvider dbFactory, ILoggerFactory loggerFactory) => + settings.DatabaseType switch { - Directory.CreateDirectory(filesPath); - } + TestDatabaseSettings.TestDatabaseType.Sqlite=> new SqliteTestDatabase(settings, dbFactory, loggerFactory), + TestDatabaseSettings.TestDatabaseType.SqlServer => CreateSqlServer(settings, loggerFactory, dbFactory), + TestDatabaseSettings.TestDatabaseType.LocalDb => CreateLocalDb(settings, loggerFactory, dbFactory), + _ => throw new ApplicationException("Unsupported test database provider") + }; + private static ITestDatabase CreateLocalDb(TestDatabaseSettings settings, ILoggerFactory loggerFactory, TestUmbracoDatabaseFactoryProvider dbFactory) + { var localDb = new LocalDb(); if (!localDb.IsAvailable) @@ -33,21 +47,12 @@ namespace Umbraco.Cms.Tests.Integration.Testing throw new InvalidOperationException("LocalDB is not available."); } - return new LocalDbTestDatabase(settings, loggerFactory, localDb, filesPath, dbFactory.Create()); + return new LocalDbTestDatabase(settings, loggerFactory, localDb, dbFactory.Create()); } - private static ITestDatabase CreateSqlDeveloper(TestDatabaseSettings settings, ILoggerFactory loggerFactory, TestUmbracoDatabaseFactoryProvider dbFactory, string connectionString) + private static ITestDatabase CreateSqlServer(TestDatabaseSettings settings, ILoggerFactory loggerFactory, TestUmbracoDatabaseFactoryProvider dbFactory) { - // NOTE: Example setup for Linux box. - // $ export SA_PASSWORD=Foobar123! - // $ export UmbracoIntegrationTestConnectionString="Server=localhost,1433;User Id=sa;Password=$SA_PASSWORD;" - // $ docker run -e 'ACCEPT_EULA=Y' -e "SA_PASSWORD=$SA_PASSWORD" -e 'MSSQL_PID=Developer' -p 1433:1433 -d mcr.microsoft.com/mssql/server:2017-latest-ubuntu - if (string.IsNullOrEmpty(connectionString)) - { - throw new InvalidOperationException("ENV: UmbracoIntegrationTestConnectionString is not set"); - } - - return new SqlDeveloperTestDatabase(settings, loggerFactory, dbFactory.Create(), connectionString); + return new SqlServerTestDatabase(settings, loggerFactory, dbFactory.Create()); } } } diff --git a/tests/Umbraco.Tests.Integration/Testing/TestDatabaseSettings.cs b/tests/Umbraco.Tests.Integration/Testing/TestDatabaseSettings.cs index b2c9b0cfa2..3387c9d3e7 100644 --- a/tests/Umbraco.Tests.Integration/Testing/TestDatabaseSettings.cs +++ b/tests/Umbraco.Tests.Integration/Testing/TestDatabaseSettings.cs @@ -3,10 +3,27 @@ namespace Umbraco.Cms.Tests.Integration.Testing { public class TestDatabaseSettings { + public TestDatabaseType DatabaseType { get; set; } + public int PrepareThreadCount { get; set; } public int SchemaDatabaseCount { get; set; } public int EmptyDatabasesCount { get; set; } + + public string FilesPath { get; set; } + + /// + /// Only used for SQL Server e.g. on Linux/MacOS (not required for localdb). + /// + public string SQLServerMasterConnectionString { get; set; } + + public enum TestDatabaseType + { + Unknown, + Sqlite, + SqlServer, + LocalDb + } } } diff --git a/tests/Umbraco.Tests.Integration/Testing/TestDbMeta.cs b/tests/Umbraco.Tests.Integration/Testing/TestDbMeta.cs index 8e3dd355d5..d4395b4461 100644 --- a/tests/Umbraco.Tests.Integration/Testing/TestDbMeta.cs +++ b/tests/Umbraco.Tests.Integration/Testing/TestDbMeta.cs @@ -1,23 +1,28 @@ // Copyright (c) Umbraco. // See LICENSE for more details. +using System.Data.Common; using System.Text.RegularExpressions; +using Umbraco.Cms.Core.Configuration.Models; namespace Umbraco.Cms.Tests.Integration.Testing { public class TestDbMeta { public string Name { get; } - public bool IsEmpty { get; } - public string ConnectionString { get; set; } + public string Provider { get; set; } + public string Path { get; set; } // Null if not embedded. + public DbConnection Connection { get; set; } // for SQLite in memory, can move to subclass later. - private TestDbMeta(string name, bool isEmpty, string connectionString) + public TestDbMeta(string name, bool isEmpty, string connectionString, string providerName, string path) { IsEmpty = isEmpty; Name = name; ConnectionString = connectionString; + Provider = providerName; + Path = path; } private static string ConstructConnectionString(string masterConnectionString, string databaseName) @@ -28,10 +33,18 @@ namespace Umbraco.Cms.Tests.Integration.Testing } public static TestDbMeta CreateWithMasterConnectionString(string name, bool isEmpty, string masterConnectionString) => - new TestDbMeta(name, isEmpty, ConstructConnectionString(masterConnectionString, name)); + new TestDbMeta(name, isEmpty, ConstructConnectionString(masterConnectionString, name), Persistence.SqlServer.Constants.ProviderName, null); // LocalDb mdf funtimes public static TestDbMeta CreateWithoutConnectionString(string name, bool isEmpty) => - new TestDbMeta(name, isEmpty, null); + new TestDbMeta(name, isEmpty, null, Persistence.SqlServer.Constants.ProviderName, null); + + public ConnectionStrings ToStronglyTypedConnectionString() => + new ConnectionStrings + { + Name = Name, + ConnectionString = ConnectionString, + ProviderName = Provider + }; } } diff --git a/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs b/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs index dd9fd3fe4e..2fd07b131d 100644 --- a/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs +++ b/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs @@ -20,6 +20,8 @@ using Umbraco.Cms.Core.Web; using Umbraco.Cms.Infrastructure.DependencyInjection; using Umbraco.Cms.Infrastructure.Persistence.Mappers; using Umbraco.Cms.Infrastructure.Scoping; +using Umbraco.Cms.Persistence.Sqlite; +using Umbraco.Cms.Persistence.SqlServer; using Umbraco.Cms.Tests.Common.Builders; using Umbraco.Cms.Tests.Integration.DependencyInjection; using Umbraco.Cms.Tests.Integration.Extensions; @@ -42,7 +44,7 @@ namespace Umbraco.Cms.Tests.Integration.Testing [SetUp] public void Setup() { - InMemoryConfiguration[Constants.Configuration.ConfigUnattended + ":" + nameof(UnattendedSettings.InstallUnattended)] = "true"; + InMemoryConfiguration[Core.Constants.Configuration.ConfigUnattended + ":" + nameof(UnattendedSettings.InstallUnattended)] = "true"; IHostBuilder hostBuilder = CreateHostBuilder(); _host = hostBuilder.Build(); @@ -74,6 +76,7 @@ namespace Umbraco.Cms.Tests.Integration.Testing context.HostingEnvironment = TestHelper.GetWebHostEnvironment(); configBuilder.Sources.Clear(); configBuilder.AddInMemoryCollection(InMemoryConfiguration); + configBuilder.AddConfiguration(GlobalSetupTeardown.TestConfiguration); Configuration = configBuilder.Build(); }) @@ -96,7 +99,6 @@ namespace Umbraco.Cms.Tests.Integration.Testing protected void ConfigureServices(IServiceCollection services) { services.AddUnique(CreateLoggerFactory()); - services.AddSingleton(TestHelper.DbProviderFactoryCreator); services.AddTransient(); IWebHostEnvironment webHostEnvironment = TestHelper.GetWebHostEnvironment(); services.AddRequiredNetCoreServices(TestHelper, webHostEnvironment); @@ -125,6 +127,8 @@ namespace Umbraco.Cms.Tests.Integration.Testing .AddBackOfficeIdentity() .AddMembersIdentity() .AddExamine() + .AddUmbracoSqlServerSupport() + .AddUmbracoSqliteSupport() .AddTestServices(TestHelper); if (TestOptions.Mapper) diff --git a/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTestBase.cs b/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTestBase.cs index b77ec1806f..237bf87daa 100644 --- a/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTestBase.cs +++ b/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTestBase.cs @@ -8,9 +8,11 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; using NUnit.Framework; using Serilog; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Tests.Common.Testing; @@ -116,105 +118,109 @@ public abstract class UmbracoIntegrationTestBase TestUmbracoDatabaseFactoryProvider testDatabaseFactoryProvider = serviceProvider.GetRequiredService(); IUmbracoDatabaseFactory databaseFactory = serviceProvider.GetRequiredService(); ILoggerFactory loggerFactory = serviceProvider.GetRequiredService(); + IOptionsMonitor connectionStrings = serviceProvider.GetRequiredService>(); // This will create a db, install the schema and ensure the app is configured to run - SetupTestDatabase(testDatabaseFactoryProvider, databaseFactory, loggerFactory, state, TestHelper.WorkingDirectory); + SetupTestDatabase(testDatabaseFactoryProvider, connectionStrings, databaseFactory, loggerFactory, state); } - private void ConfigureTestDatabaseFactory(TestDbMeta meta, IUmbracoDatabaseFactory factory, IRuntimeState state) + private void ConfigureTestDatabaseFactory( + TestDbMeta meta, + IUmbracoDatabaseFactory factory, + IRuntimeState state, + IOptionsMonitor connectionStrings) { // It's just been pulled from container and wasn't used to create test database Assert.IsFalse(factory.Configured); - factory.Configure(meta.ConnectionString, Constants.DatabaseProviders.SqlServer); + factory.Configure(meta.ToStronglyTypedConnectionString()); + connectionStrings.CurrentValue.ConnectionString = meta.ConnectionString; + connectionStrings.CurrentValue.ProviderName = meta.Provider; state.DetermineRuntimeLevel(); } private void SetupTestDatabase( - TestUmbracoDatabaseFactoryProvider testUmbracoDatabaseFactoryProvider, - IUmbracoDatabaseFactory databaseFactory, - ILoggerFactory loggerFactory, - IRuntimeState runtimeState, - string workingDirectory) - { - if (TestOptions.Database == UmbracoTestOptions.Database.None) + TestUmbracoDatabaseFactoryProvider testUmbracoDatabaseFactoryProvider, + IOptionsMonitor connectionStrings, + IUmbracoDatabaseFactory databaseFactory, + ILoggerFactory loggerFactory, + IRuntimeState runtimeState) { - return; - } + if (TestOptions.Database == UmbracoTestOptions.Database.None) + { + return; + } - // need to manually register this factory - DbProviderFactories.RegisterFactory(Constants.DbProviderNames.SqlServer, SqlClientFactory.Instance); + ITestDatabase db = GetOrCreateDatabase(loggerFactory, testUmbracoDatabaseFactoryProvider); - var dbFilePath = Path.Combine(workingDirectory, "LocalDb"); + switch (TestOptions.Database) + { + case UmbracoTestOptions.Database.NewSchemaPerTest: - ITestDatabase db = GetOrCreateDatabase(dbFilePath, loggerFactory, testUmbracoDatabaseFactoryProvider); - - switch (TestOptions.Database) - { - case UmbracoTestOptions.Database.NewSchemaPerTest: - - // New DB + Schema - TestDbMeta newSchemaDbMeta = db.AttachSchema(); - - // Add teardown callback - AddOnTestTearDown(() => db.Detach(newSchemaDbMeta)); - - ConfigureTestDatabaseFactory(newSchemaDbMeta, databaseFactory, runtimeState); - - Assert.AreEqual(RuntimeLevel.Run, runtimeState.Level); - - break; - case UmbracoTestOptions.Database.NewEmptyPerTest: - TestDbMeta newEmptyDbMeta = db.AttachEmpty(); - - // Add teardown callback - AddOnTestTearDown(() => db.Detach(newEmptyDbMeta)); - - ConfigureTestDatabaseFactory(newEmptyDbMeta, databaseFactory, runtimeState); - - Assert.AreEqual(RuntimeLevel.Install, runtimeState.Level); - - break; - case UmbracoTestOptions.Database.NewSchemaPerFixture: - // Only attach schema once per fixture - // Doing it more than once will block the process since the old db hasn't been detached - // and it would be the same as NewSchemaPerTest even if it didn't block - if (_firstTestInFixture) - { // New DB + Schema - TestDbMeta newSchemaFixtureDbMeta = db.AttachSchema(); - s_fixtureDbMeta = newSchemaFixtureDbMeta; + TestDbMeta newSchemaDbMeta = db.AttachSchema(); // Add teardown callback - AddOnFixtureTearDown(() => db.Detach(newSchemaFixtureDbMeta)); - } + AddOnTestTearDown(() => db.Detach(newSchemaDbMeta)); - ConfigureTestDatabaseFactory(s_fixtureDbMeta, databaseFactory, runtimeState); + ConfigureTestDatabaseFactory(newSchemaDbMeta, databaseFactory, runtimeState, connectionStrings); - break; - case UmbracoTestOptions.Database.NewEmptyPerFixture: - // Only attach schema once per fixture - // Doing it more than once will block the process since the old db hasn't been detached - // and it would be the same as NewSchemaPerTest even if it didn't block - if (_firstTestInFixture) - { - // New DB + Schema - TestDbMeta newEmptyFixtureDbMeta = db.AttachEmpty(); - s_fixtureDbMeta = newEmptyFixtureDbMeta; + Assert.AreEqual(RuntimeLevel.Run, runtimeState.Level); + + break; + case UmbracoTestOptions.Database.NewEmptyPerTest: + TestDbMeta newEmptyDbMeta = db.AttachEmpty(); // Add teardown callback - AddOnFixtureTearDown(() => db.Detach(newEmptyFixtureDbMeta)); - } + AddOnTestTearDown(() => db.Detach(newEmptyDbMeta)); - ConfigureTestDatabaseFactory(s_fixtureDbMeta, databaseFactory, runtimeState); + ConfigureTestDatabaseFactory(newEmptyDbMeta, databaseFactory, runtimeState, connectionStrings); - break; - default: - throw new ArgumentOutOfRangeException(nameof(TestOptions), TestOptions, null); + Assert.AreEqual(RuntimeLevel.Install, runtimeState.Level); + + break; + case UmbracoTestOptions.Database.NewSchemaPerFixture: + // Only attach schema once per fixture + // Doing it more than once will block the process since the old db hasn't been detached + // and it would be the same as NewSchemaPerTest even if it didn't block + if (_firstTestInFixture) + { + // New DB + Schema + TestDbMeta newSchemaFixtureDbMeta = db.AttachSchema(); + s_fixtureDbMeta = newSchemaFixtureDbMeta; + + // Add teardown callback + AddOnFixtureTearDown(() => db.Detach(newSchemaFixtureDbMeta)); + } + + ConfigureTestDatabaseFactory(s_fixtureDbMeta, databaseFactory, runtimeState, connectionStrings); + + break; + case UmbracoTestOptions.Database.NewEmptyPerFixture: + // Only attach schema once per fixture + // Doing it more than once will block the process since the old db hasn't been detached + // and it would be the same as NewSchemaPerTest even if it didn't block + if (_firstTestInFixture) + { + // New DB + Schema + TestDbMeta newEmptyFixtureDbMeta = db.AttachEmpty(); + s_fixtureDbMeta = newEmptyFixtureDbMeta; + + // Add teardown callback + AddOnFixtureTearDown(() => db.Detach(newEmptyFixtureDbMeta)); + } + + ConfigureTestDatabaseFactory(s_fixtureDbMeta, databaseFactory, runtimeState, connectionStrings); + + break; + default: + throw new ArgumentOutOfRangeException(nameof(TestOptions), TestOptions, null); + } } - } - private static ITestDatabase GetOrCreateDatabase(string filesPath, ILoggerFactory loggerFactory, TestUmbracoDatabaseFactoryProvider dbFactory) + + private ITestDatabase GetOrCreateDatabase(ILoggerFactory loggerFactory, + TestUmbracoDatabaseFactoryProvider dbFactory) { lock (s_dbLocker) { @@ -223,15 +229,19 @@ public abstract class UmbracoIntegrationTestBase return s_dbInstance; } - // TODO: pull from IConfiguration var settings = new TestDatabaseSettings { - PrepareThreadCount = 4, - EmptyDatabasesCount = 2, - SchemaDatabaseCount = 4 + FilesPath = Path.Combine(TestHelper.WorkingDirectory, "databases"), + DatabaseType = Configuration.GetValue("Tests:Database:DatabaseType"), + PrepareThreadCount = Configuration.GetValue("Tests:Database:PrepareThreadCount"), + EmptyDatabasesCount = Configuration.GetValue("Tests:Database:EmptyDatabasesCount"), + SchemaDatabaseCount = Configuration.GetValue("Tests:Database:SchemaDatabaseCount"), + SQLServerMasterConnectionString = Configuration.GetValue("Tests:Database:SQLServerMasterConnectionString"), }; - s_dbInstance = TestDatabaseFactory.Create(settings, filesPath, loggerFactory, dbFactory); + Directory.CreateDirectory(settings.FilesPath); + + s_dbInstance = TestDatabaseFactory.Create(settings, dbFactory, loggerFactory); return s_dbInstance; } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Migrations/AdvancedMigrationTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Migrations/AdvancedMigrationTests.cs index 19d4080524..c2e5cc6c4f 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Migrations/AdvancedMigrationTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Migrations/AdvancedMigrationTests.cs @@ -18,6 +18,7 @@ using Umbraco.Cms.Infrastructure.Migrations.Install; using Umbraco.Cms.Infrastructure.Migrations.Upgrade; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Tests.Common.TestHelpers; using Umbraco.Cms.Tests.Common.Testing; using Umbraco.Cms.Tests.Integration.Testing; @@ -47,7 +48,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Migrations return new CreateTableOfTDtoMigration(c); }); - using (IScope scope = ScopeProvider.CreateScope()) + using (ScopeProvider.CreateScope(autoComplete: true)) { var upgrader = new Upgrader( new MigrationPlan("test") @@ -56,11 +57,10 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Migrations upgrader.Execute(MigrationPlanExecutor, ScopeProvider, Mock.Of()); - var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, LoggerFactory.CreateLogger(), LoggerFactory, UmbracoVersion, EventAggregator); - bool exists = helper.TableExists("umbracoUser"); - Assert.IsTrue(exists); + var db = ScopeAccessor.AmbientScope.Database; + var exists = ScopeAccessor.AmbientScope.SqlContext.SqlSyntax.DoesTableExist(db, "umbracoUser"); - scope.Complete(); + Assert.IsTrue(exists); } } @@ -99,6 +99,13 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Migrations [Test] public void CreateKeysAndIndexesOfTDto() { + if (BaseTestDatabase.IsSqlite()) + { + // TODO: Think about this for future migrations. + Assert.Ignore("Can't add / drop keys in SQLite."); + return; + } + IMigrationBuilder builder = Mock.Of(); Mock.Get(builder) .Setup(x => x.Build(It.IsAny(), It.IsAny())) @@ -134,6 +141,13 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Migrations [Test] public void CreateKeysAndIndexes() { + if (BaseTestDatabase.IsSqlite()) + { + // TODO: Think about this for future migrations. + Assert.Ignore("Can't add / drop keys in SQLite."); + return; + } + IMigrationBuilder builder = Mock.Of(); Mock.Get(builder) .Setup(x => x.Build(It.IsAny(), It.IsAny())) @@ -167,7 +181,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Migrations } [Test] - public void CreateColumn() + public void AddColumn() { IMigrationBuilder builder = Mock.Of(); Mock.Get(builder) @@ -179,22 +193,33 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Migrations case "CreateTableOfTDtoMigration": return new CreateTableOfTDtoMigration(c); case "CreateColumnMigration": - return new CreateColumnMigration(c); + return new AddColumnMigration(c); default: throw new NotSupportedException(); } }); - using (IScope scope = ScopeProvider.CreateScope()) + using (ScopeProvider.CreateScope(autoComplete: true)) { var upgrader = new Upgrader( new MigrationPlan("test") .From(string.Empty) .To("a") - .To("done")); + .To("done")); upgrader.Execute(MigrationPlanExecutor, ScopeProvider, Mock.Of()); - scope.Complete(); + + var db = ScopeAccessor.AmbientScope.Database; + + var columnInfo = ScopeAccessor.AmbientScope.SqlContext.SqlSyntax.GetColumnsInSchema(db) + .Where(x => x.TableName == "umbracoUser") + .FirstOrDefault(x => x.ColumnName == "Foo"); + + Assert.Multiple(() => + { + Assert.NotNull(columnInfo); + Assert.IsTrue(columnInfo.DataType.Contains("nvarchar")); + }); } } @@ -273,24 +298,16 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Migrations } } - public class CreateColumnMigration : MigrationBase + public class AddColumnMigration : MigrationBase { - public CreateColumnMigration(IMigrationContext context) + public AddColumnMigration(IMigrationContext context) : base(context) { } protected override void Migrate() { - // cannot delete the column without this, of course - Delete.KeysAndIndexes("umbracoUser").Do(); - - Delete.Column("id").FromTable("umbracoUser").Do(); - - TableDefinition table = DefinitionFactory.GetTableDefinition(typeof(UserDto), SqlSyntax); - ColumnDefinition column = table.Columns.First(x => x.Name == "id"); - string create = SqlSyntax.Format(column); // returns [id] INTEGER NOT NULL IDENTITY(1060,1) - Database.Execute($"ALTER TABLE {SqlSyntax.GetQuotedTableName("umbracoUser")} ADD " + create); + Database.Execute($"ALTER TABLE {SqlSyntax.GetQuotedTableName("umbracoUser")} ADD Foo nvarchar(255)"); } } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Packaging/CreatedPackageSchemaTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Packaging/CreatedPackageSchemaTests.cs index 6a5ee88426..c07f23ecb2 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Packaging/CreatedPackageSchemaTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Packaging/CreatedPackageSchemaTests.cs @@ -53,10 +53,10 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Packaging packageDefinition.Name = "UpdatedName"; CreatedPackageSchemaRepository.SavePackage(packageDefinition); - var result = CreatedPackageSchemaRepository.GetAll().ToList(); + var results = CreatedPackageSchemaRepository.GetAll().ToList(); - Assert.AreEqual(result.Count, 1); - Assert.AreEqual(result.FirstOrDefault()?.Name, "UpdatedName"); + Assert.AreEqual(expected: 1, actual: results.Count); + Assert.AreEqual(expected: "UpdatedName", actual: results.FirstOrDefault()?.Name); } } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/LocksTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/LocksTests.cs index e27163bf3b..1214098915 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/LocksTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/LocksTests.cs @@ -4,13 +4,16 @@ using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; -using Microsoft.Data.SqlClient; +using Microsoft.Extensions.DependencyInjection; using NPoco; using NUnit.Framework; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.DistributedLocking.Exceptions; using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Persistence.Sqlite.Interceptors; using Umbraco.Cms.Tests.Common.Testing; using Umbraco.Cms.Tests.Integration.Testing; +using Umbraco.Extensions; namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence { @@ -19,6 +22,12 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest, Logger = UmbracoTestOptions.Logger.Console)] public class LocksTests : UmbracoIntegrationTest { + protected override void ConfigureTestServices(IServiceCollection services) + { + // SQLite + retry policy makes tests fail, we retry before throwing distributed locking timeout. + services.RemoveAll(x => x.ImplementationType == typeof(SqliteAddRetryPolicyInterceptor)); + } + [SetUp] protected void SetUp() { @@ -47,6 +56,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence [Test] public void ConcurrentReadersTest() { + const int threadCount = 8; var threads = new Thread[threadCount]; var exceptions = new Exception[threadCount]; @@ -145,6 +155,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence [Test] public void ConcurrentWritersTest() { + const int threadCount = 8; var threads = new Thread[threadCount]; var exceptions = new Exception[threadCount]; @@ -239,6 +250,12 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence [Test] public void DeadLockTest() { + if (BaseTestDatabase.IsSqlite()) + { + Assert.Ignore("This test doesn't work with Microsoft.Data.Sqlite - SELECT * FROM sys.dm_tran_locks;"); + return; + } + Exception e1 = null, e2 = null; AutoResetEvent ev1 = new AutoResetEvent(false), ev2 = new AutoResetEvent(false); @@ -264,7 +281,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence //Assert.IsNotNull(e1); if (e1 != null) { - AssertIsSqlLockException(e1); + AssertIsDistributedLockingTimeoutException(e1); } // the assertion below depends on timing conditions - on a fast enough environment, @@ -275,17 +292,17 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence //Assert.IsNull(e2); if (e2 != null) { - AssertIsSqlLockException(e2); + AssertIsDistributedLockingTimeoutException(e2); } } - private void AssertIsSqlLockException(Exception e) + private void AssertIsDistributedLockingTimeoutException(Exception e) { - var sqlException = e as SqlException; + var sqlException = e as DistributedLockingTimeoutException; Assert.IsNotNull(sqlException); - Assert.AreEqual(1222, sqlException.Number); } + private void DeadLockTestThread(int id1, int id2, EventWaitHandle myEv, WaitHandle otherEv, ref Exception exception) { using (var scope = ScopeProvider.CreateScope()) @@ -327,6 +344,13 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence [Test] public void NoDeadLockTest() { + if (BaseTestDatabase.IsSqlite()) + { + Assert.Ignore("This test doesn't work with Microsoft.Data.Sqlite - SELECT * FROM sys.dm_tran_locks;"); + return; + } + + Exception e1 = null, e2 = null; AutoResetEvent ev1 = new AutoResetEvent(false), ev2 = new AutoResetEvent(false); @@ -353,13 +377,56 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence Assert.IsNull(e2); } - [Test] - public void Throws_When_Lock_Timeout_Is_Exceeded() + [Test] + public void Throws_When_Lock_Timeout_Is_Exceeded_Read() + { + if (BaseTestDatabase.IsSqlite()) + { + // Reader reads snapshot, isolated from the writer. + Assert.Ignore("Doesn't apply to SQLite with journal_mode=wal"); + } + + using (ExecutionContext.SuppressFlow()) + { + var t1 = Task.Run(() => + { + using (var scope = ScopeProvider.CreateScope()) + { + Console.WriteLine("Write lock A"); + // This will acquire right away + scope.EagerWriteLock(TimeSpan.FromMilliseconds(2000), Constants.Locks.ContentTree); + Thread.Sleep(6000); // Wait longer than the Read Lock B timeout + scope.Complete(); + Console.WriteLine("Finished Write lock A"); + } + }); + + Thread.Sleep(500); // 100% sure task 1 starts first + + var t2 = Task.Run(() => + { + using (var scope = ScopeProvider.CreateScope()) + { + Console.WriteLine("Read lock B"); + + // This will wait for the write lock to release but it isn't going to wait long + // enough so an exception will be thrown. + Assert.Throws(() => + scope.EagerReadLock(TimeSpan.FromMilliseconds(3000), Constants.Locks.ContentTree)); + scope.Complete(); + Console.WriteLine("Finished Read lock B"); + } + }); + + Task.WaitAll(t1, t2); + } + } + + [Test] + public void Throws_When_Lock_Timeout_Is_Exceeded_Write() { using (ExecutionContext.SuppressFlow()) { - - var t1 = Task.Run(() => { using (var scope = ScopeProvider.CreateScope()) @@ -380,40 +447,31 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence { using (var scope = ScopeProvider.CreateScope()) { - Console.WriteLine("Read lock B"); + Console.WriteLine("Write lock B"); // This will wait for the write lock to release but it isn't going to wait long // enough so an exception will be thrown. - Assert.Throws(() => - scope.EagerReadLock(TimeSpan.FromMilliseconds(3000), Constants.Locks.ContentTree)); - scope.Complete(); - Console.WriteLine("Finished Read lock B"); - } - }); - - var t3 = Task.Run(() => - { - using (var scope = ScopeProvider.CreateScope()) - { - Console.WriteLine("Write lock C"); - - // This will wait for the write lock to release but it isn't going to wait long - // enough so an exception will be thrown. - Assert.Throws(() => + Assert.Throws(() => scope.EagerWriteLock(TimeSpan.FromMilliseconds(3000), Constants.Locks.ContentTree)); scope.Complete(); - Console.WriteLine("Finished Write lock C"); + Console.WriteLine("Finished Write lock B"); } }); - Task.WaitAll(t1, t2, t3); + Task.WaitAll(t1, t2); } } [Test] public void Read_Lock_Waits_For_Write_Lock() { + if (BaseTestDatabase.IsSqlite()) + { + // Reader reads snapshot, isolated from the writer. + Assert.Ignore("Doesn't apply to SQLite with journal_mode=wal"); + } + var locksCompleted = 0; using (ExecutionContext.SuppressFlow()) diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/NPocoTests/NPocoFetchTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/NPocoTests/NPocoFetchTests.cs index 19976da976..4954d7a371 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/NPocoTests/NPocoFetchTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/NPocoTests/NPocoFetchTests.cs @@ -438,7 +438,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.NPoco var k1 = new ThingA12Dto { Name = "a", Thing1Id = tA1A.Id, Thing2Id = tA2A.Id }; ScopeAccessor.AmbientScope.Database.Insert(k1); - var k2 = new ThingA12Dto { Name = "B", Thing1Id = tA1A.Id, Thing2Id = tA2B.Id }; + var k2 = new ThingA12Dto { Name = "b", Thing1Id = tA1A.Id, Thing2Id = tA2B.Id }; ScopeAccessor.AmbientScope.Database.Insert(k2); string sql = @"SELECT a1.id, a1.name, diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentRepositoryTest.cs index 2059564cd8..55409fe6d7 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentRepositoryTest.cs @@ -25,6 +25,7 @@ using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; using Umbraco.Cms.Infrastructure.Scoping; +using Umbraco.Cms.Persistence.SqlServer.Services; using Umbraco.Cms.Tests.Common.Builders; using Umbraco.Cms.Tests.Common.Testing; using Umbraco.Cms.Tests.Integration.Testing; diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/MacroRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/MacroRepositoryTest.cs index 498e5b10e2..bb1e04fe91 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/MacroRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/MacroRepositoryTest.cs @@ -2,6 +2,7 @@ // See LICENSE for more details. using System.Collections.Generic; +using System.Data.Common; using System.Linq; using Microsoft.Data.SqlClient; using Microsoft.Extensions.Logging; @@ -41,7 +42,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos var macro = new Macro(ShortStringHelper, "test1", "Test", "~/views/macropartials/test.cshtml"); - Assert.Throws(() => repository.Save(macro)); + Assert.That(() => repository.Save(macro), Throws.InstanceOf()); } } @@ -57,7 +58,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos IMacro macro = repository.Get(1); macro.Alias = "test2"; - Assert.Throws(() => repository.Save(macro)); + Assert.That(() => repository.Save(macro), Throws.InstanceOf()); } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/RelationRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/RelationRepositoryTest.cs index a20706c597..f9d0b5291a 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/RelationRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/RelationRepositoryTest.cs @@ -190,23 +190,23 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos // Get parent entities for child id var parents = repository.GetPagedParentEntitiesByChildId(createdMedia[0].Id, 0, 11, out long totalRecords).ToList(); - Assert.AreEqual(6, totalRecords); - Assert.AreEqual(6, parents.Count); + Assert.AreEqual(9, totalRecords); + Assert.AreEqual(9, parents.Count); - // add the next page + // Add the next page parents.AddRange(repository.GetPagedParentEntitiesByChildId(createdMedia[0].Id, 1, 11, out totalRecords)); - Assert.AreEqual(6, totalRecords); - Assert.AreEqual(6, parents.Count); + Assert.AreEqual(9, totalRecords); + Assert.AreEqual(9, parents.Count); var contentEntities = parents.OfType().ToList(); var mediaEntities = parents.OfType().ToList(); var memberEntities = parents.OfType().ToList(); Assert.AreEqual(3, contentEntities.Count); - Assert.AreEqual(0, mediaEntities.Count); + Assert.AreEqual(3, mediaEntities.Count); Assert.AreEqual(3, memberEntities.Count); - // only of a certain type + // Only of a certain type parents.AddRange(repository.GetPagedParentEntitiesByChildId(createdMedia[0].Id, 0, 100, out totalRecords, UmbracoObjectTypes.Document.GetGuid())); Assert.AreEqual(3, totalRecords); @@ -214,7 +214,32 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos Assert.AreEqual(3, totalRecords); parents.AddRange(repository.GetPagedParentEntitiesByChildId(createdMedia[0].Id, 0, 100, out totalRecords, UmbracoObjectTypes.Media.GetGuid())); - Assert.AreEqual(0, totalRecords); + Assert.AreEqual(3, totalRecords); + + // Test relations on content + var contentParents = repository.GetPagedParentEntitiesByChildId(createdContent[0].Id, 0, int.MaxValue, out totalRecords).ToList(); + Assert.AreEqual(6, totalRecords); + Assert.AreEqual(6, contentParents.Count); + + // Test getting relations of specified relation types + var relatedMediaRelType = RelationService.GetRelationTypeByAlias(Constants.Conventions.RelationTypes.RelatedMediaAlias); + var relatedContentRelType = RelationService.GetRelationTypeByAlias(Constants.Conventions.RelationTypes.RelatedDocumentAlias); + + parents = repository.GetPagedParentEntitiesByChildId(createdMedia[0].Id, 0, 11, out totalRecords, new int[] { relatedContentRelType.Id, relatedMediaRelType.Id }).ToList(); + Assert.AreEqual(6, totalRecords); + Assert.AreEqual(6, parents.Count); + + parents = repository.GetPagedParentEntitiesByChildId(createdMedia[0].Id, 1, 11, out totalRecords, new int[] { relatedContentRelType.Id, relatedMediaRelType.Id }).ToList(); + Assert.AreEqual(6, totalRecords); + Assert.AreEqual(0, parents.Count); + + parents = repository.GetPagedParentEntitiesByChildId(createdContent[0].Id, 0, 6, out totalRecords, new int[] { relatedContentRelType.Id, relatedMediaRelType.Id }).ToList(); + Assert.AreEqual(3, totalRecords); + Assert.AreEqual(3, parents.Count); + + parents = repository.GetPagedParentEntitiesByChildId(createdContent[0].Id, 1, 6, out totalRecords, new int[] { relatedContentRelType.Id, relatedMediaRelType.Id }).ToList(); + Assert.AreEqual(3, totalRecords); + Assert.AreEqual(0, parents.Count); } } @@ -256,19 +281,19 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos // Get parent entities for child id var parents = repository.GetPagedChildEntitiesByParentId(createdContent[0].Id, 0, 6, out long totalRecords).ToList(); - Assert.AreEqual(3, totalRecords); - Assert.AreEqual(3, parents.Count); + Assert.AreEqual(6, totalRecords); + Assert.AreEqual(6, parents.Count); - // add the next page + // Add the next page parents.AddRange(repository.GetPagedChildEntitiesByParentId(createdContent[0].Id, 1, 6, out totalRecords)); - Assert.AreEqual(3, totalRecords); - Assert.AreEqual(3, parents.Count); + Assert.AreEqual(6, totalRecords); + Assert.AreEqual(6, parents.Count); var contentEntities = parents.OfType().ToList(); var mediaEntities = parents.OfType().ToList(); var memberEntities = parents.OfType().ToList(); - Assert.AreEqual(0, contentEntities.Count); + Assert.AreEqual(3, contentEntities.Count); Assert.AreEqual(3, mediaEntities.Count); Assert.AreEqual(0, memberEntities.Count); @@ -281,6 +306,18 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos parents.AddRange(repository.GetPagedChildEntitiesByParentId(createdContent[0].Id, 0, 100, out totalRecords, UmbracoObjectTypes.Member.GetGuid())); Assert.AreEqual(0, totalRecords); + + // Test getting relations of specified relation types + IRelationType relatedMediaRelType = RelationService.GetRelationTypeByAlias(Constants.Conventions.RelationTypes.RelatedMediaAlias); + IRelationType relatedContentRelType = RelationService.GetRelationTypeByAlias(Constants.Conventions.RelationTypes.RelatedDocumentAlias); + + parents = repository.GetPagedChildEntitiesByParentId(createdContent[0].Id, 0, 6, out totalRecords, new int[] { relatedContentRelType.Id, relatedMediaRelType.Id }).ToList(); + Assert.AreEqual(3, totalRecords); + Assert.AreEqual(3, parents.Count); + + parents = repository.GetPagedChildEntitiesByParentId(createdContent[0].Id, 1, 6, out totalRecords, new int[] { relatedContentRelType.Id, relatedMediaRelType.Id }).ToList(); + Assert.AreEqual(3, totalRecords); + Assert.AreEqual(0, parents.Count); } } @@ -297,6 +334,15 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos createdContent.Add(c1); } + // Create related content + var relatedContent = new List(); + for (int i = 0; i < 3; i++) + { + Content c1 = ContentBuilder.CreateBasicContent(contentType); + ContentService.Save(c1); + relatedContent.Add(c1); + } + // Create media createdMedia = new List(); MediaType imageType = MediaTypeBuilder.CreateImageMediaType("myImage"); @@ -314,14 +360,24 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos createdMembers = MemberBuilder.CreateSimpleMembers(memberType, 3).ToList(); GetMemberService().Save(createdMembers); - IRelationType relType = RelationService.GetRelationTypeByAlias(Constants.Conventions.RelationTypes.RelatedMediaAlias); + IRelationType relatedMediaRelType = RelationService.GetRelationTypeByAlias(Constants.Conventions.RelationTypes.RelatedMediaAlias); + IRelationType relatedContentRelType = RelationService.GetRelationTypeByAlias(Constants.Conventions.RelationTypes.RelatedDocumentAlias); // Relate content to media foreach (IContent content in createdContent) { foreach (IMedia media in createdMedia) { - RelationService.Relate(content.Id, media.Id, relType); + RelationService.Relate(content.Id, media.Id, relatedMediaRelType); + } + } + + // Relate content to content + foreach (IContent relContent in relatedContent) + { + foreach (IContent content in createdContent) + { + RelationService.Relate(relContent.Id, content.Id, relatedContentRelType); } } @@ -330,7 +386,67 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos { foreach (IMedia media in createdMedia) { - RelationService.Relate(member.Id, media.Id, relType); + RelationService.Relate(member.Id, media.Id, relatedMediaRelType); + } + } + + // Create copied content + var copiedContent = new List(); + for (int i = 0; i < 3; i++) + { + Content c1 = ContentBuilder.CreateBasicContent(contentType); + ContentService.Save(c1); + copiedContent.Add(c1); + } + + IRelationType copiedContentRelType = RelationService.GetRelationTypeByAlias(Constants.Conventions.RelationTypes.RelateDocumentOnCopyAlias); + + // Relate content to content (mimics copy) + foreach (IContent content in createdContent) + { + foreach (IContent cpContent in copiedContent) + { + RelationService.Relate(content.Id, cpContent.Id, copiedContentRelType); + } + } + + // Create trashed content + var trashedContent = new List(); + for (int i = 0; i < 3; i++) + { + Content c1 = ContentBuilder.CreateBasicContent(contentType); + ContentService.Save(c1); + trashedContent.Add(c1); + } + + IRelationType trashedRelType = RelationService.GetRelationTypeByAlias(Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteAlias); + + // Relate to trashed content + foreach (IContent trContent in trashedContent) + { + foreach (IContent content in createdContent) + { + RelationService.Relate(trContent.Id, content.Id, trashedRelType); + } + } + + // Create trashed media + var trashedMedia = new List(); + for (int i = 0; i < 3; i++) + { + Media m1 = MediaBuilder.CreateMediaImage(imageType, -1); + MediaService.Save(m1); + trashedMedia.Add(m1); + } + + IRelationType trashedMediaRelType = RelationService.GetRelationTypeByAlias(Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteAlias); + + // Relate to trashed media + foreach (IMedia trMedia in trashedMedia) + { + foreach (IMedia media in createdMedia) + { + RelationService.Relate(trMedia.Id, media.Id, trashedMediaRelType); } } } @@ -418,14 +534,16 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos "relateContentOnCopy", true, Constants.ObjectTypes.Document, - new Guid("C66BA18E-EAF3-4CFF-8A22-41B16D66A972")); + new Guid("C66BA18E-EAF3-4CFF-8A22-41B16D66A972"), + false); _relateContentType = new RelationType( "Relate ContentType on Copy", "relateContentTypeOnCopy", true, Constants.ObjectTypes.DocumentType, - new Guid("A2CB7800-F571-4787-9638-BC48539A0EFB")); + new Guid("A2CB7800-F571-4787-9638-BC48539A0EFB"), + false); using (IScope scope = ScopeProvider.CreateScope()) { diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/RelationTypeRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/RelationTypeRepositoryTest.cs index d99de65721..1f8ba39057 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/RelationTypeRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/RelationTypeRepositoryTest.cs @@ -37,7 +37,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos RelationTypeRepository repository = CreateRepository(provider); // Act - var relateMemberToContent = new RelationType("Relate Member to Content", "relateMemberToContent", true, Constants.ObjectTypes.Member, Constants.ObjectTypes.Document); + var relateMemberToContent = new RelationType("Relate Member to Content", "relateMemberToContent", true, Constants.ObjectTypes.Member, Constants.ObjectTypes.Document, true); repository.Save(relateMemberToContent); @@ -102,12 +102,13 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos RelationTypeRepository repository = CreateRepository(provider); // Act - IRelationType relationType = repository.Get(8); + var relationType = repository.Get(8) as IRelationTypeWithIsDependency; // Assert Assert.That(relationType, Is.Not.Null); Assert.That(relationType.HasIdentity, Is.True); Assert.That(relationType.IsBidirectional, Is.True); + Assert.That(relationType.IsDependency, Is.True); Assert.That(relationType.Alias, Is.EqualTo("relateContentToMedia")); Assert.That(relationType.Name, Is.EqualTo("Relate Content to Media")); Assert.That(relationType.ChildObjectType, Is.EqualTo(Constants.ObjectTypes.Media)); @@ -216,9 +217,9 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos public void CreateTestData() { - var relateContent = new RelationType("Relate Content on Copy", "relateContentOnCopy", true, Constants.ObjectTypes.Document, Constants.ObjectTypes.Document); - var relateContentType = new RelationType("Relate ContentType on Copy", "relateContentTypeOnCopy", true, Constants.ObjectTypes.DocumentType, Constants.ObjectTypes.DocumentType); - var relateContentMedia = new RelationType("Relate Content to Media", "relateContentToMedia", true, Constants.ObjectTypes.Document, Constants.ObjectTypes.Media); + var relateContent = new RelationType("Relate Content on Copy", "relateContentOnCopy", true, Constants.ObjectTypes.Document, Constants.ObjectTypes.Document, false); + var relateContentType = new RelationType("Relate ContentType on Copy", "relateContentTypeOnCopy", true, Constants.ObjectTypes.DocumentType, Constants.ObjectTypes.DocumentType, false); + var relateContentMedia = new RelationType("Relate Content to Media", "relateContentToMedia", true, Constants.ObjectTypes.Document, Constants.ObjectTypes.Media, true); IScopeProvider provider = ScopeProvider; using (IScope scope = provider.CreateScope()) diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/ServerRegistrationRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/ServerRegistrationRepositoryTest.cs index ff4ced61ee..bc7058ed58 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/ServerRegistrationRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/ServerRegistrationRepositoryTest.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Data.Common; using System.Linq; using Microsoft.Data.SqlClient; using Microsoft.Extensions.Logging; @@ -44,7 +45,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos var server = new ServerRegistration("http://shazwazza.com", "COMPUTER1", DateTime.Now); - Assert.Throws(() => repository.Save(server)); + Assert.That(() => repository.Save(server), Throws.InstanceOf()); } } @@ -60,7 +61,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos IServerRegistration server = repository.Get(1); server.ServerIdentity = "COMPUTER2"; - Assert.Throws(() => repository.Save(server)); + Assert.That(() => repository.Save(server), Throws.InstanceOf()); } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/SchemaValidationTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/SchemaValidationTest.cs index 90f6fab9e1..cd5c45868e 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/SchemaValidationTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/SchemaValidationTest.cs @@ -9,10 +9,11 @@ using Umbraco.Cms.Tests.Integration.Testing; namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence { [TestFixture] - [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerFixture)] + [UmbracoTest(Database = UmbracoTestOptions.Database.NewEmptyPerTest)] public class SchemaValidationTest : UmbracoIntegrationTest { private IUmbracoVersion UmbracoVersion => GetRequiredService(); + private IEventAggregator EventAggregator => GetRequiredService(); [Test] @@ -20,9 +21,10 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence { DatabaseSchemaResult result; - using (var scope = ScopeProvider.CreateScope()) + using (ScopeProvider.CreateScope(autoComplete: true)) { var schema = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, LoggerFactory.CreateLogger(), LoggerFactory, UmbracoVersion, EventAggregator); + schema.InitializeDatabaseSchema(); result = schema.ValidateSchema(DatabaseSchemaCreator.OrderedTables); } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/SqlServerTableByTableTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/SqlServerTableByTableTest.cs deleted file mode 100644 index 8c34202fe4..0000000000 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/SqlServerTableByTableTest.cs +++ /dev/null @@ -1,508 +0,0 @@ -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using NUnit.Framework; -using Umbraco.Cms.Core.Configuration; -using Umbraco.Cms.Core.Events; -using Umbraco.Cms.Infrastructure.Migrations.Install; -using Umbraco.Cms.Infrastructure.Persistence.Dtos; -using Umbraco.Cms.Tests.Common.Testing; -using Umbraco.Cms.Tests.Integration.Testing; - -namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence -{ - [TestFixture] - [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] - public class SqlServerTableByTableTest : UmbracoIntegrationTest - { - private IUmbracoVersion UmbracoVersion => GetRequiredService(); - private static ILoggerFactory _loggerFactory = NullLoggerFactory.Instance; - private IEventAggregator EventAggregator => GetRequiredService(); - - [Test] - public void Can_Create_umbracoNode_Table() - { - using (var scope = ScopeProvider.CreateScope()) - { - var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); - - helper.CreateTable(); - - scope.Complete(); - } - } - - [Test] - public void Can_Create_umbracoAccess_Table() - { - using (var scope = ScopeProvider.CreateScope()) - { - var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); - - helper.CreateTable(); - helper.CreateTable(); - - scope.Complete(); - } - } - - [Test] - public void Can_Create_umbracoAccessRule_Table() - { - using (var scope = ScopeProvider.CreateScope()) - { - var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); - - helper.CreateTable(); - helper.CreateTable(); - helper.CreateTable(); - - scope.Complete(); - } - } - - [Test] - public void Can_Create_cmsContentType2ContentType_Table() - { - using (var scope = ScopeProvider.CreateScope()) - { - var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); - - helper.CreateTable(); - helper.CreateTable(); - - scope.Complete(); - } - } - - [Test] - public void Can_Create_cmsContentTypeAllowedContentType_Table() - { - using (var scope = ScopeProvider.CreateScope()) - { - var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); - - helper.CreateTable(); - helper.CreateTable(); - helper.CreateTable(); - - scope.Complete(); - } - } - - [Test] - public void Can_Create_cmsContentType_Table() - { - using (var scope = ScopeProvider.CreateScope()) - { - var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); - - helper.CreateTable(); - helper.CreateTable(); - - scope.Complete(); - } - } - - [Test] - public void Can_Create_ContentVersion_Table() - { - using (var scope = ScopeProvider.CreateScope()) - { - var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); - - helper.CreateTable(); - helper.CreateTable(); - helper.CreateTable(); - helper.CreateTable(); - - scope.Complete(); - } - } - - [Test] - public void Can_Create_cmsDataType_Table() - { - using (var scope = ScopeProvider.CreateScope()) - { - var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); - - helper.CreateTable(); - helper.CreateTable(); - - scope.Complete(); - } - } - - [Test] - public void Can_Create_cmsDictionary_Table() - { - using (var scope = ScopeProvider.CreateScope()) - { - var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); - - helper.CreateTable(); - - scope.Complete(); - } - } - - [Test] - public void Can_Create_cmsLanguageText_Table() - { - using (var scope = ScopeProvider.CreateScope()) - { - var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); - - helper.CreateTable(); - helper.CreateTable(); - helper.CreateTable(); - - scope.Complete(); - } - } - - [Test] - public void Can_Create_cmsTemplate_Table() - { - using (var scope = ScopeProvider.CreateScope()) - { - var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); - - helper.CreateTable(); - helper.CreateTable(); - - scope.Complete(); - } - } - - [Test] - public void Can_Create_Document_Table() - { - using (var scope = ScopeProvider.CreateScope()) - { - var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); - - helper.CreateTable(); - helper.CreateTable(); - helper.CreateTable(); - helper.CreateTable(); - helper.CreateTable(); - - scope.Complete(); - } - } - - [Test] - public void Can_Create_DocumentType_Table() - { - using (var scope = ScopeProvider.CreateScope()) - { - var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); - - helper.CreateTable(); - helper.CreateTable(); - helper.CreateTable(); - helper.CreateTable(); - - scope.Complete(); - } - } - - [Test] - public void Can_Create_umbracoDomains_Table() - { - using (var scope = ScopeProvider.CreateScope()) - { - var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); - - helper.CreateTable(); - helper.CreateTable(); - - scope.Complete(); - } - } - - [Test] - public void Can_Create_umbracoLogViewerQuery_Table() - { - using (var scope = ScopeProvider.CreateScope()) - { - var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); - - helper.CreateTable(); - - scope.Complete(); - } - } - - [Test] - public void Can_Create_umbracoLanguage_Table() - { - using (var scope = ScopeProvider.CreateScope()) - { - var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); - - helper.CreateTable(); - - scope.Complete(); - } - } - - [Test] - public void Can_Create_umbracoLog_Table() - { - using (var scope = ScopeProvider.CreateScope()) - { - var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); - - helper.CreateTable(); - - scope.Complete(); - } - } - - [Test] - public void Can_Create_cmsMacro_Table() - { - using (var scope = ScopeProvider.CreateScope()) - { - var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); - - helper.CreateTable(); - - scope.Complete(); - } - } - - [Test] - public void Can_Create_cmsMember_Table() - { - using (var scope = ScopeProvider.CreateScope()) - { - var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); - - helper.CreateTable(); - helper.CreateTable(); - helper.CreateTable(); - helper.CreateTable(); - - scope.Complete(); - } - } - - [Test] - public void Can_Create_cmsMember2MemberGroup_Table() - { - using (var scope = ScopeProvider.CreateScope()) - { - var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); - - helper.CreateTable(); - helper.CreateTable(); - helper.CreateTable(); - helper.CreateTable(); - helper.CreateTable(); - - scope.Complete(); - } - } - - [Test] - public void Can_Create_cmsMemberType_Table() - { - using (var scope = ScopeProvider.CreateScope()) - { - var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); - - helper.CreateTable(); - helper.CreateTable(); - helper.CreateTable(); - - scope.Complete(); - } - } - - [Test] - public void Can_Create_PropertyData_Table() - { - using (var scope = ScopeProvider.CreateScope()) - { - var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); - - helper.CreateTable(); - helper.CreateTable(); - helper.CreateTable(); - helper.CreateTable(); - helper.CreateTable(); - helper.CreateTable(); - - scope.Complete(); - } - } - - [Test] - public void Can_Create_cmsPropertyType_Table() - { - using (var scope = ScopeProvider.CreateScope()) - { - var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); - - helper.CreateTable(); - helper.CreateTable(); - helper.CreateTable(); - helper.CreateTable(); - helper.CreateTable(); - - scope.Complete(); - } - } - - [Test] - public void Can_Create_cmsPropertyTypeGroup_Table() - { - using (var scope = ScopeProvider.CreateScope()) - { - var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); - - helper.CreateTable(); - helper.CreateTable(); - helper.CreateTable(); - - scope.Complete(); - } - } - - [Test] - public void Can_Create_umbracoRelation_Table() - { - using (var scope = ScopeProvider.CreateScope()) - { - var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); - - helper.CreateTable(); - helper.CreateTable(); - helper.CreateTable(); - - scope.Complete(); - } - } - - [Test] - public void Can_Create_umbracoRelationType_Table() - { - using (var scope = ScopeProvider.CreateScope()) - { - var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); - - helper.CreateTable(); - - scope.Complete(); - } - } - - [Test] - public void Can_Create_cmsTags_Table() - { - using (var scope = ScopeProvider.CreateScope()) - { - var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); - - helper.CreateTable(); - - scope.Complete(); - } - } - - [Test] - public void Can_Create_cmsTagRelationship_Table() - { - using (var scope = ScopeProvider.CreateScope()) - { - var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); - - helper.CreateTable(); - helper.CreateTable(); - helper.CreateTable(); - helper.CreateTable(); - helper.CreateTable(); - helper.CreateTable(); - helper.CreateTable(); - helper.CreateTable(); - helper.CreateTable(); - - scope.Complete(); - } - } - - [Test] - public void Can_Create_umbracoUser_Table() - { - using (var scope = ScopeProvider.CreateScope()) - { - var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); - - helper.CreateTable(); - - scope.Complete(); - } - } - - [Test] - public void Can_Create_umbracoUserGroup_Table() - { - using (var scope = ScopeProvider.CreateScope()) - { - var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); - - helper.CreateTable(); - helper.CreateTable(); - - scope.Complete(); - } - } - - [Test] - public void Can_Create_umbracoUser2NodeNotify_Table() - { - using (var scope = ScopeProvider.CreateScope()) - { - var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); - - helper.CreateTable(); - helper.CreateTable(); - helper.CreateTable(); - - scope.Complete(); - } - } - - public void Can_Create_umbracoGroupUser2app_Table() - { - using (var scope = ScopeProvider.CreateScope()) - { - var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); - - helper.CreateTable(); - helper.CreateTable(); - helper.CreateTable(); - - scope.Complete(); - } - } - - [Test] - public void Can_Create_umbracoUserGroup2NodePermission_Table() - { - using (var scope = ScopeProvider.CreateScope()) - { - var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); - - helper.CreateTable(); - helper.CreateTable(); - helper.CreateTable(); - - scope.Complete(); - } - } - } -} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/SyntaxProvider/SqlServerSyntaxProviderTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/SyntaxProvider/SqlServerSyntaxProviderTests.cs index a90d357b0e..0fae2991a9 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/SyntaxProvider/SqlServerSyntaxProviderTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/SyntaxProvider/SqlServerSyntaxProviderTests.cs @@ -15,6 +15,7 @@ using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; +using Umbraco.Cms.Persistence.SqlServer.Services; using Umbraco.Cms.Tests.Common.TestHelpers; using Umbraco.Cms.Tests.Common.Testing; using Umbraco.Cms.Tests.Integration.Testing; @@ -56,11 +57,14 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Synta var sqlOutput = SqlContext.SqlSyntax.GetDeleteSubquery("cmsContentNu", "nodeId", subQuery); - Assert.AreEqual(@"DELETE FROM [cmsContentNu] WHERE [nodeId] IN (SELECT [nodeId] FROM (SELECT DISTINCT cmsContentNu.nodeId -FROM [cmsContentNu] -INNER JOIN [umbracoNode] -ON [cmsContentNu].[nodeId] = [umbracoNode].[id] -WHERE (([umbracoNode].[nodeObjectType] = @0))) x)".Replace(Environment.NewLine, " ").Replace("\n", " ").Replace("\r", " "), + string t(string x) => SqlContext.SqlSyntax.GetQuotedTableName(x); + string c(string x) => SqlContext.SqlSyntax.GetQuotedColumnName(x); + + Assert.AreEqual(@$"DELETE FROM {t("cmsContentNu")} WHERE {c("nodeId")} IN (SELECT {c("nodeId")} FROM (SELECT DISTINCT cmsContentNu.nodeId +FROM {t("cmsContentNu")} +INNER JOIN {t("umbracoNode")} +ON {t("cmsContentNu")}.{c("nodeId")} = {t("umbracoNode")}.{c("id")} +WHERE (({t("umbracoNode")}.{c("nodeObjectType")} = @0))) x)".Replace(Environment.NewLine, " ").Replace("\n", " ").Replace("\r", " "), sqlOutput.SQL.Replace(Environment.NewLine, " ").Replace("\n", " ").Replace("\r", " ")); Assert.AreEqual(1, sqlOutput.Arguments.Length); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/UnitOfWorkTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/UnitOfWorkTests.cs index 640321b232..4eb670c4a8 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/UnitOfWorkTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/UnitOfWorkTests.cs @@ -3,7 +3,10 @@ using System; using NUnit.Framework; +using Umbraco.Cms.Core.DistributedLocking; using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Persistence.Sqlite.Services; +using Umbraco.Cms.Persistence.SqlServer.Services; using Umbraco.Cms.Tests.Common.Testing; using Umbraco.Cms.Tests.Integration.Testing; using Constants = Umbraco.Cms.Core.Constants; @@ -17,6 +20,12 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence [Test] public void ReadLockNonExisting() { + var lockingMechanism = GetRequiredService().DistributedLockingMechanism; + if (lockingMechanism is SqliteDistributedLockingMechanism) + { + Assert.Ignore("SqliteDistributedLockingMechanism doesn't query the umbracoLock table for read locks."); + } + IScopeProvider provider = ScopeProvider; Assert.Throws(() => { diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Runtime/FileSystemMainDomLockTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Runtime/FileSystemMainDomLockTests.cs new file mode 100644 index 0000000000..ebb9b8a6a7 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Runtime/FileSystemMainDomLockTests.cs @@ -0,0 +1,111 @@ +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Hosting; +using Umbraco.Cms.Core.Runtime; +using Umbraco.Cms.Infrastructure.Runtime; +using Umbraco.Cms.Tests.Integration.Testing; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Runtime +{ + [TestFixture] + internal class FileSystemMainDomLockTests : UmbracoIntegrationTest + { + private IMainDomKeyGenerator MainDomKeyGenerator { get; set; } + + private IHostingEnvironment HostingEnvironment { get; set; } + + private FileSystemMainDomLock FileSystemMainDomLock { get; set; } + + private string LockFilePath { get; set; } + private string LockReleaseFilePath { get; set; } + + [SetUp] + public void SetUp() + { + MainDomKeyGenerator = GetRequiredService(); + HostingEnvironment = GetRequiredService(); + + var lockFileName = $"MainDom_{MainDomKeyGenerator.GenerateKey()}.lock"; + LockFilePath = Path.Combine(HostingEnvironment.LocalTempPath, lockFileName); + LockReleaseFilePath = LockFilePath + "_release"; + + var globalSettings = Mock.Of>(); + Mock.Get(globalSettings).Setup(x => x.CurrentValue).Returns(new GlobalSettings()); + + var log = GetRequiredService>(); + FileSystemMainDomLock = new FileSystemMainDomLock(log, MainDomKeyGenerator, HostingEnvironment, globalSettings); + } + + [TearDown] + public void TearDown() + { + CleanupTestFile(LockFilePath); + CleanupTestFile(LockReleaseFilePath); + } + + private static void CleanupTestFile(string path) + { + for (var i = 0; i < 3; i++) + { + try + { + File.Delete(path); + return; + } + catch + { + Thread.Sleep(500 * (i + 1)); + } + } + } + + [Test] + public async Task AcquireLockAsync_WhenNoOtherHoldsLockFileHandle_ReturnsTrue() + { + using var sut = FileSystemMainDomLock; + + var result = await sut.AcquireLockAsync(1000); + + Assert.True(result); + } + + [Test] + public async Task AcquireLockAsync_WhenTimeoutExceeded_ReturnsFalse() + { + await using var lockFile = File.Open(LockFilePath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None); + + using var sut = FileSystemMainDomLock; + + var result = await sut.AcquireLockAsync(1000); + + Assert.False(result); + } + + [Test] + public async Task ListenAsync_WhenLockReleaseSignalFileFound_DropsLockFileHandle() + { + using var sut = FileSystemMainDomLock; + + await sut.AcquireLockAsync(1000); + + var before = await sut.AcquireLockAsync(1000); + + sut.CreateLockReleaseSignalFile(); + await sut.ListenAsync(); + + var after = await sut.AcquireLockAsync(1000); + + Assert.Multiple(() => + { + Assert.False(before); + Assert.True(after); + }); + } + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServiceTests.cs index 54e344d79b..3eaa89158d 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServiceTests.cs @@ -1977,7 +1977,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services UserService.Save(admin); RelationService.Save(new RelationType("test", "test", false, Constants.ObjectTypes.Document, - Constants.ObjectTypes.Document)); + Constants.ObjectTypes.Document, false)); Assert.IsNotNull(RelationService.Relate(content1, content2, "test")); PublicAccessService.Save(new PublicAccessEntry(content1, content2, content2, diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentTypeServiceVariantsTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentTypeServiceVariantsTests.cs index ad7df3cee5..bfda211d74 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentTypeServiceVariantsTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentTypeServiceVariantsTests.cs @@ -650,7 +650,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services Console.WriteLine(GetJson(document.Id)); AssertJsonStartsWith( document.Id, - "{'pd':{'value1':[{'c':'fr','v':'v1fr'},{'c':'en','v':'v1en'}],'value2':[{'v':'v2'}]},'cd':"); + "{'pd':{'value1':[{'c':'en','v':'v1en'},{'c':'fr','v':'v1fr'}],'value2':[{'v':'v2'}]},'cd':"); } [Test] @@ -825,7 +825,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services Console.WriteLine(GetJson(document.Id)); AssertJsonStartsWith( document.Id, - "{'pd':{'value1':[{'c':'fr','v':'v1fr'},{'c':'en','v':'v1en'}],'value2':[{'v':'v2'}]},'cd':"); + "{'pd':{'value1':[{'c':'en','v':'v1en'},{'c':'fr','v':'v1fr'}],'value2':[{'v':'v2'}]},'cd':"); // switch other property to Culture contentType.PropertyTypes.First(x => x.Alias == "value2").Variations = ContentVariation.Culture; @@ -844,7 +844,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services Console.WriteLine(GetJson(document.Id)); AssertJsonStartsWith( document.Id, - "{'pd':{'value1':[{'c':'fr','v':'v1fr'},{'c':'en','v':'v1en'}],'value2':[{'c':'en','v':'v2'}]},'cd':"); + "{'pd':{'value1':[{'c':'en','v':'v1en'},{'c':'fr','v':'v1fr'}],'value2':[{'c':'en','v':'v2'}]},'cd':"); } [TestCase(ContentVariation.Culture, ContentVariation.Nothing)] @@ -1106,7 +1106,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services Console.WriteLine(GetJson(document.Id)); AssertJsonStartsWith( document.Id, - "{'pd':{'value11':[{'c':'fr','v':'v11fr'},{'c':'en','v':'v11en'}],'value12':[{'v':'v12'}],'value21':[{'v':'v21en'}],'value22':[{'v':'v22'}]},'cd':"); + "{'pd':{'value11':[{'c':'en','v':'v11en'},{'c':'fr','v':'v11fr'}],'value12':[{'v':'v12'}],'value21':[{'v':'v21en'}],'value22':[{'v':'v22'}]},'cd':"); composed.PropertyTypes.First(x => x.Alias == "value21").Variations = ContentVariation.Culture; ContentTypeService.Save(composed); @@ -1115,7 +1115,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services Console.WriteLine(GetJson(document.Id)); AssertJsonStartsWith( document.Id, - "{'pd':{'value11':[{'c':'fr','v':'v11fr'},{'c':'en','v':'v11en'}],'value12':[{'v':'v12'}],'value21':[{'c':'fr','v':'v21fr'},{'c':'en','v':'v21en'}],'value22':[{'v':'v22'}]},'cd':"); + "{'pd':{'value11':[{'c':'en','v':'v11en'},{'c':'fr','v':'v11fr'}],'value12':[{'v':'v12'}],'value21':[{'c':'en','v':'v21en'},{'c':'fr','v':'v21fr'}],'value22':[{'v':'v22'}]},'cd':"); composing.Variations = ContentVariation.Nothing; ContentTypeService.Save(composing); @@ -1124,7 +1124,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services Console.WriteLine(GetJson(document.Id)); AssertJsonStartsWith( document.Id, - "{'pd':{'value11':[{'v':'v11en'}],'value12':[{'v':'v12'}],'value21':[{'c':'fr','v':'v21fr'},{'c':'en','v':'v21en'}],'value22':[{'v':'v22'}]},'cd':"); + "{'pd':{'value11':[{'v':'v11en'}],'value12':[{'v':'v12'}],'value21':[{'c':'en','v':'v21en'},{'c':'fr','v':'v21fr'}],'value22':[{'v':'v22'}]},'cd':"); composing.Variations = ContentVariation.Culture; ContentTypeService.Save(composing); @@ -1133,7 +1133,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services Console.WriteLine(GetJson(document.Id)); AssertJsonStartsWith( document.Id, - "{'pd':{'value11':[{'v':'v11en'}],'value12':[{'v':'v12'}],'value21':[{'c':'fr','v':'v21fr'},{'c':'en','v':'v21en'}],'value22':[{'v':'v22'}]},'cd':"); + "{'pd':{'value11':[{'v':'v11en'}],'value12':[{'v':'v12'}],'value21':[{'c':'en','v':'v21en'},{'c':'fr','v':'v21fr'}],'value22':[{'v':'v22'}]},'cd':"); composing.PropertyTypes.First(x => x.Alias == "value11").Variations = ContentVariation.Culture; ContentTypeService.Save(composing); @@ -1142,7 +1142,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services Console.WriteLine(GetJson(document.Id)); AssertJsonStartsWith( document.Id, - "{'pd':{'value11':[{'c':'fr','v':'v11fr'},{'c':'en','v':'v11en'}],'value12':[{'v':'v12'}],'value21':[{'c':'fr','v':'v21fr'},{'c':'en','v':'v21en'}],'value22':[{'v':'v22'}]},'cd':"); + "{'pd':{'value11':[{'c':'en','v':'v11en'},{'c':'fr','v':'v11fr'}],'value12':[{'v':'v12'}],'value21':[{'c':'en','v':'v21en'},{'c':'fr','v':'v21fr'}],'value22':[{'v':'v22'}]},'cd':"); } [Test] @@ -1247,7 +1247,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services Console.WriteLine(GetJson(document1.Id)); AssertJsonStartsWith( document1.Id, - "{'pd':{'value11':[{'c':'fr','v':'v11fr'},{'c':'en','v':'v11en'}],'value12':[{'v':'v12'}],'value21':[{'v':'v21en'}],'value22':[{'v':'v22'}]},'cd':"); + "{'pd':{'value11':[{'c':'en','v':'v11en'},{'c':'fr','v':'v11fr'}],'value12':[{'v':'v12'}],'value21':[{'v':'v21en'}],'value22':[{'v':'v22'}]},'cd':"); Console.WriteLine(GetJson(document2.Id)); AssertJsonStartsWith( @@ -1261,7 +1261,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services Console.WriteLine(GetJson(document1.Id)); AssertJsonStartsWith( document1.Id, - "{'pd':{'value11':[{'c':'fr','v':'v11fr'},{'c':'en','v':'v11en'}],'value12':[{'v':'v12'}],'value21':[{'c':'fr','v':'v21fr'},{'c':'en','v':'v21en'}],'value22':[{'v':'v22'}]},'cd':"); + "{'pd':{'value11':[{'c':'en','v':'v11en'},{'c':'fr','v':'v11fr'}],'value12':[{'v':'v12'}],'value21':[{'c':'en','v':'v21en'},{'c':'fr','v':'v21fr'}],'value22':[{'v':'v22'}]},'cd':"); Console.WriteLine(GetJson(document2.Id)); AssertJsonStartsWith( @@ -1275,7 +1275,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services Console.WriteLine(GetJson(document1.Id)); AssertJsonStartsWith( document1.Id, - "{'pd':{'value11':[{'v':'v11en'}],'value12':[{'v':'v12'}],'value21':[{'c':'fr','v':'v21fr'},{'c':'en','v':'v21en'}],'value22':[{'v':'v22'}]},'cd':"); + "{'pd':{'value11':[{'v':'v11en'}],'value12':[{'v':'v12'}],'value21':[{'c':'en','v':'v21en'},{'c':'fr','v':'v21fr'}],'value22':[{'v':'v22'}]},'cd':"); Console.WriteLine(GetJson(document2.Id)); AssertJsonStartsWith( @@ -1289,7 +1289,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services Console.WriteLine(GetJson(document1.Id)); AssertJsonStartsWith( document1.Id, - "{'pd':{'value11':[{'v':'v11en'}],'value12':[{'v':'v12'}],'value21':[{'c':'fr','v':'v21fr'},{'c':'en','v':'v21en'}],'value22':[{'v':'v22'}]},'cd':"); + "{'pd':{'value11':[{'v':'v11en'}],'value12':[{'v':'v12'}],'value21':[{'c':'en','v':'v21en'},{'c':'fr','v':'v21fr'}],'value22':[{'v':'v22'}]},'cd':"); Console.WriteLine(GetJson(document2.Id)); AssertJsonStartsWith( @@ -1303,7 +1303,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services Console.WriteLine(GetJson(document1.Id)); AssertJsonStartsWith( document1.Id, - "{'pd':{'value11':[{'c':'fr','v':'v11fr'},{'c':'en','v':'v11en'}],'value12':[{'v':'v12'}],'value21':[{'c':'fr','v':'v21fr'},{'c':'en','v':'v21en'}],'value22':[{'v':'v22'}]},'cd':"); + "{'pd':{'value11':[{'c':'en','v':'v11en'},{'c':'fr','v':'v11fr'}],'value12':[{'v':'v12'}],'value21':[{'c':'en','v':'v21en'},{'c':'fr','v':'v21fr'}],'value22':[{'v':'v22'}]},'cd':"); Console.WriteLine(GetJson(document2.Id)); AssertJsonStartsWith( diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/RelationServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/RelationServiceTests.cs index 3edba86760..0156100f0a 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/RelationServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/RelationServiceTests.cs @@ -266,4 +266,4 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services // TODO: Create a relation for entities of the wrong Entity Type (GUID) based on the Relation Type's defined parent/child object types } -} +} \ No newline at end of file diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ThreadSafetyServiceTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ThreadSafetyServiceTest.cs index 7611bea687..21d15c13a6 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ThreadSafetyServiceTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ThreadSafetyServiceTest.cs @@ -12,6 +12,7 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.Implement; +using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Tests.Common.Builders; using Umbraco.Cms.Tests.Common.Testing; using Umbraco.Cms.Tests.Integration.Testing; @@ -54,7 +55,11 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services { using (IScope scope = ScopeProvider.CreateScope()) { - ScopeAccessor.AmbientScope.Database.Execute("SET LOCK_TIMEOUT 60000"); + if (ScopeAccessor.AmbientScope.Database.DatabaseType.IsSqlServer()) + { + ScopeAccessor.AmbientScope.Database.Execute("SET LOCK_TIMEOUT 60000"); + } + service.Save(content); scope.Complete(); } @@ -64,7 +69,11 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services { using (IScope scope = ScopeProvider.CreateScope()) { - ScopeAccessor.AmbientScope.Database.Execute("SET LOCK_TIMEOUT 60000"); + if (ScopeAccessor.AmbientScope.Database.DatabaseType.IsSqlServer()) + { + ScopeAccessor.AmbientScope.Database.Execute("SET LOCK_TIMEOUT 60000"); + } + service.Save(media); scope.Complete(); } @@ -121,8 +130,10 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services { try { - ConcurrentStack currentStack = ((ScopeProvider)ScopeProvider).GetCallContextScopeValue(); - log.LogInformation("[{ThreadId}] Current Stack? {CurrentStack}", Thread.CurrentThread.ManagedThreadId, currentStack?.Count); + ConcurrentStack + currentStack = ((ScopeProvider)ScopeProvider).GetCallContextScopeValue(); + log.LogInformation("[{ThreadId}] Current Stack? {CurrentStack}", + Thread.CurrentThread.ManagedThreadId, currentStack?.Count); // NOTE: This is NULL because we have supressed the execution context flow. // If we don't do that we will get various exceptions because we're trying to run concurrent threads @@ -135,7 +146,8 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services // So although the test passes in v8, there's actually some strange things occuring because Scopes // are being created and disposed concurrently and out of order. var currentScope = ScopeAccessor.AmbientScope; - log.LogInformation("[{ThreadId}] Current Scope? {CurrentScope}", Thread.CurrentThread.ManagedThreadId, currentScope?.GetDebugInfo()); + log.LogInformation("[{ThreadId}] Current Scope? {CurrentScope}", + Thread.CurrentThread.ManagedThreadId, currentScope?.GetDebugInfo()); Assert.IsNull(currentScope); string name1 = "test-" + Guid.NewGuid(); @@ -218,8 +230,10 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services { try { - ConcurrentStack currentStack = ((ScopeProvider)ScopeProvider).GetCallContextScopeValue(); - log.LogInformation("[{ThreadId}] Current Stack? {CurrentStack}", Thread.CurrentThread.ManagedThreadId, currentStack?.Count); + ConcurrentStack + currentStack = ((ScopeProvider)ScopeProvider).GetCallContextScopeValue(); + log.LogInformation("[{ThreadId}] Current Stack? {CurrentStack}", + Thread.CurrentThread.ManagedThreadId, currentStack?.Count); // NOTE: This is NULL because we have supressed the execution context flow. // If we don't do that we will get various exceptions because we're trying to run concurrent threads @@ -232,7 +246,8 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services // So although the test passes in v8, there's actually some strange things occuring because Scopes // are being created and disposed concurrently and out of order. var currentScope = ScopeAccessor.AmbientScope; - log.LogInformation("[{ThreadId}] Current Scope? {CurrentScope}", Thread.CurrentThread.ManagedThreadId, currentScope?.GetDebugInfo()); + log.LogInformation("[{ThreadId}] Current Scope? {CurrentScope}", + Thread.CurrentThread.ManagedThreadId, currentScope?.GetDebugInfo()); Assert.IsNull(currentScope); string name1 = "test-" + Guid.NewGuid(); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj b/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj index 6070f468b1..103dbc3feb 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj +++ b/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj @@ -78,6 +78,9 @@ + + PreserveNewest + @@ -98,6 +101,8 @@ + + diff --git a/tests/Umbraco.Tests.Integration/appsettings.Tests.json b/tests/Umbraco.Tests.Integration/appsettings.Tests.json new file mode 100644 index 0000000000..8580268550 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/appsettings.Tests.json @@ -0,0 +1,11 @@ +{ + "Tests": { + "Database": { + "DatabaseType": "SQLite", + "PrepareThreadCount": 4, + "SchemaDatabaseCount": 4, + "EmptyDatabasesCount": 2, + "SQLServerMasterConnectionString": "" + } + } +} diff --git a/tests/Umbraco.Tests.UnitTests/AutoFixture/Customizations/UmbracoCustomizations.cs b/tests/Umbraco.Tests.UnitTests/AutoFixture/Customizations/UmbracoCustomizations.cs index 672bbd0862..ea6c76225b 100644 --- a/tests/Umbraco.Tests.UnitTests/AutoFixture/Customizations/UmbracoCustomizations.cs +++ b/tests/Umbraco.Tests.UnitTests/AutoFixture/Customizations/UmbracoCustomizations.cs @@ -39,10 +39,6 @@ namespace Umbraco.Cms.Tests.UnitTests.AutoFixture.Customizations // When requesting an IUserStore ensure we actually uses a IUserLockoutStore fixture.Customize>(cc => cc.FromFactory(Mock.Of>)); - fixture.Customize( - u => u.FromFactory( - (a, b, c) => new ConfigConnectionString(a, b, c))); - fixture.Customize( u => u.FromFactory( () => new UmbracoVersion())); @@ -63,11 +59,6 @@ namespace Umbraco.Cms.Tests.UnitTests.AutoFixture.Customizations Mock.Of(x => x.ToAbsolute(It.IsAny()) == "/umbraco" && x.ApplicationVirtualPath == string.Empty), Mock.Of(x => x.Level == RuntimeLevel.Run)))); - var configConnectionString = new ConfigConnectionString( - "ss", - "Data Source=(localdb)\\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\\Umbraco.mdf;Integrated Security=True"); - fixture.Customize(x => x.FromFactory(() => configConnectionString)); - var httpContextAccessor = new HttpContextAccessor { HttpContext = new DefaultHttpContext() }; fixture.Customize(x => x.FromFactory(() => httpContextAccessor.HttpContext)); fixture.Customize(x => x.FromFactory(() => httpContextAccessor)); diff --git a/tests/Umbraco.Tests.UnitTests/TestHelpers/BaseUsingSqlSyntax.cs b/tests/Umbraco.Tests.UnitTests/TestHelpers/BaseUsingSqlSyntax.cs index ca7950a7cf..05ebc64c65 100644 --- a/tests/Umbraco.Tests.UnitTests/TestHelpers/BaseUsingSqlSyntax.cs +++ b/tests/Umbraco.Tests.UnitTests/TestHelpers/BaseUsingSqlSyntax.cs @@ -14,6 +14,7 @@ using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Mappers; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; +using Umbraco.Cms.Persistence.SqlServer.Services; using Umbraco.Extensions; namespace Umbraco.Cms.Tests.UnitTests.TestHelpers diff --git a/tests/Umbraco.Tests.UnitTests/TestHelpers/TestHelper.cs b/tests/Umbraco.Tests.UnitTests/TestHelpers/TestHelper.cs index 5c7d4ea3bf..3b13e3ca0c 100644 --- a/tests/Umbraco.Tests.UnitTests/TestHelpers/TestHelper.cs +++ b/tests/Umbraco.Tests.UnitTests/TestHelpers/TestHelper.cs @@ -38,6 +38,7 @@ using Umbraco.Cms.Infrastructure.Mail; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Mappers; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; +using Umbraco.Cms.Persistence.SqlServer.Services; using Umbraco.Cms.Tests.Common; using Umbraco.Cms.Tests.Common.Testing; using Umbraco.Extensions; @@ -61,8 +62,6 @@ namespace Umbraco.Cms.Tests.UnitTests.TestHelpers { } - public override IDbProviderFactoryCreator DbProviderFactoryCreator { get; } = Mock.Of(); - public override IBulkSqlInsertProvider BulkSqlInsertProvider { get; } = Mock.Of(); public override IMarchal Marchal { get; } = Mock.Of(); @@ -113,8 +112,6 @@ namespace Umbraco.Cms.Tests.UnitTests.TestHelpers public static IVariationContextAccessor VariationContextAccessor => s_testHelperInternal.VariationContextAccessor; - public static IDbProviderFactoryCreator DbProviderFactoryCreator => s_testHelperInternal.DbProviderFactoryCreator; - public static IBulkSqlInsertProvider BulkSqlInsertProvider => s_testHelperInternal.BulkSqlInsertProvider; public static IMarchal Marchal => s_testHelperInternal.Marchal; diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Configuration/ConfigureConnectionStringsTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Configuration/ConfigureConnectionStringsTests.cs new file mode 100644 index 0000000000..4e7d6f7f1f --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Configuration/ConfigureConnectionStringsTests.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Configuration; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Tests.UnitTests.AutoFixture; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Configuration; + +[TestFixture] +public class ConfigureConnectionStringsTests +{ + private const string UmbracoDbDsn = Constants.System.UmbracoConnectionName; + + private IOptionsSnapshot GetOptions(IDictionary configValues = null) + { + var configurationBuilder = new ConfigurationBuilder(); + if (configValues != null) + { + configurationBuilder.AddInMemoryCollection(configValues); + } + + var configuration = configurationBuilder.Build(); + + var services = new ServiceCollection(); + services.AddOptions().Bind(configuration.GetSection("ConnectionStrings")); + services.AddSingleton, ConfigureConnectionStrings>(); + services.AddSingleton(configuration); + + var container = services.BuildServiceProvider(); + return container.GetRequiredService>(); + } + + [Test] + public void Configure_WithConfigMissingProvider_SetsDefaultValue() + { + var result = GetOptions(); + Assert.Multiple(() => + { + Assert.That(result.Value.ProviderName, Is.Not.Null); + Assert.That(result.Value.ProviderName, Is.EqualTo(ConnectionStrings.DefaultProviderName)); + + Assert.That(result.Get(UmbracoDbDsn).ProviderName, Is.Not.Null); + Assert.That(result.Get(UmbracoDbDsn).ProviderName, Is.EqualTo(ConnectionStrings.DefaultProviderName)); + }); + } + + [Test] + [AutoMoqData] + public void Configure_WithConfiguredProvider_RespectsProviderValue( + string aConnectionString, + string aProviderName) + { + var config = new Dictionary + { + [$"ConnectionStrings:{UmbracoDbDsn}"] = aConnectionString, + [$"ConnectionStrings:{UmbracoDbDsn}_ProviderName"] = aProviderName, + }; + + var result = GetOptions(config); + + Assert.Multiple(() => + { + Assert.That(result.Value.ProviderName, Is.Not.Null); + Assert.That(result.Value.ProviderName, Is.EqualTo(aProviderName)); + + Assert.That(result.Get(UmbracoDbDsn).ProviderName, Is.Not.Null); + Assert.That(result.Get(UmbracoDbDsn).ProviderName, Is.EqualTo(aProviderName)); + }); + } + + [Test] + [AutoMoqData] + public void Configure_WithDataDirectoryPlaceholderInConnectionStringConfig_ReplacesDataDirectoryPlaceholder( + string aDataDirectory, + string aConnectionString, + string aProviderName) + { + AppDomain.CurrentDomain.SetData("DataDirectory", aDataDirectory); + var config = new Dictionary + { + [$"ConnectionStrings:{UmbracoDbDsn}"] = $"{ConnectionStrings.DataDirectoryPlaceholder}/{aConnectionString}", + [$"ConnectionStrings:{UmbracoDbDsn}_ProviderName"] = aProviderName, + }; + + var result = GetOptions(config); + + Assert.Multiple(() => + { + Assert.That(result.Value.ConnectionString, Is.Not.Null); + Assert.That(result.Value.ConnectionString, Contains.Substring($"{aDataDirectory}/{aConnectionString}")); + + Assert.That(result.Get(UmbracoDbDsn).ConnectionString, Is.Not.Null); + Assert.That(result.Get(UmbracoDbDsn).ConnectionString, Contains.Substring($"{aDataDirectory}/{aConnectionString}")); + }); + } +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Configuration/Models/ConnectionStringsTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Configuration/Models/ConnectionStringsTests.cs index c545c10c8f..5398dfc7fd 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Configuration/Models/ConnectionStringsTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Configuration/Models/ConnectionStringsTests.cs @@ -1,35 +1,36 @@ // Copyright (c) Umbraco. // See LICENSE for more details. +using System; using NUnit.Framework; using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Tests.UnitTests.AutoFixture; using Constants = Umbraco.Cms.Core.Constants; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Configuration.Models { + [TestFixture] public class ConnectionStringsTests { [Test] - [TestCase("", ExpectedResult = null)] - [TestCase(null, ExpectedResult = null)] - [TestCase(@"Data Source=|DataDirectory|\Umbraco.sdf;Flush Interval=1;", ExpectedResult = Constants.DbProviderNames.SqlCe)] - [TestCase(@"Server=(LocalDb)\Umbraco;Database=NetCore;Integrated Security=true", ExpectedResult = Constants.DbProviderNames.SqlServer)] - [TestCase(@"Data Source=(LocalDb)\Umbraco;Initial Catalog=NetCore;Integrated Security=true;", ExpectedResult = Constants.DbProviderNames.SqlServer)] - [TestCase(@"Data Source=.\SQLExpress;Integrated Security=true;AttachDbFilename=MyDataFile.mdf;", ExpectedResult = Constants.DbProviderNames.SqlServer)] - public string ParseProviderName(string connectionString) + public void ProviderName_WhenNotExplicitlySet_HasDefaultSet() { - var connectionStrings = new ConnectionStrings + var sut = new ConnectionStrings(); + Assert.That(sut.ProviderName, Is.EqualTo(ConnectionStrings.DefaultProviderName)); + } + + [Test] + [AutoMoqData] + public void ConnectionString_WhenSetterCalled_ReplacesDataDirectoryPlaceholder(string aDataDirectory) + { + AppDomain.CurrentDomain.SetData("DataDirectory", aDataDirectory); + + var sut = new ConnectionStrings { - UmbracoConnectionString = new ConfigConnectionString(Constants.System.UmbracoConnectionName, connectionString) + ConnectionString = $"{ConnectionStrings.DataDirectoryPlaceholder}/foo" }; - - var actual = connectionStrings.UmbracoConnectionString; - - Assert.AreEqual(connectionString, actual.ConnectionString); - Assert.AreEqual(Constants.System.UmbracoConnectionName, actual.Name); - - return connectionStrings.UmbracoConnectionString.ProviderName; + Assert.That(sut.ConnectionString, Contains.Substring($"{aDataDirectory}/foo")); } } } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Components/ComponentTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Components/ComponentTests.cs index 152808d6f3..c952f40f82 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Components/ComponentTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Components/ComponentTests.cs @@ -18,6 +18,7 @@ using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.DistributedLocking; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.IO; @@ -27,6 +28,7 @@ using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Infrastructure.Migrations.Install; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Mappers; +using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Cms.Tests.Common; using Umbraco.Cms.Tests.UnitTests.TestHelpers; @@ -53,9 +55,9 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Components loggerFactory.CreateLogger(), loggerFactory, Options.Create(globalSettings), - Mock.Of>(x => x.CurrentValue == connectionStrings), + Mock.Of>(x => x.Get(It.IsAny()) == connectionStrings), new MapperCollection(() => Enumerable.Empty()), - TestHelper.DbProviderFactoryCreator, + Mock.Of(), new DatabaseSchemaCreatorFactory(loggerFactory.CreateLogger(), loggerFactory, new UmbracoVersion(), Mock.Of()), mapperCollection); @@ -69,7 +71,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Components Mock.Of(), Options.Create(new ContentSettings())); IEventAggregator eventAggregator = Mock.Of(); - var scopeProvider = new ScopeProvider(f, fs, new TestOptionsMonitor(coreDebug), loggerFactory, NoAppCache.Instance, eventAggregator); + var scopeProvider = new ScopeProvider(Mock.Of(),f , fs, new TestOptionsMonitor(coreDebug), loggerFactory, NoAppCache.Instance, eventAggregator); mock.Setup(x => x.GetService(typeof(ILogger))).Returns(logger); mock.Setup(x => x.GetService(typeof(ILogger))).Returns(loggerFactory.CreateLogger); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Configuration/Models/Validation/GlobalSettingsValidatorTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Configuration/Models/Validation/GlobalSettingsValidatorTests.cs index 9228182a8a..1c63e16ad3 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Configuration/Models/Validation/GlobalSettingsValidatorTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Configuration/Models/Validation/GlobalSettingsValidatorTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Umbraco. // See LICENSE for more details. +using System; using Microsoft.Extensions.Options; using NUnit.Framework; using Umbraco.Cms.Core.Configuration.Models; @@ -15,7 +16,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Configuration.Models.Validati public void Returns_Success_ForValid_Configuration() { var validator = new GlobalSettingsValidator(); - GlobalSettings options = BuildGlobalSettings(); + var options = new GlobalSettings(); ValidateOptionsResult result = validator.Validate("settings", options); Assert.True(result.Succeeded); } @@ -24,18 +25,55 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Configuration.Models.Validati public void Returns_Fail_For_Configuration_With_Invalid_SmtpFrom_Field() { var validator = new GlobalSettingsValidator(); - GlobalSettings options = BuildGlobalSettings(smtpEmail: "invalid"); + var options = new GlobalSettings + { + Smtp = new SmtpSettings + { + From = "invalid", + } + }; + ValidateOptionsResult result = validator.Validate("settings", options); Assert.False(result.Succeeded); } - private static GlobalSettings BuildGlobalSettings(string smtpEmail = "test@test.com") => - new GlobalSettings + [Test] + public void Returns_Fail_For_Configuration_With_Insufficient_SqlWriteLockTimeOut() + { + var validator = new GlobalSettingsValidator(); + var options = new GlobalSettings { - Smtp = new SmtpSettings - { - From = smtpEmail, - } + DistributedLockingWriteLockDefaultTimeout = TimeSpan.Parse("00:00:00.099") }; + + ValidateOptionsResult result = validator.Validate("settings", options); + Assert.False(result.Succeeded); + } + + [Test] + public void Returns_Fail_For_Configuration_With_Excessive_SqlWriteLockTimeOut() + { + var validator = new GlobalSettingsValidator(); + var options = new GlobalSettings + { + DistributedLockingWriteLockDefaultTimeout = TimeSpan.Parse("00:00:21") + }; + + ValidateOptionsResult result = validator.Validate("settings", options); + Assert.False(result.Succeeded); + } + + [Test] + public void Returns_Success_For_Configuration_With_Valid_SqlWriteLockTimeOut() + { + var validator = new GlobalSettingsValidator(); + var options = new GlobalSettings + { + DistributedLockingWriteLockDefaultTimeout = TimeSpan.Parse("00:00:20") + }; + + ValidateOptionsResult result = validator.Validate("settings", options); + Assert.True(result.Succeeded); + } } } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Extensions/UmbracoBuilderExtensionsTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Extensions/UmbracoBuilderExtensionsTests.cs index 4894a15ba0..aaaa16b68a 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Extensions/UmbracoBuilderExtensionsTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Extensions/UmbracoBuilderExtensionsTests.cs @@ -64,7 +64,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Extensions public IHostingEnvironment BuilderHostingEnvironment { get; } public IProfiler Profiler { get; } public AppCaches AppCaches { get; } - public TBuilder WithCollectionBuilder() where TBuilder : ICollectionBuilder, new() => default; + public TBuilder WithCollectionBuilder() where TBuilder : ICollectionBuilder => default; public UmbracoBuildStub() => Services = new ServiceCollection(); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Models/ContentTypeHistoryCleanupTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Models/ContentTypeHistoryCleanupTests.cs new file mode 100644 index 0000000000..1a9d293527 --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Models/ContentTypeHistoryCleanupTests.cs @@ -0,0 +1,105 @@ +using System.Linq; +using NUnit.Framework; +using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Tests.Common.Builders; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Models +{ + [TestFixture] + public class ContentTypeHistoryCleanupTests + { + [Test] + public void Changing_Keep_all_Makes_ContentType_Dirty() + { + var contentType = ContentTypeBuilder.CreateBasicContentType(); + + Assert.IsFalse(contentType.IsDirty()); + + var newValue = 2; + contentType.HistoryCleanup.KeepAllVersionsNewerThanDays = newValue; + Assert.IsTrue(contentType.IsDirty()); + Assert.AreEqual(newValue, contentType.HistoryCleanup.KeepAllVersionsNewerThanDays); + } + + [Test] + public void Changing_Keep_latest_Makes_ContentType_Dirty() + { + var contentType = ContentTypeBuilder.CreateBasicContentType(); + + Assert.IsFalse(contentType.IsDirty()); + + var newValue = 2; + contentType.HistoryCleanup.KeepLatestVersionPerDayForDays = newValue; + Assert.IsTrue(contentType.IsDirty()); + Assert.AreEqual(newValue, contentType.HistoryCleanup.KeepLatestVersionPerDayForDays); + } + + [Test] + public void Changing_Prevent_Cleanup_Makes_ContentType_Dirty() + { + var contentType = ContentTypeBuilder.CreateBasicContentType(); + + Assert.IsFalse(contentType.IsDirty()); + + var newValue = true; + contentType.HistoryCleanup.PreventCleanup = newValue; + Assert.IsTrue(contentType.IsDirty()); + Assert.AreEqual(newValue, contentType.HistoryCleanup.PreventCleanup); + } + + [Test] + public void Replacing_History_Cleanup_Registers_As_Dirty() + { + var contentType = ContentTypeBuilder.CreateBasicContentType(); + Assert.IsFalse(contentType.IsDirty()); + + contentType.HistoryCleanup = new HistoryCleanup(); + + Assert.IsTrue(contentType.IsDirty()); + Assert.IsTrue(contentType.IsPropertyDirty(nameof(contentType.HistoryCleanup))); + } + + [Test] + public void Replacing_History_Cleanup_Removes_Old_Dirty_History_Properties() + { + var contentType = ContentTypeBuilder.CreateBasicContentType(); + + contentType.Alias = "NewValue"; + contentType.HistoryCleanup.KeepAllVersionsNewerThanDays = 2; + + contentType.PropertyChanged += (sender, args) => + { + // Ensure that property changed is only invoked for history cleanup + Assert.AreEqual(nameof(contentType.HistoryCleanup), args.PropertyName); + }; + + // Since we're replacing the entire HistoryCleanup the changed property is no longer dirty, the entire HistoryCleanup is + contentType.HistoryCleanup = new HistoryCleanup(); + + Assert.Multiple(() => + { + Assert.IsTrue(contentType.IsDirty()); + Assert.IsFalse(contentType.WasDirty()); + Assert.AreEqual(2, contentType.GetDirtyProperties().Count()); + Assert.IsTrue(contentType.IsPropertyDirty(nameof(contentType.HistoryCleanup))); + Assert.IsTrue(contentType.IsPropertyDirty(nameof(contentType.Alias))); + }); + } + + [Test] + public void Old_History_Cleanup_Reference_Doesnt_Make_Content_Type_Dirty() + { + var contentType = ContentTypeBuilder.CreateBasicContentType(); + var oldHistoryCleanup = contentType.HistoryCleanup; + + contentType.HistoryCleanup = new HistoryCleanup(); + contentType.ResetDirtyProperties(); + contentType.ResetWereDirtyProperties(); + + oldHistoryCleanup.KeepAllVersionsNewerThanDays = 2; + + Assert.IsFalse(contentType.IsDirty()); + Assert.IsFalse(contentType.WasDirty()); + } + } +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Scoping/ScopedNotificationPublisherTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Scoping/ScopedNotificationPublisherTests.cs index 6d614b62e9..031a9767c1 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Scoping/ScopedNotificationPublisherTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Scoping/ScopedNotificationPublisherTests.cs @@ -7,6 +7,7 @@ using Moq; using NUnit.Framework; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.DistributedLocking; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.IO; @@ -16,6 +17,7 @@ using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Cms.Tests.Common; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Scoping @@ -91,6 +93,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Scoping eventAggregatorMock = new Mock(); return new ScopeProvider( + Mock.Of(), Mock.Of(), fileSystems, new TestOptionsMonitor(new CoreDebugSettings()), diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Telemetry/SiteIdentifierServiceTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Telemetry/SiteIdentifierServiceTests.cs new file mode 100644 index 0000000000..81934cc1be --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Telemetry/SiteIdentifierServiceTests.cs @@ -0,0 +1,77 @@ +using System; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Core.Configuration; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Telemetry; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Telemetry +{ + [TestFixture] + public class SiteIdentifierServiceTests + { + [TestCase("0F1785C5-7BA0-4C52-AB62-863BD2C8F3FE", true)] + [TestCase("This is not a guid", false)] + [TestCase("", false)] + [TestCase("00000000-0000-0000-0000-000000000000", false)] // Don't count empty GUID as valid + public void TryGetOnlyPassesIfValidId(string guidString, bool shouldSucceed) + { + var globalSettings = CreateGlobalSettings(guidString); + var sut = new SiteIdentifierService( + globalSettings, + Mock.Of(), + Mock.Of>()); + + var result = sut.TryGetSiteIdentifier(out var siteIdentifier); + + Assert.AreEqual(shouldSucceed, result); + if (shouldSucceed) + { + // When toString is called on a GUID it will to lower, so do the same to our guidString + Assert.AreEqual(guidString.ToLower(), siteIdentifier.ToString()); + } + else + { + Assert.AreEqual(Guid.Empty, siteIdentifier); + } + } + + [TestCase("0F1785C5-7BA0-4C52-AB62-863BD2C8F3FE", false)] + [TestCase("This is not a guid", true)] + [TestCase("", true)] + [TestCase("00000000-0000-0000-0000-000000000000", true)] // Don't count empty GUID as valid + public void TryGetOrCreateOnlyCreatesNewGuidIfCurrentIsMissingOrInvalid(string guidString, bool shouldCreate) + { + var globalSettings = CreateGlobalSettings(guidString); + var configManipulatorMock = new Mock(); + + var sut = new SiteIdentifierService( + globalSettings, + configManipulatorMock.Object, + Mock.Of>()); + + var result = sut.TryGetOrCreateSiteIdentifier(out var identifier); + + if (shouldCreate) + { + configManipulatorMock.Verify(x => x.SetGlobalId(It.IsAny()), Times.Once); + Assert.AreNotEqual(Guid.Empty, identifier); + Assert.IsTrue(result); + } + else + { + configManipulatorMock.Verify(x => x.SetGlobalId(It.IsAny()), Times.Never()); + Assert.AreEqual(guidString.ToLower(), identifier.ToString()); + Assert.IsTrue(result); + } + } + + private IOptionsMonitor CreateGlobalSettings(string guidString) + { + var globalSettings = new GlobalSettings { Id = guidString }; + return Mock.Of>(x => x.CurrentValue == globalSettings); + } + } +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Telemetry/TelemetryServiceTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Telemetry/TelemetryServiceTests.cs index 1c92569695..910ca7c792 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Telemetry/TelemetryServiceTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Telemetry/TelemetryServiceTests.cs @@ -15,36 +15,36 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Telemetry [TestFixture] public class TelemetryServiceTests { - [TestCase("0F1785C5-7BA0-4C52-AB62-863BD2C8F3FE", true)] - [TestCase("This is not a guid", false)] - [TestCase("", false)] - public void OnlyParsesIfValidId(string guidString, bool shouldSucceed) + [Test] + public void UsesGetOrCreateSiteId() { - var globalSettings = CreateGlobalSettings(guidString); - var version = CreateUmbracoVersion(9, 1, 1); - var sut = new TelemetryService(globalSettings, Mock.Of(), version); + var version = CreateUmbracoVersion(9, 3, 1); + var siteIdentifierServiceMock = new Mock(); + var sut = new TelemetryService(Mock.Of(), version, siteIdentifierServiceMock.Object); + Guid guid; + + var result = sut.TryGetTelemetryReportData(out var telemetryReportData); + siteIdentifierServiceMock.Verify(x => x.TryGetOrCreateSiteIdentifier(out guid), Times.Once); + } + + [Test] + public void SkipsIfCantGetOrCreateId() + { + var version = CreateUmbracoVersion(9, 3, 1); + var sut = new TelemetryService(Mock.Of(), version, createSiteIdentifierService(false)); var result = sut.TryGetTelemetryReportData(out var telemetry); - Assert.AreEqual(shouldSucceed, result); - if (shouldSucceed) - { - // When toString is called on a GUID it will to lower, so do the same to our guidString - Assert.AreEqual(guidString.ToLower(), telemetry.Id.ToString()); - } - else - { - Assert.IsNull(telemetry); - } + Assert.IsFalse(result); + Assert.IsNull(telemetry); } [Test] public void ReturnsSemanticVersionWithoutBuild() { - var globalSettings = CreateGlobalSettings(); var version = CreateUmbracoVersion(9, 1, 1, "-rc", "-ad2f4k2d"); - var sut = new TelemetryService(globalSettings, Mock.Of(), version); + var sut = new TelemetryService(Mock.Of(), version, createSiteIdentifierService()); var result = sut.TryGetTelemetryReportData(out var telemetry); @@ -55,7 +55,6 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Telemetry [Test] public void CanGatherPackageTelemetry() { - var globalSettings = CreateGlobalSettings(); var version = CreateUmbracoVersion(9, 1, 1); var versionPackageName = "VersionPackage"; var packageVersion = "1.0.0"; @@ -66,7 +65,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Telemetry new () { PackageName = noVersionPackageName } }; var manifestParser = CreateManifestParser(manifests); - var sut = new TelemetryService(globalSettings, manifestParser, version); + var sut = new TelemetryService(manifestParser, version, createSiteIdentifierService()); var success = sut.TryGetTelemetryReportData(out var telemetry); @@ -87,15 +86,14 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Telemetry [Test] public void RespectsAllowPackageTelemetry() { - var globalSettings = CreateGlobalSettings(); var version = CreateUmbracoVersion(9, 1, 1); PackageManifest[] manifests = { new () { PackageName = "DoNotTrack", AllowPackageTelemetry = false }, - new () { PackageName = "TrackingAllowed", AllowPackageTelemetry = true } + new () { PackageName = "TrackingAllowed", AllowPackageTelemetry = true }, }; var manifestParser = CreateManifestParser(manifests); - var sut = new TelemetryService(globalSettings, manifestParser, version); + var sut = new TelemetryService(manifestParser, version, createSiteIdentifierService()); var success = sut.TryGetTelemetryReportData(out var telemetry); @@ -121,15 +119,12 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Telemetry return Mock.Of(x => x.SemanticVersion == version); } - private IOptionsMonitor CreateGlobalSettings(string guidString = null) + private ISiteIdentifierService createSiteIdentifierService(bool shouldSucceed = true) { - if (guidString is null) - { - guidString = Guid.NewGuid().ToString(); - } - - var globalSettings = new GlobalSettings { Id = guidString }; - return Mock.Of>(x => x.CurrentValue == globalSettings); + var mock = new Mock(); + var siteIdentifier = shouldSucceed ? Guid.NewGuid() : Guid.Empty; + mock.Setup(x => x.TryGetOrCreateSiteIdentifier(out siteIdentifier)).Returns(shouldSucceed); + return mock.Object; } } } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/ScheduledPublishingTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/ScheduledPublishingTests.cs index 2a8f006ab6..1379e99b99 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/ScheduledPublishingTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/ScheduledPublishingTests.cs @@ -2,12 +2,15 @@ // See LICENSE for more details. using System; +using System.Data; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Moq; using NUnit.Framework; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Runtime; +using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Sync; @@ -108,6 +111,11 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.HostedServices var mockServerMessenger = new Mock(); + var mockScopeProvider = new Mock(); + mockScopeProvider + .Setup(x => x.CreateScope(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Mock.Of()); + return new ScheduledPublishing( mockRunTimeState.Object, mockMainDom.Object, @@ -115,7 +123,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.HostedServices _mockContentService.Object, mockUmbracoContextFactory.Object, _mockLogger.Object, - mockServerMessenger.Object); + mockServerMessenger.Object, + mockScopeProvider.Object); } private void VerifyScheduledPublishingNotPerformed() => VerifyScheduledPublishingPerformed(Times.Never()); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Logging/LogviewerTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Logging/LogviewerTests.cs index 755e42b15d..ab1c2151e9 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Logging/LogviewerTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Logging/LogviewerTests.cs @@ -186,10 +186,13 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Logging [TestCase("", 102)] [TestCase("Has(@Exception)", 1)] + [TestCase("Has(@x)", 1)] [TestCase("Has(Duration) and Duration > 1000", 2)] - [TestCase("Not(@Level = 'Verbose') and Not(@Level= 'Debug')", 45)] + [TestCase("Not(@Level = 'Verbose') and Not(@Level = 'Debug')", 45)] + [TestCase("Not(@l = 'Verbose') and Not(@l = 'Debug')", 45)] [TestCase("StartsWith(SourceContext, 'Umbraco.Core')", 86)] [TestCase("@MessageTemplate = '{EndMessage} ({Duration}ms) [Timing {TimingId}]'", 26)] + [TestCase("@mt = '{EndMessage} ({Duration}ms) [Timing {TimingId}]'", 26)] [TestCase("SortedComponentTypes[?] = 'Umbraco.Web.Search.ExamineComponent'", 1)] [TestCase("Contains(SortedComponentTypes[?], 'DatabaseServer')", 1)] [Test] diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Migrations/MigrationPlanTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Migrations/MigrationPlanTests.cs index 2e1ac83cd8..b9f45afd67 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Migrations/MigrationPlanTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Migrations/MigrationPlanTests.cs @@ -20,6 +20,7 @@ using Umbraco.Cms.Infrastructure.Migrations.Upgrade; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; using Umbraco.Cms.Infrastructure.Scoping; +using Umbraco.Cms.Persistence.SqlServer.Services; using Umbraco.Cms.Tests.Common.TestHelpers; using Umbraco.Cms.Tests.UnitTests.TestHelpers; using Umbraco.Extensions; diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Migrations/PostMigrationTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Migrations/PostMigrationTests.cs index 09d0cb99d1..37cb93116b 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Migrations/PostMigrationTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Migrations/PostMigrationTests.cs @@ -19,6 +19,7 @@ using Umbraco.Cms.Infrastructure.Migrations.Upgrade; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; using Umbraco.Cms.Infrastructure.Scoping; +using Umbraco.Cms.Persistence.SqlServer.Services; using Umbraco.Cms.Tests.Common.TestHelpers; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Migrations diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Persistence/BulkDataReaderTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Persistence/BulkDataReaderTests.cs index 8ecf6870a4..c1b2bc052b 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Persistence/BulkDataReaderTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Persistence/BulkDataReaderTests.cs @@ -9,6 +9,7 @@ using System.Data.Common; using Microsoft.Data.SqlClient; using NUnit.Framework; using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Persistence.SqlServer.Services; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Persistence { diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Persistence/NPocoTests/NPocoSqlTemplateTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Persistence/NPocoTests/NPocoSqlTemplateTests.cs index 3eddb0fe29..b32f3e9823 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Persistence/NPocoTests/NPocoSqlTemplateTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Persistence/NPocoTests/NPocoSqlTemplateTests.cs @@ -10,6 +10,7 @@ using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Mappers; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; +using Umbraco.Cms.Persistence.SqlServer.Services; using Umbraco.Cms.Tests.Common.TestHelpers; using Umbraco.Extensions; diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Persistence/Querying/ExpressionTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Persistence/Querying/ExpressionTests.cs index 6b8287946c..b94d8bbaa7 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Persistence/Querying/ExpressionTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Persistence/Querying/ExpressionTests.cs @@ -20,6 +20,7 @@ using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Persistence.Querying; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; using Umbraco.Cms.Infrastructure.Serialization; +using Umbraco.Cms.Persistence.SqlServer.Services; using Umbraco.Cms.Tests.UnitTests.TestHelpers; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Persistence.Querying diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Persistence/DatabaseContextTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Persistence/SqlAzureDatabaseProviderMetadataTests.cs similarity index 72% rename from tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Persistence/DatabaseContextTests.cs rename to tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Persistence/SqlAzureDatabaseProviderMetadataTests.cs index 34e629d596..eed43b3aba 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Persistence/DatabaseContextTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Persistence/SqlAzureDatabaseProviderMetadataTests.cs @@ -1,12 +1,13 @@ using NUnit.Framework; -using Umbraco.Cms.Infrastructure.Migrations.Install; +using Umbraco.Cms.Core.Install.Models; +using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Persistence.SqlServer.Services; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Persistence { [TestFixture] - public class DatabaseContextTests + public class SqlAzureDatabaseProviderMetadataTests { - [TestCase("MyServer", "MyDatabase", "MyUser", "MyPassword")] [TestCase("MyServer", "MyDatabase", "MyUser@MyServer", "MyPassword")] [TestCase("tcp:MyServer", "MyDatabase", "MyUser", "MyPassword")] @@ -19,7 +20,13 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Persistence [TestCase("tcp:MyServer.database.windows.net,1433", "MyDatabase", "MyUser@MyServer", "MyPassword")] public void Build_Azure_Connection_String_Regular(string server, string databaseName, string userName, string password) { - var connectionString = DatabaseBuilder.GetAzureConnectionString(server, databaseName, userName, password); + var settings = new DatabaseModel + { + Server = server, DatabaseName = databaseName, Login = userName, Password = password + }; + + var sut = new SqlAzureDatabaseProviderMetadata(); + var connectionString = sut.GenerateConnectionString(settings); Assert.AreEqual(connectionString, "Server=tcp:MyServer.database.windows.net,1433;Database=MyDatabase;User ID=MyUser@MyServer;Password=MyPassword"); } @@ -29,7 +36,13 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Persistence [TestCase("tcp:kzeej5z8ty.ssmsawacluster4.windowsazure.mscds.com", "MyDatabase", "MyUser@kzeej5z8ty", "MyPassword")] public void Build_Azure_Connection_String_CustomServer(string server, string databaseName, string userName, string password) { - var connectionString = DatabaseBuilder.GetAzureConnectionString(server, databaseName, userName, password); + var settings = new DatabaseModel + { + Server = server, DatabaseName = databaseName, Login = userName, Password = password + }; + + var sut = new SqlAzureDatabaseProviderMetadata(); + var connectionString = sut.GenerateConnectionString(settings); Assert.AreEqual(connectionString, "Server=tcp:kzeej5z8ty.ssmsawacluster4.windowsazure.mscds.com,1433;Database=MyDatabase;User ID=MyUser@kzeej5z8ty;Password=MyPassword"); } } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Runtime/DefaultMainDomKeyGeneratorTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Runtime/DefaultMainDomKeyGeneratorTests.cs new file mode 100644 index 0000000000..9b013aa38f --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Runtime/DefaultMainDomKeyGeneratorTests.cs @@ -0,0 +1,47 @@ +using AutoFixture.NUnit3; +using Microsoft.Extensions.Options; +using NUnit.Framework; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Hosting; +using Umbraco.Cms.Infrastructure.Runtime; +using Umbraco.Cms.Tests.UnitTests.AutoFixture; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Runtime +{ + [TestFixture] + internal class DefaultMainDomKeyGeneratorTests + { + [Test] + [AutoMoqData] + public void GenerateKey_WithConfiguredDiscriminatorValue_AltersHash( + [Frozen] IHostingEnvironment hostingEnvironment, + [Frozen] GlobalSettings globalSettings, + [Frozen] IOptionsMonitor globalSettingsMonitor, + DefaultMainDomKeyGenerator sut, + string aDiscriminator) + { + var withoutDiscriminator = sut.GenerateKey(); + globalSettings.MainDomKeyDiscriminator = aDiscriminator; + var withDiscriminator = sut.GenerateKey(); + + Assert.AreNotEqual(withoutDiscriminator, withDiscriminator); + } + + [Test] + [AutoMoqData] + public void GenerateKey_WithUnchangedDiscriminatorValue_ReturnsSameValue( + [Frozen] IHostingEnvironment hostingEnvironment, + [Frozen] GlobalSettings globalSettings, + [Frozen] IOptionsMonitor globalSettingsMonitor, + DefaultMainDomKeyGenerator sut, + string aDiscriminator) + { + globalSettings.MainDomKeyDiscriminator = aDiscriminator; + + var a = sut.GenerateKey(); + var b = sut.GenerateKey(); + + Assert.AreEqual(a, b); + } + } +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Scoping/ScopeUnitTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Scoping/ScopeUnitTests.cs index 6ef176ceef..ac2cd27f41 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Scoping/ScopeUnitTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Scoping/ScopeUnitTests.cs @@ -11,6 +11,7 @@ using NUnit.Framework; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.DistributedLocking; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.IO; @@ -18,7 +19,9 @@ using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; +using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Cms.Tests.Common; +using Umbraco.Cms.Tests.UnitTests.AutoFixture; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Scoping { @@ -30,7 +33,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Scoping /// /// The mock of the ISqlSyntaxProvider2, used to count method calls. /// - private ScopeProvider GetScopeProvider(out Mock syntaxProviderMock) + private ScopeProvider GetScopeProvider(out Mock lockingMechanism) { var loggerFactory = NullLoggerFactory.Instance; var fileSystems = new FileSystems(loggerFactory, @@ -45,7 +48,16 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Scoping var databaseFactory = new Mock(); var database = new Mock(); var sqlContext = new Mock(); - syntaxProviderMock = new Mock(); + + lockingMechanism = new Mock(); + lockingMechanism.Setup(x => x.ReadLock(It.IsAny(), It.IsAny())) + .Returns(Mock.Of()); + lockingMechanism.Setup(x => x.WriteLock(It.IsAny(), It.IsAny())) + .Returns(Mock.Of()); + + var lockingMechanismFactory = new Mock(); + lockingMechanismFactory.Setup(x => x.DistributedLockingMechanism) + .Returns(lockingMechanism.Object); // Setup mock of database factory to return mock of database. databaseFactory.Setup(x => x.CreateDatabase()).Returns(database.Object); @@ -54,10 +66,12 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Scoping // Setup mock of database to return mock of sql SqlContext database.Setup(x => x.SqlContext).Returns(sqlContext.Object); + var syntaxProviderMock = new Mock(); // Setup mock of ISqlContext to return syntaxProviderMock sqlContext.Setup(x => x.SqlSyntax).Returns(syntaxProviderMock.Object); return new ScopeProvider( + lockingMechanismFactory.Object, databaseFactory.Object, fileSystems, new TestOptionsMonitor(new CoreDebugSettings()), @@ -125,8 +139,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Scoping outerScope.Complete(); } - syntaxProviderMock.Verify(x => x.WriteLock(It.IsAny(), Constants.Locks.Domains), Times.Once); - syntaxProviderMock.Verify(x => x.WriteLock(It.IsAny(), Constants.Locks.Languages), Times.Once); + syntaxProviderMock.Verify(x => x.WriteLock(Constants.Locks.Domains, It.IsAny()), Times.Once); + syntaxProviderMock.Verify(x => x.WriteLock(Constants.Locks.Languages, It.IsAny()), Times.Once); } [Test] @@ -149,8 +163,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Scoping outerScope.Complete(); } - syntaxProviderMock.Verify(x => x.WriteLock(It.IsAny(), Constants.Locks.Languages), Times.Once); - syntaxProviderMock.Verify(x => x.WriteLock(It.IsAny(), Constants.Locks.ContentTree), Times.Once); + syntaxProviderMock.Verify(x => x.WriteLock(Constants.Locks.Languages, It.IsAny()), Times.Once); + syntaxProviderMock.Verify(x => x.WriteLock(Constants.Locks.ContentTree, It.IsAny()), Times.Once); } [Test] @@ -181,8 +195,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Scoping outerScope.Complete(); } - syntaxProviderMock.Verify(x => x.WriteLock(It.IsAny(), timeout, Constants.Locks.Domains), Times.Once); - syntaxProviderMock.Verify(x => x.WriteLock(It.IsAny(), timeout, Constants.Locks.Languages), Times.Once); + syntaxProviderMock.Verify(x => x.WriteLock(Constants.Locks.Domains, It.IsAny()), Times.Once); + syntaxProviderMock.Verify(x => x.WriteLock(Constants.Locks.Languages, It.IsAny()), Times.Once); } [Test] @@ -214,8 +228,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Scoping outerScope.Complete(); } - syntaxProviderMock.Verify(x => x.ReadLock(It.IsAny(), Constants.Locks.Domains), Times.Once); - syntaxProviderMock.Verify(x => x.ReadLock(It.IsAny(), Constants.Locks.Languages), Times.Once); + syntaxProviderMock.Verify(x => x.ReadLock(Constants.Locks.Domains, It.IsAny()), Times.Once); + syntaxProviderMock.Verify(x => x.ReadLock(Constants.Locks.Languages, It.IsAny()), Times.Once); } [Test] @@ -248,8 +262,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Scoping outerScope.Complete(); } - syntaxProviderMock.Verify(x => x.ReadLock(It.IsAny(), timeOut, Constants.Locks.Domains), Times.Once); - syntaxProviderMock.Verify(x => x.ReadLock(It.IsAny(), timeOut, Constants.Locks.Languages), Times.Once); + syntaxProviderMock.Verify(x => x.ReadLock(Constants.Locks.Domains, It.IsAny()), Times.Once); + syntaxProviderMock.Verify(x => x.ReadLock(Constants.Locks.Languages, It.IsAny()), Times.Once); } [Test] @@ -272,8 +286,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Scoping outerScope.Complete(); } - syntaxProviderMock.Verify(x => x.ReadLock(It.IsAny(), Constants.Locks.Languages), Times.Once); - syntaxProviderMock.Verify(x => x.ReadLock(It.IsAny(), Constants.Locks.ContentTree), Times.Once); + syntaxProviderMock.Verify(x => x.ReadLock(Constants.Locks.Languages, It.IsAny()), Times.Once); + syntaxProviderMock.Verify(x => x.ReadLock(Constants.Locks.ContentTree, It.IsAny()), Times.Once); } [Test] @@ -467,7 +481,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Scoping public void WriteLock_Doesnt_Increment_On_Error() { var scopeProvider = GetScopeProvider(out var syntaxProviderMock); - syntaxProviderMock.Setup(x => x.WriteLock(It.IsAny(), It.IsAny())).Throws(new Exception("Boom")); + syntaxProviderMock.Setup(x => x.WriteLock(It.IsAny(), It.IsAny())).Throws(new Exception("Boom")); using (var scope = (Scope)scopeProvider.CreateScope()) { @@ -481,7 +495,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Scoping public void ReadLock_Doesnt_Increment_On_Error() { var scopeProvider = GetScopeProvider(out var syntaxProviderMock); - syntaxProviderMock.Setup(x => x.ReadLock(It.IsAny(), It.IsAny())).Throws(new Exception("Boom")); + syntaxProviderMock.Setup(x => x.ReadLock(It.IsAny(), It.IsAny())).Throws(new Exception("Boom")); using (var scope = (Scope)scopeProvider.CreateScope()) { diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/UmbracoPasswordHasherTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/UmbracoPasswordHasherTests.cs new file mode 100644 index 0000000000..aa6bb4156b --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/UmbracoPasswordHasherTests.cs @@ -0,0 +1,81 @@ +using AutoFixture.NUnit3; +using Microsoft.AspNetCore.Identity; +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Serialization; +using Umbraco.Cms.Tests.UnitTests.AutoFixture; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security +{ + [TestFixture] + public class UmbracoPasswordHasherTests + { + // Technically MD5, HMACSHA384 & HMACSHA512 were also possible but opt in as opposed to historic defaults. + [Test] + [InlineAutoMoqData("HMACSHA256", "Umbraco9Rocks!", "uB/pLEhhe1W7EtWMv/pSgg==1y8+aso9+h3AKRtJXlVYeg2TZKJUr64hccj82ZZ7Ksk=")] // Actually HMACSHA256 + [InlineAutoMoqData("HMACSHA256", "Umbraco9Rocks!", "t0U8atXTX/efNCtTafukwZeIpr8=")] // v4 site legacy password, with incorrect algorithm specified in database actually HMACSHA1 with password used as key. + [InlineAutoMoqData("SHA1", "Umbraco9Rocks!", "6tZGfG9NTxJJYp19Fac9og==zzRggqANxhb+CbD/VabEt8cIde8=")] // When SHA1 is set on machine key. + public void VerifyHashedPassword_WithValidLegacyPasswordHash_ReturnsSuccessRehashNeeded( + string algorithm, + string providedPassword, + string hashedPassword, + [Frozen] IJsonSerializer jsonSerializer, + TestUserStub aUser, + UmbracoPasswordHasher sut) + { + Mock.Get(jsonSerializer) + .Setup(x => x.Deserialize(It.IsAny())) + .Returns(new PersistedPasswordSettings{ HashAlgorithm = algorithm }); + + var result = sut.VerifyHashedPassword(aUser, hashedPassword, providedPassword); + + Assert.AreEqual(PasswordVerificationResult.SuccessRehashNeeded, result); + } + + + [Test] + [InlineAutoMoqData("PBKDF2.ASPNETCORE.V3", "Umbraco9Rocks!", "AQAAAAEAACcQAAAAEDCrYcnIhHKr38yuchsDu6AFqqmLNvRooKObV25GC1LC1tLY+gWGU4xNug0lc17PHA==")] + public void VerifyHashedPassword_WithValidModernPasswordHash_ReturnsSuccess( + string algorithm, + string providedPassword, + string hashedPassword, + [Frozen] IJsonSerializer jsonSerializer, + TestUserStub aUser, + UmbracoPasswordHasher sut) + { + Mock.Get(jsonSerializer) + .Setup(x => x.Deserialize(It.IsAny())) + .Returns(new PersistedPasswordSettings { HashAlgorithm = algorithm }); + + var result = sut.VerifyHashedPassword(aUser, hashedPassword, providedPassword); + + Assert.AreEqual(PasswordVerificationResult.Success, result); + } + + [Test] + [InlineAutoMoqData("HMACSHA256", "Umbraco9Rocks!", "aB/cDeFaBcDefAbcD/EfaB==1y8+aso9+h3AKRtJXlVYeg2TZKJUr64hccj82ZZ7Ksk=")] + public void VerifyHashedPassword_WithIncorrectPassword_ReturnsFailed( + string algorithm, + string providedPassword, + string hashedPassword, + [Frozen] IJsonSerializer jsonSerializer, + TestUserStub aUser, + UmbracoPasswordHasher sut) + { + Mock.Get(jsonSerializer) + .Setup(x => x.Deserialize(It.IsAny())) + .Returns(new PersistedPasswordSettings { HashAlgorithm = algorithm }); + + var result = sut.VerifyHashedPassword(aUser, hashedPassword, providedPassword); + + Assert.AreEqual(PasswordVerificationResult.Failed, result); + } + + public class TestUserStub : UmbracoIdentityUser + { + public TestUserStub() => PasswordConfig = "not null or empty"; + } + } +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.Common/Builders/RelationBuilderTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.Common/Builders/RelationBuilderTests.cs index 67f887d123..bde8b8f092 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.Common/Builders/RelationBuilderTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.Common/Builders/RelationBuilderTests.cs @@ -44,6 +44,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Tests.Common.Builders .WithAlias(relationTypeAlias) .WithName(relationTypeName) .WithIsBidirectional(false) + .WithIsDependency(true) .WithParentObjectType(parentObjectType) .WithChildObjectType(childObjectType) .Done() @@ -61,6 +62,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Tests.Common.Builders Assert.AreEqual(relationTypeAlias, relation.RelationType.Alias); Assert.AreEqual(relationTypeName, relation.RelationType.Name); Assert.IsFalse(relation.RelationType.IsBidirectional); + + Assert.IsTrue((relation.RelationType as IRelationTypeWithIsDependency).IsDependency); Assert.AreEqual(parentObjectType, relation.RelationType.ParentObjectType); Assert.AreEqual(childObjectType, relation.RelationType.ChildObjectType); } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.Common/Builders/RelationTypeBuilderTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.Common/Builders/RelationTypeBuilderTests.cs index 877023f82f..e99f5ae273 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.Common/Builders/RelationTypeBuilderTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.Common/Builders/RelationTypeBuilderTests.cs @@ -26,11 +26,12 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Tests.Common.Builders var parentObjectType = Guid.NewGuid(); var childObjectType = Guid.NewGuid(); const bool isBidirectional = true; + const bool isDependency = true; var builder = new RelationTypeBuilder(); // Act - IRelationType relationType = builder + var relationType = builder .WithId(id) .WithAlias(alias) .WithName(name) @@ -41,6 +42,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Tests.Common.Builders .WithParentObjectType(parentObjectType) .WithChildObjectType(childObjectType) .WithIsBidirectional(isBidirectional) + .WithIsDependency(isDependency) .Build(); // Assert @@ -54,6 +56,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Tests.Common.Builders Assert.AreEqual(parentObjectType, relationType.ParentObjectType); Assert.AreEqual(childObjectType, relationType.ChildObjectType); Assert.AreEqual(isBidirectional, relationType.IsBidirectional); + Assert.AreEqual(isDependency, relationType.IsDependency); } } } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Security/BackOfficeAuthenticationBuilderTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Security/BackOfficeAuthenticationBuilderTests.cs new file mode 100644 index 0000000000..06cf2fad43 --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Security/BackOfficeAuthenticationBuilderTests.cs @@ -0,0 +1,44 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using Microsoft.AspNetCore.Authentication; +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Web.BackOffice.Security; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.BackOffice.Security +{ + [TestFixture] + public class BackOfficeAuthenticationBuilderTests + { + [Test] + public void EnsureBackOfficeScheme_When_Backoffice_Auth_Scheme_Expect_Updated_SignInScheme() + { + var scheme = $"{Constants.Security.BackOfficeExternalAuthenticationTypePrefix}test"; + var options = new RemoteAuthenticationOptions + { + SignInScheme = "my_cookie" + }; + + var sut = new BackOfficeAuthenticationBuilder.EnsureBackOfficeScheme(); + sut.PostConfigure(scheme, options); + + Assert.AreEqual(options.SignInScheme, Constants.Security.BackOfficeExternalAuthenticationType); + } + + [Test] + public void EnsureBackOfficeScheme_When_Not_Backoffice_Auth_Scheme_Expect_No_Change() + { + var scheme = "test"; + var options = new RemoteAuthenticationOptions + { + SignInScheme = "my_cookie" + }; + + var sut = new BackOfficeAuthenticationBuilder.EnsureBackOfficeScheme(); + sut.PostConfigure(scheme, options); + + Assert.AreNotEqual(options.SignInScheme, Constants.Security.BackOfficeExternalAuthenticationType); + } + } +} \ No newline at end of file diff --git a/umbraco-netcore-only.sln b/umbraco-netcore-only.sln deleted file mode 100644 index 80a688bde5..0000000000 --- a/umbraco-netcore-only.sln +++ /dev/null @@ -1,225 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.29209.152 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Umbraco.Web.UI", "src\Umbraco.Web.UI\Umbraco.Web.UI.csproj", "{DCDFE97C-5630-4F6F-855D-8AEEB96556A5}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Build", "Build", "{2849E9D4-3B4E-40A3-A309-F3CB4F0E125F}" - ProjectSection(SolutionItems) = preProject - build\azure-pipelines.yml = build\azure-pipelines.yml - build\build-bootstrap.ps1 = build\build-bootstrap.ps1 - build\build.ps1 = build\build.ps1 - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Docs", "Docs", "{FD962632-184C-4005-A5F3-E705D92FC645}" - ProjectSection(SolutionItems) = preProject - .github\BUILD.md = .github\BUILD.md - .github\CLEAR.md = .github\CLEAR.md - .github\CODE_OF_CONDUCT.md = .github\CODE_OF_CONDUCT.md - .github\CONTRIBUTING.md = .github\CONTRIBUTING.md - .github\CONTRIBUTING_DETAILED.md = .github\CONTRIBUTING_DETAILED.md - .github\CONTRIBUTION_GUIDELINES.md = .github\CONTRIBUTION_GUIDELINES.md - .github\PULL_REQUEST_TEMPLATE.md = .github\PULL_REQUEST_TEMPLATE.md - .github\README.md = .github\README.md - .github\REVIEW_PROCESS.md = .github\REVIEW_PROCESS.md - .github\V8_GETTING_STARTED.md = .github\V8_GETTING_STARTED.md - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{B5BD12C1-A454-435E-8A46-FF4A364C0382}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "NuSpecs", "NuSpecs", "{227C3B55-80E5-4E7E-A802-BE16C5128B9D}" - ProjectSection(SolutionItems) = preProject - build\NuSpecs\UmbracoCms.nuspec = build\NuSpecs\UmbracoCms.nuspec - build\NuSpecs\UmbracoCms.SqlCe.nuspec = build\NuSpecs\UmbracoCms.SqlCe.nuspec - build\NuSpecs\UmbracoCms.StaticAssets.nuspec = build\NuSpecs\UmbracoCms.StaticAssets.nuspec - EndProjectSection -EndProject -Project("{E24C65DC-7377-472B-9ABA-BC803B73C61A}") = "Umbraco.Web.UI.Client", "http://localhost:3961", "{3819A550-DCEC-4153-91B4-8BA9F7F0B9B4}" - ProjectSection(WebsiteProperties) = preProject - UseIISExpress = "true" - TargetFrameworkMoniker = ".NETFramework,Version%3Dv4.5" - Debug.AspNetCompiler.VirtualPath = "/localhost_3961" - Debug.AspNetCompiler.PhysicalPath = "src\Umbraco.Web.UI.Client\" - Debug.AspNetCompiler.TargetPath = "PrecompiledWeb\localhost_3961\" - Debug.AspNetCompiler.Updateable = "true" - Debug.AspNetCompiler.ForceOverwrite = "true" - Debug.AspNetCompiler.FixedNames = "false" - Debug.AspNetCompiler.Debug = "True" - Release.AspNetCompiler.VirtualPath = "/localhost_3961" - Release.AspNetCompiler.PhysicalPath = "src\Umbraco.Web.UI.Client\" - Release.AspNetCompiler.TargetPath = "PrecompiledWeb\localhost_3961\" - Release.AspNetCompiler.Updateable = "true" - Release.AspNetCompiler.ForceOverwrite = "true" - Release.AspNetCompiler.FixedNames = "false" - Release.AspNetCompiler.Debug = "False" - SlnRelativePath = "src\Umbraco.Web.UI.Client\" - DefaultWebSiteLanguage = "Visual C#" - StartServerOnDebug = "false" - EndProjectSection -EndProject -Project("{E24C65DC-7377-472B-9ABA-BC803B73C61A}") = "Umbraco.Tests.AcceptanceTest", "http://localhost:58896", "{9E4C8A12-FBE0-4673-8CE2-DF99D5D57817}" - ProjectSection(WebsiteProperties) = preProject - UseIISExpress = "true" - TargetFrameworkMoniker = ".NETFramework,Version%3Dv4.5" - Debug.AspNetCompiler.VirtualPath = "/localhost_62926" - Debug.AspNetCompiler.PhysicalPath = "tests\Umbraco.Tests.AcceptanceTest\" - Debug.AspNetCompiler.TargetPath = "PrecompiledWeb\localhost_62926\" - Debug.AspNetCompiler.Updateable = "true" - Debug.AspNetCompiler.ForceOverwrite = "true" - Debug.AspNetCompiler.FixedNames = "false" - Debug.AspNetCompiler.Debug = "True" - Release.AspNetCompiler.VirtualPath = "/localhost_62926" - Release.AspNetCompiler.PhysicalPath = "tests\Umbraco.Tests.AcceptanceTest\" - Release.AspNetCompiler.TargetPath = "PrecompiledWeb\localhost_62926\" - Release.AspNetCompiler.Updateable = "true" - Release.AspNetCompiler.ForceOverwrite = "true" - Release.AspNetCompiler.FixedNames = "false" - Release.AspNetCompiler.Debug = "False" - SlnRelativePath = "tests\Umbraco.Tests.AcceptanceTest\" - DefaultWebSiteLanguage = "Visual C#" - StartServerOnDebug = "false" - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "DocTools", "DocTools", "{53594E5B-64A2-4545-8367-E3627D266AE8}" - ProjectSection(SolutionItems) = preProject - src\ApiDocs\docfx.filter.yml = src\ApiDocs\docfx.filter.yml - src\ApiDocs\docfx.json = src\ApiDocs\docfx.json - src\ApiDocs\index.md = src\ApiDocs\index.md - src\ApiDocs\toc.yml = src\ApiDocs\toc.yml - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "IssueTemplates", "IssueTemplates", "{C7311C00-2184-409B-B506-52A5FAEA8736}" - ProjectSection(SolutionItems) = preProject - .github\ISSUE_TEMPLATE\1_Bug.md = .github\ISSUE_TEMPLATE\1_Bug.md - .github\ISSUE_TEMPLATE\2_Feature_request.md = .github\ISSUE_TEMPLATE\2_Feature_request.md - .github\ISSUE_TEMPLATE\3_Support_question.md = .github\ISSUE_TEMPLATE\3_Support_question.md - .github\ISSUE_TEMPLATE\4_Documentation_issue.md = .github\ISSUE_TEMPLATE\4_Documentation_issue.md - .github\ISSUE_TEMPLATE\5_Security_issue.md = .github\ISSUE_TEMPLATE\5_Security_issue.md - EndProjectSection -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Umbraco.Core", "src\Umbraco.Core\Umbraco.Core.csproj", "{29AA69D9-B597-4395-8D42-43B1263C240A}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Umbraco.Infrastructure", "src\Umbraco.Infrastructure\Umbraco.Infrastructure.csproj", "{3AE7BF57-966B-45A5-910A-954D7C554441}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Umbraco.PublishedCache.NuCache", "src\Umbraco.PublishedCache.NuCache\Umbraco.PublishedCache.NuCache.csproj", "{F6DE8DA0-07CC-4EF2-8A59-2BC81DBB3830}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Umbraco.Web.BackOffice", "src\Umbraco.Web.BackOffice\Umbraco.Web.BackOffice.csproj", "{9B95EEF7-63FE-4432-8C63-166BE9C1A929}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Umbraco.Web.Website", "src\Umbraco.Web.Website\Umbraco.Web.Website.csproj", "{5A246D54-3109-4D2B-BE7D-FC0787D126AE}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Umbraco.Tests.Common", "tests\Umbraco.Tests.Common\Umbraco.Tests.Common.csproj", "{A499779C-1B3B-48A8-B551-458E582E6E96}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Umbraco.Tests.UnitTests", "tests\Umbraco.Tests.UnitTests\Umbraco.Tests.UnitTests.csproj", "{9102ABDF-E537-4E46-B525-C9ED4833EED0}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Umbraco.Web.Common", "src\Umbraco.Web.Common\Umbraco.Web.Common.csproj", "{79E4293D-C92C-4649-AEC8-F1EFD95BDEB1}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Umbraco.Tests.Integration", "tests\Umbraco.Tests.Integration\Umbraco.Tests.Integration.csproj", "{1B885D2F-1599-4557-A4EC-474CC74DEB10}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Umbraco.Tests.Integration.SqlCe", "tests\Umbraco.Tests.Integration.SqlCe\Umbraco.Tests.Integration.SqlCe.csproj", "{7A58F7CB-786F-43D6-A946-7BFA1B70D0AA}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Umbraco.Examine.Lucene", "src\Umbraco.Examine.Lucene\Umbraco.Examine.Lucene.csproj", "{96EF355C-A7C8-460E-96D7-ABCE0D489762}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Umbraco.TestData", "tests\Umbraco.TestData\Umbraco.TestData.csproj", "{C44B4389-6E2A-441E-9A10-7CE818CA63A9}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Umbraco.Tests.Benchmarks", "tests\Umbraco.Tests.Benchmarks\Umbraco.Tests.Benchmarks.csproj", "{37B6264F-A279-42A6-AB83-CC3A3950C3E2}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "buildTransitive", "buildTransitive", "{81D03F06-94BE-4246-8F52-A68B58486F94}" - ProjectSection(SolutionItems) = preProject - build\NuSpecs\buildTransitive\Umbraco.Cms.StaticAssets.props = build\NuSpecs\buildTransitive\Umbraco.Cms.StaticAssets.props - build\NuSpecs\buildTransitive\Umbraco.Cms.StaticAssets.targets = build\NuSpecs\buildTransitive\Umbraco.Cms.StaticAssets.targets - EndProjectSection -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JsonSchema", "src\JsonSchema\JsonSchema.csproj", "{B172BC0E-B535-4CEF-9204-F3F397003829}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {DCDFE97C-5630-4F6F-855D-8AEEB96556A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {DCDFE97C-5630-4F6F-855D-8AEEB96556A5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {DCDFE97C-5630-4F6F-855D-8AEEB96556A5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {DCDFE97C-5630-4F6F-855D-8AEEB96556A5}.Release|Any CPU.Build.0 = Release|Any CPU - {3819A550-DCEC-4153-91B4-8BA9F7F0B9B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3819A550-DCEC-4153-91B4-8BA9F7F0B9B4}.Release|Any CPU.ActiveCfg = Debug|Any CPU - {9E4C8A12-FBE0-4673-8CE2-DF99D5D57817}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9E4C8A12-FBE0-4673-8CE2-DF99D5D57817}.Release|Any CPU.ActiveCfg = Debug|Any CPU - {29AA69D9-B597-4395-8D42-43B1263C240A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {29AA69D9-B597-4395-8D42-43B1263C240A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {29AA69D9-B597-4395-8D42-43B1263C240A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {29AA69D9-B597-4395-8D42-43B1263C240A}.Release|Any CPU.Build.0 = Release|Any CPU - {3AE7BF57-966B-45A5-910A-954D7C554441}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3AE7BF57-966B-45A5-910A-954D7C554441}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3AE7BF57-966B-45A5-910A-954D7C554441}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3AE7BF57-966B-45A5-910A-954D7C554441}.Release|Any CPU.Build.0 = Release|Any CPU - {F6DE8DA0-07CC-4EF2-8A59-2BC81DBB3830}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F6DE8DA0-07CC-4EF2-8A59-2BC81DBB3830}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F6DE8DA0-07CC-4EF2-8A59-2BC81DBB3830}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F6DE8DA0-07CC-4EF2-8A59-2BC81DBB3830}.Release|Any CPU.Build.0 = Release|Any CPU - {9B95EEF7-63FE-4432-8C63-166BE9C1A929}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9B95EEF7-63FE-4432-8C63-166BE9C1A929}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9B95EEF7-63FE-4432-8C63-166BE9C1A929}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9B95EEF7-63FE-4432-8C63-166BE9C1A929}.Release|Any CPU.Build.0 = Release|Any CPU - {5A246D54-3109-4D2B-BE7D-FC0787D126AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5A246D54-3109-4D2B-BE7D-FC0787D126AE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5A246D54-3109-4D2B-BE7D-FC0787D126AE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5A246D54-3109-4D2B-BE7D-FC0787D126AE}.Release|Any CPU.Build.0 = Release|Any CPU - {A499779C-1B3B-48A8-B551-458E582E6E96}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A499779C-1B3B-48A8-B551-458E582E6E96}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A499779C-1B3B-48A8-B551-458E582E6E96}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A499779C-1B3B-48A8-B551-458E582E6E96}.Release|Any CPU.Build.0 = Release|Any CPU - {9102ABDF-E537-4E46-B525-C9ED4833EED0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9102ABDF-E537-4E46-B525-C9ED4833EED0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9102ABDF-E537-4E46-B525-C9ED4833EED0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9102ABDF-E537-4E46-B525-C9ED4833EED0}.Release|Any CPU.Build.0 = Release|Any CPU - {79E4293D-C92C-4649-AEC8-F1EFD95BDEB1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {79E4293D-C92C-4649-AEC8-F1EFD95BDEB1}.Debug|Any CPU.Build.0 = Debug|Any CPU - {79E4293D-C92C-4649-AEC8-F1EFD95BDEB1}.Release|Any CPU.ActiveCfg = Release|Any CPU - {79E4293D-C92C-4649-AEC8-F1EFD95BDEB1}.Release|Any CPU.Build.0 = Release|Any CPU - {1B885D2F-1599-4557-A4EC-474CC74DEB10}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1B885D2F-1599-4557-A4EC-474CC74DEB10}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1B885D2F-1599-4557-A4EC-474CC74DEB10}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1B885D2F-1599-4557-A4EC-474CC74DEB10}.Release|Any CPU.Build.0 = Release|Any CPU - {7A58F7CB-786F-43D6-A946-7BFA1B70D0AA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7A58F7CB-786F-43D6-A946-7BFA1B70D0AA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7A58F7CB-786F-43D6-A946-7BFA1B70D0AA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7A58F7CB-786F-43D6-A946-7BFA1B70D0AA}.Release|Any CPU.Build.0 = Release|Any CPU - {96EF355C-A7C8-460E-96D7-ABCE0D489762}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {96EF355C-A7C8-460E-96D7-ABCE0D489762}.Debug|Any CPU.Build.0 = Debug|Any CPU - {96EF355C-A7C8-460E-96D7-ABCE0D489762}.Release|Any CPU.ActiveCfg = Release|Any CPU - {96EF355C-A7C8-460E-96D7-ABCE0D489762}.Release|Any CPU.Build.0 = Release|Any CPU - {C44B4389-6E2A-441E-9A10-7CE818CA63A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C44B4389-6E2A-441E-9A10-7CE818CA63A9}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C44B4389-6E2A-441E-9A10-7CE818CA63A9}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C44B4389-6E2A-441E-9A10-7CE818CA63A9}.Release|Any CPU.Build.0 = Release|Any CPU - {37B6264F-A279-42A6-AB83-CC3A3950C3E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {37B6264F-A279-42A6-AB83-CC3A3950C3E2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {37B6264F-A279-42A6-AB83-CC3A3950C3E2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {37B6264F-A279-42A6-AB83-CC3A3950C3E2}.Release|Any CPU.Build.0 = Release|Any CPU - {B172BC0E-B535-4CEF-9204-F3F397003829}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B172BC0E-B535-4CEF-9204-F3F397003829}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B172BC0E-B535-4CEF-9204-F3F397003829}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B172BC0E-B535-4CEF-9204-F3F397003829}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {227C3B55-80E5-4E7E-A802-BE16C5128B9D} = {2849E9D4-3B4E-40A3-A309-F3CB4F0E125F} - {9E4C8A12-FBE0-4673-8CE2-DF99D5D57817} = {B5BD12C1-A454-435E-8A46-FF4A364C0382} - {53594E5B-64A2-4545-8367-E3627D266AE8} = {FD962632-184C-4005-A5F3-E705D92FC645} - {C7311C00-2184-409B-B506-52A5FAEA8736} = {FD962632-184C-4005-A5F3-E705D92FC645} - {A499779C-1B3B-48A8-B551-458E582E6E96} = {B5BD12C1-A454-435E-8A46-FF4A364C0382} - {9102ABDF-E537-4E46-B525-C9ED4833EED0} = {B5BD12C1-A454-435E-8A46-FF4A364C0382} - {1B885D2F-1599-4557-A4EC-474CC74DEB10} = {B5BD12C1-A454-435E-8A46-FF4A364C0382} - {7A58F7CB-786F-43D6-A946-7BFA1B70D0AA} = {B5BD12C1-A454-435E-8A46-FF4A364C0382} - {C44B4389-6E2A-441E-9A10-7CE818CA63A9} = {B5BD12C1-A454-435E-8A46-FF4A364C0382} - {37B6264F-A279-42A6-AB83-CC3A3950C3E2} = {B5BD12C1-A454-435E-8A46-FF4A364C0382} - {81D03F06-94BE-4246-8F52-A68B58486F94} = {227C3B55-80E5-4E7E-A802-BE16C5128B9D} - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {7A0F2E34-D2AF-4DAB-86A0-7D7764B3D0EC} - EndGlobalSection -EndGlobal diff --git a/umbraco.sln b/umbraco.sln index 497258c699..b124ff573a 100644 --- a/umbraco.sln +++ b/umbraco.sln @@ -30,7 +30,6 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "NuSpecs", "NuSpecs", "{227C3B55-80E5-4E7E-A802-BE16C5128B9D}" ProjectSection(SolutionItems) = preProject build\NuSpecs\UmbracoCms.nuspec = build\NuSpecs\UmbracoCms.nuspec - build\NuSpecs\UmbracoCms.SqlCe.nuspec = build\NuSpecs\UmbracoCms.SqlCe.nuspec build\NuSpecs\UmbracoCms.StaticAssets.nuspec = build\NuSpecs\UmbracoCms.StaticAssets.nuspec EndProjectSection EndProject @@ -109,8 +108,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Umbraco.Core", "src\Umbraco EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Umbraco.Infrastructure", "src\Umbraco.Infrastructure\Umbraco.Infrastructure.csproj", "{3AE7BF57-966B-45A5-910A-954D7C554441}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Umbraco.Persistence.SqlCe", "src\Umbraco.Persistence.SqlCe\Umbraco.Persistence.SqlCe.csproj", "{33085570-9BF2-4065-A9B0-A29D920D13BA}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Umbraco.TestData", "tests\Umbraco.TestData\Umbraco.TestData.csproj", "{FB5676ED-7A69-492C-B802-E7B24144C0FC}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Umbraco.PublishedCache.NuCache", "src\Umbraco.PublishedCache.NuCache\Umbraco.PublishedCache.NuCache.csproj", "{F6DE8DA0-07CC-4EF2-8A59-2BC81DBB3830}" @@ -131,82 +128,114 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Umbraco.Web.Common", "src\U EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JsonSchema", "src\JsonSchema\JsonSchema.csproj", "{2A5027D9-F71D-4957-929E-F7A56AA1B95A}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Umbraco.Tests.Integration.SqlCe", "tests\Umbraco.Tests.Integration.SqlCe\Umbraco.Tests.Integration.SqlCe.csproj", "{1B28FC3E-3D5B-4A46-8961-5483835548D7}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Umbraco.Cms.Persistence.Sqlite", "src\Umbraco.Cms.Persistence.Sqlite\Umbraco.Cms.Persistence.Sqlite.csproj", "{32F6A309-EC1E-4CDB-BA80-C804CF680BEE}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Umbraco.Cms.Persistence.SqlServer", "src\Umbraco.Cms.Persistence.SqlServer\Umbraco.Cms.Persistence.SqlServer.csproj", "{93C5910D-2E36-475D-88EB-A11BA5B50F65}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU + SkipTests|Any CPU = SkipTests|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {DCDFE97C-5630-4F6F-855D-8AEEB96556A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {DCDFE97C-5630-4F6F-855D-8AEEB96556A5}.Debug|Any CPU.Build.0 = Debug|Any CPU {DCDFE97C-5630-4F6F-855D-8AEEB96556A5}.Release|Any CPU.ActiveCfg = Release|Any CPU {DCDFE97C-5630-4F6F-855D-8AEEB96556A5}.Release|Any CPU.Build.0 = Release|Any CPU + {DCDFE97C-5630-4F6F-855D-8AEEB96556A5}.SkipTests|Any CPU.ActiveCfg = Debug|Any CPU + {DCDFE97C-5630-4F6F-855D-8AEEB96556A5}.SkipTests|Any CPU.Build.0 = Debug|Any CPU {3819A550-DCEC-4153-91B4-8BA9F7F0B9B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3819A550-DCEC-4153-91B4-8BA9F7F0B9B4}.Release|Any CPU.ActiveCfg = Debug|Any CPU + {3819A550-DCEC-4153-91B4-8BA9F7F0B9B4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3819A550-DCEC-4153-91B4-8BA9F7F0B9B4}.SkipTests|Any CPU.ActiveCfg = Debug|Any CPU {9E4C8A12-FBE0-4673-8CE2-DF99D5D57817}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9E4C8A12-FBE0-4673-8CE2-DF99D5D57817}.Release|Any CPU.ActiveCfg = Debug|Any CPU + {9E4C8A12-FBE0-4673-8CE2-DF99D5D57817}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9E4C8A12-FBE0-4673-8CE2-DF99D5D57817}.SkipTests|Any CPU.ActiveCfg = Debug|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 + {3A33ADC9-C6C0-4DB1-A613-A9AF0210DF3D}.SkipTests|Any CPU.ActiveCfg = Debug|Any CPU {29AA69D9-B597-4395-8D42-43B1263C240A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {29AA69D9-B597-4395-8D42-43B1263C240A}.Debug|Any CPU.Build.0 = Debug|Any CPU {29AA69D9-B597-4395-8D42-43B1263C240A}.Release|Any CPU.ActiveCfg = Release|Any CPU {29AA69D9-B597-4395-8D42-43B1263C240A}.Release|Any CPU.Build.0 = Release|Any CPU + {29AA69D9-B597-4395-8D42-43B1263C240A}.SkipTests|Any CPU.ActiveCfg = Debug|Any CPU + {29AA69D9-B597-4395-8D42-43B1263C240A}.SkipTests|Any CPU.Build.0 = Debug|Any CPU {3AE7BF57-966B-45A5-910A-954D7C554441}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3AE7BF57-966B-45A5-910A-954D7C554441}.Debug|Any CPU.Build.0 = Debug|Any CPU {3AE7BF57-966B-45A5-910A-954D7C554441}.Release|Any CPU.ActiveCfg = Release|Any CPU {3AE7BF57-966B-45A5-910A-954D7C554441}.Release|Any CPU.Build.0 = Release|Any CPU - {33085570-9BF2-4065-A9B0-A29D920D13BA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {33085570-9BF2-4065-A9B0-A29D920D13BA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {33085570-9BF2-4065-A9B0-A29D920D13BA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {33085570-9BF2-4065-A9B0-A29D920D13BA}.Release|Any CPU.Build.0 = Release|Any CPU + {3AE7BF57-966B-45A5-910A-954D7C554441}.SkipTests|Any CPU.ActiveCfg = Debug|Any CPU + {3AE7BF57-966B-45A5-910A-954D7C554441}.SkipTests|Any CPU.Build.0 = Debug|Any CPU {FB5676ED-7A69-492C-B802-E7B24144C0FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {FB5676ED-7A69-492C-B802-E7B24144C0FC}.Debug|Any CPU.Build.0 = Debug|Any CPU {FB5676ED-7A69-492C-B802-E7B24144C0FC}.Release|Any CPU.ActiveCfg = Release|Any CPU {FB5676ED-7A69-492C-B802-E7B24144C0FC}.Release|Any CPU.Build.0 = Release|Any CPU + {FB5676ED-7A69-492C-B802-E7B24144C0FC}.SkipTests|Any CPU.ActiveCfg = Debug|Any CPU {F6DE8DA0-07CC-4EF2-8A59-2BC81DBB3830}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F6DE8DA0-07CC-4EF2-8A59-2BC81DBB3830}.Debug|Any CPU.Build.0 = Debug|Any CPU {F6DE8DA0-07CC-4EF2-8A59-2BC81DBB3830}.Release|Any CPU.ActiveCfg = Release|Any CPU {F6DE8DA0-07CC-4EF2-8A59-2BC81DBB3830}.Release|Any CPU.Build.0 = Release|Any CPU + {F6DE8DA0-07CC-4EF2-8A59-2BC81DBB3830}.SkipTests|Any CPU.ActiveCfg = Debug|Any CPU + {F6DE8DA0-07CC-4EF2-8A59-2BC81DBB3830}.SkipTests|Any CPU.Build.0 = Debug|Any CPU {0FAD7D2A-D7DD-45B1-91FD-488BB6CDACEA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {0FAD7D2A-D7DD-45B1-91FD-488BB6CDACEA}.Debug|Any CPU.Build.0 = Debug|Any CPU {0FAD7D2A-D7DD-45B1-91FD-488BB6CDACEA}.Release|Any CPU.ActiveCfg = Release|Any CPU {0FAD7D2A-D7DD-45B1-91FD-488BB6CDACEA}.Release|Any CPU.Build.0 = Release|Any CPU + {0FAD7D2A-D7DD-45B1-91FD-488BB6CDACEA}.SkipTests|Any CPU.ActiveCfg = Debug|Any CPU + {0FAD7D2A-D7DD-45B1-91FD-488BB6CDACEA}.SkipTests|Any CPU.Build.0 = Debug|Any CPU {9B95EEF7-63FE-4432-8C63-166BE9C1A929}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {9B95EEF7-63FE-4432-8C63-166BE9C1A929}.Debug|Any CPU.Build.0 = Debug|Any CPU {9B95EEF7-63FE-4432-8C63-166BE9C1A929}.Release|Any CPU.ActiveCfg = Release|Any CPU {9B95EEF7-63FE-4432-8C63-166BE9C1A929}.Release|Any CPU.Build.0 = Release|Any CPU + {9B95EEF7-63FE-4432-8C63-166BE9C1A929}.SkipTests|Any CPU.ActiveCfg = Debug|Any CPU + {9B95EEF7-63FE-4432-8C63-166BE9C1A929}.SkipTests|Any CPU.Build.0 = Debug|Any CPU {5A246D54-3109-4D2B-BE7D-FC0787D126AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {5A246D54-3109-4D2B-BE7D-FC0787D126AE}.Debug|Any CPU.Build.0 = Debug|Any CPU {5A246D54-3109-4D2B-BE7D-FC0787D126AE}.Release|Any CPU.ActiveCfg = Release|Any CPU {5A246D54-3109-4D2B-BE7D-FC0787D126AE}.Release|Any CPU.Build.0 = Release|Any CPU + {5A246D54-3109-4D2B-BE7D-FC0787D126AE}.SkipTests|Any CPU.ActiveCfg = Debug|Any CPU + {5A246D54-3109-4D2B-BE7D-FC0787D126AE}.SkipTests|Any CPU.Build.0 = Debug|Any CPU {D6319409-777A-4BD0-93ED-B2DFD805B32C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D6319409-777A-4BD0-93ED-B2DFD805B32C}.Debug|Any CPU.Build.0 = Debug|Any CPU {D6319409-777A-4BD0-93ED-B2DFD805B32C}.Release|Any CPU.ActiveCfg = Release|Any CPU {D6319409-777A-4BD0-93ED-B2DFD805B32C}.Release|Any CPU.Build.0 = Release|Any CPU + {D6319409-777A-4BD0-93ED-B2DFD805B32C}.SkipTests|Any CPU.ActiveCfg = Debug|Any CPU {A499779C-1B3B-48A8-B551-458E582E6E96}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A499779C-1B3B-48A8-B551-458E582E6E96}.Debug|Any CPU.Build.0 = Debug|Any CPU {A499779C-1B3B-48A8-B551-458E582E6E96}.Release|Any CPU.ActiveCfg = Release|Any CPU {A499779C-1B3B-48A8-B551-458E582E6E96}.Release|Any CPU.Build.0 = Release|Any CPU + {A499779C-1B3B-48A8-B551-458E582E6E96}.SkipTests|Any CPU.ActiveCfg = Debug|Any CPU {9102ABDF-E537-4E46-B525-C9ED4833EED0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {9102ABDF-E537-4E46-B525-C9ED4833EED0}.Debug|Any CPU.Build.0 = Debug|Any CPU {9102ABDF-E537-4E46-B525-C9ED4833EED0}.Release|Any CPU.ActiveCfg = Release|Any CPU {9102ABDF-E537-4E46-B525-C9ED4833EED0}.Release|Any CPU.Build.0 = Release|Any CPU + {9102ABDF-E537-4E46-B525-C9ED4833EED0}.SkipTests|Any CPU.ActiveCfg = Debug|Any CPU {79E4293D-C92C-4649-AEC8-F1EFD95BDEB1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {79E4293D-C92C-4649-AEC8-F1EFD95BDEB1}.Debug|Any CPU.Build.0 = Debug|Any CPU {79E4293D-C92C-4649-AEC8-F1EFD95BDEB1}.Release|Any CPU.ActiveCfg = Release|Any CPU {79E4293D-C92C-4649-AEC8-F1EFD95BDEB1}.Release|Any CPU.Build.0 = Release|Any CPU + {79E4293D-C92C-4649-AEC8-F1EFD95BDEB1}.SkipTests|Any CPU.ActiveCfg = Debug|Any CPU + {79E4293D-C92C-4649-AEC8-F1EFD95BDEB1}.SkipTests|Any CPU.Build.0 = Debug|Any CPU {2A5027D9-F71D-4957-929E-F7A56AA1B95A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {2A5027D9-F71D-4957-929E-F7A56AA1B95A}.Debug|Any CPU.Build.0 = Debug|Any CPU {2A5027D9-F71D-4957-929E-F7A56AA1B95A}.Release|Any CPU.ActiveCfg = Release|Any CPU {2A5027D9-F71D-4957-929E-F7A56AA1B95A}.Release|Any CPU.Build.0 = Release|Any CPU - {1B28FC3E-3D5B-4A46-8961-5483835548D7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1B28FC3E-3D5B-4A46-8961-5483835548D7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1B28FC3E-3D5B-4A46-8961-5483835548D7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1B28FC3E-3D5B-4A46-8961-5483835548D7}.Release|Any CPU.Build.0 = Release|Any CPU + {2A5027D9-F71D-4957-929E-F7A56AA1B95A}.SkipTests|Any CPU.ActiveCfg = Debug|Any CPU + {2A5027D9-F71D-4957-929E-F7A56AA1B95A}.SkipTests|Any CPU.Build.0 = Debug|Any CPU + {32F6A309-EC1E-4CDB-BA80-C804CF680BEE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {32F6A309-EC1E-4CDB-BA80-C804CF680BEE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {32F6A309-EC1E-4CDB-BA80-C804CF680BEE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {32F6A309-EC1E-4CDB-BA80-C804CF680BEE}.Release|Any CPU.Build.0 = Release|Any CPU + {32F6A309-EC1E-4CDB-BA80-C804CF680BEE}.SkipTests|Any CPU.ActiveCfg = Debug|Any CPU + {32F6A309-EC1E-4CDB-BA80-C804CF680BEE}.SkipTests|Any CPU.Build.0 = Debug|Any CPU + {93C5910D-2E36-475D-88EB-A11BA5B50F65}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {93C5910D-2E36-475D-88EB-A11BA5B50F65}.Debug|Any CPU.Build.0 = Debug|Any CPU + {93C5910D-2E36-475D-88EB-A11BA5B50F65}.Release|Any CPU.ActiveCfg = Release|Any CPU + {93C5910D-2E36-475D-88EB-A11BA5B50F65}.Release|Any CPU.Build.0 = Release|Any CPU + {93C5910D-2E36-475D-88EB-A11BA5B50F65}.SkipTests|Any CPU.ActiveCfg = Debug|Any CPU + {93C5910D-2E36-475D-88EB-A11BA5B50F65}.SkipTests|Any CPU.Build.0 = Debug|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -222,7 +251,6 @@ Global {D6319409-777A-4BD0-93ED-B2DFD805B32C} = {B5BD12C1-A454-435E-8A46-FF4A364C0382} {A499779C-1B3B-48A8-B551-458E582E6E96} = {B5BD12C1-A454-435E-8A46-FF4A364C0382} {9102ABDF-E537-4E46-B525-C9ED4833EED0} = {B5BD12C1-A454-435E-8A46-FF4A364C0382} - {1B28FC3E-3D5B-4A46-8961-5483835548D7} = {B5BD12C1-A454-435E-8A46-FF4A364C0382} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {7A0F2E34-D2AF-4DAB-86A0-7D7764B3D0EC}
Group + + Template