diff --git a/src/Umbraco.Core/Models/Rdbms/UserGroupDto.cs b/src/Umbraco.Core/Models/Rdbms/UserGroupDto.cs index fdad78bfcd..f6bb09066d 100644 --- a/src/Umbraco.Core/Models/Rdbms/UserGroupDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/UserGroupDto.cs @@ -51,10 +51,12 @@ namespace Umbraco.Core.Models.Rdbms [Column("startContentId")] [NullSetting(NullSetting = NullSettings.Null)] + [ForeignKey(typeof(NodeDto), Name = "FK_startContentId_umbracoNode_id")] public int? StartContentId { get; set; } [Column("startMediaId")] [NullSetting(NullSetting = NullSettings.Null)] + [ForeignKey(typeof(NodeDto), Name = "FK_startMediaId_umbracoNode_id")] public int? StartMediaId { get; set; } [ResultColumn] diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSevenZero/AddUserGroupTables.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSevenZero/AddUserGroupTables.cs index 05eef52e37..bac46f0de7 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSevenZero/AddUserGroupTables.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSevenZero/AddUserGroupTables.cs @@ -29,6 +29,45 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenSevenZe DeleteOldTables(tables, constraints); SetDefaultIcons(); } + else + { + //if we aren't adding the tables, make sure that the umbracoUserGroup table has the correct FKs - these + //were added after the beta release so we need to do some cleanup + //if the FK doesn't exist + if (constraints.Any(x => x.Item1.InvariantEquals("umbracoUserGroup") + && x.Item2.InvariantEquals("startContentId") + && x.Item3.InvariantEquals("FK_startContentId_umbracoNode_id")) == false) + { + //before we add any foreign key we need to make sure there's no stale data in there which would have happened in the beta + //release if a start node was assigned and then that start node was deleted. + Execute.Sql(@"UPDATE umbracoUserGroup SET startContentId = NULL WHERE startContentId NOT IN (SELECT id FROM umbracoNode)"); + + Create.ForeignKey("FK_startContentId_umbracoNode_id") + .FromTable("umbracoUserGroup") + .ForeignColumn("startContentId") + .ToTable("umbracoNode") + .PrimaryColumn("id") + .OnDelete(Rule.None) + .OnUpdate(Rule.None); + } + + if (constraints.Any(x => x.Item1.InvariantEquals("umbracoUserGroup") + && x.Item2.InvariantEquals("startMediaId") + && x.Item3.InvariantEquals("FK_startMediaId_umbracoNode_id")) == false) + { + //before we add any foreign key we need to make sure there's no stale data in there which would have happened in the beta + //release if a start node was assigned and then that start node was deleted. + Execute.Sql(@"UPDATE umbracoUserGroup SET startMediaId = NULL WHERE startMediaId NOT IN (SELECT id FROM umbracoNode)"); + + Create.ForeignKey("FK_startMediaId_umbracoNode_id") + .FromTable("umbracoUserGroup") + .ForeignColumn("startMediaId") + .ToTable("umbracoNode") + .PrimaryColumn("id") + .OnDelete(Rule.None) + .OnUpdate(Rule.None); + } + } } private void SetDefaultIcons() diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs index 236f0ed311..d384106293 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs @@ -184,6 +184,8 @@ namespace Umbraco.Core.Persistence.Repositories "DELETE FROM cmsTask WHERE nodeId = @Id", "DELETE FROM umbracoUser2NodeNotify WHERE nodeId = @Id", "DELETE FROM umbracoUserGroup2NodePermission WHERE nodeId = @Id", + "DELETE FROM umbracoUserStartNode WHERE startNode = @Id", + "UPDATE umbracoUserGroup SET startContentId = NULL WHERE startContentId = @Id", "DELETE FROM umbracoRelation WHERE parentId = @Id", "DELETE FROM umbracoRelation WHERE childId = @Id", "DELETE FROM cmsTagRelationship WHERE nodeId = @Id", @@ -195,7 +197,7 @@ namespace Umbraco.Core.Persistence.Repositories "DELETE FROM cmsContentXml WHERE nodeId = @Id", "DELETE FROM cmsContent WHERE nodeId = @Id", "DELETE FROM umbracoAccess WHERE nodeId = @Id", - "DELETE FROM umbracoNode WHERE id = @Id" + "DELETE FROM umbracoNode WHERE id = @Id" }; return list; } @@ -1234,31 +1236,10 @@ ORDER BY cmsContentVersion.id DESC if (EnsureUniqueNaming == false) return nodeName; - var sql = new Sql(); - sql.Select("*") - .From() - .Where(x => x.NodeObjectType == NodeObjectTypeId && x.ParentId == parentId && x.Text.StartsWith(nodeName)); + var names = Database.Fetch("SELECT id, text AS name FROM umbracoNode WHERE nodeObjectType=@objectType AND parentId=@parentId", + new { objectType = NodeObjectTypeId, parentId }); - int uniqueNumber = 1; - var currentName = nodeName; - - var dtos = Database.Fetch(sql); - if (dtos.Any()) - { - var results = dtos.OrderBy(x => x.Text, new SimilarNodeNameComparer()); - foreach (var dto in results) - { - if (id != 0 && id == dto.NodeId) continue; - - if (dto.Text.ToLowerInvariant().Equals(currentName.ToLowerInvariant())) - { - currentName = nodeName + string.Format(" ({0})", uniqueNumber); - uniqueNumber++; - } - } - } - - return currentName; + return SimilarNodeName.GetUniqueName(names, id, nodeName); } /// diff --git a/src/Umbraco.Core/Persistence/Repositories/DataTypeDefinitionRepository.cs b/src/Umbraco.Core/Persistence/Repositories/DataTypeDefinitionRepository.cs index 8c263f4211..44be068b4b 100644 --- a/src/Umbraco.Core/Persistence/Repositories/DataTypeDefinitionRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/DataTypeDefinitionRepository.cs @@ -450,33 +450,10 @@ AND umbracoNode.id <> @id", private string EnsureUniqueNodeName(string nodeName, int id = 0) { + var names = Database.Fetch("SELECT id, text AS name FROM umbracoNode WHERE nodeObjectType=@objectType", + new { objectType = NodeObjectTypeId }); - - var sql = new Sql(); - sql.Select("*") - .From(SqlSyntax) - .Where(x => x.NodeObjectType == NodeObjectTypeId && x.Text.StartsWith(nodeName)); - - int uniqueNumber = 1; - var currentName = nodeName; - - var dtos = Database.Fetch(sql); - if (dtos.Any()) - { - var results = dtos.OrderBy(x => x.Text, new SimilarNodeNameComparer()); - foreach (var dto in results) - { - if (id != 0 && id == dto.NodeId) continue; - - if (dto.Text.ToLowerInvariant().Equals(currentName.ToLowerInvariant())) - { - currentName = nodeName + string.Format(" ({0})", uniqueNumber); - uniqueNumber++; - } - } - } - - return currentName; + return SimilarNodeName.GetUniqueName(names, id, nodeName); } /// diff --git a/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs index ad2dd26a43..48b9698474 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs @@ -120,6 +120,8 @@ namespace Umbraco.Core.Persistence.Repositories "DELETE FROM cmsTask WHERE nodeId = @Id", "DELETE FROM umbracoUser2NodeNotify WHERE nodeId = @Id", "DELETE FROM umbracoUserGroup2NodePermission WHERE nodeId = @Id", + "DELETE FROM umbracoUserStartNode WHERE startNode = @Id", + "UPDATE umbracoUserGroup SET startMediaId = NULL WHERE startMediaId = @Id", "DELETE FROM umbracoRelation WHERE parentId = @Id", "DELETE FROM umbracoRelation WHERE childId = @Id", "DELETE FROM cmsTagRelationship WHERE nodeId = @Id", @@ -684,31 +686,10 @@ namespace Umbraco.Core.Persistence.Repositories if (EnsureUniqueNaming == false) return nodeName; - var sql = new Sql(); - sql.Select("*") - .From() - .Where(x => x.NodeObjectType == NodeObjectTypeId && x.ParentId == parentId && x.Text.StartsWith(nodeName)); + var names = Database.Fetch("SELECT id, text AS name FROM umbracoNode WHERE nodeObjectType=@objectType AND parentId=@parentId", + new { objectType = NodeObjectTypeId, parentId }); - int uniqueNumber = 1; - var currentName = nodeName; - - var dtos = Database.Fetch(sql); - if (dtos.Any()) - { - var results = dtos.OrderBy(x => x.Text, new SimilarNodeNameComparer()); - foreach (var dto in results) - { - if (id != 0 && id == dto.NodeId) continue; - - if (dto.Text.ToLowerInvariant().Equals(currentName.ToLowerInvariant())) - { - currentName = nodeName + string.Format(" ({0})", uniqueNumber); - uniqueNumber++; - } - } - } - - return currentName; + return SimilarNodeName.GetUniqueName(names, id, nodeName); } /// diff --git a/src/Umbraco.Core/Persistence/Repositories/RecycleBinRepository.cs b/src/Umbraco.Core/Persistence/Repositories/RecycleBinRepository.cs index efb7487fdf..efb93bcff3 100644 --- a/src/Umbraco.Core/Persistence/Repositories/RecycleBinRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/RecycleBinRepository.cs @@ -53,7 +53,10 @@ namespace Umbraco.Core.Persistence.Repositories @"DELETE FROM umbracoRedirectUrl WHERE umbracoRedirectUrl.id IN( SELECT TB1.id FROM umbracoRedirectUrl as TB1 INNER JOIN umbracoNode as TB2 ON TB1.contentKey = TB2.uniqueId - WHERE TB2.trashed = '1' AND TB2.nodeObjectType = @NodeObjectType)", + WHERE TB2.trashed = '1' AND TB2.nodeObjectType = @NodeObjectType)", + FormatDeleteStatement("umbracoUserStartNode", "startNode"), + FormatUpdateStatement("umbracoUserGroup", "startContentId"), + FormatUpdateStatement("umbracoUserGroup", "startMediaId"), FormatDeleteStatement("umbracoRelation", "parentId"), FormatDeleteStatement("umbracoRelation", "childId"), FormatDeleteStatement("cmsTagRelationship", "nodeId"), @@ -108,6 +111,21 @@ namespace Umbraco.Core.Persistence.Repositories tableName, keyName); } + /// + /// An update statement that will update a value to NULL in the table specified where its PK (keyName) is found in the + /// list of umbracoNode.id that have trashed flag set + /// + /// + /// + /// + private string FormatUpdateStatement(string tableName, string keyName) + { + return + string.Format( + "UPDATE {0} SET {0}.{1} = NULL WHERE {0}.{1} IN (SELECT id FROM umbracoNode WHERE trashed = '1' AND nodeObjectType = @NodeObjectType)", + tableName, keyName); + } + /// /// Gets a list of files, which are referenced on items in the Recycle Bin. /// The list is generated by the convention that a file is referenced by diff --git a/src/Umbraco.Core/Persistence/Repositories/SimilarNodeName.cs b/src/Umbraco.Core/Persistence/Repositories/SimilarNodeName.cs new file mode 100644 index 0000000000..371f73b27f --- /dev/null +++ b/src/Umbraco.Core/Persistence/Repositories/SimilarNodeName.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Umbraco.Core.Persistence.Repositories +{ + internal class SimilarNodeName + { + private int _numPos = -2; + + public int Id { get; set; } + public string Name { get; set; } + + // cached - reused + public int NumPos + { + get + { + if (_numPos != -2) return _numPos; + + var name = Name; + + if (name[name.Length - 1] != ')') + return _numPos = -1; + + var pos = name.LastIndexOf('('); + if (pos < 2 || pos == name.Length - 2) // < 2 and not < 0, because we want at least "x (" + return _numPos = -1; + + return _numPos = pos; + } + } + + // not cached - used only once + public int NumVal + { + get + { + if (NumPos < 0) + throw new InvalidOperationException(); + int num; + if (int.TryParse(Name.Substring(NumPos + 1, Name.Length - 2 - NumPos), out num)) + return num; + return 0; + } + } + + // compare without allocating, nor parsing integers + internal class Comparer : IComparer + { + public int Compare(SimilarNodeName x, SimilarNodeName y) + { + if (x == null) throw new ArgumentNullException("x"); + if (y == null) throw new ArgumentNullException("y"); + + var xpos = x.NumPos; + var ypos = y.NumPos; + + var xname = x.Name; + var yname = y.Name; + + if (xpos < 0 || ypos < 0 || xpos != ypos) + return string.Compare(xname, yname, StringComparison.Ordinal); + + // compare the part before (number) + var n = string.Compare(xname, 0, yname, 0, xpos, StringComparison.Ordinal); + if (n != 0) + return n; + + // compare (number) lengths + var diff = xname.Length - yname.Length; + if (diff != 0) return diff < 0 ? -1 : +1; + + // actually compare (number) + var i = xpos; + while (i < xname.Length - 1) + { + if (xname[i] != yname[i]) + return xname[i] < yname[i] ? -1 : +1; + i++; + } + return 0; + } + } + + // gets a unique name + public static string GetUniqueName(IEnumerable names, int nodeId, string nodeName) + { + var uniqueNumber = 1; + var uniqueing = false; + foreach (var name in names.OrderBy(x => x, new Comparer())) + { + // ignore self + if (nodeId != 0 && name.Id == nodeId) continue; + + if (uniqueing) + { + if (name.NumPos > 0 && name.Name.StartsWith(nodeName) && name.NumVal == uniqueNumber) + uniqueNumber++; + else + break; + } + else if (name.Name.InvariantEquals(nodeName)) + { + uniqueing = true; + } + } + + return uniqueing ? string.Concat(nodeName, " (", uniqueNumber.ToString(), ")") : nodeName; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/SimilarNodeNameComparer.cs b/src/Umbraco.Core/Persistence/Repositories/SimilarNodeNameComparer.cs deleted file mode 100644 index ce25486422..0000000000 --- a/src/Umbraco.Core/Persistence/Repositories/SimilarNodeNameComparer.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Umbraco.Core.Persistence.Repositories -{ - /// - /// Comparer that takes into account the duplicate index of a node name - /// This is needed as a normal alphabetic sort would go Page (1), Page (10), Page (2) etc. - /// - internal class SimilarNodeNameComparer : IComparer - { - public int Compare(string x, string y) - { - if (x.LastIndexOf('(') != -1 && x.LastIndexOf(')') == x.Length - 1 && y.LastIndexOf(')') == y.Length - 1) - { - if (x.ToLower().Substring(0, x.LastIndexOf('(')) == y.ToLower().Substring(0, y.LastIndexOf('('))) - { - int xDuplicateIndex = ExtractDuplicateIndex(x); - int yDuplicateIndex = ExtractDuplicateIndex(y); - - if (xDuplicateIndex != 0 && yDuplicateIndex != 0) - { - return xDuplicateIndex.CompareTo(yDuplicateIndex); - } - } - } - return String.Compare(x.ToLower(), y.ToLower(), StringComparison.Ordinal); - } - - private int ExtractDuplicateIndex(string text) - { - int index = 0; - - if (text.LastIndexOf('(') != -1 && text.LastIndexOf('(') < text.Length - 2) - { - int startPos = text.LastIndexOf('(') + 1; - int length = text.Length - 1 - startPos; - - int.TryParse(text.Substring(startPos, length), out index); - } - - return index; - } - } -} \ No newline at end of file diff --git a/src/Umbraco.Core/Security/BackOfficeUserManager.cs b/src/Umbraco.Core/Security/BackOfficeUserManager.cs index 76cf2971ea..5e04659b43 100644 --- a/src/Umbraco.Core/Security/BackOfficeUserManager.cs +++ b/src/Umbraco.Core/Security/BackOfficeUserManager.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; using System.Linq; using System.Text; using System.Threading.Tasks; @@ -35,6 +36,19 @@ namespace Umbraco.Core.Security #region Static Create methods + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Use the overload specifying all dependencies instead")] + public static BackOfficeUserManager Create( + IdentityFactoryOptions options, + IUserService userService, + IExternalLoginService externalLoginService, + MembershipProviderBase membershipProvider) + { + return Create(options, userService, + ApplicationContext.Current.Services.EntityService, + externalLoginService, membershipProvider); + } + /// /// Creates a BackOfficeUserManager instance with all default options and the default BackOfficeUserManager /// diff --git a/src/Umbraco.Core/StringExtensions.cs b/src/Umbraco.Core/StringExtensions.cs index 0780fe799c..5ef09d55a0 100644 --- a/src/Umbraco.Core/StringExtensions.cs +++ b/src/Umbraco.Core/StringExtensions.cs @@ -890,7 +890,7 @@ namespace Umbraco.Core } /// - /// Ensures that the folder path endds with a DirectorySeperatorChar + /// Ensures that the folder path ends with a DirectorySeperatorChar /// /// /// diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index a676d8caa3..026ae03c44 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -628,6 +628,7 @@ + @@ -1231,7 +1232,6 @@ - diff --git a/src/Umbraco.Tests/Persistence/Repositories/SimilarNodeNameTests.cs b/src/Umbraco.Tests/Persistence/Repositories/SimilarNodeNameTests.cs new file mode 100644 index 0000000000..72f3e39874 --- /dev/null +++ b/src/Umbraco.Tests/Persistence/Repositories/SimilarNodeNameTests.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using NUnit.Framework; +using Umbraco.Core.Persistence.Repositories; + +namespace Umbraco.Tests.Persistence.Repositories +{ + [TestFixture] + public class SimilarNodeNameTests + { + [TestCase("Alpha", "Alpha", 0)] + [TestCase("Alpha", "ALPHA", +1)] // case is important + [TestCase("Alpha", "Bravo", -1)] + [TestCase("Bravo", "Alpha", +1)] + [TestCase("Alpha (1)", "Alpha (1)", 0)] + [TestCase("Alpha", "Alpha (1)", -1)] + [TestCase("Alpha (1)", "Alpha", +1)] + [TestCase("Alpha (1)", "Alpha (2)", -1)] + [TestCase("Alpha (2)", "Alpha (1)", +1)] + [TestCase("Alpha (2)", "Alpha (10)", -1)] // this is the real stuff + [TestCase("Alpha (10)", "Alpha (2)", +1)] // this is the real stuff + [TestCase("Kilo", "Golf (2)", +1)] + [TestCase("Kilo (1)", "Golf (2)", +1)] + public void ComparerTest(string name1, string name2, int expected) + { + var comparer = new SimilarNodeName.Comparer(); + + var result = comparer.Compare(new SimilarNodeName { Name = name1 }, new SimilarNodeName { Name = name2 }); + if (expected == 0) + Assert.AreEqual(0, result); + else if (expected < 0) + Assert.IsTrue(result < 0, "Expected <0 but was " + result); + else if (expected > 0) + Assert.IsTrue(result > 0, "Expected >0 but was " + result); + } + + [Test] + public void OrderByTest() + { + var names = new[] + { + new SimilarNodeName { Id = 1, Name = "Alpha (2)" }, + new SimilarNodeName { Id = 2, Name = "Alpha" }, + new SimilarNodeName { Id = 3, Name = "Golf" }, + new SimilarNodeName { Id = 4, Name = "Zulu" }, + new SimilarNodeName { Id = 5, Name = "Mike" }, + new SimilarNodeName { Id = 6, Name = "Kilo (1)" }, + new SimilarNodeName { Id = 7, Name = "Yankee" }, + new SimilarNodeName { Id = 8, Name = "Kilo" }, + new SimilarNodeName { Id = 9, Name = "Golf (2)" }, + new SimilarNodeName { Id = 10, Name = "Alpha (1)" }, + }; + + var ordered = names.OrderBy(x => x, new SimilarNodeName.Comparer()).ToArray(); + + var i = 0; + Assert.AreEqual(2, ordered[i++].Id); + Assert.AreEqual(10, ordered[i++].Id); + Assert.AreEqual(1, ordered[i++].Id); + Assert.AreEqual(3, ordered[i++].Id); + Assert.AreEqual(9, ordered[i++].Id); + Assert.AreEqual(8, ordered[i++].Id); + Assert.AreEqual(6, ordered[i++].Id); + Assert.AreEqual(5, ordered[i++].Id); + Assert.AreEqual(7, ordered[i++].Id); + Assert.AreEqual(4, ordered[i++].Id); + } + + [TestCase(0, "Charlie", "Charlie")] + [TestCase(0, "Zulu", "Zulu (1)")] + [TestCase(0, "Golf", "Golf (1)")] + [TestCase(0, "Kilo", "Kilo (2)")] + [TestCase(0, "Alpha", "Alpha (3)")] + [TestCase(0, "Kilo (1)", "Kilo (1) (1)")] // though... we might consider "Kilo (2)" + [TestCase(6, "Kilo (1)", "Kilo (1)")] // because of the id + public void Test(int nodeId, string nodeName, string expected) + { + var names = new[] + { + new SimilarNodeName { Id = 1, Name = "Alpha (2)" }, + new SimilarNodeName { Id = 2, Name = "Alpha" }, + new SimilarNodeName { Id = 3, Name = "Golf" }, + new SimilarNodeName { Id = 4, Name = "Zulu" }, + new SimilarNodeName { Id = 5, Name = "Mike" }, + new SimilarNodeName { Id = 6, Name = "Kilo (1)" }, + new SimilarNodeName { Id = 7, Name = "Yankee" }, + new SimilarNodeName { Id = 8, Name = "Kilo" }, + new SimilarNodeName { Id = 9, Name = "Golf (2)" }, + new SimilarNodeName { Id = 10, Name = "Alpha (1)" }, + }; + + Assert.AreEqual(expected, SimilarNodeName.GetUniqueName(names, nodeId, nodeName)); + } + } +} diff --git a/src/Umbraco.Tests/Services/ContentServiceTests.cs b/src/Umbraco.Tests/Services/ContentServiceTests.cs index 5a2b9f23e7..c9daeb0022 100644 --- a/src/Umbraco.Tests/Services/ContentServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentServiceTests.cs @@ -1583,6 +1583,14 @@ namespace Umbraco.Tests.Services ServiceContext.ContentService.Save(content2, 0); Assert.IsTrue(ServiceContext.ContentService.PublishWithStatus(content2, 0).Success); + var editorGroup = ServiceContext.UserService.GetUserGroupByAlias("editor"); + editorGroup.StartContentId = content1.Id; + ServiceContext.UserService.Save(editorGroup); + + var admin = ServiceContext.UserService.GetUserById(0); + admin.StartContentIds = new[] {content1.Id}; + ServiceContext.UserService.Save(admin); + ServiceContext.RelationService.Save(new RelationType(Constants.ObjectTypes.DocumentGuid, Constants.ObjectTypes.DocumentGuid, "test")); Assert.IsNotNull(ServiceContext.RelationService.Relate(content1, content2, "test")); @@ -1608,6 +1616,7 @@ namespace Umbraco.Tests.Services }).Success); // Act + ServiceContext.ContentService.MoveToRecycleBin(content1); ServiceContext.ContentService.EmptyRecycleBin(); var contents = ServiceContext.ContentService.GetContentInRecycleBin(); diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 2791087173..5474d445cd 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -187,6 +187,7 @@ + diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/html/umbbox/umbboxheader.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/html/umbbox/umbboxheader.directive.js index 4ed1d68e59..bb16edf761 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/html/umbbox/umbboxheader.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/html/umbbox/umbboxheader.directive.js @@ -10,7 +10,7 @@ Use this directive to construct a title. Recommended to use it inside an {@link

Markup example

     
-        
+        
         
             // Content here
         
@@ -21,7 +21,7 @@ Use this directive to construct a title. Recommended to use it inside an {@link
 
     
         // the title-key property needs an areaAlias_keyAlias from the language files
-        
+        
         
             // Content here
         
@@ -35,8 +35,10 @@ Use this directive to construct a title. Recommended to use it inside an {@link
     
  • {@link umbraco.directives.directive:umbBoxContent umbBoxContent}
  • -@param {string} title (attrbute): Custom title text. -@param {string} title-key (attrbute): the key alias of the language xml files. +@param {string=} title (attrbute): Custom title text. +@param {string=} titleKey (attrbute): The translation key from the language xml files. +@param {string=} description (attrbute): Custom description text. +@param {string=} descriptionKey (attrbute): The translation key from the language xml files. **/ @@ -52,7 +54,9 @@ Use this directive to construct a title. Recommended to use it inside an {@link templateUrl: 'views/components/html/umb-box/umb-box-header.html', scope: { titleKey: "@?", - title: "@?" + title: "@?", + descriptionKey: "@?", + description: "@?" } }; diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/users/umbusergrouppreview.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/users/umbusergrouppreview.directive.js index 7623dc0ae8..fdbfd088ab 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/users/umbusergrouppreview.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/users/umbusergrouppreview.directive.js @@ -1,3 +1,51 @@ +/** +@ngdoc directive +@name umbraco.directives.directive:umbUserGroupPreview +@restrict E +@scope + +@description +Use this directive to render a user group preview, where you can see the permissions the user or group has in the back office. + +

    Markup example

    +
    +    
    + + +
    +
    + +@param {string} icon (binding): The user group icon. +@param {string} name (binding): The user group name. +@param {array} sections (binding) Lists out the sections where the user has authority to edit. +@param {string} contentStartNode (binding) +
      +
    • The starting point in the tree of the content section.
    • +
    • So the user has no authority to work on other branches, only on this branch in the content section.
    • +
    +@param {boolean} hideContentStartNode (binding) Hides the contentStartNode. +@param {string} mediaStartNode (binding) +
      +
    • The starting point in the tree of the media section.
    • +
    • So the user has no authority to work on other branches, only on this branch in the media section.
    • +
    +@param {boolean} hideMediaStartNode (binding) Hides the mediaStartNode. +@param {array} permissions (binding) A list of permissions, the user can have. +@param {boolean} allowRemove (binding): Shows or Hides the remove button. +@param {function} onRemove (expression): Callback function when the remove button is clicked. +@param {boolean} allowEdit (binding): Shows or Hides the edit button. +@param {function} onEdit (expression): Callback function when the edit button is clicked. +**/ + + (function () { 'use strict'; @@ -14,9 +62,8 @@ scope: { icon: "=?", name: "=", - description: "=?", sections: "=?", - contentStartNode: "=?", + contentStartNode: "=?", hideContentStartNode: "@?", mediaStartNode: "=?", hideMediaStartNode: "@?", diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-box.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-box.less index cfbd929a3a..492a257b38 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-box.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-box.less @@ -15,6 +15,13 @@ font-weight: bold; } +.umb-box-header-description { + font-size: 13px; + color: @gray-3; + line-height: 1.6em; + margin-top: 1px; +} + .umb-box-content { padding: 20px; } \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/components/html/umb-box/umb-box-header.html b/src/Umbraco.Web.UI.Client/src/views/components/html/umb-box/umb-box-header.html index 6feffe9d3d..8c59061788 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/html/umb-box/umb-box-header.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/html/umb-box/umb-box-header.html @@ -1,6 +1,10 @@
    -
    +
    {{title}}
    +
    + + {{description}} +
    \ No newline at end of file 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 949374cbc2..cf1c3f6411 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 @@ -14,8 +14,8 @@ -
    No content has been added
    -
    No members have been added
    +
    No content has been added
    +
    No members have been added
    -
    No content has been added
    -
    No members have been added
    +
    No content has been added
    +
    No members have been added
    diff --git a/src/Umbraco.Web.UI.Client/src/views/users/group.html b/src/Umbraco.Web.UI.Client/src/views/users/group.html index f1427e3266..455f2177c6 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/group.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/group.html @@ -23,7 +23,7 @@ - + diff --git a/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js index a9b17998d5..89b1334baf 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js @@ -66,11 +66,7 @@ vm.user = user; makeBreadcrumbs(vm.user); setUserDisplayState(); - - // format dates to local - if(vm.user.lastLoginDate) { - vm.user.formattedLastLogin = getLocalDate(vm.user.lastLoginDate, "MMMM Do YYYY, HH:mm"); - } + formatDatesToLocal(vm.user); vm.emailIsUsername = user.email === user.username; @@ -89,18 +85,20 @@ } function getLocalDate(date, format) { - var dateVal; - var serverOffset = Umbraco.Sys.ServerVariables.application.serverTimeOffset; - var localOffset = new Date().getTimezoneOffset(); - var serverTimeNeedsOffsetting = (-serverOffset !== localOffset); + if(date) { + var dateVal; + var serverOffset = Umbraco.Sys.ServerVariables.application.serverTimeOffset; + var localOffset = new Date().getTimezoneOffset(); + var serverTimeNeedsOffsetting = (-serverOffset !== localOffset); - if(serverTimeNeedsOffsetting) { - dateVal = dateHelper.convertToLocalMomentTime(date, serverOffset); - } else { - dateVal = moment(date, "YYYY-MM-DD HH:mm:ss"); + if(serverTimeNeedsOffsetting) { + dateVal = dateHelper.convertToLocalMomentTime(date, serverOffset); + } else { + dateVal = moment(date, "YYYY-MM-DD HH:mm:ss"); + } + + return dateVal.format(format); } - - return dateVal.format(format); } function toggleChangePassword() { @@ -128,6 +126,7 @@ vm.user = saved; setUserDisplayState(); + formatDatesToLocal(vm.user); vm.changePasswordModel.isChanging = false; vm.page.saveButtonState = "success"; @@ -380,6 +379,14 @@ vm.user.userDisplayState = usersHelper.getUserStateFromValue(vm.user.userState); } + function formatDatesToLocal(user) { + user.formattedLastLogin = getLocalDate(user.lastLoginDate, "MMMM Do YYYY, HH:mm"); + user.formattedLastLockoutDate = getLocalDate(user.lastLockoutDate, "MMMM Do YYYY, HH:mm"); + user.formattedCreateDate = getLocalDate(user.createDate, "MMMM Do YYYY, HH:mm"); + user.formattedUpdateDate = getLocalDate(user.updateDate, "MMMM Do YYYY, HH:mm"); + user.formattedLastPasswordChangeDate = getLocalDate(user.lastPasswordChangeDate, "MMMM Do YYYY, HH:mm"); + } + init(); } angular.module("umbraco").controller("Umbraco.Editors.Users.UserController", UserEditController); diff --git a/src/Umbraco.Web.UI.Client/src/views/users/user.html b/src/Umbraco.Web.UI.Client/src/views/users/user.html index 88ae4bf46e..ebe493fce8 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/user.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/user.html @@ -1,9 +1,5 @@
    - - -
    @@ -17,6 +13,10 @@ + + +
    @@ -73,7 +73,7 @@ - + @@ -87,7 +87,7 @@ media-start-node="userGroup.mediaStartNode" allow-remove="!vm.user.isCurrentUser" on-remove="vm.removeSelectedItem($index, vm.user.userGroups)"> - + + + + + + + + + + + + + + + + + + + + + +
    +
    @@ -209,27 +245,8 @@
    -
    -
    - Status:
    -
    - - {{vm.user.userDisplayState.name}} - -
    -
    - -
    -
    - Last login: -
    -
    - {{ vm.user.formattedLastLogin }} - {{ vm.user.name }} has not logged in yet -
    -
    - -
    + +
    + +
    +
    + Status: +
    +
    + + {{vm.user.userDisplayState.name}} + +
    +
    + +
    +
    + Last login: +
    +
    + {{ vm.user.formattedLastLogin }} + {{ vm.user.name | umbWordLimit:1 }} has not logged in yet +
    +
    + +
    +
    + Failed login attempts: +
    +
    + {{ vm.user.failedPasswordAttempts }} +
    +
    + +
    +
    + Last lockout date: +
    +
    + + {{ vm.user.name | umbWordLimit:1 }} hasn't been locked out + + {{ vm.user.formattedLastLockoutDate }} +
    +
    + +
    +
    + Password is last changed: +
    +
    + + The password hasn't been changed + + {{ vm.user.formattedLastPasswordChangeDate }} +
    +
    + +
    +
    + User is created: +
    +
    + {{ vm.user.formattedCreateDate }} +
    +
    + +
    +
    + User is last updated: +
    +
    + {{ vm.user.formattedUpdateDate }} +
    +
    +
    @@ -332,7 +422,8 @@ type="button" action="vm.goToPage(vm.breadcrumbs[0])" label="Return to list" - label-key="buttons_returnToList"> + label-key="buttons_returnToList" + disabled="vm.loading"> + label-key="buttons_save" + disabled="vm.loading"> diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml index 122fec64df..5d0779f8b6 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml @@ -172,6 +172,8 @@ Last published There are no items to show There are no items to show in the list. + No content has been added + No members have been added Media Type Link to media item(s) Member Group 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 7d949cdbe5..ed0edf8b6d 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml @@ -175,6 +175,8 @@ Last published There are no items to show There are no items to show in the list. + No content has been added + No members have been added Media Type Link to media item(s) Member Group @@ -1584,11 +1586,16 @@ To manage your website, simply open the Umbraco back office and start adding con Access + Based on the assigned groups and start nodes, the user has access to the following nodes + Assign access Administrator Category field + User created Change Your Password Change photo New password + hasn't been locked out + The password hasn't been changed Confirm new password You can change your password for accessing the Umbraco Back Office by filling out the form below and click the 'Change Password' button Content Channel @@ -1599,13 +1606,16 @@ To manage your website, simply open the Umbraco back office and start adding con Document Type Editor Excerpt field + Failed login attempts Go to user profile Add groups to assign access and permissions Invite another 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. Language Set the language you will see in menus and dialogs + Last lockout date Last login + Password last changed Login Media start node Limit the media library to a specific start node @@ -1641,6 +1651,7 @@ To manage your website, simply open the Umbraco back office and start adding con Limit the content tree to a specific start node Content start nodes Limit the content tree to specific start nodes + User last updated has been created The new user has successfully been created. To log in to Umbraco use the password below. Name diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/ru.xml b/src/Umbraco.Web.UI/umbraco/config/lang/ru.xml index 6228faead2..01692dd593 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/ru.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/ru.xml @@ -181,6 +181,8 @@ Документ опубликован Здесь еще нет элементов. В этом списке пока нет элементов. + Содержимое пока еще не добавлено + Участники пока еще не добавлены Ссылка на медиа-элементы Тип медиа-контента Группа участников diff --git a/src/Umbraco.Web/HealthCheck/Checks/Permissions/FolderAndFilePermissionsCheck.cs b/src/Umbraco.Web/HealthCheck/Checks/Permissions/FolderAndFilePermissionsCheck.cs index 36e774287b..ff8c2646cc 100644 --- a/src/Umbraco.Web/HealthCheck/Checks/Permissions/FolderAndFilePermissionsCheck.cs +++ b/src/Umbraco.Web/HealthCheck/Checks/Permissions/FolderAndFilePermissionsCheck.cs @@ -59,12 +59,10 @@ namespace Umbraco.Web.HealthCheck.Checks.Permissions // in ALL circumstances or just some var pathsToCheck = new Dictionary { - { SystemDirectories.AppCode, PermissionCheckRequirement.Optional }, { SystemDirectories.Data, PermissionCheckRequirement.Required }, { SystemDirectories.Packages, PermissionCheckRequirement.Required}, { SystemDirectories.Preview, PermissionCheckRequirement.Required }, { SystemDirectories.AppPlugins, PermissionCheckRequirement.Required }, - { SystemDirectories.Bin, PermissionCheckRequirement.Optional }, { SystemDirectories.Config, PermissionCheckRequirement.Optional }, { SystemDirectories.Css, PermissionCheckRequirement.Optional }, { SystemDirectories.Masterpages, PermissionCheckRequirement.Optional }, @@ -77,12 +75,33 @@ namespace Umbraco.Web.HealthCheck.Checks.Permissions { SystemDirectories.Xslt, PermissionCheckRequirement.Optional }, }; + //These are special paths to check that will restart an app domain if a file is written to them, + //so these need to be tested differently + var pathsToCheckWithRestarts = new Dictionary + { + { SystemDirectories.AppCode, PermissionCheckRequirement.Optional }, + { SystemDirectories.Bin, PermissionCheckRequirement.Optional } + }; + // Run checks for required and optional paths for modify permission List requiredFailedPaths; List optionalFailedPaths; var requiredPathCheckResult = FilePermissionHelper.TestDirectories(GetPathsToCheck(pathsToCheck, PermissionCheckRequirement.Required), out requiredFailedPaths); var optionalPathCheckResult = FilePermissionHelper.TestDirectories(GetPathsToCheck(pathsToCheck, PermissionCheckRequirement.Optional), out optionalFailedPaths); + //now check the special folders + List requiredFailedPaths2; + List optionalFailedPaths2; + var requiredPathCheckResult2 = FilePermissionHelper.TestDirectories(GetPathsToCheck(pathsToCheckWithRestarts, PermissionCheckRequirement.Required), out requiredFailedPaths2, writeCausesRestart:true); + var optionalPathCheckResult2 = FilePermissionHelper.TestDirectories(GetPathsToCheck(pathsToCheckWithRestarts, PermissionCheckRequirement.Optional), out optionalFailedPaths2, writeCausesRestart: true); + + requiredPathCheckResult = requiredPathCheckResult && requiredPathCheckResult2; + optionalPathCheckResult = optionalPathCheckResult && optionalPathCheckResult2; + + //combine the paths + requiredFailedPaths = requiredFailedPaths.Concat(requiredFailedPaths2).ToList(); + optionalFailedPaths = requiredFailedPaths.Concat(optionalFailedPaths2).ToList(); + return GetStatus(requiredPathCheckResult, requiredFailedPaths, optionalPathCheckResult, optionalFailedPaths, PermissionCheckFor.Folder); } diff --git a/src/Umbraco.Web/Install/FilePermissionHelper.cs b/src/Umbraco.Web/Install/FilePermissionHelper.cs index 87491b93a8..a91bd6d306 100644 --- a/src/Umbraco.Web/Install/FilePermissionHelper.cs +++ b/src/Umbraco.Web/Install/FilePermissionHelper.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Web; using System.IO; +using System.Security.AccessControl; using Umbraco.Core.IO; using umbraco; @@ -37,8 +38,19 @@ namespace Umbraco.Web.Install return errorReport.Any() == false; } - - public static bool TestDirectories(string[] directories, out List errorReport) + + /// + /// This will test the directories for write access + /// + /// + /// + /// + /// If this is false, the easiest way to test for write access is to write a temp file, however some folder will cause + /// an App Domain restart if a file is written to the folder, so in that case we need to use the ACL APIs which aren't as + /// reliable but we cannot write a file since it will cause an app domain restart. + /// + /// + public static bool TestDirectories(string[] directories, out List errorReport, bool writeCausesRestart = false) { errorReport = new List(); bool succes = true; @@ -46,7 +58,11 @@ namespace Umbraco.Web.Install { if (Directory.Exists(dir) == false) continue; - bool result = SaveAndDeleteFile(IOHelper.MapPath(dir + "/configWizardPermissionTest.txt")); + var folder = IOHelper.MapPath(dir); + + var result = writeCausesRestart + ? HasWritePermissionOnDir(folder) + : SaveAndDeleteFile(Path.Combine(folder, "configWizardPermissionTest.txt")); if (result == false) { @@ -131,7 +147,42 @@ namespace Umbraco.Web.Install { return false; } + } + private static bool HasWritePermissionOnDir(string path) + { + var writeAllow = false; + var writeDeny = false; + var accessControlList = Directory.GetAccessControl(path); + if (accessControlList == null) + return false; + AuthorizationRuleCollection accessRules; + try + { + accessRules = accessControlList.GetAccessRules(true, true, typeof(System.Security.Principal.SecurityIdentifier)); + if (accessRules == null) + return false; + } + catch (Exception e) + { + //This is not 100% accurate btw because it could turn out that the current user doesn't + //have access to read the current permissions but does have write access. + //I think this is an edge case however + return false; + } + + foreach (FileSystemAccessRule rule in accessRules) + { + if ((FileSystemRights.Write & rule.FileSystemRights) != FileSystemRights.Write) + continue; + + if (rule.AccessControlType == AccessControlType.Allow) + writeAllow = true; + else if (rule.AccessControlType == AccessControlType.Deny) + writeDeny = true; + } + + return writeAllow && writeDeny == false; } private static bool OpenFileForWrite(string file) diff --git a/src/Umbraco.Web/Models/ContentEditing/UserDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/UserDisplay.cs index 4cff43e3b8..8a79344c8e 100644 --- a/src/Umbraco.Web/Models/ContentEditing/UserDisplay.cs +++ b/src/Umbraco.Web/Models/ContentEditing/UserDisplay.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.ComponentModel; using System.Globalization; using System.Runtime.Serialization; @@ -38,6 +39,40 @@ namespace Umbraco.Web.Models.ContentEditing [DataMember(Name = "resetPasswordValue")] [ReadOnly(true)] public string ResetPasswordValue { get; set; } - + + /// + /// A readonly value showing the user's current calculated start content ids + /// + [DataMember(Name = "calculatedStartContentIds")] + [ReadOnly(true)] + public IEnumerable CalculatedStartContentIds { get; set; } + + /// + /// A readonly value showing the user's current calculated start media ids + /// + [DataMember(Name = "calculatedStartMediaIds")] + [ReadOnly(true)] + public IEnumerable CalculatedStartMediaIds { get; set; } + + [DataMember(Name = "failedPasswordAttempts")] + [ReadOnly(true)] + public int FailedPasswordAttempts { get; set; } + + [DataMember(Name = "lastLockoutDate")] + [ReadOnly(true)] + public DateTime LastLockoutDate { get; set; } + + [DataMember(Name = "lastPasswordChangeDate")] + [ReadOnly(true)] + public DateTime LastPasswordChangeDate { get; set; } + + [DataMember(Name = "createDate")] + [ReadOnly(true)] + public DateTime CreateDate { get; set; } + + [DataMember(Name = "updateDate")] + [ReadOnly(true)] + public DateTime UpdateDate { get; set; } + } } \ No newline at end of file diff --git a/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs b/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs index 02f400cdf9..bd25c5da07 100644 --- a/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs @@ -228,13 +228,45 @@ namespace Umbraco.Web.Models.Mapping display.AssignedPermissions = allAssignedPermissions; }); + //Important! Currently we are never mapping to multiple UserDisplay objects but if we start doing that + // this will cause an N+1 and we'll need to change how this works. config.CreateMap() .ForMember(detail => detail.Avatars, opt => opt.MapFrom(user => user.GetCurrentUserAvatarUrls(applicationContext.Services.UserService, applicationContext.ApplicationCache.RuntimeCache))) .ForMember(detail => detail.Username, opt => opt.MapFrom(user => user.Username)) .ForMember(detail => detail.LastLoginDate, opt => opt.MapFrom(user => user.LastLoginDate == default(DateTime) ? null : (DateTime?) user.LastLoginDate)) .ForMember(detail => detail.UserGroups, opt => opt.MapFrom(user => user.Groups)) - .ForMember(detail => detail.StartContentIds, opt => opt.UseValue(Enumerable.Empty())) - .ForMember(detail => detail.StartMediaIds, opt => opt.UseValue(Enumerable.Empty())) + .ForMember( + detail => detail.CalculatedStartContentIds, + opt => opt.MapFrom(user => GetStartNodeValues( + user.CalculateContentStartNodeIds(applicationContext.Services.EntityService), + applicationContext.Services.TextService, + applicationContext.Services.EntityService, + UmbracoObjectTypes.Document, + "content/contentRoot"))) + .ForMember( + detail => detail.CalculatedStartMediaIds, + opt => opt.MapFrom(user => GetStartNodeValues( + user.CalculateMediaStartNodeIds(applicationContext.Services.EntityService), + applicationContext.Services.TextService, + applicationContext.Services.EntityService, + UmbracoObjectTypes.Document, + "media/mediaRoot"))) + .ForMember( + detail => detail.StartContentIds, + opt => opt.MapFrom(user => GetStartNodeValues( + user.StartContentIds.ToArray(), + applicationContext.Services.TextService, + applicationContext.Services.EntityService, + UmbracoObjectTypes.Document, + "content/contentRoot"))) + .ForMember( + detail => detail.StartMediaIds, + opt => opt.MapFrom(user => GetStartNodeValues( + user.StartMediaIds.ToArray(), + applicationContext.Services.TextService, + applicationContext.Services.EntityService, + UmbracoObjectTypes.Media, + "media/mediaRoot"))) .ForMember(detail => detail.Culture, opt => opt.MapFrom(user => user.GetUserCulture(applicationContext.Services.TextService))) .ForMember( detail => detail.AvailableCultures, @@ -252,40 +284,7 @@ namespace Umbraco.Web.Models.Mapping .ForMember(detail => detail.ResetPasswordValue, opt => opt.Ignore()) .ForMember(detail => detail.Alias, opt => opt.Ignore()) .ForMember(detail => detail.Trashed, opt => opt.Ignore()) - .ForMember(detail => detail.AdditionalData, opt => opt.Ignore()) - .AfterMap((user, display) => - { - //Important! Currently we are never mapping to multiple UserDisplay objects but if we start doing that - // this will cause an N+1 and we'll need to change how this works. - - var startContentIds = user.StartContentIds.ToArray(); - if (startContentIds.Length > 0) - { - //TODO: Update GetAll to be able to pass in a parameter like on the normal Get to NOT load in the entire object! - var startNodes = new List(); - if (startContentIds.Contains(-1)) - { - startNodes.Add(RootNode(applicationContext.Services.TextService.Localize("content/contentRoot"))); - } - var contentItems = applicationContext.Services.EntityService.GetAll(UmbracoObjectTypes.Document, startContentIds); - startNodes.AddRange(Mapper.Map, IEnumerable>(contentItems)); - display.StartContentIds = startNodes; - - - } - var startMediaIds = user.StartMediaIds.ToArray(); - if (startMediaIds.Length > 0) - { - var startNodes = new List(); - if (startContentIds.Contains(-1)) - { - startNodes.Add(RootNode(applicationContext.Services.TextService.Localize("media/mediaRoot"))); - } - var mediaItems = applicationContext.Services.EntityService.GetAll(UmbracoObjectTypes.Media, startMediaIds); - startNodes.AddRange(Mapper.Map, IEnumerable>(mediaItems)); - display.StartMediaIds = startNodes; - } - }); + .ForMember(detail => detail.AdditionalData, opt => opt.Ignore()); config.CreateMap() //Loading in the user avatar's requires an external request if they don't have a local file avatar, this means that initial load of paging may incur a cost @@ -323,7 +322,7 @@ namespace Umbraco.Web.Models.Mapping opt => opt.MapFrom(user => user.Email.ToLowerInvariant().Trim().GenerateHash())) .ForMember(detail => detail.SecondsUntilTimeout, opt => opt.Ignore()) .AfterMap((user, detail) => - { + { //we need to map the legacy UserType //the best we can do here is to return the user's first user group as a IUserType object //but we should attempt to return any group that is the built in ones first @@ -342,7 +341,7 @@ namespace Umbraco.Web.Models.Mapping detail.UserType = foundBuiltIn.Alias; } else - { + { //otherwise return the first detail.UserType = groups[0].Alias; } @@ -367,6 +366,24 @@ namespace Umbraco.Web.Models.Mapping } + private IEnumerable GetStartNodeValues(int[] startNodeIds, + ILocalizedTextService textService, IEntityService entityService, UmbracoObjectTypes objectType, + string localizedKey) + { + if (startNodeIds.Length > 0) + { + var startNodes = new List(); + if (startNodeIds.Contains(-1)) + { + startNodes.Add(RootNode(textService.Localize(localizedKey))); + } + var mediaItems = entityService.GetAll(objectType, startNodeIds); + startNodes.AddRange(Mapper.Map, IEnumerable>(mediaItems)); + return startNodes; + } + return Enumerable.Empty(); + } + private void MapUserGroupBasic(ServiceContext services, dynamic group, UserGroupBasic display) { var allSections = services.SectionService.GetSections();