diff --git a/src/Umbraco.Core/Collections/StackQueue.cs b/src/Umbraco.Core/Collections/StackQueue.cs index 9bf9c365f077..1d8b19f88945 100644 --- a/src/Umbraco.Core/Collections/StackQueue.cs +++ b/src/Umbraco.Core/Collections/StackQueue.cs @@ -1,60 +1,39 @@ using System.Collections.Generic; -namespace Umbraco.Core.Collections +namespace Umbraco.Cms.Core.Collections { /// - /// Collection that can be both a queue and a stack. + /// Collection that can be both a queue and a stack. /// /// public class StackQueue { - private readonly LinkedList _linkedList = new LinkedList(); + private readonly LinkedList _linkedList = new(); - public void Clear() - { - _linkedList.Clear(); - } + public int Count => _linkedList.Count; - public void Push(T obj) - { - _linkedList.AddFirst(obj); - } + public void Clear() => _linkedList.Clear(); - public void Enqueue(T obj) - { - _linkedList.AddFirst(obj); - } + public void Push(T obj) => _linkedList.AddFirst(obj); + + public void Enqueue(T obj) => _linkedList.AddFirst(obj); public T Pop() { - var obj = _linkedList.First.Value; + T obj = _linkedList.First.Value; _linkedList.RemoveFirst(); return obj; } public T Dequeue() { - var obj = _linkedList.Last.Value; + T obj = _linkedList.Last.Value; _linkedList.RemoveLast(); return obj; } - public T PeekStack() - { - return _linkedList.First.Value; - } + public T PeekStack() => _linkedList.First.Value; - public T PeekQueue() - { - return _linkedList.Last.Value; - } - - public int Count - { - get - { - return _linkedList.Count; - } - } + public T PeekQueue() => _linkedList.Last.Value; } } diff --git a/src/Umbraco.Core/Constants-Sql.cs b/src/Umbraco.Core/Constants-Sql.cs new file mode 100644 index 000000000000..b57861c92ac8 --- /dev/null +++ b/src/Umbraco.Core/Constants-Sql.cs @@ -0,0 +1,18 @@ +namespace Umbraco.Cms.Core +{ + public static partial class Constants + { + public static class Sql + { + /// + /// The maximum amount of parameters that can be used in a query. + /// + /// + /// The actual limit is 2100 + /// (https://docs.microsoft.com/en-us/sql/sql-server/maximum-capacity-specifications-for-sql-server), + /// but we want to ensure there's room for additional parameters if this value is used to create groups/batches. + /// + public const int MaxParameterCount = 2000; + } + } +} diff --git a/src/Umbraco.Core/Models/Mapping/UserMapDefinition.cs b/src/Umbraco.Core/Models/Mapping/UserMapDefinition.cs index 238065718002..e4d101ff062d 100644 --- a/src/Umbraco.Core/Models/Mapping/UserMapDefinition.cs +++ b/src/Umbraco.Core/Models/Mapping/UserMapDefinition.cs @@ -251,7 +251,7 @@ private void Map(IUserGroup source, UserGroupDisplay target, MapperContext conte // the entity service due to too many Sql parameters. var list = new List(); - foreach (var idGroup in allContentPermissions.Keys.InGroupsOf(2000)) + foreach (var idGroup in allContentPermissions.Keys.InGroupsOf(Constants.Sql.MaxParameterCount)) list.AddRange(_entityService.GetAll(UmbracoObjectTypes.Document, idGroup.ToArray())); contentEntities = list.ToArray(); } diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 734ed2261d18..8ed2205f5950 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -1,61 +1,61 @@ - - netstandard2.0 - Umbraco.Cms.Core - Umbraco CMS - Umbraco.Cms.Core - Umbraco CMS Core - Contains the core assembly needed to run Umbraco Cms. This package only contains the assembly, and can be used for package development. Use the template in the Umbraco.Templates package to setup Umbraco - Umbraco CMS - + + netstandard2.0 + Umbraco.Cms.Core + Umbraco CMS + Umbraco.Cms.Core + Umbraco CMS Core + Contains the core assembly needed to run Umbraco Cms. This package only contains the assembly, and can be used for package development. Use the template in the Umbraco.Templates package to setup Umbraco + Umbraco CMS + - - bin\Release\Umbraco.Core.xml - + + bin\Release\Umbraco.Core.xml + - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - all - - + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + all + + - - - <_Parameter1>Umbraco.Tests - - - <_Parameter1>Umbraco.Tests.Common - - - <_Parameter1>Umbraco.Tests.UnitTests - - - <_Parameter1>Umbraco.Tests.Benchmarks - - - <_Parameter1>Umbraco.Tests.Integration - - - <_Parameter1>DynamicProxyGenAssembly2 - - + + + <_Parameter1>Umbraco.Tests + + + <_Parameter1>Umbraco.Tests.Common + + + <_Parameter1>Umbraco.Tests.UnitTests + + + <_Parameter1>Umbraco.Tests.Benchmarks + + + <_Parameter1>Umbraco.Tests.Integration + + + <_Parameter1>DynamicProxyGenAssembly2 + + - - - + + + diff --git a/src/Umbraco.Infrastructure/Persistence/NPocoDatabaseExtensions-Bulk.cs b/src/Umbraco.Infrastructure/Persistence/NPocoDatabaseExtensions-Bulk.cs index 443032c67ae8..f07867cccc83 100644 --- a/src/Umbraco.Infrastructure/Persistence/NPocoDatabaseExtensions-Bulk.cs +++ b/src/Umbraco.Infrastructure/Persistence/NPocoDatabaseExtensions-Bulk.cs @@ -1,38 +1,43 @@ using System; using System.Collections.Generic; using System.Data; +using System.Data.Common; using System.Data.SqlClient; using System.Linq; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence; namespace Umbraco.Extensions { /// - /// Provides extension methods to NPoco Database class. + /// Provides extension methods to NPoco Database class. /// public static partial class NPocoDatabaseExtensions { /// - /// Configures NPoco's SqlBulkCopyHelper to use the correct SqlConnection and SqlTransaction instances from the underlying RetryDbConnection and ProfiledDbTransaction + /// Configures NPoco's SqlBulkCopyHelper to use the correct SqlConnection and SqlTransaction instances from the + /// underlying RetryDbConnection and ProfiledDbTransaction /// /// - /// This is required to use NPoco's own method because we use wrapped DbConnection and DbTransaction instances. - /// NPoco's InsertBulk method only caters for efficient bulk inserting records for Sql Server, it does not cater for bulk inserting of records for - /// any other database type and in which case will just insert records one at a time. - /// NPoco's InsertBulk method also deals with updating the passed in entity's PK/ID once it's inserted whereas our own BulkInsertRecords methods - /// do not handle this scenario. + /// This is required to use NPoco's own method because we use + /// wrapped DbConnection and DbTransaction instances. + /// NPoco's InsertBulk method only caters for efficient bulk inserting records for Sql Server, it does not cater for + /// bulk inserting of records for + /// any other database type and in which case will just insert records one at a time. + /// NPoco's InsertBulk method also deals with updating the passed in entity's PK/ID once it's inserted whereas our own + /// BulkInsertRecords methods + /// do not handle this scenario. /// public static void ConfigureNPocoBulkExtensions() { - SqlBulkCopyHelper.SqlConnectionResolver = dbConn => GetTypedConnection(dbConn); SqlBulkCopyHelper.SqlTransactionResolver = dbTran => GetTypedTransaction(dbTran); } /// - /// Creates bulk-insert commands. + /// Creates bulk-insert commands. /// /// The type of the records. /// The database. @@ -40,17 +45,22 @@ public static void ConfigureNPocoBulkExtensions() /// The sql commands to execute. internal static IDbCommand[] GenerateBulkInsertCommands(this IUmbracoDatabase database, T[] records) { - if (database?.Connection == null) throw new ArgumentException("Null database?.connection.", nameof(database)); + if (database?.Connection == null) + { + throw new ArgumentException("Null database?.connection.", nameof(database)); + } - var pocoData = database.PocoDataFactory.ForType(typeof(T)); + PocoData pocoData = database.PocoDataFactory.ForType(typeof(T)); // get columns to include, = number of parameters per row - var columns = pocoData.Columns.Where(c => IncludeColumn(pocoData, c)).ToArray(); + KeyValuePair[] columns = + pocoData.Columns.Where(c => IncludeColumn(pocoData, c)).ToArray(); var paramsPerRecord = columns.Length; // format columns to sql var tableName = database.DatabaseType.EscapeTableName(pocoData.TableInfo.TableName); - var columnNames = string.Join(", ", columns.Select(c => tableName + "." + database.DatabaseType.EscapeSqlIdentifier(c.Key))); + var columnNames = string.Join(", ", + columns.Select(c => tableName + "." + database.DatabaseType.EscapeSqlIdentifier(c.Key))); // example: // assume 4168 records, each record containing 8 fields, ie 8 command parameters @@ -58,7 +68,9 @@ internal static IDbCommand[] GenerateBulkInsertCommands(this IUmbracoDatabase // Math.Floor(2100 / 8) = 262 record per command // 4168 / 262 = 15.908... = there will be 16 command in total // (if we have disabled db parameters, then all records will be included, in only one command) - var recordsPerCommand = paramsPerRecord == 0 ? int.MaxValue : Convert.ToInt32(Math.Floor(2000.00 / paramsPerRecord)); + var recordsPerCommand = paramsPerRecord == 0 + ? int.MaxValue + : Convert.ToInt32(Math.Floor((double)Constants.Sql.MaxParameterCount / paramsPerRecord)); var commandsCount = Convert.ToInt32(Math.Ceiling((double)records.Length / recordsPerCommand)); var commands = new IDbCommand[commandsCount]; @@ -67,23 +79,27 @@ internal static IDbCommand[] GenerateBulkInsertCommands(this IUmbracoDatabase var prefix = database.DatabaseType.GetParameterPrefix(database.ConnectionString); for (var commandIndex = 0; commandIndex < commandsCount; commandIndex++) { - var command = database.CreateCommand(database.Connection, CommandType.Text, string.Empty); + DbCommand command = database.CreateCommand(database.Connection, CommandType.Text, string.Empty); var parameterIndex = 0; var commandRecords = Math.Min(recordsPerCommand, recordsLeftToInsert); var recordsValues = new string[commandRecords]; - for (var commandRecordIndex = 0; commandRecordIndex < commandRecords; commandRecordIndex++, recordsIndex++, recordsLeftToInsert--) + for (var commandRecordIndex = 0; + commandRecordIndex < commandRecords; + commandRecordIndex++, recordsIndex++, recordsLeftToInsert--) { - var record = records[recordsIndex]; + T record = records[recordsIndex]; var recordValues = new string[columns.Length]; for (var columnIndex = 0; columnIndex < columns.Length; columnIndex++) { database.AddParameter(command, columns[columnIndex].Value.GetValue(record)); recordValues[columnIndex] = prefix + parameterIndex++; } + recordsValues[commandRecordIndex] = "(" + string.Join(",", recordValues) + ")"; } - command.CommandText = $"INSERT INTO {tableName} ({columnNames}) VALUES {string.Join(", ", recordsValues)}"; + command.CommandText = + $"INSERT INTO {tableName} ({columnNames}) VALUES {string.Join(", ", recordsValues)}"; commands[commandIndex] = command; } @@ -91,19 +107,14 @@ internal static IDbCommand[] GenerateBulkInsertCommands(this IUmbracoDatabase } /// - /// Determines whether a column should be part of a bulk-insert. + /// Determines whether a column should be part of a bulk-insert. /// /// The PocoData object corresponding to the record's type. /// The column. /// A value indicating whether the column should be part of the bulk-insert. /// Columns that are primary keys and auto-incremental, or result columns, are excluded from bulk-inserts. - public static bool IncludeColumn(PocoData pocoData, KeyValuePair column) - { - return column.Value.ResultColumn == false - && (pocoData.TableInfo.AutoIncrement == false || column.Key != pocoData.TableInfo.PrimaryKey); - } - - - + public static bool IncludeColumn(PocoData pocoData, KeyValuePair column) => + column.Value.ResultColumn == false + && (pocoData.TableInfo.AutoIncrement == false || column.Key != pocoData.TableInfo.PrimaryKey); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/AuditEntryRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/AuditEntryRepository.cs index 16c411c772a5..e3685dd32ccf 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/AuditEntryRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/AuditEntryRepository.cs @@ -3,6 +3,7 @@ using System.Linq; using Microsoft.Extensions.Logging; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Querying; @@ -16,26 +17,47 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement { /// - /// Represents the NPoco implementation of . + /// Represents the NPoco implementation of . /// internal class AuditEntryRepository : EntityRepositoryBase, IAuditEntryRepository { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// public AuditEntryRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) : base(scopeAccessor, cache, logger) - { } + { + } + + /// + public IEnumerable GetPage(long pageIndex, int pageCount, out long records) + { + Sql sql = Sql() + .Select() + .From() + .OrderByDescending(x => x.EventDateUtc); + + Page page = Database.Page(pageIndex + 1, pageCount, sql); + records = page.TotalItems; + return page.Items.Select(AuditEntryFactory.BuildEntity); + } + + /// + public bool IsAvailable() + { + var tables = SqlSyntax.GetTablesInSchema(Database).ToArray(); + return tables.InvariantContains(Constants.DatabaseSchema.Tables.AuditEntry); + } /// protected override IAuditEntry PerformGet(int id) { - var sql = Sql() + Sql sql = Sql() .Select() .From() .Where(x => x.Id == id); - var dto = Database.FirstOrDefault(sql); + AuditEntryDto dto = Database.FirstOrDefault(sql); return dto == null ? null : AuditEntryFactory.BuildEntity(dto); } @@ -44,7 +66,7 @@ protected override IEnumerable PerformGetAll(params int[] ids) { if (ids.Length == 0) { - var sql = Sql() + Sql sql = Sql() .Select() .From(); @@ -53,9 +75,9 @@ protected override IEnumerable PerformGetAll(params int[] ids) var entries = new List(); - foreach (var group in ids.InGroupsOf(2000)) + foreach (IEnumerable group in ids.InGroupsOf(Constants.Sql.MaxParameterCount)) { - var sql = Sql() + Sql sql = Sql() .Select() .From() .WhereIn(x => x.Id, group); @@ -69,68 +91,41 @@ protected override IEnumerable PerformGetAll(params int[] ids) /// protected override IEnumerable PerformGetByQuery(IQuery query) { - var sqlClause = GetBaseQuery(false); + Sql sqlClause = GetBaseQuery(false); var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate(); + Sql sql = translator.Translate(); return Database.Fetch(sql).Select(AuditEntryFactory.BuildEntity); } /// protected override Sql GetBaseQuery(bool isCount) { - var sql = Sql(); + Sql sql = Sql(); sql = isCount ? sql.SelectCount() : sql.Select(); sql = sql.From(); return sql; } /// - protected override string GetBaseWhereClause() - { - return $"{Cms.Core.Constants.DatabaseSchema.Tables.AuditEntry}.id = @id"; - } + protected override string GetBaseWhereClause() => $"{Constants.DatabaseSchema.Tables.AuditEntry}.id = @id"; /// - protected override IEnumerable GetDeleteClauses() - { + protected override IEnumerable GetDeleteClauses() => throw new NotSupportedException("Audit entries cannot be deleted."); - } /// protected override void PersistNewItem(IAuditEntry entity) { entity.AddingEntity(); - var dto = AuditEntryFactory.BuildDto(entity); + AuditEntryDto dto = AuditEntryFactory.BuildDto(entity); Database.Insert(dto); entity.Id = dto.Id; entity.ResetDirtyProperties(); } /// - protected override void PersistUpdatedItem(IAuditEntry entity) - { + protected override void PersistUpdatedItem(IAuditEntry entity) => throw new NotSupportedException("Audit entries cannot be updated."); - } - - /// - public IEnumerable GetPage(long pageIndex, int pageCount, out long records) - { - var sql = Sql() - .Select() - .From() - .OrderByDescending(x => x.EventDateUtc); - - var page = Database.Page(pageIndex + 1, pageCount, sql); - records = page.TotalItems; - return page.Items.Select(AuditEntryFactory.BuildEntity); - } - - /// - public bool IsAvailable() - { - var tables = SqlSyntax.GetTablesInSchema(Database).ToArray(); - return tables.InvariantContains(Cms.Core.Constants.DatabaseSchema.Tables.AuditEntry); - } } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs index 239f0a89ed71..fd4d1c33b9ca 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs @@ -657,7 +657,7 @@ protected IDictionary GetPropertyCollections(List(versions, 2000, batch => + var allPropertyDataDtos = Database.FetchByGroups(versions, Constants.Sql.MaxParameterCount, batch => SqlContext.Sql() .Select() .From() @@ -666,7 +666,7 @@ protected IDictionary GetPropertyCollections(List x.PropertyTypeId).Distinct().ToList(); - var allPropertyTypeDtos = Database.FetchByGroups(allPropertyTypeIds, 2000, batch => + var allPropertyTypeDtos = Database.FetchByGroups(allPropertyTypeIds, Constants.Sql.MaxParameterCount, batch => SqlContext.Sql() .Select(r => r.Select(x => x.DataTypeDto)) .From() diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs index d93c2c832280..d7be081fe144 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs @@ -795,7 +795,7 @@ private void CopyTagData(int? sourceLanguageId, int? targetLanguageId, IReadOnly // note: important to use SqlNullableEquals for nullable types, cannot directly compare language identifiers var whereInArgsCount = propertyTypeIds.Count + (contentTypeIds?.Count ?? 0); - if (whereInArgsCount > 2000) + if (whereInArgsCount > Constants.Sql.MaxParameterCount) throw new NotSupportedException("Too many property/content types."); // delete existing relations (for target language) @@ -933,7 +933,7 @@ private void CopyPropertyData(int? sourceLanguageId, int? targetLanguageId, IRea // note: important to use SqlNullableEquals for nullable types, cannot directly compare language identifiers // var whereInArgsCount = propertyTypeIds.Count + (contentTypeIds?.Count ?? 0); - if (whereInArgsCount > 2000) + if (whereInArgsCount > Constants.Sql.MaxParameterCount) throw new NotSupportedException("Too many property/content types."); //first clear out any existing property data that might already exists under the target language @@ -1032,7 +1032,7 @@ private void RenormalizeDocumentEditedFlags(IReadOnlyCollection propertyTyp //based on the current variance of each item to see if it's 'edited' value should be true/false. var whereInArgsCount = propertyTypeIds.Count + (contentTypeIds?.Count ?? 0); - if (whereInArgsCount > 2000) + if (whereInArgsCount > Constants.Sql.MaxParameterCount) throw new NotSupportedException("Too many property/content types."); var propertySql = Sql() @@ -1121,14 +1121,20 @@ private void RenormalizeDocumentEditedFlags(IReadOnlyCollection propertyTyp } } - //lookup all matching rows in umbracoDocumentCultureVariation - var docCultureVariationsToUpdate = editedLanguageVersions.InGroupsOf(2000) - .SelectMany(_ => Database.Fetch( - Sql().Select().From() - .WhereIn(x => x.LanguageId, editedLanguageVersions.Keys.Select(x => x.langId).ToList()) - .WhereIn(x => x.NodeId, editedLanguageVersions.Keys.Select(x => x.nodeId)))) - //convert to dictionary with the same key type - .ToDictionary(x => (x.NodeId, (int?)x.LanguageId), x => x); + // lookup all matching rows in umbracoDocumentCultureVariation + // fetch in batches to account for maximum parameter count (distinct languages can't exceed 2000) + var languageIds = editedLanguageVersions.Keys.Select(x => x.langId).Distinct().ToArray(); + var nodeIds = editedLanguageVersions.Keys.Select(x => x.nodeId).Distinct(); + var docCultureVariationsToUpdate = nodeIds.InGroupsOf(Constants.Sql.MaxParameterCount - languageIds.Length) + .SelectMany(group => + { + var sql = Sql().Select().From() + .WhereIn(x => x.LanguageId, languageIds) + .WhereIn(x => x.NodeId, group); + + return Database.Fetch(sql); + }) + .ToDictionary(x => (x.NodeId, (int?)x.LanguageId), x => x); //convert to dictionary with the same key type var toUpdate = new List(); foreach (var ev in editedLanguageVersions) diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DictionaryRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DictionaryRepository.cs index 0ec31d843fe3..bc9892b1ee0e 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DictionaryRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DictionaryRepository.cs @@ -263,13 +263,12 @@ public IEnumerable GetDictionaryItemDescendants(Guid? parentId) Func>> getItemsFromParents = guids => { - //needs to be in groups of 2000 because we are doing an IN clause and there's a max parameter count that can be used. - return guids.InGroupsOf(2000) - .Select(@group => + return guids.InGroupsOf(Constants.Sql.MaxParameterCount) + .Select(group => { var sqlClause = GetBaseQuery(false) .Where(x => x.Parent != null) - .Where($"{SqlSyntax.GetQuotedColumnName("parent")} IN (@parentIds)", new { parentIds = @group }); + .WhereIn(x => x.Parent, group); var translator = new SqlTranslator(sqlClause, Query()); var sql = translator.Translate(); diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs index 4c9b19f1a91f..75d66cf09d7f 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs @@ -1390,7 +1390,7 @@ private IDictionary GetContentSchedule(params in { var result = new Dictionary(); - var scheduleDtos = Database.FetchByGroups(contentIds, 2000, batch => Sql() + var scheduleDtos = Database.FetchByGroups(contentIds, Constants.Sql.MaxParameterCount, batch => Sql() .Select() .From() .WhereIn(x => x.NodeId, batch)); @@ -1440,7 +1440,7 @@ private IDictionary> GetContentVariations(List>(); - var dtos = Database.FetchByGroups(versions, 2000, batch + var dtos = Database.FetchByGroups(versions, Constants.Sql.MaxParameterCount, batch => Sql() .Select() .From() @@ -1469,7 +1469,7 @@ private IDictionary> GetDocumentVariations(List< { var ids = temps.Select(x => x.Id); - var dtos = Database.FetchByGroups(ids, 2000, batch => + var dtos = Database.FetchByGroups(ids, Constants.Sql.MaxParameterCount, batch => Sql() .Select() .From() diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityContainerRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityContainerRepository.cs index bc1b1b1881c8..b30c5ae1a44d 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityContainerRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityContainerRepository.cs @@ -3,6 +3,7 @@ using System.Linq; using Microsoft.Extensions.Logging; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Querying; @@ -14,47 +15,55 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement { /// - /// An internal repository for managing entity containers such as doc type, media type, data type containers. + /// An internal repository for managing entity containers such as doc type, media type, data type containers. /// internal class EntityContainerRepository : EntityRepositoryBase, IEntityContainerRepository { - private readonly Guid _containerObjectType; - - public EntityContainerRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, Guid containerObjectType) + public EntityContainerRepository(IScopeAccessor scopeAccessor, AppCaches cache, + ILogger logger, Guid containerObjectType) : base(scopeAccessor, cache, logger) { - var allowedContainers = new[] { Cms.Core.Constants.ObjectTypes.DocumentTypeContainer, Cms.Core.Constants.ObjectTypes.MediaTypeContainer, Cms.Core.Constants.ObjectTypes.DataTypeContainer }; - _containerObjectType = containerObjectType; - if (allowedContainers.Contains(_containerObjectType) == false) - throw new InvalidOperationException("No container type exists with ID: " + _containerObjectType); + Guid[] allowedContainers = new[] + { + Constants.ObjectTypes.DocumentTypeContainer, Constants.ObjectTypes.MediaTypeContainer, + Constants.ObjectTypes.DataTypeContainer + }; + NodeObjectTypeId = containerObjectType; + if (allowedContainers.Contains(NodeObjectTypeId) == false) + { + throw new InvalidOperationException("No container type exists with ID: " + NodeObjectTypeId); + } } + protected Guid NodeObjectTypeId { get; } + // never cache - protected override IRepositoryCachePolicy CreateCachePolicy() - { - return NoCacheRepositoryCachePolicy.Instance; - } + protected override IRepositoryCachePolicy CreateCachePolicy() => + NoCacheRepositoryCachePolicy.Instance; protected override EntityContainer PerformGet(int id) { - var sql = GetBaseQuery(false).Where(GetBaseWhereClause(), new { id = id, NodeObjectType = NodeObjectTypeId }); + Sql sql = GetBaseQuery(false) + .Where(GetBaseWhereClause(), new { id, NodeObjectType = NodeObjectTypeId }); - var nodeDto = Database.Fetch(SqlSyntax.SelectTop(sql, 1)).FirstOrDefault(); + NodeDto nodeDto = Database.Fetch(SqlSyntax.SelectTop(sql, 1)).FirstOrDefault(); return nodeDto == null ? null : CreateEntity(nodeDto); } // temp - so we don't have to implement GetByQuery public EntityContainer Get(Guid id) { - var sql = GetBaseQuery(false).Where("UniqueId=@uniqueId", new { uniqueId = id }); + Sql sql = GetBaseQuery(false).Where("UniqueId=@uniqueId", new { uniqueId = id }); - var nodeDto = Database.Fetch(sql).FirstOrDefault(); + NodeDto nodeDto = Database.Fetch(sql).FirstOrDefault(); return nodeDto == null ? null : CreateEntity(nodeDto); } public IEnumerable Get(string name, int level) { - var sql = GetBaseQuery(false).Where("text=@name AND level=@level AND nodeObjectType=@umbracoObjectTypeId", new { name, level, umbracoObjectTypeId = NodeObjectTypeId }); + Sql sql = GetBaseQuery(false) + .Where("text=@name AND level=@level AND nodeObjectType=@umbracoObjectTypeId", + new { name, level, umbracoObjectTypeId = NodeObjectTypeId }); return Database.Fetch(sql).Select(CreateEntity); } @@ -62,39 +71,39 @@ protected override IEnumerable PerformGetAll(params int[] ids) { if (ids.Any()) { - return Database.FetchByGroups(ids, 2000, batch => - GetBaseQuery(false) - .Where(x => x.NodeObjectType == NodeObjectTypeId) - .WhereIn(x => x.NodeId, batch)) + return Database.FetchByGroups(ids, Constants.Sql.MaxParameterCount, batch => + GetBaseQuery(false) + .Where(x => x.NodeObjectType == NodeObjectTypeId) + .WhereIn(x => x.NodeId, batch)) .Select(CreateEntity); } // else - var sql = GetBaseQuery(false) + Sql sql = GetBaseQuery(false) .Where("nodeObjectType=@umbracoObjectTypeId", new { umbracoObjectTypeId = NodeObjectTypeId }) .OrderBy(x => x.Level); return Database.Fetch(sql).Select(CreateEntity); } - protected override IEnumerable PerformGetByQuery(IQuery query) - { + protected override IEnumerable PerformGetByQuery(IQuery query) => throw new NotImplementedException(); - } private static EntityContainer CreateEntity(NodeDto nodeDto) { if (nodeDto.NodeObjectType.HasValue == false) + { throw new InvalidOperationException("Node with id " + nodeDto.NodeId + " has no object type."); + } // throws if node is not a container - var containedObjectType = EntityContainer.GetContainedObjectType(nodeDto.NodeObjectType.Value); + Guid containedObjectType = EntityContainer.GetContainedObjectType(nodeDto.NodeObjectType.Value); var entity = new EntityContainer(nodeDto.NodeId, nodeDto.UniqueId, nodeDto.ParentId, nodeDto.Path, nodeDto.Level, nodeDto.SortOrder, containedObjectType, - nodeDto.Text, nodeDto.UserId ?? Cms.Core.Constants.Security.UnknownUserId); + nodeDto.Text, nodeDto.UserId ?? Constants.Security.UnknownUserId); // reset dirty initial properties (U4-1946) entity.ResetDirtyProperties(false); @@ -104,11 +113,16 @@ private static EntityContainer CreateEntity(NodeDto nodeDto) protected override Sql GetBaseQuery(bool isCount) { - var sql = Sql(); + Sql sql = Sql(); if (isCount) + { sql.SelectCount(); + } else + { sql.SelectAll(); + } + sql.From(); return sql; } @@ -117,23 +131,29 @@ protected override Sql GetBaseQuery(bool isCount) protected override IEnumerable GetDeleteClauses() => throw new NotImplementedException(); - protected Guid NodeObjectTypeId => _containerObjectType; - protected override void PersistDeletedItem(EntityContainer entity) { - if (entity == null) throw new ArgumentNullException(nameof(entity)); + if (entity == null) + { + throw new ArgumentNullException(nameof(entity)); + } + EnsureContainerType(entity); - var nodeDto = Database.FirstOrDefault(Sql().SelectAll() + NodeDto nodeDto = Database.FirstOrDefault(Sql().SelectAll() .From() .Where(dto => dto.NodeId == entity.Id && dto.NodeObjectType == entity.ContainerObjectType)); - if (nodeDto == null) return; + if (nodeDto == null) + { + return; + } // move children to the parent so they are not orphans - var childDtos = Database.Fetch(Sql().SelectAll() + List childDtos = Database.Fetch(Sql().SelectAll() .From() - .Where("parentID=@parentID AND (nodeObjectType=@containedObjectType OR nodeObjectType=@containerObjectType)", + .Where( + "parentID=@parentID AND (nodeObjectType=@containedObjectType OR nodeObjectType=@containerObjectType)", new { parentID = entity.Id, @@ -141,7 +161,7 @@ protected override void PersistDeletedItem(EntityContainer entity) containerObjectType = entity.ContainerObjectType })); - foreach (var childDto in childDtos) + foreach (NodeDto childDto in childDtos) { childDto.ParentId = nodeDto.ParentId; Database.Update(childDto); @@ -155,31 +175,51 @@ protected override void PersistDeletedItem(EntityContainer entity) protected override void PersistNewItem(EntityContainer entity) { - if (entity == null) throw new ArgumentNullException(nameof(entity)); + if (entity == null) + { + throw new ArgumentNullException(nameof(entity)); + } + EnsureContainerType(entity); - if (entity.Name == null) throw new InvalidOperationException("Entity name can't be null."); - if (string.IsNullOrWhiteSpace(entity.Name)) throw new InvalidOperationException("Entity name can't be empty or consist only of white-space characters."); + if (entity.Name == null) + { + throw new InvalidOperationException("Entity name can't be null."); + } + + if (string.IsNullOrWhiteSpace(entity.Name)) + { + throw new InvalidOperationException( + "Entity name can't be empty or consist only of white-space characters."); + } + entity.Name = entity.Name.Trim(); // guard against duplicates - var nodeDto = Database.FirstOrDefault(Sql().SelectAll() + NodeDto nodeDto = Database.FirstOrDefault(Sql().SelectAll() .From() - .Where(dto => dto.ParentId == entity.ParentId && dto.Text == entity.Name && dto.NodeObjectType == entity.ContainerObjectType)); + .Where(dto => + dto.ParentId == entity.ParentId && dto.Text == entity.Name && + dto.NodeObjectType == entity.ContainerObjectType)); if (nodeDto != null) + { throw new InvalidOperationException("A container with the same name already exists."); + } // create var level = 0; var path = "-1"; if (entity.ParentId > -1) { - var parentDto = Database.FirstOrDefault(Sql().SelectAll() + NodeDto parentDto = Database.FirstOrDefault(Sql().SelectAll() .From() - .Where(dto => dto.NodeId == entity.ParentId && dto.NodeObjectType == entity.ContainerObjectType)); + .Where(dto => + dto.NodeId == entity.ParentId && dto.NodeObjectType == entity.ContainerObjectType)); if (parentDto == null) + { throw new InvalidOperationException("Could not find parent container with id " + entity.ParentId); + } level = parentDto.Level; path = parentDto.Path; @@ -203,7 +243,7 @@ protected override void PersistNewItem(EntityContainer entity) // insert, get the id, update the path with the id var id = Convert.ToInt32(Database.Insert(nodeDto)); nodeDto.Path = nodeDto.Path + "," + nodeDto.NodeId; - Database.Save(nodeDto); + Database.Save(nodeDto); // refresh the entity entity.Id = id; @@ -218,26 +258,45 @@ protected override void PersistNewItem(EntityContainer entity) // protected override void PersistUpdatedItem(EntityContainer entity) { - if (entity == null) throw new ArgumentNullException(nameof(entity)); + if (entity == null) + { + throw new ArgumentNullException(nameof(entity)); + } + EnsureContainerType(entity); - if (entity.Name == null) throw new InvalidOperationException("Entity name can't be null."); - if (string.IsNullOrWhiteSpace(entity.Name)) throw new InvalidOperationException("Entity name can't be empty or consist only of white-space characters."); + if (entity.Name == null) + { + throw new InvalidOperationException("Entity name can't be null."); + } + + if (string.IsNullOrWhiteSpace(entity.Name)) + { + throw new InvalidOperationException( + "Entity name can't be empty or consist only of white-space characters."); + } + entity.Name = entity.Name.Trim(); // find container to update - var nodeDto = Database.FirstOrDefault(Sql().SelectAll() + NodeDto nodeDto = Database.FirstOrDefault(Sql().SelectAll() .From() .Where(dto => dto.NodeId == entity.Id && dto.NodeObjectType == entity.ContainerObjectType)); if (nodeDto == null) + { throw new InvalidOperationException("Could not find container with id " + entity.Id); + } // guard against duplicates - var dupNodeDto = Database.FirstOrDefault(Sql().SelectAll() + NodeDto dupNodeDto = Database.FirstOrDefault(Sql().SelectAll() .From() - .Where(dto => dto.ParentId == entity.ParentId && dto.Text == entity.Name && dto.NodeObjectType == entity.ContainerObjectType)); + .Where(dto => + dto.ParentId == entity.ParentId && dto.Text == entity.Name && + dto.NodeObjectType == entity.ContainerObjectType)); if (dupNodeDto != null && dupNodeDto.NodeId != nodeDto.NodeId) + { throw new InvalidOperationException("A container with the same name already exists."); + } // update nodeDto.Text = entity.Name; @@ -247,16 +306,21 @@ protected override void PersistUpdatedItem(EntityContainer entity) nodeDto.Path = "-1"; if (entity.ParentId > -1) { - var parent = Database.FirstOrDefault( Sql().SelectAll() + NodeDto parent = Database.FirstOrDefault(Sql().SelectAll() .From() - .Where(dto => dto.NodeId == entity.ParentId && dto.NodeObjectType == entity.ContainerObjectType)); + .Where(dto => + dto.NodeId == entity.ParentId && dto.NodeObjectType == entity.ContainerObjectType)); if (parent == null) - throw new InvalidOperationException("Could not find parent container with id " + entity.ParentId); + { + throw new InvalidOperationException( + "Could not find parent container with id " + entity.ParentId); + } nodeDto.Level = Convert.ToInt16(parent.Level + 1); nodeDto.Path = parent.Path + "," + nodeDto.NodeId; } + nodeDto.ParentId = entity.ParentId; } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepository.cs index 4eb4f108ce80..f1b9c77d0aa8 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepository.cs @@ -279,7 +279,7 @@ private IEnumerable BuildVariants(IEnumerable(v.Select(x => x.Id), 2000, GetVariantInfos); + var dtos = Database.FetchByGroups(v.Select(x => x.Id), Constants.Sql.MaxParameterCount, GetVariantInfos); // group by node id (each group contains all languages) var xdtos = dtos.GroupBy(x => x.NodeId).ToDictionary(x => x.Key, x => x); diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepositoryBase.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepositoryBase.cs index 25927213e542..79e6f732a2a9 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepositoryBase.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepositoryBase.cs @@ -3,6 +3,7 @@ using System.Linq; using Microsoft.Extensions.Logging; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Persistence; @@ -14,38 +15,37 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement { /// - /// Provides a base class to all based repositories. + /// Provides a base class to all based repositories. /// /// The type of the entity's unique identifier. /// The type of the entity managed by this repository. public abstract class EntityRepositoryBase : RepositoryBase, IReadWriteQueryRepository where TEntity : class, IEntity { + private static RepositoryCachePolicyOptions s_defaultOptions; private IRepositoryCachePolicy _cachePolicy; private IQuery _hasIdQuery; - private static RepositoryCachePolicyOptions s_defaultOptions; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - protected EntityRepositoryBase(IScopeAccessor scopeAccessor, AppCaches appCaches, ILogger> logger) - : base(scopeAccessor, appCaches) - { + protected EntityRepositoryBase(IScopeAccessor scopeAccessor, AppCaches appCaches, + ILogger> logger) + : base(scopeAccessor, appCaches) => Logger = logger ?? throw new ArgumentNullException(nameof(logger)); - } /// - /// Gets the logger + /// Gets the logger /// protected ILogger> Logger { get; } /// - /// Gets the isolated cache for the + /// Gets the isolated cache for the /// protected IAppPolicyCache GlobalIsolatedCache => AppCaches.IsolatedCaches.GetOrCreate(); /// - /// Gets the isolated cache. + /// Gets the isolated cache. /// /// Depends on the ambient scope cache mode. protected IAppPolicyCache IsolatedCache @@ -67,19 +67,20 @@ protected IAppPolicyCache IsolatedCache } /// - /// Gets the default + /// Gets the default /// protected virtual RepositoryCachePolicyOptions DefaultOptions => s_defaultOptions ?? (s_defaultOptions - = new RepositoryCachePolicyOptions(() => - { - // get count of all entities of current type (TEntity) to ensure cached result is correct - // create query once if it is needed (no need for locking here) - query is static! - IQuery query = _hasIdQuery ?? (_hasIdQuery = AmbientScope.SqlContext.Query().Where(x => x.Id != 0)); - return PerformCount(query); - })); + = new RepositoryCachePolicyOptions(() => + { + // get count of all entities of current type (TEntity) to ensure cached result is correct + // create query once if it is needed (no need for locking here) - query is static! + IQuery query = _hasIdQuery ?? + (_hasIdQuery = AmbientScope.SqlContext.Query().Where(x => x.Id != 0)); + return PerformCount(query); + })); /// - /// Gets the repository cache policy + /// Gets the repository cache policy /// protected IRepositoryCachePolicy CachePolicy { @@ -110,21 +111,9 @@ protected IRepositoryCachePolicy CachePolicy } /// - /// Get the entity id for the - /// - protected virtual TId GetEntityId(TEntity entity) - => (TId)(object)entity.Id; - - /// - /// Create the repository cache policy + /// Adds or Updates an entity of type TEntity /// - protected virtual IRepositoryCachePolicy CreateCachePolicy() - => new DefaultRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, DefaultOptions); - - /// - /// Adds or Updates an entity of type TEntity - /// - /// This method is backed by an cache + /// This method is backed by an cache public virtual void Save(TEntity entity) { if (entity.HasIdentity == false) @@ -138,64 +127,19 @@ public virtual void Save(TEntity entity) } /// - /// Deletes the passed in entity + /// Deletes the passed in entity /// public virtual void Delete(TEntity entity) => CachePolicy.Delete(entity, PersistDeletedItem); - protected abstract TEntity PerformGet(TId id); - - protected abstract IEnumerable PerformGetAll(params TId[] ids); - - protected abstract IEnumerable PerformGetByQuery(IQuery query); - - protected abstract void PersistNewItem(TEntity item); - - protected abstract void PersistUpdatedItem(TEntity item); - - // TODO: obsolete, use QueryType instead everywhere like GetBaseQuery(QueryType queryType); - protected abstract Sql GetBaseQuery(bool isCount); - - protected abstract string GetBaseWhereClause(); - - protected abstract IEnumerable GetDeleteClauses(); - - protected virtual bool PerformExists(TId id) - { - var sql = GetBaseQuery(true); - sql.Where(GetBaseWhereClause(), new { id = id }); - var count = Database.ExecuteScalar(sql); - return count == 1; - } - - protected virtual int PerformCount(IQuery query) - { - var sqlClause = GetBaseQuery(true); - var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate(); - - return Database.ExecuteScalar(sql); - } - - protected virtual void PersistDeletedItem(TEntity entity) - { - var deletes = GetDeleteClauses(); - foreach (var delete in deletes) - { - Database.Execute(delete, new { id = GetEntityId(entity) }); - } - - entity.DeleteDate = DateTime.Now; - } - /// - /// Gets an entity by the passed in Id utilizing the repository's cache policy + /// Gets an entity by the passed in Id utilizing the repository's cache policy /// public TEntity Get(TId id) => CachePolicy.Get(id, PerformGet, PerformGetAll); /// - /// Gets all entities of type TEntity or a list according to the passed in Ids + /// Gets all entities of type TEntity or a list according to the passed in Ids /// public IEnumerable GetMany(params TId[] ids) { @@ -209,38 +153,94 @@ public IEnumerable GetMany(params TId[] ids) // can't query more than 2000 ids at a time... but if someone is really querying 2000+ entities, // the additional overhead of fetching them in groups is minimal compared to the lookup time of each group - const int maxParams = 2000; - if (ids.Length <= maxParams) + if (ids.Length <= Constants.Sql.MaxParameterCount) { return CachePolicy.GetAll(ids, PerformGetAll); } var entities = new List(); - foreach (var groupOfIds in ids.InGroupsOf(maxParams)) + foreach (IEnumerable group in ids.InGroupsOf(Constants.Sql.MaxParameterCount)) { - entities.AddRange(CachePolicy.GetAll(groupOfIds.ToArray(), PerformGetAll)); + entities.AddRange(CachePolicy.GetAll(group.ToArray(), PerformGetAll)); } return entities; } /// - /// Gets a list of entities by the passed in query + /// Gets a list of entities by the passed in query /// public IEnumerable Get(IQuery query) => PerformGetByQuery(query) .WhereNotNull(); // ensure we don't include any null refs in the returned collection! /// - /// Returns a boolean indicating whether an entity with the passed Id exists + /// Returns a boolean indicating whether an entity with the passed Id exists /// public bool Exists(TId id) => CachePolicy.Exists(id, PerformExists, PerformGetAll); /// - /// Returns an integer with the count of entities found with the passed in query + /// Returns an integer with the count of entities found with the passed in query /// public int Count(IQuery query) => PerformCount(query); + + /// + /// Get the entity id for the + /// + protected virtual TId GetEntityId(TEntity entity) + => (TId)(object)entity.Id; + + /// + /// Create the repository cache policy + /// + protected virtual IRepositoryCachePolicy CreateCachePolicy() + => new DefaultRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, DefaultOptions); + + protected abstract TEntity PerformGet(TId id); + + protected abstract IEnumerable PerformGetAll(params TId[] ids); + + protected abstract IEnumerable PerformGetByQuery(IQuery query); + + protected abstract void PersistNewItem(TEntity item); + + protected abstract void PersistUpdatedItem(TEntity item); + + // TODO: obsolete, use QueryType instead everywhere like GetBaseQuery(QueryType queryType); + protected abstract Sql GetBaseQuery(bool isCount); + + protected abstract string GetBaseWhereClause(); + + protected abstract IEnumerable GetDeleteClauses(); + + protected virtual bool PerformExists(TId id) + { + Sql sql = GetBaseQuery(true); + sql.Where(GetBaseWhereClause(), new { id }); + var count = Database.ExecuteScalar(sql); + return count == 1; + } + + protected virtual int PerformCount(IQuery query) + { + Sql sqlClause = GetBaseQuery(true); + var translator = new SqlTranslator(sqlClause, query); + Sql sql = translator.Translate(); + + return Database.ExecuteScalar(sql); + } + + protected virtual void PersistDeletedItem(TEntity entity) + { + IEnumerable deletes = GetDeleteClauses(); + foreach (var delete in deletes) + { + Database.Execute(delete, new { id = GetEntityId(entity) }); + } + + entity.DeleteDate = DateTime.Now; + } } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs index 22083eae30f3..4031971ddcc7 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Events; @@ -26,17 +27,17 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement { /// - /// Represents a repository for doing CRUD operations for + /// Represents a repository for doing CRUD operations for /// public class MemberRepository : ContentRepositoryBase, IMemberRepository { - private readonly MemberPasswordConfigurationSettings _passwordConfiguration; - private readonly IMemberTypeRepository _memberTypeRepository; - private readonly ITagRepository _tagRepository; - private readonly IPasswordHasher _passwordHasher; private readonly IJsonSerializer _jsonSerializer; - private readonly IMemberGroupRepository _memberGroupRepository; private readonly IRepositoryCachePolicy _memberByUsernameCachePolicy; + private readonly IMemberGroupRepository _memberGroupRepository; + private readonly IMemberTypeRepository _memberTypeRepository; + private readonly MemberPasswordConfigurationSettings _passwordConfiguration; + private readonly IPasswordHasher _passwordHasher; + private readonly ITagRepository _tagRepository; private bool _passwordConfigInitialized; private string _passwordConfigJson; @@ -57,19 +58,22 @@ public MemberRepository( IJsonSerializer serializer, IEventAggregator eventAggregator, IOptions passwordConfiguration) - : base(scopeAccessor, cache, logger, languageRepository, relationRepository, relationTypeRepository, propertyEditors, dataValueReferenceFactories, dataTypeService, eventAggregator) + : base(scopeAccessor, cache, logger, languageRepository, relationRepository, relationTypeRepository, + propertyEditors, dataValueReferenceFactories, dataTypeService, eventAggregator) { - _memberTypeRepository = memberTypeRepository ?? throw new ArgumentNullException(nameof(memberTypeRepository)); + _memberTypeRepository = + memberTypeRepository ?? throw new ArgumentNullException(nameof(memberTypeRepository)); _tagRepository = tagRepository ?? throw new ArgumentNullException(nameof(tagRepository)); _passwordHasher = passwordHasher; _jsonSerializer = serializer; _memberGroupRepository = memberGroupRepository; _passwordConfiguration = passwordConfiguration.Value; - _memberByUsernameCachePolicy = new DefaultRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, DefaultOptions); + _memberByUsernameCachePolicy = + new DefaultRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, DefaultOptions); } /// - /// Returns a serialized dictionary of the password configuration that is stored against the member in the database + /// Returns a serialized dictionary of the password configuration that is stored against the member in the database /// private string DefaultPasswordConfigJson { @@ -95,17 +99,341 @@ private string DefaultPasswordConfigJson public override int RecycleBinId => throw new NotSupportedException(); + public IEnumerable FindMembersInRole(string roleName, string usernameToMatch, + StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith) + { + //get the group id + IQuery grpQry = Query().Where(group => group.Name.Equals(roleName)); + IMemberGroup memberGroup = _memberGroupRepository.Get(grpQry).FirstOrDefault(); + if (memberGroup == null) + { + return Enumerable.Empty(); + } + + // get the members by username + IQuery query = Query(); + switch (matchType) + { + case StringPropertyMatchType.Exact: + query.Where(member => member.Username.Equals(usernameToMatch)); + break; + case StringPropertyMatchType.Contains: + query.Where(member => member.Username.Contains(usernameToMatch)); + break; + case StringPropertyMatchType.StartsWith: + query.Where(member => member.Username.StartsWith(usernameToMatch)); + break; + case StringPropertyMatchType.EndsWith: + query.Where(member => member.Username.EndsWith(usernameToMatch)); + break; + case StringPropertyMatchType.Wildcard: + query.Where(member => member.Username.SqlWildcard(usernameToMatch, TextColumnType.NVarchar)); + break; + default: + throw new ArgumentOutOfRangeException(nameof(matchType)); + } + + IMember[] matchedMembers = Get(query).ToArray(); + + var membersInGroup = new List(); + + //then we need to filter the matched members that are in the role + foreach (IEnumerable group in matchedMembers.Select(x => x.Id) + .InGroupsOf(Constants.Sql.MaxParameterCount)) + { + Sql sql = Sql().SelectAll().From() + .Where(dto => dto.MemberGroup == memberGroup.Id) + .WhereIn(dto => dto.Member, group); + + var memberIdsInGroup = Database.Fetch(sql) + .Select(x => x.Member).ToArray(); + + membersInGroup.AddRange(matchedMembers.Where(x => memberIdsInGroup.Contains(x.Id))); + } + + return membersInGroup; + } + + /// + /// Get all members in a specific group + /// + /// + /// + public IEnumerable GetByMemberGroup(string groupName) + { + IQuery grpQry = Query().Where(group => group.Name.Equals(groupName)); + IMemberGroup memberGroup = _memberGroupRepository.Get(grpQry).FirstOrDefault(); + if (memberGroup == null) + { + return Enumerable.Empty(); + } + + Sql subQuery = Sql().Select("Member").From() + .Where(dto => dto.MemberGroup == memberGroup.Id); + + Sql sql = GetBaseQuery(false) + // TODO: An inner join would be better, though I've read that the query optimizer will always turn a + // subquery with an IN clause into an inner join anyways. + .Append("WHERE umbracoNode.id IN (" + subQuery.SQL + ")", subQuery.Arguments) + .OrderByDescending(x => x.VersionDate) + .OrderBy(x => x.SortOrder); + + return MapDtosToContent(Database.Fetch(sql)); + } + + public bool Exists(string username) + { + Sql sql = Sql() + .SelectCount() + .From() + .Where(x => x.LoginName == username); + + return Database.ExecuteScalar(sql) > 0; + } + + public int GetCountByQuery(IQuery query) + { + Sql sqlWithProps = GetNodeIdQueryWithPropertyData(); + var translator = new SqlTranslator(sqlWithProps, query); + Sql sql = translator.Translate(); + + //get the COUNT base query + Sql fullSql = GetBaseQuery(true) + .Append(new Sql("WHERE umbracoNode.id IN (" + sql.SQL + ")", sql.Arguments)); + + return Database.ExecuteScalar(fullSql); + } + + /// + public void SetLastLogin(string username, DateTime date) + { + // Important - these queries are designed to execute without an exclusive WriteLock taken in our distributed lock + // table. However due to the data that we are updating which relies on version data we cannot update this data + // without taking some locks, otherwise we'll end up with strange situations because when a member is updated, that operation + // deletes and re-inserts all property data. So if there are concurrent transactions, one deleting and re-inserting and another trying + // to update there can be problems. This is only an issue for cmsPropertyData, not umbracoContentVersion because that table just + // maintains a single row and it isn't deleted/re-inserted. + // So the important part here is the ForUpdate() call on the select to fetch the property data to update. + + // Update the cms property value for the member + + SqlTemplate sqlSelectTemplateProperty = SqlContext.Templates.Get( + "Umbraco.Core.MemberRepository.SetLastLogin1", s => s + .Select(x => x.Id) + .From() + .InnerJoin() + .On((l, r) => l.Id == r.PropertyTypeId) + .InnerJoin() + .On((l, r) => l.Id == r.VersionId) + .InnerJoin().On((l, r) => l.NodeId == r.NodeId) + .InnerJoin().On((l, r) => l.NodeId == r.NodeId) + .Where(x => x.NodeObjectType == SqlTemplate.Arg("nodeObjectType")) + .Where(x => x.Alias == SqlTemplate.Arg("propertyTypeAlias")) + .Where(x => x.LoginName == SqlTemplate.Arg("username")) + .ForUpdate()); + Sql sqlSelectProperty = sqlSelectTemplateProperty.Sql(Constants.ObjectTypes.Member, + Constants.Conventions.Member.LastLoginDate, username); + + Sql update = Sql() + .Update(u => u + .Set(x => x.DateValue, date)) + .WhereIn(x => x.Id, sqlSelectProperty); + + Database.Execute(update); + + // Update the umbracoContentVersion value for the member + + SqlTemplate sqlSelectTemplateVersion = SqlContext.Templates.Get( + "Umbraco.Core.MemberRepository.SetLastLogin2", s => s + .Select(x => x.Id) + .From() + .InnerJoin().On((l, r) => l.NodeId == r.NodeId) + .InnerJoin().On((l, r) => l.NodeId == r.NodeId) + .Where(x => x.NodeObjectType == SqlTemplate.Arg("nodeObjectType")) + .Where(x => x.LoginName == SqlTemplate.Arg("username"))); + Sql sqlSelectVersion = sqlSelectTemplateVersion.Sql(Constants.ObjectTypes.Member, username); + + Database.Execute(Sql() + .Update(u => u + .Set(x => x.VersionDate, date)) + .WhereIn(x => x.Id, sqlSelectVersion)); + } + + /// + /// Gets paged member results. + /// + public override IEnumerable GetPage(IQuery query, + long pageIndex, int pageSize, out long totalRecords, + IQuery filter, + Ordering ordering) + { + Sql filterSql = null; + + if (filter != null) + { + filterSql = Sql(); + foreach (Tuple clause in filter.GetWhereClauses()) + { + filterSql = filterSql.Append($"AND ({clause.Item1})", clause.Item2); + } + } + + return GetPage(query, pageIndex, pageSize, out totalRecords, + x => MapDtosToContent(x), + filterSql, + ordering); + } + + public IMember GetByUsername(string username) => + _memberByUsernameCachePolicy.Get(username, PerformGetByUsername, PerformGetAllByUsername); + + public int[] GetMemberIds(string[] usernames) + { + Guid memberObjectType = Constants.ObjectTypes.Member; + + Sql memberSql = Sql() + .Select("umbracoNode.id") + .From() + .InnerJoin() + .On(dto => dto.NodeId, dto => dto.NodeId) + .Where(x => x.NodeObjectType == memberObjectType) + .Where("cmsMember.LoginName in (@usernames)", new + { + /*usernames =*/ + usernames + }); + return Database.Fetch(memberSql).ToArray(); + } + + protected override string ApplySystemOrdering(ref Sql sql, Ordering ordering) + { + if (ordering.OrderBy.InvariantEquals("email")) + { + return SqlSyntax.GetFieldName(x => x.Email); + } + + if (ordering.OrderBy.InvariantEquals("loginName")) + { + return SqlSyntax.GetFieldName(x => x.LoginName); + } + + if (ordering.OrderBy.InvariantEquals("userName")) + { + return SqlSyntax.GetFieldName(x => x.LoginName); + } + + if (ordering.OrderBy.InvariantEquals("updateDate")) + { + return SqlSyntax.GetFieldName(x => x.VersionDate); + } + + if (ordering.OrderBy.InvariantEquals("createDate")) + { + return SqlSyntax.GetFieldName(x => x.CreateDate); + } + + if (ordering.OrderBy.InvariantEquals("contentTypeAlias")) + { + return SqlSyntax.GetFieldName(x => x.Alias); + } + + return base.ApplySystemOrdering(ref sql, ordering); + } + + private IEnumerable MapDtosToContent(List dtos, bool withCache = false) + { + var temps = new List>(); + var contentTypes = new Dictionary(); + var content = new Member[dtos.Count]; + + for (var i = 0; i < dtos.Count; i++) + { + MemberDto dto = dtos[i]; + + if (withCache) + { + // if the cache contains the (proper version of the) item, use it + IMember cached = + IsolatedCache.GetCacheItem(RepositoryCacheKeys.GetKey(dto.NodeId)); + if (cached != null && cached.VersionId == dto.ContentVersionDto.Id) + { + content[i] = (Member)cached; + continue; + } + } + + // else, need to build it + + // get the content type - the repository is full cache *but* still deep-clones + // whatever comes out of it, so use our own local index here to avoid this + var contentTypeId = dto.ContentDto.ContentTypeId; + if (contentTypes.TryGetValue(contentTypeId, out IMemberType contentType) == false) + { + contentTypes[contentTypeId] = contentType = _memberTypeRepository.Get(contentTypeId); + } + + Member c = content[i] = ContentBaseFactory.BuildEntity(dto, contentType); + + // need properties + var versionId = dto.ContentVersionDto.Id; + temps.Add(new TempContent(dto.NodeId, versionId, 0, contentType, c)); + } + + // load all properties for all documents from database in 1 query - indexed by version id + IDictionary properties = GetPropertyCollections(temps); + + // assign properties + foreach (TempContent temp in temps) + { + temp.Content.Properties = properties[temp.VersionId]; + + // reset dirty initial properties (U4-1946) + temp.Content.ResetDirtyProperties(false); + } + + return content; + } + + private IMember MapDtoToContent(MemberDto dto) + { + IMemberType memberType = _memberTypeRepository.Get(dto.ContentDto.ContentTypeId); + Member member = ContentBaseFactory.BuildEntity(dto, memberType); + + // get properties - indexed by version id + var versionId = dto.ContentVersionDto.Id; + var temp = new TempContent(dto.ContentDto.NodeId, versionId, 0, memberType); + IDictionary properties = + GetPropertyCollections(new List> { temp }); + member.Properties = properties[versionId]; + + // reset dirty initial properties (U4-1946) + member.ResetDirtyProperties(false); + return member; + } + + private IMember PerformGetByUsername(string username) + { + IQuery query = Query().Where(x => x.Username.Equals(username)); + return PerformGetByQuery(query).FirstOrDefault(); + } + + private IEnumerable PerformGetAllByUsername(params string[] usernames) + { + IQuery query = Query().WhereIn(x => x.Username, usernames); + return PerformGetByQuery(query); + } + #region Repository Base - protected override Guid NodeObjectTypeId => Cms.Core.Constants.ObjectTypes.Member; + protected override Guid NodeObjectTypeId => Constants.ObjectTypes.Member; protected override IMember PerformGet(int id) { - var sql = GetBaseQuery(QueryType.Single) + Sql sql = GetBaseQuery(QueryType.Single) .Where(x => x.NodeId == id) .SelectTop(1); - var dto = Database.Fetch(sql).FirstOrDefault(); + MemberDto dto = Database.Fetch(sql).FirstOrDefault(); return dto == null ? null : MapDtoToContent(dto); @@ -113,29 +441,31 @@ protected override IMember PerformGet(int id) protected override IEnumerable PerformGetAll(params int[] ids) { - var sql = GetBaseQuery(QueryType.Many); + Sql sql = GetBaseQuery(QueryType.Many); if (ids.Any()) + { sql.WhereIn(x => x.NodeId, ids); + } return MapDtosToContent(Database.Fetch(sql)); } protected override IEnumerable PerformGetByQuery(IQuery query) { - var baseQuery = GetBaseQuery(false); + Sql baseQuery = GetBaseQuery(false); // TODO: why is this different from content/media?! // check if the query is based on properties or not - var wheres = query.GetWhereClauses(); + IEnumerable> wheres = query.GetWhereClauses(); //this is a pretty rudimentary check but will work, we just need to know if this query requires property // level queries if (wheres.Any(x => x.Item1.Contains("cmsPropertyType"))) { - var sqlWithProps = GetNodeIdQueryWithPropertyData(); + Sql sqlWithProps = GetNodeIdQueryWithPropertyData(); var translator = new SqlTranslator(sqlWithProps, query); - var sql = translator.Translate(); + Sql sql = translator.Translate(); baseQuery.Append("WHERE umbracoNode.id IN (" + sql.SQL + ")", sql.Arguments) .OrderBy(x => x.SortOrder); @@ -145,22 +475,18 @@ protected override IEnumerable PerformGetByQuery(IQuery query) else { var translator = new SqlTranslator(baseQuery, query); - var sql = translator.Translate() + Sql sql = translator.Translate() .OrderBy(x => x.SortOrder); return MapDtosToContent(Database.Fetch(sql)); } - } - protected override Sql GetBaseQuery(QueryType queryType) - { - return GetBaseQuery(queryType, true); - } + protected override Sql GetBaseQuery(QueryType queryType) => GetBaseQuery(queryType, true); protected virtual Sql GetBaseQuery(QueryType queryType, bool current) { - var sql = SqlContext.Sql(); + Sql sql = SqlContext.Sql(); switch (queryType) // TODO: pretend we still need these queries for now { @@ -187,52 +513,52 @@ protected virtual Sql GetBaseQuery(QueryType queryType, bool curren .From() .InnerJoin().On(left => left.NodeId, right => right.NodeId) .InnerJoin().On(left => left.NodeId, right => right.NodeId) - .InnerJoin().On(left => left.NodeId, right => right.NodeId) + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) // joining the type so we can do a query against the member type - not sure if this adds much overhead or not? // the execution plan says it doesn't so we'll go with that and in that case, it might be worth joining the content // types by default on the document and media repos so we can query by content type there too. - .InnerJoin().On(left => left.ContentTypeId, right => right.NodeId); + .InnerJoin() + .On(left => left.ContentTypeId, right => right.NodeId); sql.Where(x => x.NodeObjectType == NodeObjectTypeId); if (current) + { sql.Where(x => x.Current); // always get the current version + } return sql; } // TODO: move that one up to Versionable! or better: kill it! - protected override Sql GetBaseQuery(bool isCount) - { - return GetBaseQuery(isCount ? QueryType.Count : QueryType.Single); - } + protected override Sql GetBaseQuery(bool isCount) => + GetBaseQuery(isCount ? QueryType.Count : QueryType.Single); protected override string GetBaseWhereClause() // TODO: can we kill / refactor this? - { - return "umbracoNode.id = @id"; - } + => + "umbracoNode.id = @id"; // TODO: document/understand that one - protected Sql GetNodeIdQueryWithPropertyData() - { - return Sql() + protected Sql GetNodeIdQueryWithPropertyData() => + Sql() .Select("DISTINCT(umbracoNode.id)") .From() .InnerJoin().On((left, right) => left.NodeId == right.NodeId) - .InnerJoin().On((left, right) => left.ContentTypeId == right.NodeId) - .InnerJoin().On((left, right) => left.NodeId == right.NodeId) + .InnerJoin() + .On((left, right) => left.ContentTypeId == right.NodeId) + .InnerJoin() + .On((left, right) => left.NodeId == right.NodeId) .InnerJoin().On((left, right) => left.NodeId == right.NodeId) - - .LeftJoin().On(left => left.ContentTypeId, right => right.ContentTypeId) - .LeftJoin().On(left => left.DataTypeId, right => right.NodeId) - + .LeftJoin() + .On(left => left.ContentTypeId, right => right.ContentTypeId) + .LeftJoin() + .On(left => left.DataTypeId, right => right.NodeId) .LeftJoin().On(x => x .Where((left, right) => left.PropertyTypeId == right.Id) .Where((left, right) => left.VersionId == right.Id)) - .Where(x => x.NodeObjectType == NodeObjectTypeId); - } protected override IEnumerable GetDeleteClauses() { @@ -243,11 +569,13 @@ protected override IEnumerable GetDeleteClauses() "DELETE FROM umbracoRelation WHERE parentId = @id", "DELETE FROM umbracoRelation WHERE childId = @id", "DELETE FROM cmsTagRelationship WHERE nodeId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.PropertyData + " WHERE versionId IN (SELECT id FROM " + Cms.Core.Constants.DatabaseSchema.Tables.ContentVersion + " WHERE nodeId = @id)", + "DELETE FROM " + Constants.DatabaseSchema.Tables.PropertyData + + " WHERE versionId IN (SELECT id FROM " + Constants.DatabaseSchema.Tables.ContentVersion + + " WHERE nodeId = @id)", "DELETE FROM cmsMember2MemberGroup WHERE Member = @id", "DELETE FROM cmsMember WHERE nodeId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.ContentVersion + " WHERE nodeId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.Content + " WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.ContentVersion + " WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.Content + " WHERE nodeId = @id", "DELETE FROM umbracoNode WHERE id = @id" }; return list; @@ -259,7 +587,7 @@ protected override IEnumerable GetDeleteClauses() public override IEnumerable GetAllVersions(int nodeId) { - var sql = GetBaseQuery(QueryType.Many, false) + Sql sql = GetBaseQuery(QueryType.Many, false) .Where(x => x.NodeId == nodeId) .OrderByDescending(x => x.Current) .AndByDescending(x => x.VersionDate); @@ -269,10 +597,10 @@ public override IEnumerable GetAllVersions(int nodeId) public override IMember GetVersion(int versionId) { - var sql = GetBaseQuery(QueryType.Single) + Sql sql = GetBaseQuery(QueryType.Single) .Where(x => x.Id == versionId); - var dto = Database.Fetch(sql).FirstOrDefault(); + MemberDto dto = Database.Fetch(sql).FirstOrDefault(); return dto == null ? null : MapDtoToContent(dto); } @@ -346,13 +674,13 @@ protected override void PersistNewItem(IMember entity) entity.Level = level; // persist the content dto - var contentDto = memberDto.ContentDto; + ContentDto contentDto = memberDto.ContentDto; contentDto.NodeId = nodeDto.NodeId; Database.Insert(contentDto); // persist the content version dto // assumes a new version id and version date (modified date) has been set - var contentVersionDto = memberDto.ContentVersionDto; + ContentVersionDto contentVersionDto = memberDto.ContentVersionDto; contentVersionDto.NodeId = nodeDto.NodeId; contentVersionDto.Current = true; Database.Insert(contentVersionDto); @@ -365,8 +693,8 @@ protected override void PersistNewItem(IMember entity) // this will hash the guid with a salt so should be nicely random if (entity.RawPasswordValue.IsNullOrWhiteSpace()) { - - memberDto.Password = Cms.Core.Constants.Security.EmptyPasswordPrefix + _passwordHasher.HashPassword(Guid.NewGuid().ToString("N")); + memberDto.Password = Constants.Security.EmptyPasswordPrefix + + _passwordHasher.HashPassword(Guid.NewGuid().ToString("N")); entity.RawPasswordValue = memberDto.Password; } @@ -496,301 +824,5 @@ protected override void PersistUpdatedItem(IMember entity) } #endregion - - public IEnumerable FindMembersInRole(string roleName, string usernameToMatch, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith) - { - //get the group id - var grpQry = Query().Where(group => group.Name.Equals(roleName)); - var memberGroup = _memberGroupRepository.Get(grpQry).FirstOrDefault(); - if (memberGroup == null) - return Enumerable.Empty(); - - // get the members by username - var query = Query(); - switch (matchType) - { - case StringPropertyMatchType.Exact: - query.Where(member => member.Username.Equals(usernameToMatch)); - break; - case StringPropertyMatchType.Contains: - query.Where(member => member.Username.Contains(usernameToMatch)); - break; - case StringPropertyMatchType.StartsWith: - query.Where(member => member.Username.StartsWith(usernameToMatch)); - break; - case StringPropertyMatchType.EndsWith: - query.Where(member => member.Username.EndsWith(usernameToMatch)); - break; - case StringPropertyMatchType.Wildcard: - query.Where(member => member.Username.SqlWildcard(usernameToMatch, TextColumnType.NVarchar)); - break; - default: - throw new ArgumentOutOfRangeException(nameof(matchType)); - } - var matchedMembers = Get(query).ToArray(); - - var membersInGroup = new List(); - //then we need to filter the matched members that are in the role - //since the max sql params are 2100 on sql server, we'll reduce that to be safe for potentially other servers and run the queries in batches - var inGroups = matchedMembers.InGroupsOf(1000); - foreach (var batch in inGroups) - { - var memberIdBatch = batch.Select(x => x.Id); - - var sql = Sql().SelectAll().From() - .Where(dto => dto.MemberGroup == memberGroup.Id) - .WhereIn(dto => dto.Member, memberIdBatch); - - var memberIdsInGroup = Database.Fetch(sql) - .Select(x => x.Member).ToArray(); - - membersInGroup.AddRange(matchedMembers.Where(x => memberIdsInGroup.Contains(x.Id))); - } - - return membersInGroup; - - } - - /// - /// Get all members in a specific group - /// - /// - /// - public IEnumerable GetByMemberGroup(string groupName) - { - var grpQry = Query().Where(group => group.Name.Equals(groupName)); - var memberGroup = _memberGroupRepository.Get(grpQry).FirstOrDefault(); - if (memberGroup == null) - return Enumerable.Empty(); - - var subQuery = Sql().Select("Member").From().Where(dto => dto.MemberGroup == memberGroup.Id); - - var sql = GetBaseQuery(false) - // TODO: An inner join would be better, though I've read that the query optimizer will always turn a - // subquery with an IN clause into an inner join anyways. - .Append("WHERE umbracoNode.id IN (" + subQuery.SQL + ")", subQuery.Arguments) - .OrderByDescending(x => x.VersionDate) - .OrderBy(x => x.SortOrder); - - return MapDtosToContent(Database.Fetch(sql)); - - } - - public bool Exists(string username) - { - var sql = Sql() - .SelectCount() - .From() - .Where(x => x.LoginName == username); - - return Database.ExecuteScalar(sql) > 0; - } - - public int GetCountByQuery(IQuery query) - { - var sqlWithProps = GetNodeIdQueryWithPropertyData(); - var translator = new SqlTranslator(sqlWithProps, query); - var sql = translator.Translate(); - - //get the COUNT base query - var fullSql = GetBaseQuery(true) - .Append(new Sql("WHERE umbracoNode.id IN (" + sql.SQL + ")", sql.Arguments)); - - return Database.ExecuteScalar(fullSql); - } - - /// - public void SetLastLogin(string username, DateTime date) - { - // Important - these queries are designed to execute without an exclusive WriteLock taken in our distributed lock - // table. However due to the data that we are updating which relies on version data we cannot update this data - // without taking some locks, otherwise we'll end up with strange situations because when a member is updated, that operation - // deletes and re-inserts all property data. So if there are concurrent transactions, one deleting and re-inserting and another trying - // to update there can be problems. This is only an issue for cmsPropertyData, not umbracoContentVersion because that table just - // maintains a single row and it isn't deleted/re-inserted. - // So the important part here is the ForUpdate() call on the select to fetch the property data to update. - - // Update the cms property value for the member - - var sqlSelectTemplateProperty = SqlContext.Templates.Get("Umbraco.Core.MemberRepository.SetLastLogin1", s => s - .Select(x => x.Id) - .From() - .InnerJoin().On((l, r) => l.Id == r.PropertyTypeId) - .InnerJoin().On((l, r) => l.Id == r.VersionId) - .InnerJoin().On((l, r) => l.NodeId == r.NodeId) - .InnerJoin().On((l, r) => l.NodeId == r.NodeId) - .Where(x => x.NodeObjectType == SqlTemplate.Arg("nodeObjectType")) - .Where(x => x.Alias == SqlTemplate.Arg("propertyTypeAlias")) - .Where(x => x.LoginName == SqlTemplate.Arg("username")) - .ForUpdate()); - var sqlSelectProperty = sqlSelectTemplateProperty.Sql(Cms.Core.Constants.ObjectTypes.Member, Cms.Core.Constants.Conventions.Member.LastLoginDate, username); - - var update = Sql() - .Update(u => u - .Set(x => x.DateValue, date)) - .WhereIn(x => x.Id, sqlSelectProperty); - - Database.Execute(update); - - // Update the umbracoContentVersion value for the member - - var sqlSelectTemplateVersion = SqlContext.Templates.Get("Umbraco.Core.MemberRepository.SetLastLogin2", s => s - .Select(x => x.Id) - .From() - .InnerJoin().On((l, r) => l.NodeId == r.NodeId) - .InnerJoin().On((l, r) => l.NodeId == r.NodeId) - .Where(x => x.NodeObjectType == SqlTemplate.Arg("nodeObjectType")) - .Where(x => x.LoginName == SqlTemplate.Arg("username"))); - var sqlSelectVersion = sqlSelectTemplateVersion.Sql(Cms.Core.Constants.ObjectTypes.Member, username); - - Database.Execute(Sql() - .Update(u => u - .Set(x => x.VersionDate, date)) - .WhereIn(x => x.Id, sqlSelectVersion)); - } - - /// - /// Gets paged member results. - /// - public override IEnumerable GetPage(IQuery query, - long pageIndex, int pageSize, out long totalRecords, - IQuery filter, - Ordering ordering) - { - Sql filterSql = null; - - if (filter != null) - { - filterSql = Sql(); - foreach (var clause in filter.GetWhereClauses()) - filterSql = filterSql.Append($"AND ({clause.Item1})", clause.Item2); - } - - return GetPage(query, pageIndex, pageSize, out totalRecords, - x => MapDtosToContent(x), - filterSql, - ordering); - } - - protected override string ApplySystemOrdering(ref Sql sql, Ordering ordering) - { - if (ordering.OrderBy.InvariantEquals("email")) - return SqlSyntax.GetFieldName(x => x.Email); - - if (ordering.OrderBy.InvariantEquals("loginName")) - return SqlSyntax.GetFieldName(x => x.LoginName); - - if (ordering.OrderBy.InvariantEquals("userName")) - return SqlSyntax.GetFieldName(x => x.LoginName); - - if (ordering.OrderBy.InvariantEquals("updateDate")) - return SqlSyntax.GetFieldName(x => x.VersionDate); - - if (ordering.OrderBy.InvariantEquals("createDate")) - return SqlSyntax.GetFieldName(x => x.CreateDate); - - if (ordering.OrderBy.InvariantEquals("contentTypeAlias")) - return SqlSyntax.GetFieldName(x => x.Alias); - - return base.ApplySystemOrdering(ref sql, ordering); - } - - private IEnumerable MapDtosToContent(List dtos, bool withCache = false) - { - var temps = new List>(); - var contentTypes = new Dictionary(); - var content = new Member[dtos.Count]; - - for (var i = 0; i < dtos.Count; i++) - { - var dto = dtos[i]; - - if (withCache) - { - // if the cache contains the (proper version of the) item, use it - var cached = IsolatedCache.GetCacheItem(RepositoryCacheKeys.GetKey(dto.NodeId)); - if (cached != null && cached.VersionId == dto.ContentVersionDto.Id) - { - content[i] = (Member)cached; - continue; - } - } - - // else, need to build it - - // get the content type - the repository is full cache *but* still deep-clones - // whatever comes out of it, so use our own local index here to avoid this - var contentTypeId = dto.ContentDto.ContentTypeId; - if (contentTypes.TryGetValue(contentTypeId, out var contentType) == false) - contentTypes[contentTypeId] = contentType = _memberTypeRepository.Get(contentTypeId); - - var c = content[i] = ContentBaseFactory.BuildEntity(dto, contentType); - - // need properties - var versionId = dto.ContentVersionDto.Id; - temps.Add(new TempContent(dto.NodeId, versionId, 0, contentType, c)); - } - - // load all properties for all documents from database in 1 query - indexed by version id - var properties = GetPropertyCollections(temps); - - // assign properties - foreach (var temp in temps) - { - temp.Content.Properties = properties[temp.VersionId]; - - // reset dirty initial properties (U4-1946) - temp.Content.ResetDirtyProperties(false); - } - - return content; - } - - private IMember MapDtoToContent(MemberDto dto) - { - IMemberType memberType = _memberTypeRepository.Get(dto.ContentDto.ContentTypeId); - Member member = ContentBaseFactory.BuildEntity(dto, memberType); - - // get properties - indexed by version id - var versionId = dto.ContentVersionDto.Id; - var temp = new TempContent(dto.ContentDto.NodeId, versionId, 0, memberType); - var properties = GetPropertyCollections(new List> { temp }); - member.Properties = properties[versionId]; - - // reset dirty initial properties (U4-1946) - member.ResetDirtyProperties(false); - return member; - } - - public IMember GetByUsername(string username) - { - return _memberByUsernameCachePolicy.Get(username, PerformGetByUsername, PerformGetAllByUsername); - } - - public int[] GetMemberIds(string[] usernames) - { - var memberObjectType = Cms.Core.Constants.ObjectTypes.Member; - - var memberSql = Sql() - .Select("umbracoNode.id") - .From() - .InnerJoin() - .On(dto => dto.NodeId, dto => dto.NodeId) - .Where(x => x.NodeObjectType == memberObjectType) - .Where("cmsMember.LoginName in (@usernames)", new { /*usernames =*/ usernames }); - return Database.Fetch(memberSql).ToArray(); - } - - private IMember PerformGetByUsername(string username) - { - var query = Query().Where(x => x.Username.Equals(username)); - return PerformGetByQuery(query).FirstOrDefault(); - } - - private IEnumerable PerformGetAllByUsername(params string[] usernames) - { - var query = Query().WhereIn(x => x.Username, usernames); - return PerformGetByQuery(query); - } } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PermissionRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PermissionRepository.cs index 582120992b10..a1cfee69a905 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PermissionRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PermissionRepository.cs @@ -4,6 +4,7 @@ using System.Linq; using Microsoft.Extensions.Logging; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Models.Membership; @@ -15,67 +16,72 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement { /// - /// A (sub) repository that exposes functionality to modify assigned permissions to a node + /// A (sub) repository that exposes functionality to modify assigned permissions to a node /// /// /// - /// This repo implements the base class so that permissions can be queued to be persisted - /// like the normal repository pattern but the standard repository Get commands don't apply and will throw + /// This repo implements the base class so that permissions can be + /// queued to be persisted + /// like the normal repository pattern but the standard repository Get commands don't apply and will throw + /// /// internal class PermissionRepository : EntityRepositoryBase where TEntity : class, IEntity { - public PermissionRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger> logger) + public PermissionRepository(IScopeAccessor scopeAccessor, AppCaches cache, + ILogger> logger) : base(scopeAccessor, cache, logger) - { } + { + } /// - /// Returns explicitly defined permissions for a user group for any number of nodes + /// Returns explicitly defined permissions for a user group for any number of nodes /// /// - /// The group ids to lookup permissions for + /// The group ids to lookup permissions for /// /// /// /// - /// This method will not support passing in more than 2000 group Ids + /// This method will not support passing in more than 2000 group IDs when also passing in entity IDs. /// public EntityPermissionCollection GetPermissionsForEntities(int[] groupIds, params int[] entityIds) { var result = new EntityPermissionCollection(); - foreach (var groupOfGroupIds in groupIds.InGroupsOf(2000)) + if (entityIds.Length == 0) { - //copy local - var localIds = groupOfGroupIds.ToArray(); - - if (entityIds.Length == 0) + foreach (IEnumerable group in groupIds.InGroupsOf(Constants.Sql.MaxParameterCount)) { - var sql = Sql() + Sql sql = Sql() .SelectAll() .From() - .Where(dto => localIds.Contains(dto.UserGroupId)); - var permissions = AmbientScope.Database.Fetch(sql); - foreach (var permission in ConvertToPermissionList(permissions)) + .Where(dto => group.Contains(dto.UserGroupId)); + + List permissions = + AmbientScope.Database.Fetch(sql); + foreach (EntityPermission permission in ConvertToPermissionList(permissions)) { result.Add(permission); } } - else + } + else + { + foreach (IEnumerable group in entityIds.InGroupsOf(Constants.Sql.MaxParameterCount - + groupIds.Length)) { - //iterate in groups of 2000 since we don't want to exceed the max SQL param count - foreach (var groupOfEntityIds in entityIds.InGroupsOf(2000)) + Sql sql = Sql() + .SelectAll() + .From() + .Where(dto => + groupIds.Contains(dto.UserGroupId) && group.Contains(dto.NodeId)); + + List permissions = + AmbientScope.Database.Fetch(sql); + foreach (EntityPermission permission in ConvertToPermissionList(permissions)) { - var ids = groupOfEntityIds; - var sql = Sql() - .SelectAll() - .From() - .Where(dto => localIds.Contains(dto.UserGroupId) && ids.Contains(dto.NodeId)); - var permissions = AmbientScope.Database.Fetch(sql); - foreach (var permission in ConvertToPermissionList(permissions)) - { - result.Add(permission); - } + result.Add(permission); } } } @@ -84,60 +90,62 @@ public EntityPermissionCollection GetPermissionsForEntities(int[] groupIds, para } /// - /// Returns permissions directly assigned to the content items for all user groups + /// Returns permissions directly assigned to the content items for all user groups /// /// /// public IEnumerable GetPermissionsForEntities(int[] entityIds) { - var sql = Sql() + Sql sql = Sql() .SelectAll() .From() .Where(dto => entityIds.Contains(dto.NodeId)) .OrderBy(dto => dto.NodeId); - var result = AmbientScope.Database.Fetch(sql); + List result = AmbientScope.Database.Fetch(sql); return ConvertToPermissionList(result); } /// - /// Returns permissions directly assigned to the content item for all user groups + /// Returns permissions directly assigned to the content item for all user groups /// /// /// public EntityPermissionCollection GetPermissionsForEntity(int entityId) { - var sql = Sql() + Sql sql = Sql() .SelectAll() .From() .Where(dto => dto.NodeId == entityId) .OrderBy(dto => dto.NodeId); - var result = AmbientScope.Database.Fetch(sql); + List result = AmbientScope.Database.Fetch(sql); return ConvertToPermissionList(result); } /// - /// Assigns the same permission set for a single group to any number of entities + /// Assigns the same permission set for a single group to any number of entities /// /// /// /// /// - /// This will first clear the permissions for this user and entities and recreate them + /// This will first clear the permissions for this user and entities and recreate them /// public void ReplacePermissions(int groupId, IEnumerable permissions, params int[] entityIds) { if (entityIds.Length == 0) + { return; + } - var db = AmbientScope.Database; + IUmbracoDatabase db = AmbientScope.Database; - //we need to batch these in groups of 2000 so we don't exceed the max 2100 limit - var sql = "DELETE FROM umbracoUserGroup2NodePermission WHERE userGroupId = @groupId AND nodeId in (@nodeIds)"; - foreach (var idGroup in entityIds.InGroupsOf(2000)) + var sql = + "DELETE FROM umbracoUserGroup2NodePermission WHERE userGroupId = @groupId AND nodeId in (@nodeIds)"; + foreach (IEnumerable group in entityIds.InGroupsOf(Constants.Sql.MaxParameterCount)) { - db.Execute(sql, new { groupId, nodeIds = idGroup }); + db.Execute(sql, new { groupId, nodeIds = group }); } var toInsert = new List(); @@ -147,9 +155,7 @@ public void ReplacePermissions(int groupId, IEnumerable permissions, param { toInsert.Add(new UserGroup2NodePermissionDto { - NodeId = e, - Permission = p.ToString(CultureInfo.InvariantCulture), - UserGroupId = groupId + NodeId = e, Permission = p.ToString(CultureInfo.InvariantCulture), UserGroupId = groupId }); } } @@ -158,46 +164,41 @@ public void ReplacePermissions(int groupId, IEnumerable permissions, param } /// - /// Assigns one permission for a user to many entities + /// Assigns one permission for a user to many entities /// /// /// /// public void AssignPermission(int groupId, char permission, params int[] entityIds) { - var db = AmbientScope.Database; + IUmbracoDatabase db = AmbientScope.Database; - var sql = "DELETE FROM umbracoUserGroup2NodePermission WHERE userGroupId = @groupId AND permission=@permission AND nodeId in (@entityIds)"; + var sql = + "DELETE FROM umbracoUserGroup2NodePermission WHERE userGroupId = @groupId AND permission=@permission AND nodeId in (@entityIds)"; db.Execute(sql, - new - { - groupId, - permission = permission.ToString(CultureInfo.InvariantCulture), - entityIds - }); + new { groupId, permission = permission.ToString(CultureInfo.InvariantCulture), entityIds }); - var actions = entityIds.Select(id => new UserGroup2NodePermissionDto + UserGroup2NodePermissionDto[] actions = entityIds.Select(id => new UserGroup2NodePermissionDto { - NodeId = id, - Permission = permission.ToString(CultureInfo.InvariantCulture), - UserGroupId = groupId + NodeId = id, Permission = permission.ToString(CultureInfo.InvariantCulture), UserGroupId = groupId }).ToArray(); db.BulkInsertRecords(actions); } /// - /// Assigns one permission to an entity for multiple groups + /// Assigns one permission to an entity for multiple groups /// /// /// /// public void AssignEntityPermission(TEntity entity, char permission, IEnumerable groupIds) { - var db = AmbientScope.Database; + IUmbracoDatabase db = AmbientScope.Database; var groupIdsA = groupIds.ToArray(); - const string sql = "DELETE FROM umbracoUserGroup2NodePermission WHERE nodeId = @nodeId AND permission = @permission AND userGroupId in (@groupIds)"; + const string sql = + "DELETE FROM umbracoUserGroup2NodePermission WHERE nodeId = @nodeId AND permission = @permission AND userGroupId in (@groupIds)"; db.Execute(sql, new { @@ -206,33 +207,31 @@ public void AssignEntityPermission(TEntity entity, char permission, IEnumerable< groupIds = groupIdsA }); - var actions = groupIdsA.Select(id => new UserGroup2NodePermissionDto + UserGroup2NodePermissionDto[] actions = groupIdsA.Select(id => new UserGroup2NodePermissionDto { - NodeId = entity.Id, - Permission = permission.ToString(CultureInfo.InvariantCulture), - UserGroupId = id + NodeId = entity.Id, Permission = permission.ToString(CultureInfo.InvariantCulture), UserGroupId = id }).ToArray(); db.BulkInsertRecords(actions); } /// - /// Assigns permissions to an entity for multiple group/permission entries + /// Assigns permissions to an entity for multiple group/permission entries /// /// /// /// - /// This will first clear the permissions for this entity then re-create them + /// This will first clear the permissions for this entity then re-create them /// public void ReplaceEntityPermissions(EntityPermissionSet permissionSet) { - var db = AmbientScope.Database; + IUmbracoDatabase db = AmbientScope.Database; const string sql = "DELETE FROM umbracoUserGroup2NodePermission WHERE nodeId = @nodeId"; db.Execute(sql, new { nodeId = permissionSet.EntityId }); var toInsert = new List(); - foreach (var entityPermission in permissionSet.PermissionsSet) + foreach (EntityPermission entityPermission in permissionSet.PermissionsSet) { foreach (var permission in entityPermission.AssignedPermissions) { @@ -248,62 +247,21 @@ public void ReplaceEntityPermissions(EntityPermissionSet permissionSet) db.BulkInsertRecords(toInsert); } - #region Not implemented (don't need to for the purposes of this repo) - - protected override ContentPermissionSet PerformGet(int id) - { - throw new InvalidOperationException("This method won't be implemented."); - } - - protected override IEnumerable PerformGetAll(params int[] ids) - { - throw new InvalidOperationException("This method won't be implemented."); - } - - protected override IEnumerable PerformGetByQuery(IQuery query) - { - throw new InvalidOperationException("This method won't be implemented."); - } - - protected override Sql GetBaseQuery(bool isCount) - { - throw new InvalidOperationException("This method won't be implemented."); - } - - protected override string GetBaseWhereClause() - { - throw new InvalidOperationException("This method won't be implemented."); - } - - protected override IEnumerable GetDeleteClauses() - { - return new List(); - } - - protected override void PersistDeletedItem(ContentPermissionSet entity) - { - throw new InvalidOperationException("This method won't be implemented."); - } - - #endregion - /// - /// Used to add or update entity permissions during a content item being updated + /// Used to add or update entity permissions during a content item being updated /// /// - protected override void PersistNewItem(ContentPermissionSet entity) - { + protected override void PersistNewItem(ContentPermissionSet entity) => //does the same thing as update PersistUpdatedItem(entity); - } /// - /// Used to add or update entity permissions during a content item being updated + /// Used to add or update entity permissions during a content item being updated /// /// protected override void PersistUpdatedItem(ContentPermissionSet entity) { - var asIEntity = (IEntity) entity; + var asIEntity = (IEntity)entity; if (asIEntity.HasIdentity == false) { throw new InvalidOperationException("Cannot create permissions for an entity without an Id"); @@ -312,14 +270,16 @@ protected override void PersistUpdatedItem(ContentPermissionSet entity) ReplaceEntityPermissions(entity); } - private static EntityPermissionCollection ConvertToPermissionList(IEnumerable result) + private static EntityPermissionCollection ConvertToPermissionList( + IEnumerable result) { var permissions = new EntityPermissionCollection(); - var nodePermissions = result.GroupBy(x => x.NodeId); - foreach (var np in nodePermissions) + IEnumerable> nodePermissions = result.GroupBy(x => x.NodeId); + foreach (IGrouping np in nodePermissions) { - var userGroupPermissions = np.GroupBy(x => x.UserGroupId); - foreach (var permission in userGroupPermissions) + IEnumerable> userGroupPermissions = + np.GroupBy(x => x.UserGroupId); + foreach (IGrouping permission in userGroupPermissions) { var perms = permission.Select(x => x.Permission).Distinct().ToArray(); permissions.Add(new EntityPermission(permission.Key, np.Key, perms)); @@ -328,5 +288,29 @@ private static EntityPermissionCollection ConvertToPermissionList(IEnumerable + throw new InvalidOperationException("This method won't be implemented."); + + protected override IEnumerable PerformGetAll(params int[] ids) => + throw new InvalidOperationException("This method won't be implemented."); + + protected override IEnumerable PerformGetByQuery(IQuery query) => + throw new InvalidOperationException("This method won't be implemented."); + + protected override Sql GetBaseQuery(bool isCount) => + throw new InvalidOperationException("This method won't be implemented."); + + protected override string GetBaseWhereClause() => + throw new InvalidOperationException("This method won't be implemented."); + + protected override IEnumerable GetDeleteClauses() => new List(); + + protected override void PersistDeletedItem(ContentPermissionSet entity) => + throw new InvalidOperationException("This method won't be implemented."); + + #endregion } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RedirectUrlRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RedirectUrlRepository.cs index 1536768bae97..6ab29aa47e4a 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RedirectUrlRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RedirectUrlRepository.cs @@ -4,6 +4,7 @@ using System.Security.Cryptography; using Microsoft.Extensions.Logging; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Querying; @@ -16,85 +17,183 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement { internal class RedirectUrlRepository : EntityRepositoryBase, IRedirectUrlRepository { - public RedirectUrlRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) + public RedirectUrlRepository(IScopeAccessor scopeAccessor, AppCaches cache, + ILogger logger) : base(scopeAccessor, cache, logger) - { } + { + } - protected override int PerformCount(IQuery query) + public IRedirectUrl Get(string url, Guid contentKey, string culture) { - throw new NotSupportedException("This repository does not support this method."); + var urlHash = url.GenerateHash(); + Sql sql = GetBaseQuery(false).Where(x => + x.Url == url && x.UrlHash == urlHash && x.ContentKey == contentKey && x.Culture == culture); + RedirectUrlDto dto = Database.Fetch(sql).FirstOrDefault(); + return dto == null ? null : Map(dto); + } + + public void DeleteAll() => Database.Execute("DELETE FROM umbracoRedirectUrl"); + + public void DeleteContentUrls(Guid contentKey) => + Database.Execute("DELETE FROM umbracoRedirectUrl WHERE contentKey=@contentKey", new { contentKey }); + + public void Delete(Guid id) => Database.Delete(id); + + public IRedirectUrl GetMostRecentUrl(string url) + { + var urlHash = url.GenerateHash(); + Sql sql = GetBaseQuery(false) + .Where(x => x.Url == url && x.UrlHash == urlHash) + .OrderByDescending(x => x.CreateDateUtc); + List dtos = Database.Fetch(sql); + RedirectUrlDto dto = dtos.FirstOrDefault(); + return dto == null ? null : Map(dto); + } + + public IRedirectUrl GetMostRecentUrl(string url, string culture) + { + if (string.IsNullOrWhiteSpace(culture)) + { + return GetMostRecentUrl(url); + } + + var urlHash = url.GenerateHash(); + Sql sql = GetBaseQuery(false) + .Where(x => x.Url == url && x.UrlHash == urlHash && + (x.Culture == culture.ToLower() || x.Culture == string.Empty)) + .OrderByDescending(x => x.CreateDateUtc); + List dtos = Database.Fetch(sql); + RedirectUrlDto dto = dtos.FirstOrDefault(f => f.Culture == culture.ToLower()); + + if (dto == null) + { + dto = dtos.FirstOrDefault(f => f.Culture == string.Empty); + } + + return dto == null ? null : Map(dto); + } + + public IEnumerable GetContentUrls(Guid contentKey) + { + Sql sql = GetBaseQuery(false) + .Where(x => x.ContentKey == contentKey) + .OrderByDescending(x => x.CreateDateUtc); + List dtos = Database.Fetch(sql); + return dtos.Select(Map); } - protected override bool PerformExists(Guid id) + public IEnumerable GetAllUrls(long pageIndex, int pageSize, out long total) { - return PerformGet(id) != null; + Sql sql = GetBaseQuery(false) + .OrderByDescending(x => x.CreateDateUtc); + Page result = Database.Page(pageIndex + 1, pageSize, sql); + total = Convert.ToInt32(result.TotalItems); + return result.Items.Select(Map); } + public IEnumerable GetAllUrls(int rootContentId, long pageIndex, int pageSize, out long total) + { + Sql sql = GetBaseQuery(false) + .Where( + string.Format("{0}.{1} LIKE @path", SqlSyntax.GetQuotedTableName("umbracoNode"), + SqlSyntax.GetQuotedColumnName("path")), new { path = "%," + rootContentId + ",%" }) + .OrderByDescending(x => x.CreateDateUtc); + Page result = Database.Page(pageIndex + 1, pageSize, sql); + total = Convert.ToInt32(result.TotalItems); + + IEnumerable rules = result.Items.Select(Map); + return rules; + } + + public IEnumerable SearchUrls(string searchTerm, long pageIndex, int pageSize, out long total) + { + Sql sql = GetBaseQuery(false) + .Where( + string.Format("{0}.{1} LIKE @url", SqlSyntax.GetQuotedTableName("umbracoRedirectUrl"), + SqlSyntax.GetQuotedColumnName("Url")), + new { url = "%" + searchTerm.Trim().ToLowerInvariant() + "%" }) + .OrderByDescending(x => x.CreateDateUtc); + Page result = Database.Page(pageIndex + 1, pageSize, sql); + total = Convert.ToInt32(result.TotalItems); + + IEnumerable rules = result.Items.Select(Map); + return rules; + } + + protected override int PerformCount(IQuery query) => + throw new NotSupportedException("This repository does not support this method."); + + protected override bool PerformExists(Guid id) => PerformGet(id) != null; + protected override IRedirectUrl PerformGet(Guid id) { - var sql = GetBaseQuery(false).Where(x => x.Id == id); - var dto = Database.Fetch(sql.SelectTop(1)).FirstOrDefault(); + Sql sql = GetBaseQuery(false).Where(x => x.Id == id); + RedirectUrlDto dto = Database.Fetch(sql.SelectTop(1)).FirstOrDefault(); return dto == null ? null : Map(dto); } protected override IEnumerable PerformGetAll(params Guid[] ids) { - if (ids.Length > 2000) - throw new NotSupportedException("This repository does not support more than 2000 ids."); - var sql = GetBaseQuery(false).WhereIn(x => x.Id, ids); - var dtos = Database.Fetch(sql); + if (ids.Length > Constants.Sql.MaxParameterCount) + { + throw new NotSupportedException( + $"This repository does not support more than {Constants.Sql.MaxParameterCount} ids."); + } + + Sql sql = GetBaseQuery(false).WhereIn(x => x.Id, ids); + List dtos = Database.Fetch(sql); return dtos.WhereNotNull().Select(Map); } - protected override IEnumerable PerformGetByQuery(IQuery query) - { + protected override IEnumerable PerformGetByQuery(IQuery query) => throw new NotSupportedException("This repository does not support this method."); - } protected override Sql GetBaseQuery(bool isCount) { - var sql = Sql(); + Sql sql = Sql(); if (isCount) + { sql.Select(@"COUNT(*) FROM umbracoRedirectUrl JOIN umbracoNode ON umbracoRedirectUrl.contentKey=umbracoNode.uniqueID"); + } else + { sql.Select(@"umbracoRedirectUrl.*, umbracoNode.id AS contentId FROM umbracoRedirectUrl JOIN umbracoNode ON umbracoRedirectUrl.contentKey=umbracoNode.uniqueID"); + } + return sql; } - protected override string GetBaseWhereClause() - { - return "id = @id"; - } + protected override string GetBaseWhereClause() => "id = @id"; protected override IEnumerable GetDeleteClauses() { - var list = new List - { - "DELETE FROM umbracoRedirectUrl WHERE id = @id" - }; + var list = new List { "DELETE FROM umbracoRedirectUrl WHERE id = @id" }; return list; } protected override void PersistNewItem(IRedirectUrl entity) { - var dto = Map(entity); + RedirectUrlDto dto = Map(entity); Database.Insert(dto); entity.Id = entity.Key.GetHashCode(); } protected override void PersistUpdatedItem(IRedirectUrl entity) { - var dto = Map(entity); + RedirectUrlDto dto = Map(entity); Database.Update(dto); } private static RedirectUrlDto Map(IRedirectUrl redirectUrl) { - if (redirectUrl == null) return null; + if (redirectUrl == null) + { + return null; + } return new RedirectUrlDto { @@ -109,7 +208,10 @@ private static RedirectUrlDto Map(IRedirectUrl redirectUrl) private static IRedirectUrl Map(RedirectUrlDto dto) { - if (dto == null) return null; + if (dto == null) + { + return null; + } var url = new RedirectUrl(); try @@ -129,98 +231,5 @@ private static IRedirectUrl Map(RedirectUrlDto dto) url.EnableChangeTracking(); } } - - public IRedirectUrl Get(string url, Guid contentKey, string culture) - { - var urlHash = url.GenerateHash(); - var sql = GetBaseQuery(false).Where(x => x.Url == url && x.UrlHash == urlHash && x.ContentKey == contentKey && x.Culture == culture); - var dto = Database.Fetch(sql).FirstOrDefault(); - return dto == null ? null : Map(dto); - } - - public void DeleteAll() - { - Database.Execute("DELETE FROM umbracoRedirectUrl"); - } - - public void DeleteContentUrls(Guid contentKey) - { - Database.Execute("DELETE FROM umbracoRedirectUrl WHERE contentKey=@contentKey", new { contentKey }); - } - - public void Delete(Guid id) - { - Database.Delete(id); - } - - public IRedirectUrl GetMostRecentUrl(string url) - { - var urlHash = url.GenerateHash(); - var sql = GetBaseQuery(false) - .Where(x => x.Url == url && x.UrlHash == urlHash) - .OrderByDescending(x => x.CreateDateUtc); - var dtos = Database.Fetch(sql); - var dto = dtos.FirstOrDefault(); - return dto == null ? null : Map(dto); - } - - public IRedirectUrl GetMostRecentUrl(string url, string culture) - { - if (string.IsNullOrWhiteSpace(culture)) return GetMostRecentUrl(url); - var urlHash = url.GenerateHash(); - var sql = GetBaseQuery(false) - .Where(x => x.Url == url && x.UrlHash == urlHash && - (x.Culture == culture.ToLower() || x.Culture == string.Empty)) - .OrderByDescending(x => x.CreateDateUtc); - var dtos = Database.Fetch(sql); - var dto = dtos.FirstOrDefault(f => f.Culture == culture.ToLower()); - - if (dto == null) - dto = dtos.FirstOrDefault(f => f.Culture == string.Empty); - - return dto == null ? null : Map(dto); - } - - public IEnumerable GetContentUrls(Guid contentKey) - { - var sql = GetBaseQuery(false) - .Where(x => x.ContentKey == contentKey) - .OrderByDescending(x => x.CreateDateUtc); - var dtos = Database.Fetch(sql); - return dtos.Select(Map); - } - - public IEnumerable GetAllUrls(long pageIndex, int pageSize, out long total) - { - var sql = GetBaseQuery(false) - .OrderByDescending(x => x.CreateDateUtc); - var result = Database.Page(pageIndex + 1, pageSize, sql); - total = Convert.ToInt32(result.TotalItems); - return result.Items.Select(Map); - } - - public IEnumerable GetAllUrls(int rootContentId, long pageIndex, int pageSize, out long total) - { - var sql = GetBaseQuery(false) - .Where(string.Format("{0}.{1} LIKE @path", SqlSyntax.GetQuotedTableName("umbracoNode"), SqlSyntax.GetQuotedColumnName("path")), new { path = "%," + rootContentId + ",%" }) - .OrderByDescending(x => x.CreateDateUtc); - var result = Database.Page(pageIndex + 1, pageSize, sql); - total = Convert.ToInt32(result.TotalItems); - - var rules = result.Items.Select(Map); - return rules; - } - - public IEnumerable SearchUrls(string searchTerm, long pageIndex, int pageSize, out long total) - { - var sql = GetBaseQuery(false) - .Where(string.Format("{0}.{1} LIKE @url", SqlSyntax.GetQuotedTableName("umbracoRedirectUrl"), SqlSyntax.GetQuotedColumnName("Url")), new { url = "%" + searchTerm.Trim().ToLowerInvariant() + "%" }) - .OrderByDescending(x => x.CreateDateUtc); - var result = Database.Page(pageIndex + 1, pageSize, sql); - total = Convert.ToInt32(result.TotalItems); - - var rules = result.Items.Select(Map); - return rules; - } } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TagRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TagRepository.cs index 8fbc7845769b..919bbeea312d 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TagRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TagRepository.cs @@ -4,6 +4,7 @@ using System.Text; using Microsoft.Extensions.Logging; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Querying; @@ -21,24 +22,26 @@ internal class TagRepository : EntityRepositoryBase, ITagRepository { public TagRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) : base(scopeAccessor, cache, logger) - { } + { + } #region Manage Tag Entities /// protected override ITag PerformGet(int id) { - var sql = Sql().Select().From().Where(x => x.Id == id); - var dto = Database.Fetch(SqlSyntax.SelectTop(sql, 1)).FirstOrDefault(); + Sql sql = Sql().Select().From().Where(x => x.Id == id); + TagDto dto = Database.Fetch(SqlSyntax.SelectTop(sql, 1)).FirstOrDefault(); return dto == null ? null : TagFactory.BuildEntity(dto); } /// protected override IEnumerable PerformGetAll(params int[] ids) { - var dtos = ids.Length == 0 + IEnumerable dtos = ids.Length == 0 ? Database.Fetch(Sql().Select().From()) - : Database.FetchByGroups(ids, 2000, batch => Sql().Select().From().WhereIn(x => x.Id, batch)); + : Database.FetchByGroups(ids, Constants.Sql.MaxParameterCount, + batch => Sql().Select().From().WhereIn(x => x.Id, batch)); return dtos.Select(TagFactory.BuildEntity).ToList(); } @@ -46,7 +49,7 @@ protected override IEnumerable PerformGetAll(params int[] ids) /// protected override IEnumerable PerformGetByQuery(IQuery query) { - var sql = Sql().Select().From(); + Sql sql = Sql().Select().From(); var translator = new SqlTranslator(sql, query); sql = translator.Translate(); @@ -54,30 +57,21 @@ protected override IEnumerable PerformGetByQuery(IQuery query) } /// - protected override Sql GetBaseQuery(bool isCount) - { - return isCount ? Sql().SelectCount().From() : GetBaseQuery(); - } + protected override Sql GetBaseQuery(bool isCount) => + isCount ? Sql().SelectCount().From() : GetBaseQuery(); - private Sql GetBaseQuery() - { - return Sql().Select().From(); - } + private Sql GetBaseQuery() => Sql().Select().From(); /// - protected override string GetBaseWhereClause() - { - return "id = @id"; - } + protected override string GetBaseWhereClause() => "id = @id"; /// protected override IEnumerable GetDeleteClauses() { var list = new List - { - "DELETE FROM cmsTagRelationship WHERE tagId = @id", - "DELETE FROM cmsTags WHERE id = @id" - }; + { + "DELETE FROM cmsTagRelationship WHERE tagId = @id", "DELETE FROM cmsTags WHERE id = @id" + }; return list; } @@ -86,7 +80,7 @@ protected override void PersistNewItem(ITag entity) { entity.AddingEntity(); - var dto = TagFactory.BuildDto(entity); + TagDto dto = TagFactory.BuildDto(entity); var id = Convert.ToInt32(Database.Insert(dto)); entity.Id = id; @@ -98,7 +92,7 @@ protected override void PersistUpdatedItem(ITag entity) { entity.UpdatingEntity(); - var dto = TagFactory.BuildDto(entity); + TagDto dto = TagFactory.BuildDto(entity); Database.Update(dto); entity.ResetDirtyProperties(); @@ -113,18 +107,21 @@ protected override void PersistUpdatedItem(ITag entity) public void Assign(int contentId, int propertyTypeId, IEnumerable tags, bool replaceTags = true) { // to no-duplicates array - var tagsA = tags.Distinct(new TagComparer()).ToArray(); + ITag[] tagsA = tags.Distinct(new TagComparer()).ToArray(); // replacing = clear all if (replaceTags) { - var sql0 = Sql().Delete().Where(x => x.NodeId == contentId && x.PropertyTypeId == propertyTypeId); + Sql sql0 = Sql().Delete() + .Where(x => x.NodeId == contentId && x.PropertyTypeId == propertyTypeId); Database.Execute(sql0); } // no tags? nothing else to do if (tagsA.Length == 0) + { return; + } // tags // using some clever logic (?) to insert tags that don't exist in 1 query @@ -163,7 +160,8 @@ public void Remove(int contentId, int propertyTypeId, IEnumerable tags) var tagSetSql = GetTagSet(tags); var group = SqlSyntax.GetQuotedColumnName("group"); - var deleteSql = $@"DELETE FROM cmsTagRelationship WHERE nodeId = {contentId} AND propertyTypeId = {propertyTypeId} AND tagId IN ( + var deleteSql = + $@"DELETE FROM cmsTagRelationship WHERE nodeId = {contentId} AND propertyTypeId = {propertyTypeId} AND tagId IN ( SELECT id FROM cmsTags INNER JOIN {tagSetSql} ON ( tagSet.tag = cmsTags.tag AND tagSet.{group} = cmsTags.{group} AND COALESCE(tagSet.languageId, -1) = COALESCE(cmsTags.languageId, -1) ) @@ -173,18 +171,15 @@ public void Remove(int contentId, int propertyTypeId, IEnumerable tags) } /// - public void RemoveAll(int contentId, int propertyTypeId) - { - Database.Execute("DELETE FROM cmsTagRelationship WHERE nodeId = @nodeId AND propertyTypeId = @propertyTypeId", - new { nodeId = contentId, propertyTypeId = propertyTypeId }); - } + public void RemoveAll(int contentId, int propertyTypeId) => + Database.Execute( + "DELETE FROM cmsTagRelationship WHERE nodeId = @nodeId AND propertyTypeId = @propertyTypeId", + new { nodeId = contentId, propertyTypeId }); /// - public void RemoveAll(int contentId) - { + public void RemoveAll(int contentId) => Database.Execute("DELETE FROM cmsTagRelationship WHERE nodeId = @nodeId", new { nodeId = contentId }); - } // this is a clever way to produce an SQL statement like this: // @@ -204,10 +199,16 @@ private string GetTagSet(IEnumerable tags) sql.Append("("); - foreach (var tag in tags) + foreach (ITag tag in tags) { - if (first) first = false; - else sql.Append(" UNION "); + if (first) + { + first = false; + } + else + { + sql.Append(" UNION "); + } sql.Append("SELECT N'"); sql.Append(SqlSyntax.EscapeString(tag.Text)); @@ -217,9 +218,14 @@ private string GetTagSet(IEnumerable tags) sql.Append(group); sql.Append(" , "); if (tag.LanguageId.HasValue) + { sql.Append(tag.LanguageId); + } else + { sql.Append("NULL"); + } + sql.Append(" AS languageId"); } @@ -231,19 +237,17 @@ private string GetTagSet(IEnumerable tags) // used to run Distinct() on tags private class TagComparer : IEqualityComparer { - public bool Equals(ITag x, ITag y) - { - return ReferenceEquals(x, y) // takes care of both being null - || x != null && y != null && x.Text == y.Text && x.Group == y.Group && x.LanguageId == y.LanguageId; - } + public bool Equals(ITag x, ITag y) => + ReferenceEquals(x, y) // takes care of both being null + || (x != null && y != null && x.Text == y.Text && x.Group == y.Group && x.LanguageId == y.LanguageId); public int GetHashCode(ITag obj) { unchecked { var h = obj.Text.GetHashCode(); - h = h * 397 ^ obj.Group.GetHashCode(); - h = h * 397 ^ (obj.LanguageId?.GetHashCode() ?? 0); + h = (h * 397) ^ obj.Group.GetHashCode(); + h = (h * 397) ^ (obj.LanguageId?.GetHashCode() ?? 0); return h; } } @@ -273,7 +277,7 @@ private class TaggedEntityDto /// public TaggedEntity GetTaggedEntityByKey(Guid key) { - var sql = GetTaggedEntitiesSql(TaggableObjectTypes.All, "*"); + Sql sql = GetTaggedEntitiesSql(TaggableObjectTypes.All, "*"); sql = sql .Where(dto => dto.UniqueId == key); @@ -284,7 +288,7 @@ public TaggedEntity GetTaggedEntityByKey(Guid key) /// public TaggedEntity GetTaggedEntityById(int id) { - var sql = GetTaggedEntitiesSql(TaggableObjectTypes.All, "*"); + Sql sql = GetTaggedEntitiesSql(TaggableObjectTypes.All, "*"); sql = sql .Where(dto => dto.NodeId == id); @@ -293,9 +297,10 @@ public TaggedEntity GetTaggedEntityById(int id) } /// - public IEnumerable GetTaggedEntitiesByTagGroup(TaggableObjectTypes objectType, string group, string culture = null) + public IEnumerable GetTaggedEntitiesByTagGroup(TaggableObjectTypes objectType, string group, + string culture = null) { - var sql = GetTaggedEntitiesSql(objectType, culture); + Sql sql = GetTaggedEntitiesSql(objectType, culture); sql = sql .Where(x => x.Group == group); @@ -304,30 +309,37 @@ public IEnumerable GetTaggedEntitiesByTagGroup(TaggableObjectTypes } /// - public IEnumerable GetTaggedEntitiesByTag(TaggableObjectTypes objectType, string tag, string group = null, string culture = null) + public IEnumerable GetTaggedEntitiesByTag(TaggableObjectTypes objectType, string tag, + string group = null, string culture = null) { - var sql = GetTaggedEntitiesSql(objectType, culture); + Sql sql = GetTaggedEntitiesSql(objectType, culture); sql = sql .Where(dto => dto.Text == tag); if (group.IsNullOrWhiteSpace() == false) + { sql = sql .Where(dto => dto.Group == group); + } return Map(Database.Fetch(sql)); } private Sql GetTaggedEntitiesSql(TaggableObjectTypes objectType, string culture) { - var sql = Sql() + Sql sql = Sql() .Select(x => Alias(x.NodeId, "NodeId")) - .AndSelect(x => Alias(x.Alias, "PropertyTypeAlias"), x => Alias(x.Id, "PropertyTypeId")) - .AndSelect(x => Alias(x.Id, "TagId"), x => Alias(x.Text, "TagText"), x => Alias(x.Group, "TagGroup"), x => Alias(x.LanguageId, "TagLanguage")) + .AndSelect(x => Alias(x.Alias, "PropertyTypeAlias"), + x => Alias(x.Id, "PropertyTypeId")) + .AndSelect(x => Alias(x.Id, "TagId"), x => Alias(x.Text, "TagText"), + x => Alias(x.Group, "TagGroup"), x => Alias(x.LanguageId, "TagLanguage")) .From() .InnerJoin().On((tag, rel) => tag.Id == rel.TagId) - .InnerJoin().On((rel, content) => rel.NodeId == content.NodeId) - .InnerJoin().On((rel, prop) => rel.PropertyTypeId == prop.Id) + .InnerJoin() + .On((rel, content) => rel.NodeId == content.NodeId) + .InnerJoin() + .On((rel, prop) => rel.PropertyTypeId == prop.Id) .InnerJoin().On((content, node) => content.NodeId == node.NodeId); if (culture == null) @@ -344,16 +356,15 @@ private Sql GetTaggedEntitiesSql(TaggableObjectTypes objectType, st if (objectType != TaggableObjectTypes.All) { - var nodeObjectType = GetNodeObjectType(objectType); + Guid nodeObjectType = GetNodeObjectType(objectType); sql = sql.Where(dto => dto.NodeObjectType == nodeObjectType); } return sql; } - private static IEnumerable Map(IEnumerable dtos) - { - return dtos.GroupBy(x => x.NodeId).Select(dtosForNode => + private static IEnumerable Map(IEnumerable dtos) => + dtos.GroupBy(x => x.NodeId).Select(dtosForNode => { var taggedProperties = dtosForNode.GroupBy(x => x.PropertyTypeId).Select(dtosForProperty => { @@ -368,25 +379,27 @@ private static IEnumerable Map(IEnumerable dtos) return new TaggedEntity(dtosForNode.Key, taggedProperties); }).ToList(); - } /// - public IEnumerable GetTagsForEntityType(TaggableObjectTypes objectType, string group = null, string culture = null) + public IEnumerable GetTagsForEntityType(TaggableObjectTypes objectType, string group = null, + string culture = null) { - var sql = GetTagsSql(culture, true); + Sql sql = GetTagsSql(culture, true); AddTagsSqlWhere(sql, culture); if (objectType != TaggableObjectTypes.All) { - var nodeObjectType = GetNodeObjectType(objectType); + Guid nodeObjectType = GetNodeObjectType(objectType); sql = sql .Where(dto => dto.NodeObjectType == nodeObjectType); } if (group.IsNullOrWhiteSpace() == false) + { sql = sql .Where(dto => dto.Group == group); + } sql = sql .GroupBy(x => x.Id, x => x.Text, x => x.Group, x => x.LanguageId); @@ -397,7 +410,7 @@ public IEnumerable GetTagsForEntityType(TaggableObjectTypes objectType, st /// public IEnumerable GetTagsForEntity(int contentId, string group = null, string culture = null) { - var sql = GetTagsSql(culture); + Sql sql = GetTagsSql(culture); AddTagsSqlWhere(sql, culture); @@ -405,8 +418,10 @@ public IEnumerable GetTagsForEntity(int contentId, string group = null, st .Where(dto => dto.NodeId == contentId); if (group.IsNullOrWhiteSpace() == false) + { sql = sql .Where(dto => dto.Group == group); + } return ExecuteTagsQuery(sql); } @@ -414,7 +429,7 @@ public IEnumerable GetTagsForEntity(int contentId, string group = null, st /// public IEnumerable GetTagsForEntity(Guid contentId, string group = null, string culture = null) { - var sql = GetTagsSql(culture); + Sql sql = GetTagsSql(culture); AddTagsSqlWhere(sql, culture); @@ -422,63 +437,76 @@ public IEnumerable GetTagsForEntity(Guid contentId, string group = null, s .Where(dto => dto.UniqueId == contentId); if (group.IsNullOrWhiteSpace() == false) + { sql = sql .Where(dto => dto.Group == group); + } return ExecuteTagsQuery(sql); } /// - public IEnumerable GetTagsForProperty(int contentId, string propertyTypeAlias, string group = null, string culture = null) + public IEnumerable GetTagsForProperty(int contentId, string propertyTypeAlias, string group = null, + string culture = null) { - var sql = GetTagsSql(culture); + Sql sql = GetTagsSql(culture); sql = sql - .InnerJoin().On((prop, rel) => prop.Id == rel.PropertyTypeId) + .InnerJoin() + .On((prop, rel) => prop.Id == rel.PropertyTypeId) .Where(x => x.NodeId == contentId) .Where(x => x.Alias == propertyTypeAlias); AddTagsSqlWhere(sql, culture); if (group.IsNullOrWhiteSpace() == false) + { sql = sql .Where(dto => dto.Group == group); + } return ExecuteTagsQuery(sql); } /// - public IEnumerable GetTagsForProperty(Guid contentId, string propertyTypeAlias, string group = null, string culture = null) + public IEnumerable GetTagsForProperty(Guid contentId, string propertyTypeAlias, string group = null, + string culture = null) { - var sql = GetTagsSql(culture); + Sql sql = GetTagsSql(culture); sql = sql - .InnerJoin().On((prop, rel) => prop.Id == rel.PropertyTypeId) + .InnerJoin() + .On((prop, rel) => prop.Id == rel.PropertyTypeId) .Where(dto => dto.UniqueId == contentId) .Where(dto => dto.Alias == propertyTypeAlias); AddTagsSqlWhere(sql, culture); if (group.IsNullOrWhiteSpace() == false) + { sql = sql .Where(dto => dto.Group == group); + } return ExecuteTagsQuery(sql); } private Sql GetTagsSql(string culture, bool withGrouping = false) { - var sql = Sql() + Sql sql = Sql() .Select(); if (withGrouping) + { sql = sql .AndSelectCount("NodeCount"); + } sql = sql .From() .InnerJoin().On((rel, tag) => tag.Id == rel.TagId) - .InnerJoin().On((content, rel) => content.NodeId == rel.NodeId) + .InnerJoin() + .On((content, rel) => content.NodeId == rel.NodeId) .InnerJoin().On((node, content) => node.NodeId == content.NodeId); if (culture != null && culture != "*") @@ -506,21 +534,19 @@ private Sql AddTagsSqlWhere(Sql sql, string culture) return sql; } - private IEnumerable ExecuteTagsQuery(Sql sql) - { - return Database.Fetch(sql).Select(TagFactory.BuildEntity); - } + private IEnumerable ExecuteTagsQuery(Sql sql) => + Database.Fetch(sql).Select(TagFactory.BuildEntity); private Guid GetNodeObjectType(TaggableObjectTypes type) { switch (type) { case TaggableObjectTypes.Content: - return Cms.Core.Constants.ObjectTypes.Document; + return Constants.ObjectTypes.Document; case TaggableObjectTypes.Media: - return Cms.Core.Constants.ObjectTypes.Media; + return Constants.ObjectTypes.Media; case TaggableObjectTypes.Member: - return Cms.Core.Constants.ObjectTypes.Member; + return Constants.ObjectTypes.Member; default: throw new ArgumentOutOfRangeException(nameof(type)); } diff --git a/src/Umbraco.Infrastructure/Scoping/Scope.cs b/src/Umbraco.Infrastructure/Scoping/Scope.cs index e0423cc340d3..e68d3f78336d 100644 --- a/src/Umbraco.Infrastructure/Scoping/Scope.cs +++ b/src/Umbraco.Infrastructure/Scoping/Scope.cs @@ -7,8 +7,8 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Extensions; -using Umbraco.Core.Collections; using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Collections; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Infrastructure.Persistence; diff --git a/src/Umbraco.Web.BackOffice/Controllers/EntityController.cs b/src/Umbraco.Web.BackOffice/Controllers/EntityController.cs index 91af3724343f..c3f36e92cbbb 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/EntityController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/EntityController.cs @@ -204,7 +204,7 @@ public IDictionary SearchAll(string query) var allowedSections = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.AllowedSections.ToArray(); foreach (KeyValuePair searchableTree in _searchableTreeCollection - .SearchableApplicationTrees.OrderBy(t => t.Value.SortOrder)) + .SearchableApplicationTrees.OrderBy(t => t.Value.SortOrder)) { if (allowedSections.Contains(searchableTree.Value.AppAlias)) { @@ -1026,6 +1026,15 @@ private ActionResult GetResultForKey(Guid key, UmbracoEntityTypes e case UmbracoEntityTypes.Macro: + case UmbracoEntityTypes.Template: + ITemplate template = _fileService.GetTemplate(key); + if (template is null) + { + return NotFound(); + } + + return _umbracoMapper.Map(template); + default: throw new NotSupportedException("The " + typeof(EntityController) + " does not currently support data for the type " + entityType); @@ -1059,6 +1068,15 @@ private ActionResult GetResultForId(int id, UmbracoEntityTypes enti case UmbracoEntityTypes.Macro: + case UmbracoEntityTypes.Template: + ITemplate template = _fileService.GetTemplate(id); + if (template is null) + { + return NotFound(); + } + + return _umbracoMapper.Map(template); + default: throw new NotSupportedException("The " + typeof(EntityController) + " does not currently support data for the type " + entityType); @@ -1429,7 +1447,7 @@ private IEnumerable GetAllDictionaryItems() var list = new List(); foreach (IDictionaryItem dictionaryItem in _localizationService.GetRootDictionaryItems() - .OrderBy(DictionaryItemSort())) + .OrderBy(DictionaryItemSort())) { EntityBasic item = _umbracoMapper.Map(dictionaryItem); list.Add(item); @@ -1444,7 +1462,7 @@ private IEnumerable GetAllDictionaryItems() private void GetChildItemsForList(IDictionaryItem dictionaryItem, ICollection list) { foreach (IDictionaryItem childItem in _localizationService.GetDictionaryItemChildren(dictionaryItem.Key) - .OrderBy(DictionaryItemSort())) + .OrderBy(DictionaryItemSort())) { EntityBasic item = _umbracoMapper.Map(childItem); list.Add(item); diff --git a/src/Umbraco.Web.UI.Client/package-lock.json b/src/Umbraco.Web.UI.Client/package-lock.json index b282b469a36d..a9b706a67761 100644 --- a/src/Umbraco.Web.UI.Client/package-lock.json +++ b/src/Umbraco.Web.UI.Client/package-lock.json @@ -8715,9 +8715,9 @@ "dev": true }, "nouislider": { - "version": "15.4.0", - "resolved": "https://registry.npmjs.org/nouislider/-/nouislider-15.4.0.tgz", - "integrity": "sha512-AV7UMhGhZ4Mj6ToMT812Ib8OJ4tAXR2/Um7C4l4ZvvsqujF0WpQTpqqHJ+9xt4174R7ueQOUrBR4yakJpAIPCA==" + "version": "15.5.0", + "resolved": "https://registry.npmjs.org/nouislider/-/nouislider-15.5.0.tgz", + "integrity": "sha512-p0Rn0a4XzrBJ+JZRhNDYpRYr6sDPkajsjbvEQoTp/AZlNI3NirO15s1t11D25Gk3zVyvNJAzc1DO48cq/KX5Sw==" }, "now-and-later": { "version": "2.0.1", diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index ec4ead689cff..43d7a3cecddb 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -44,7 +44,7 @@ "lazyload-js": "1.0.0", "moment": "2.22.2", "ng-file-upload": "12.2.13", - "nouislider": "15.4.0", + "nouislider": "15.5.0", "npm": "^6.14.7", "spectrum-colorpicker2": "2.0.8", "tinymce": "4.9.11", diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js index 4a1988cc275c..50a32e0b0576 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js @@ -200,6 +200,10 @@ } })); + evts.push(eventsService.on("rte.shortcut.saveAndPublish", function () { + $scope.saveAndPublish(); + })); + evts.push(eventsService.on("content.saved", function () { // Clear out localstorage keys that start with tinymce__ // When we save/perist a content node diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgridselector.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgridselector.directive.js index bf03749faa7c..4c628391cb97 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgridselector.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgridselector.directive.js @@ -1,9 +1,9 @@ (function () { 'use strict'; - function GridSelector($location, overlayService, editorService) { + function GridSelector(overlayService, editorService) { - function link(scope, el, attr, ctrl) { + function link(scope) { var eventBindings = []; scope.dialogModel = {}; @@ -33,26 +33,30 @@ }; scope.openItemPicker = function ($event) { - var dialogModel = { - view: "itempicker", - title: "Choose " + scope.itemLabel, - availableItems: scope.availableItems, - selectedItems: scope.selectedItems, - position: "target", - event: $event, - submit: function (model) { - scope.selectedItems.push(model.selectedItem); - // if no default item - set item as default - if (scope.defaultItem === null) { - scope.setAsDefaultItem(model.selectedItem); + if (scope.itemPicker) { + scope.itemPicker(); + } else { + var dialogModel = { + view: "itempicker", + title: "Choose " + scope.itemLabel, + availableItems: scope.availableItems, + selectedItems: scope.selectedItems, + position: "target", + event: $event, + submit: function (model) { + scope.selectedItems.push(model.selectedItem); + // if no default item - set item as default + if (scope.defaultItem === null) { + scope.setAsDefaultItem(model.selectedItem); + } + overlayService.close(); + }, + close: function () { + overlayService.close(); } - overlayService.close(); - }, - close: function() { - overlayService.close(); - } - }; - overlayService.open(dialogModel); + }; + overlayService.open(dialogModel); + } }; scope.openTemplate = function (selectedItem) { @@ -156,7 +160,8 @@ availableItems: "=", defaultItem: "=", itemName: "@", - updatePlaceholder: "=" + updatePlaceholder: "=", + itemPicker: "=" }, link: link }; diff --git a/src/Umbraco.Web.UI.Client/src/common/services/editor.service.js b/src/Umbraco.Web.UI.Client/src/common/services/editor.service.js index fe802a8a2872..1f65bd7cea01 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/editor.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/editor.service.js @@ -179,7 +179,7 @@ When building a custom infinite editor view you can use the same components as a } else { focus(); } - }); + }); /** * @ngdoc method @@ -972,6 +972,28 @@ When building a custom infinite editor view you can use the same components as a open(editor); } + /** + * @ngdoc method + * @name umbraco.services.editorService#templatePicker + * @methodOf umbraco.services.editorService + * + * @description + * Opens a template picker in infinite editing, the submit callback returns an array of selected items. + * + * @param {object} editor rendering options. + * @param {boolean} editor.multiPicker Pick one or multiple items. + * @param {function} editor.submit Callback function when the submit button is clicked. Returns the editor model object. + * @param {function} editor.close Callback function when the close button is clicked. + * @returns {object} editor object. + */ + function templatePicker(editor) { + editor.view = "views/common/infiniteeditors/treepicker/treepicker.html"; + if (!editor.size) editor.size = "small"; + editor.section = "settings"; + editor.treeAlias = "templates"; + open(editor); + } + /** * @ngdoc method * @name umbraco.services.editorService#macroPicker @@ -1134,6 +1156,7 @@ When building a custom infinite editor view you can use the same components as a templateSections: templateSections, userPicker: userPicker, itemPicker: itemPicker, + templatePicker: templatePicker, macroPicker: macroPicker, memberGroupPicker: memberGroupPicker, memberPicker: memberPicker, 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 4c3901e63c48..4556641eca76 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 @@ -1226,6 +1226,12 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s }); }); + editor.addShortcut('Ctrl+P', '', function () { + angularHelper.safeApply($rootScope, function () { + eventsService.emit("rte.shortcut.saveAndPublish"); + }); + }); + }, insertLinkInEditor: function (editor, target, anchorElm) { diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/compositions/compositions.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/compositions/compositions.html index 7ec69018b64c..9ebb9d4e45d8 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/compositions/compositions.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/compositions/compositions.html @@ -30,21 +30,22 @@
- + Inherit tabs and properties from an existing Document Type. New tabs will be added to the current Document Type or + merged if a tab with an identical name exists.
- + There are no Content Types available to use as a composition. - + This Content Type is used in a composition, and therefore cannot be composed itself.
-
-

+
Where is this composition used?
+

This composition is currently used in the composition of the following Content Types:

  • diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/compositions/overlays/confirmremove.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/compositions/overlays/confirmremove.html index 6fc3bd826d48..33f48b8c1ea8 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/compositions/overlays/confirmremove.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/compositions/overlays/confirmremove.html @@ -1,9 +1,10 @@
    - + Removing a composition will delete all the associated property data. Once you save the Document Type there's no way + back.
    - + Are you sure?
    diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypesettings/datatypesettings.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypesettings/datatypesettings.html index a4fef2874073..d3c6a3538da0 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypesettings/datatypesettings.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypesettings/datatypesettings.html @@ -23,22 +23,22 @@
    - using this editor will get updated with the new settings. + All Document Types using this editor will get updated with the new settings.
    - +
    - +
    -
    +
    Configuration
    diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/overlays/mediacropdetails.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/overlays/mediacropdetails.html index 99f4fc04e7a2..9a01d3329da1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/overlays/mediacropdetails.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/overlays/mediacropdetails.html @@ -12,21 +12,21 @@
    - + URL
    - + Alternative text (optional)
    - + Caption (optional)
    diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/propertysettings/propertysettings.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/propertysettings/propertysettings.html index 8379062807a8..564f0911fe56 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/propertysettings/propertysettings.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/propertysettings/propertysettings.html @@ -55,9 +55,11 @@
    @@ -87,7 +89,7 @@
    -
    +
    Validation
    ng-keypress="vm.submitOnEnter($event)" />
    Current version

    {{vm.currentVersion.name}} (Created: {{vm.currentVersion.createDate}})

    -
    +
    Rollback to
    - + Required
    - +
    - + If mandatory, the child template must contain a @section definition, otherwise an error is shown.
    - +
    - +
    - +
    -
    +
    Define a named section
    - + Defines a part of your template as a named section by wrapping it in @section { ... }. + This can be rendered in a specific area of the parent of this template, by using @RenderSection. +
    - +
    - + - + Required
    - +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/treepicker/treepicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/treepicker/treepicker.controller.js index ed5c4096bcca..72eb504c600d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/treepicker/treepicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/treepicker/treepicker.controller.js @@ -141,6 +141,9 @@ angular.module("umbraco").controller("Umbraco.Editors.TreePickerController", }); } } + else if (vm.treeAlias === "templates") { + vm.entityType = "Template"; + } // TODO: Seems odd this logic is here, i don't think it needs to be and should just exist on the property editor using this if ($scope.model.minNumber) { diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.html index 8f2627795f7c..b926d9e26681 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.html @@ -1,141 +1,144 @@
    -
    - -
    - - - - - - - - - - - +
    + +
    + Your profile +
    + + + + + + + + + + +
    + +
    + +
    + External login providers +
    + +
    + +
    + +
    +
    + + +
    + + +
    -
    - -
    - External login providers -
    - -
    - -
    - -
    -
    - - -
    - - -
    - -
    - -
    - - -
    -
    - -
    - -
    - -
    - Change password -
    - -
    - - - - - - - - - - -
    - -
    - -
    -
    -
    {{tab.label}}
    -
    -
    -
    -
    +
    + + +
    +
    + Your recent history +
    + +
    + +
    + +
    + Change password +
    + +
    + + + + + + + + + + +
    + +
    + +
    +
    +
    {{tab.label}}
    +
    +
    +
    +
    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 3ad4ebc18815..e0fb4aeb7793 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 @@ -10,7 +10,7 @@
    • -
    • -
    @@ -34,7 +34,7 @@
    - : {{ vm.group.inheritedFromName }} + Inherited from: {{ vm.group.inheritedFromName }} , @@ -46,9 +46,9 @@ -
    +
    Required
    -
    +
    Required
    diff --git a/src/Umbraco.Web.UI.Client/src/views/components/contenttype/umb-content-type-property.html b/src/Umbraco.Web.UI.Client/src/views/components/contenttype/umb-content-type-property.html index 8751a4723fe5..14f647e761f4 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/contenttype/umb-content-type-property.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/contenttype/umb-content-type-property.html @@ -28,7 +28,7 @@
    {{propertyTypeForm.groupName.errorMsg}}
    -
    +
    Required label
    @@ -61,37 +61,37 @@ {{vm.property.dataTypeName}} - + Preview
    * - + Mandatory
    - + Show on member profile
    - + Member can edit
    - + Is sensitive data
    - + Vary by culture
    - + Vary by segments
    @@ -100,13 +100,13 @@
    - + Inherited from {{vm.property.contentTypeName}}
    - + Locked
    diff --git a/src/Umbraco.Web.UI.Client/src/views/components/contenttype/umb-content-type-tab.html b/src/Umbraco.Web.UI.Client/src/views/components/contenttype/umb-content-type-tab.html index 59476f7e26c6..b62e3f17d914 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/contenttype/umb-content-type-tab.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/contenttype/umb-content-type-tab.html @@ -1,6 +1,6 @@ -
    - : {{ vm.tab.inheritedFromName }} + Inherited from: {{ vm.tab.inheritedFromName }} , @@ -50,7 +50,7 @@
    {{tabNameForm.tabName.errorMsg}}
    -
    +
    Required
    @@ -61,9 +61,9 @@ -
    +
    Required
    -
    +
    Required
    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 993622e8807a..3575453bd8e9 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 @@ -55,7 +55,7 @@ ng-if="sortingMode" class="umb-group-builder__convert-dropzone" umb-droppable="droppableOptionsConvert"> - + Convert to tab
    - + You have not added any groups
    @@ -118,7 +118,7 @@ data-element="property-add" class="umb-group-builder__group-add-property" ng-click="addNewProperty(tab)"> - + Add property - + Add property
    @@ -183,6 +183,6 @@ class="umb-group-builder__group -placeholder" ng-click="addGroupToActiveTab()" data-element="group-add"> - + Add group
    diff --git a/src/Umbraco.Web.UI.Client/src/views/content/assigndomain.html b/src/Umbraco.Web.UI.Client/src/views/content/assigndomain.html index 3bf10d610e99..1625552955a9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/assigndomain.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/assigndomain.html @@ -8,7 +8,7 @@
    Culture
    - + @@ -25,18 +25,19 @@
    CultureDomains
    - + Valid domain names are: "example.com", "www.example.com", "example.com:8080", or "https://www.example.com/". + Furthermore also one-level paths in domains are supported, eg. "example.com/en" or "/en".
    @@ -49,9 +50,9 @@
    Domains - + Value cannot be empty - ({{domain.other}}) + Domain has already been assigned.({{domain.other}})
    - + Domain * - + Language * @@ -72,7 +72,7 @@
    - + Media Types
    @@ -106,7 +106,7 @@
    - + Member Types
    diff --git a/src/Umbraco.Web.UI.Client/src/views/dataTypes/views/datatype.info.html b/src/Umbraco.Web.UI.Client/src/views/dataTypes/views/datatype.info.html index 7bf985ca5618..2b59a8c4e75f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/dataTypes/views/datatype.info.html +++ b/src/Umbraco.Web.UI.Client/src/views/dataTypes/views/datatype.info.html @@ -24,7 +24,7 @@
    - + Used in Document Types
    @@ -85,7 +85,7 @@
    - + Used in Member Types
    diff --git a/src/Umbraco.Web.UI.Client/src/views/documentTypes/import.controller.js b/src/Umbraco.Web.UI.Client/src/views/documentTypes/import.controller.js index 617034824366..3983377550cf 100644 --- a/src/Umbraco.Web.UI.Client/src/views/documentTypes/import.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/documentTypes/import.controller.js @@ -9,7 +9,7 @@ angular.module("umbraco") vm.cancelButtonLabel = "cancel"; - $scope.handleFiles = function (files, event) { + $scope.handleFiles = function (files, event, invalidFiles) { if (files && files.length > 0) { $scope.upload(files[0]); } diff --git a/src/Umbraco.Web.UI.Client/src/views/documentTypes/importdocumenttype.html b/src/Umbraco.Web.UI.Client/src/views/documentTypes/importdocumenttype.html index 347f94039c3b..32322558bdd2 100644 --- a/src/Umbraco.Web.UI.Client/src/views/documentTypes/importdocumenttype.html +++ b/src/Umbraco.Web.UI.Client/src/views/documentTypes/importdocumenttype.html @@ -5,18 +5,19 @@

    - To import a document type, find the '.udt' file on your computer by clicking the 'Browse' button and click - 'Import' (you'll be asked for confirmation on the next screen) + To import a document type, find the '.udt' file on your computer by clicking the 'Import' button (you'll be + asked for confirmation on the next screen)

    -
    -
    - +
    Allowed child node types
    + Allow content of the specified types to be created underneath content of this type.
    @@ -43,8 +43,8 @@
    -
    - +
    Allow vary by culture
    + Allow editors to create content of different languages.
    @@ -61,8 +61,8 @@
    -
    - +
    Allow segmentation
    + Allow editors to create segments of this content.
    @@ -77,9 +77,9 @@
    -
    - -
    +
    Is an Element Type
    + An Element Type is meant to be used for instance in Nested Content, and not in the tree. +
    A Document Type cannot be changed to an Element Type once it has been used to create one or more content items.
    diff --git a/src/Umbraco.Web.UI.Client/src/views/documentTypes/views/templates/templates.controller.js b/src/Umbraco.Web.UI.Client/src/views/documentTypes/views/templates/templates.controller.js index e2a964c29365..46e856410d94 100644 --- a/src/Umbraco.Web.UI.Client/src/views/documentTypes/views/templates/templates.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/documentTypes/views/templates/templates.controller.js @@ -9,7 +9,7 @@ (function () { 'use strict'; - function TemplatesController($scope, entityResource, contentTypeHelper, templateResource, contentTypeResource, $routeParams) { + function TemplatesController($scope, entityResource, contentTypeHelper, contentTypeResource, editorService, $routeParams) { /* ----------- SCOPE VARIABLES ----------- */ @@ -22,6 +22,7 @@ vm.isElement = $scope.model.isElement; vm.createTemplate = createTemplate; + vm.openTemplatePicker = openTemplatePicker; /* ---------- INIT ---------- */ @@ -81,6 +82,29 @@ vm.canCreateTemplate = existingTemplate ? false : true; } + function openTemplatePicker() { + const editor = { + title: "Choose template", + filterCssClass: 'not-allowed', + multiPicker: true, + filter: item => { + return !vm.availableTemplates.some(template => template.id == item.id) || + $scope.model.allowedTemplates.some(template => template.id == item.id); + }, + submit: model => { + model.selection.forEach(item => { + $scope.model.allowedTemplates.push(item); + }); + editorService.close(); + }, + close: function() { + editorService.close(); + } + } + + editorService.templatePicker(editor); + } + var unbindWatcher = $scope.$watch("model.isElement", function(newValue, oldValue) { vm.isElement = newValue; diff --git a/src/Umbraco.Web.UI.Client/src/views/documentTypes/views/templates/templates.html b/src/Umbraco.Web.UI.Client/src/views/documentTypes/views/templates/templates.html index 279ffb73c0e4..04fd61be3cd7 100644 --- a/src/Umbraco.Web.UI.Client/src/views/documentTypes/views/templates/templates.html +++ b/src/Umbraco.Web.UI.Client/src/views/documentTypes/views/templates/templates.html @@ -17,7 +17,8 @@
    item-name="template" name="model.name" alias="model.alias" - update-placeholder="vm.updateTemplatePlaceholder"> + update-placeholder="vm.updateTemplatePlaceholder" + item-picker="vm.openTemplatePicker"> Create under {{currentNode.name}}
    -

    +

    The selected media in the tree doesn't allow for any other media to be created below it.

    -

    +

    There are no allowed Media Types available for creating media here. You must enable these in Media Types + Types within the Settings section, by editing the Allowed child node + types under Permissions.

    - + Edit permissions for this Media Type
    diff --git a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/mediafolderpicker.html b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/mediafolderpicker.html index e3449f07b9fa..f72846ea9888 100644 --- a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/mediafolderpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/mediafolderpicker.html @@ -5,7 +5,7 @@
  • - + Trashed

    @@ -18,15 +18,21 @@
    -
  • -
  • diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbBlockListPropertyEditor.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbBlockListPropertyEditor.component.js index 7334fbeadf5e..11ef37029c54 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbBlockListPropertyEditor.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbBlockListPropertyEditor.component.js @@ -65,6 +65,9 @@ vm.layout = []; // The layout object specific to this Block Editor, will be a direct reference from Property Model. vm.availableBlockTypes = []; // Available block entries of this property editor. vm.labels = {}; + vm.options = { + createFlow: false + }; localizationService.localizeMany(["grid_addElement", "content_createEmpty"]).then(function (data) { vm.labels.grid_addElement = data[0]; @@ -380,7 +383,7 @@ function editBlock(blockObject, openSettings, blockIndex, parentForm, options) { - options = options || {}; + options = options || vm.options; // this must be set if (blockIndex === undefined) { @@ -560,7 +563,9 @@ if (inlineEditing === true) { blockObject.activate(); } else if (inlineEditing === false && blockObject.hideContentInOverlay !== true) { + vm.options.createFlow = true; blockObject.edit(); + vm.options.createFlow = false; } } } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.html index e93637c67142..fa148ecfc725 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.html @@ -1,7 +1,7 @@
    -

    -

    +

    You have picked a content item currently deleted or in the recycle bin

    +

    You have picked content items currently deleted or in the recycle bin

    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/rowconfig.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/rowconfig.html index f5a6191ad13d..698ff6daeb6b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/rowconfig.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/rowconfig.html @@ -18,12 +18,14 @@
    -

    +

    Adjust the row by setting cell widths and adding additional cells

    -

    Modifying a row configuration name will result in loss of - data for any existing content that is based on this configuration.

    -

    Modifying only the label will not result in data loss.

    + +

    Modifying a row configuration name will result in loss of + data for any existing content that is based on this configuration.

    +

    Modifying only the label will not result in data loss.

    +
    @@ -70,10 +72,16 @@
    {{currentCell.grid}}
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/overlays/rowdeleteconfirm.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/overlays/rowdeleteconfirm.html index 2ba56a5b8832..ded2e5b9408f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/overlays/rowdeleteconfirm.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/overlays/rowdeleteconfirm.html @@ -11,6 +11,6 @@

    - ? + Are you sure you want to delete?
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/grid/grid.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/grid/grid.html index 07d521579315..8bbcb2204c3f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/grid/grid.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/grid/grid.html @@ -17,12 +17,12 @@ - + Sorry, we can not find what you are looking for. - + Your recycle bin is empty
    @@ -69,12 +69,12 @@ - + Your recycle bin is empty - + Sorry, we can not find what you are looking for.
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/overlays/listviewpublish.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/overlays/listviewpublish.html index c6f7c85c0b3c..b6933dffc06e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/overlays/listviewpublish.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/overlays/listviewpublish.html @@ -2,7 +2,7 @@
    -

    +

    Publishing will make the selected items visible on the site.

    @@ -13,13 +13,13 @@
    -

    +

    What languages would you like to publish?

    - + Languages
    @@ -34,7 +34,7 @@
    - + Mandatory language
    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 5806bb8f0265..9a6af50f3e6c 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 @@ -2,7 +2,7 @@
    -

    +

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

    @@ -13,13 +13,13 @@
    -

    +

    Select the languages to unpublish. Unpublishing a mandatory language will unpublish all languages.

    - + Languages
    @@ -35,7 +35,7 @@
    - + Mandatory language
    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 7b527804f586..856886a8701a 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 @@ -559,13 +559,13 @@ } }); - localizationService.localize("template_mastertemplate").then(function (value) { - var title = value; - var masterTemplate = { - title: title, - availableItems: availableMasterTemplates, - submit: function (model) { - var template = model.selectedItem; + 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"); @@ -575,14 +575,10 @@ } editorService.close(); }, - close: function (oldModel) { - // close dialog - editorService.close(); - // focus editor - vm.editor.focus(); - } - }; - editorService.itemPicker(masterTemplate); + close: () => editorService.close() + } + + editorService.templatePicker(editor); }); } diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html index 29be78241535..5569e0a985be 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html @@ -181,7 +181,7 @@ - + Sorry, we can not find what you are looking for. @@ -305,7 +305,7 @@

    Invite User

    - + Invite new users to give them access to Umbraco. An invite email will be sent to the user with information on how to log in to Umbraco. Invites last for 72 hours.

    @@ -313,7 +313,7 @@

    Create user

    - + Create new users to give them access to Umbraco. When a new user is created a password will be generated that you can share with the user.

    @@ -437,7 +437,7 @@

    -

    +

    The new user has successfully been created. To log in to Umbraco use the password below.

    @@ -520,7 +520,7 @@

    -

    +

    An invitation has been sent to the new user with details about how to log in to Umbraco.

    diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml index 748220f671c0..72ad387baed4 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml @@ -1398,6 +1398,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Remove all medias? Clipboard Not allowed + Open media picker enter external link @@ -1419,7 +1420,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Created Current version - Red text will not be shown in the selected version. , green means added]]> + Red text will be removed in the selected version, green text will be added]]> Document has been rolled back Select a version to compare with the current version This displays the selected version as HTML, if you wish to see the difference between 2 @@ -1458,7 +1459,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Default template To import a Document Type, find the ".udt" file on your computer by clicking the - "Browse" button and click "Import" (you'll be asked for confirmation on the next screen) + "Import" button (you'll be asked for confirmation on the next screen) New Tab Title Node type @@ -1711,6 +1712,9 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Choose extra Choose default are added + Warning + + Modifying a row configuration name will result in loss of data for any existing content that is based on this configuration.

    Modifying only the label will not result in data loss.

    ]]>
    You are deleting the row configuration Deleting a row configuration name will result in loss of data for any existing content that is based on this @@ -1819,6 +1823,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Removing a child node will limit the editors options to create different content types beneath a node. + using this editor will get updated with the new settings. Add language 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 7f139b2da83a..eb8538af2078 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml @@ -1424,6 +1424,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Remove all medias? Clipboard Not allowed + Open media picker enter external link @@ -1446,7 +1447,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Select a version to compare with the current version Current version - Red text will not be shown in the selected version. , green means added]]> + Red text will be removed in the selected version, green text will be added]]> Document has been rolled back This displays the selected version as HTML, if you wish to see the difference between 2 versions at the same time, use the diff view @@ -1477,7 +1478,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Default template To import a Document Type, find the ".udt" file on your computer by clicking the - "Browse" button and click "Import" (you'll be asked for confirmation on the next screen) + "Import" button (you'll be asked for confirmation on the next screen) New Tab Title Node type @@ -1753,6 +1754,9 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Choose extra Choose default are added + Warning + + Modifying a row configuration name will result in loss of data for any existing content that is based on this configuration.

    Modifying only the label will not result in data loss.

    ]]>
    You are deleting the row configuration Deleting a row configuration name will result in loss of data for any existing content that is based on this @@ -1876,6 +1880,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Removing a child node will limit the editors options to create different content types beneath a node. + using this editor will get updated with the new settings. Add language diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/nl.xml b/src/Umbraco.Web.UI/umbraco/config/lang/nl.xml index bedf544ea003..3ef84f2354d2 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/nl.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/nl.xml @@ -832,6 +832,7 @@ Blauw + Tabblad toevoegen Groep toevoegen Eigenschap toevoegen Editor toevoegen @@ -1678,6 +1679,7 @@ Echter, Runway biedt een gemakkelijke basis om je snel op weg te helpen. Als je Je hebt wijzigingen aangebracht aan deze eigenschap. Ben je zeker dat je ze wil weggooien? + Tabblad toevoegen Taal toevoegen diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Collections/StackQueueTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Collections/StackQueueTests.cs index 4caf7bd00536..c20a5a3acaa4 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Collections/StackQueueTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Collections/StackQueueTests.cs @@ -1,5 +1,5 @@ using NUnit.Framework; -using Umbraco.Core.Collections; +using Umbraco.Cms.Core.Collections; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Collections { @@ -10,7 +10,7 @@ public class StackQueueTests public void Queue() { var sq = new StackQueue(); - for (int i = 0; i < 3; i++) + for (var i = 0; i < 3; i++) { sq.Enqueue(i); } @@ -28,7 +28,7 @@ public void Queue() public void Stack() { var sq = new StackQueue(); - for (int i = 0; i < 3; i++) + for (var i = 0; i < 3; i++) { sq.Push(i); } @@ -46,7 +46,7 @@ public void Stack() public void Stack_And_Queue() { var sq = new StackQueue(); - for (int i = 0; i < 5; i++) + for (var i = 0; i < 5; i++) { if (i % 2 == 0) {