From d54658009cdeb676257747bf0992599feee24554 Mon Sep 17 00:00:00 2001 From: Stephan Date: Mon, 18 Sep 2017 15:33:13 +0200 Subject: [PATCH] Port 7.7 - WIP --- .gitignore | 1 + .../ClientDependencyConfiguration.cs | 2 +- .../AddIndexToDictionaryKeyColumn.cs | 1 + .../AddUserGroupTables.cs | 223 ++++++++++++++-- .../AddUserStartNodeTable.cs | 1 + .../EnsureContentTemplatePermissions.cs | 1 + .../ReduceDictionaryKeyColumnsSize.cs | 1 + .../UpdateUserTables.cs | 4 + .../Repositories/ContentRepository.cs | 48 ++-- .../Interfaces/IContentRepository.cs | 3 +- .../Interfaces/IMediaRepository.cs | 6 +- .../Interfaces/IUserRepository.cs | 3 +- .../Repositories/MediaRepository.cs | 23 +- .../PropertyEditors/PropertyValueEditor.cs | 2 +- .../MultipleTextStringValueConverter.cs | 2 +- .../Security/BackOfficeUserManager.cs | 109 +++++++- .../Security/BackOfficeUserStore.cs | 3 + src/Umbraco.Core/Security/EmailService.cs | 42 +++- .../Security/MembershipProviderBase.cs | 40 ++- .../Security/MembershipProviderExtensions.cs | 3 + .../Security/UmbracoEmailMessage.cs | 17 ++ .../KnownTypeUdiJsonConverter.cs | 26 ++ .../Serialization/UdiJsonConverter.cs | 3 +- src/Umbraco.Core/Services/ContentService.cs | 238 +++++++++++++----- .../Services/ContentServiceExtensions.cs | 40 ++- .../ContentTypeServiceBaseOfTItemTService.cs | 6 + ...peServiceBaseOfTRepositoryTItemTService.cs | 77 ++++-- src/Umbraco.Core/Services/DataTypeService.cs | 66 ++++- src/Umbraco.Core/Services/DomainService.cs | 14 +- src/Umbraco.Core/Services/EntityService.cs | 73 +++++- src/Umbraco.Core/Services/FileService.cs | 68 ++--- src/Umbraco.Core/Services/IContentService.cs | 17 ++ .../Services/IContentTypeServiceBase.cs | 1 + src/Umbraco.Core/Services/IDataTypeService.cs | 1 + src/Umbraco.Core/Services/IEntityService.cs | 8 + .../Services/ILocalizationService.cs | 8 +- src/Umbraco.Core/Services/IMediaService.cs | 17 ++ src/Umbraco.Core/Services/IMemberService.cs | 12 +- src/Umbraco.Core/Services/IUserService.cs | 32 ++- src/Umbraco.Core/Services/IdkMap.cs | 65 +++-- .../Services/LocalizationService.cs | 36 ++- src/Umbraco.Core/Services/MacroService.cs | 14 +- src/Umbraco.Core/Services/MediaService.cs | 123 +++++++-- .../Services/MediaServiceExtensions.cs | 40 +++ .../Services/MemberGroupService.cs | 15 +- src/Umbraco.Core/Services/MemberService.cs | 58 +++-- src/Umbraco.Core/Services/OperationStatus.cs | 14 +- .../Services/PublicAccessService.cs | 54 ++-- .../Services/PublicAccessServiceExtensions.cs | 20 +- src/Umbraco.Core/Services/RelationService.cs | 41 +-- src/Umbraco.Core/Services/UserService.cs | 86 ++++--- .../Sync/WebServiceServerMessenger.cs | 10 +- src/Umbraco.Core/Umbraco.Core.csproj | 4 +- .../Xml/UmbracoXPathPathSyntaxParser.cs | 2 +- 54 files changed, 1423 insertions(+), 401 deletions(-) create mode 100644 src/Umbraco.Core/Security/UmbracoEmailMessage.cs create mode 100644 src/Umbraco.Core/Serialization/KnownTypeUdiJsonConverter.cs create mode 100644 src/Umbraco.Core/Services/MediaServiceExtensions.cs diff --git a/.gitignore b/.gitignore index 86d37350ff..b37b1c5832 100644 --- a/.gitignore +++ b/.gitignore @@ -70,6 +70,7 @@ src/Umbraco.Web.UI/[Ww]eb.config *.transformed node_modules +lib-bower src/Umbraco.Web.UI/[Uu]mbraco/[Ll]ib/* src/Umbraco.Web.UI/[Uu]mbraco/[Jj]s/umbraco.* diff --git a/src/Umbraco.Core/Configuration/ClientDependencyConfiguration.cs b/src/Umbraco.Core/Configuration/ClientDependencyConfiguration.cs index 62cd1ab59c..0add427ee3 100644 --- a/src/Umbraco.Core/Configuration/ClientDependencyConfiguration.cs +++ b/src/Umbraco.Core/Configuration/ClientDependencyConfiguration.cs @@ -9,7 +9,7 @@ using Umbraco.Core.IO; using Umbraco.Core.Logging; namespace Umbraco.Core.Configuration -{ +{ /// /// A utility class for working with CDF config and cache files - use sparingly! /// diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSevenZero/AddIndexToDictionaryKeyColumn.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSevenZero/AddIndexToDictionaryKeyColumn.cs index 5f682e07cd..3ada30ace9 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSevenZero/AddIndexToDictionaryKeyColumn.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSevenZero/AddIndexToDictionaryKeyColumn.cs @@ -4,6 +4,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenSevenZero { [Migration("7.7.0", 5, Constants.System.UmbracoMigrationName)] + [Migration("8.0.0", 5, Constants.System.UmbracoMigrationName)] public class AddIndexToDictionaryKeyColumn : MigrationBase { public AddIndexToDictionaryKeyColumn(IMigrationContext context) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSevenZero/AddUserGroupTables.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSevenZero/AddUserGroupTables.cs index 3061fe97bc..df0b71987a 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSevenZero/AddUserGroupTables.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSevenZero/AddUserGroupTables.cs @@ -1,20 +1,39 @@ using System; +using System.Collections.Generic; +using System.Data; using System.Linq; using Umbraco.Core.Models.Rdbms; +using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenSevenZero { + [Migration("7.7.0", 5, Constants.System.UmbracoMigrationName)] [Migration("8.0.0", 1, Constants.System.UmbracoMigrationName)] public class AddUserGroupTables : MigrationBase { + private readonly string _collateSyntax; + public AddUserGroupTables(IMigrationContext context) : base(context) - { } + { + //For some of the migration data inserts we require to use a special MSSQL collate expression since + //some databases may have a custom collation specified and if that is the case, when we compare strings + //in dynamic SQL it will try to compare strings in different collations and this will yield errors. + _collateSyntax = SqlSyntax is MySqlSyntaxProvider || SqlSyntax is SqlCeSyntaxProvider + ? string.Empty + : "COLLATE DATABASE_DEFAULT"; + } public override void Up() { - var tables = SqlSyntax.GetTablesInSchema(Context.Database).ToArray(); + var tables = SqlSyntax.GetTablesInSchema(Context.Database).ToList(); var constraints = SqlSyntax.GetConstraintsPerColumn(Context.Database).Distinct().ToArray(); + var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToArray(); + + //In some very rare cases, there might alraedy be user group tables that we'll need to remove first + //but of course we don't want to remove the tables we will be creating below if they already exist so + //need to do some checks first since these old rare tables have a different schema + RemoveOldTablesIfExist(tables, columns); if (AddNewTables(tables)) { @@ -23,6 +42,83 @@ 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); + } + } + } + + /// + /// In some very rare cases, there might alraedy be user group tables that we'll need to remove first + /// but of course we don't want to remove the tables we will be creating below if they already exist so + /// need to do some checks first since these old rare tables have a different schema + /// + /// + /// + private void RemoveOldTablesIfExist(List tables, ColumnInfo[] columns) + { + if (tables.Contains("umbracoUser2userGroup", StringComparer.InvariantCultureIgnoreCase)) + { + //this column doesn't exist in the 7.7 schema, so if it's there, then this is a super old table + var foundOldColumn = columns + .FirstOrDefault(x => + x.ColumnName.Equals("user", StringComparison.InvariantCultureIgnoreCase) + && x.TableName.Equals("umbracoUser2userGroup", StringComparison.InvariantCultureIgnoreCase)); + if (foundOldColumn != null) + { + Delete.Table("umbracoUser2userGroup"); + //remove from the tables list since this will be re-checked in further logic + tables.Remove("umbracoUser2userGroup"); + } + } + + if (tables.Contains("umbracoUserGroup", StringComparer.InvariantCultureIgnoreCase)) + { + //The new schema has several columns, the super old one for this table only had 2 so if it's 2 get rid of it + var countOfCols = columns + .Count(x => x.TableName.Equals("umbracoUserGroup", StringComparison.InvariantCultureIgnoreCase)); + if (countOfCols == 2) + { + Delete.Table("umbracoUserGroup"); + //remove from the tables list since this will be re-checked in further logic + tables.Remove("umbracoUserGroup"); + } + } } private void SetDefaultIcons() @@ -33,7 +129,7 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenSevenZe Execute.Sql("UPDATE umbracoUserGroup SET icon = \'icon-globe\' WHERE userGroupAlias = \'translator\'"); } - private bool AddNewTables(string[] tables) + private bool AddNewTables(List tables) { var updated = false; if (tables.InvariantContains("umbracoUserGroup") == false) @@ -71,13 +167,15 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenSevenZe FROM umbracoUserType"); // Add each user to the group created from their type - Execute.Sql(@"INSERT INTO umbracoUser2UserGroup (userId, userGroupId) + Execute.Sql(string.Format(@"INSERT INTO umbracoUser2UserGroup (userId, userGroupId) SELECT u.id, ug.id FROM umbracoUser u INNER JOIN umbracoUserType ut ON ut.id = u.userType - INNER JOIN umbracoUserGroup ug ON ug.userGroupAlias = ut.userTypeAlias"); + INNER JOIN umbracoUserGroup ug ON ug.userGroupAlias {0} = ut.userTypeAlias {0}", _collateSyntax)); // Add the built-in administrator account to all apps + // this will lookup all of the apps that the admin currently has access to in order to assign the sections + // instead of use statically assigning since there could be extra sections we don't know about. Execute.Sql(@"INSERT INTO umbracoUserGroup2app (userGroupId,app) SELECT ug.id, app FROM umbracoUserGroup ug @@ -86,6 +184,89 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenSevenZe INNER JOIN umbracoUser2app u2a ON u2a." + SqlSyntax.GetQuotedColumnName("user") + @" = u.id WHERE u.id = 0"); + // Add the default section access to the other built-in accounts + // writer: + Execute.Sql(string.Format(@"INSERT INTO umbracoUserGroup2app (userGroupId, app) + SELECT ug.id, 'content' as app + FROM umbracoUserGroup ug + WHERE ug.userGroupAlias {0} = 'writer' {0}", _collateSyntax)); + // editor + Execute.Sql(string.Format(@"INSERT INTO umbracoUserGroup2app (userGroupId, app) + SELECT ug.id, 'content' as app + FROM umbracoUserGroup ug + WHERE ug.userGroupAlias {0} = 'editor' {0}", _collateSyntax)); + Execute.Sql(string.Format(@"INSERT INTO umbracoUserGroup2app (userGroupId, app) + SELECT ug.id, 'media' as app + FROM umbracoUserGroup ug + WHERE ug.userGroupAlias {0} = 'editor' {0}", _collateSyntax)); + // translator + Execute.Sql(string.Format(@"INSERT INTO umbracoUserGroup2app (userGroupId, app) + SELECT ug.id, 'translation' as app + FROM umbracoUserGroup ug + WHERE ug.userGroupAlias {0} = 'translator' {0}", _collateSyntax)); + + //We need to lookup all distinct combinations of section access and create a group for each distinct collection + //and assign groups accordingly. We'll perform the lookup 'now' to then create the queued SQL migrations. + var userAppsData = Context.Database.Query(@"SELECT u.id, u2a.app FROM umbracoUser u + INNER JOIN umbracoUser2app u2a ON u2a." + SqlSyntax.GetQuotedColumnName("user") + @" = u.id + ORDER BY u.id, u2a.app"); + var usersWithApps = new Dictionary>(); + foreach (var userApps in userAppsData) + { + List apps; + if (usersWithApps.TryGetValue(userApps.id, out apps) == false) + { + apps = new List {userApps.app}; + usersWithApps.Add(userApps.id, apps); + } + else + { + apps.Add(userApps.app); + } + } + //At this stage we have a dictionary of users with a collection of their apps which are sorted + //and we need to determine the unique/distinct app collections for each user to create groups with. + //We can do this by creating a hash value of all of the app values and since they are already sorted we can get a distinct + //collection by this hash. + var distinctApps = usersWithApps + .Select(x => new {appCollection = x.Value, appsHash = string.Join("", x.Value).GenerateHash()}) + .DistinctBy(x => x.appsHash) + .ToArray(); + //Now we need to create user groups for each of these distinct app collections, and then assign the corresponding users to those groups + for (var i = 0; i < distinctApps.Length; i++) + { + //create the group + var alias = "MigratedSectionAccessGroup_" + (i + 1); + Insert.IntoTable("umbracoUserGroup").Row(new + { + userGroupAlias = "MigratedSectionAccessGroup_" + (i + 1), + userGroupName = "Migrated Section Access Group " + (i + 1) + }); + //now assign the apps + var distinctApp = distinctApps[i]; + foreach (var app in distinctApp.appCollection) + { + Execute.Sql(string.Format(@"INSERT INTO umbracoUserGroup2app (userGroupId, app) + SELECT ug.id, '" + app + @"' as app + FROM umbracoUserGroup ug + WHERE ug.userGroupAlias {0} = '" + alias + "' {0}", _collateSyntax)); + } + //now assign the corresponding users to this group + foreach (var userWithApps in usersWithApps) + { + //check if this user's groups hash matches the current groups hash + var hash = string.Join("", userWithApps.Value).GenerateHash(); + if (hash == distinctApp.appsHash) + { + //it matches so assign the user to this group + Execute.Sql(string.Format(@"INSERT INTO umbracoUser2UserGroup (userId, userGroupId) + SELECT " + userWithApps.Key + @", ug.id + FROM umbracoUserGroup ug + WHERE ug.userGroupAlias {0} = '" + alias + "' {0}", _collateSyntax)); + } + } + } + // Rename some groups for consistency (plural form) Execute.Sql("UPDATE umbracoUserGroup SET userGroupName = 'Writers' WHERE userGroupAlias = 'writer'"); Execute.Sql("UPDATE umbracoUserGroup SET userGroupName = 'Translators' WHERE userGroupAlias = 'translator'"); @@ -105,49 +286,46 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenSevenZe { // Create user group records for all non-admin users that have specific permissions set Execute.Sql(@"INSERT INTO umbracoUserGroup(userGroupAlias, userGroupName) - SELECT userName + 'Group', 'Group for ' + userName + SELECT 'permissionGroupFor' + userLogin, 'Migrated Permission Group for ' + userLogin FROM umbracoUser WHERE (id IN ( - SELECT " + SqlSyntax.GetQuotedColumnName("user") + @" - FROM umbracoUser2app - ) OR id IN ( SELECT userid FROM umbracoUser2NodePermission )) AND id > 0"); // Associate those groups with the users - Execute.Sql(@"INSERT INTO umbracoUser2UserGroup (userId, userGroupId) + Execute.Sql(string.Format(@"INSERT INTO umbracoUser2UserGroup (userId, userGroupId) SELECT u.id, ug.id FROM umbracoUser u - INNER JOIN umbracoUserGroup ug ON ug.userGroupAlias = userName + 'Group'"); + INNER JOIN umbracoUserGroup ug ON ug.userGroupAlias {0} = 'permissionGroupFor' + userLogin {0}", _collateSyntax)); // Create node permissions on the groups - Execute.Sql(@"INSERT INTO umbracoUserGroup2NodePermission (userGroupId,nodeId,permission) + Execute.Sql(string.Format(@"INSERT INTO umbracoUserGroup2NodePermission (userGroupId,nodeId,permission) SELECT ug.id, nodeId, permission FROM umbracoUserGroup ug INNER JOIN umbracoUser2UserGroup u2ug ON u2ug.userGroupId = ug.id INNER JOIN umbracoUser u ON u.id = u2ug.userId INNER JOIN umbracoUser2NodePermission u2np ON u2np.userId = u.id - WHERE ug.userGroupAlias NOT IN ( - SELECT userTypeAlias + WHERE ug.userGroupAlias {0} NOT IN ( + SELECT userTypeAlias {0} FROM umbracoUserType - )"); + )", _collateSyntax)); // Create app permissions on the groups - Execute.Sql(@"INSERT INTO umbracoUserGroup2app (userGroupId,app) + Execute.Sql(string.Format(@"INSERT INTO umbracoUserGroup2app (userGroupId,app) SELECT ug.id, app FROM umbracoUserGroup ug INNER JOIN umbracoUser2UserGroup u2ug ON u2ug.userGroupId = ug.id INNER JOIN umbracoUser u ON u.id = u2ug.userId INNER JOIN umbracoUser2app u2a ON u2a." + SqlSyntax.GetQuotedColumnName("user") + @" = u.id - WHERE ug.userGroupAlias NOT IN ( - SELECT userTypeAlias + WHERE ug.userGroupAlias {0} NOT IN ( + SELECT userTypeAlias {0} FROM umbracoUserType - )"); + )", _collateSyntax)); } - private void DeleteOldTables(string[] tables, Tuple[] constraints) + private void DeleteOldTables(List tables, Tuple[] constraints) { if (tables.InvariantContains("umbracoUser2App")) { @@ -165,6 +343,11 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenSevenZe { Delete.ForeignKey("FK_umbracoUser_umbracoUserType_id").OnTable("umbracoUser"); } + //This is the super old constraint name of the FK for user type so check this one too + if (constraints.Any(x => x.Item1.InvariantEquals("umbracoUser") && x.Item3.InvariantEquals("FK_user_userType"))) + { + Delete.ForeignKey("FK_user_userType").OnTable("umbracoUser"); + } Delete.Column("userType").FromTable("umbracoUser"); Delete.Table("umbracoUserType"); diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSevenZero/AddUserStartNodeTable.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSevenZero/AddUserStartNodeTable.cs index c446cd072d..8ec8a5d541 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSevenZero/AddUserStartNodeTable.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSevenZero/AddUserStartNodeTable.cs @@ -3,6 +3,7 @@ using Umbraco.Core.Models.Rdbms; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenSevenZero { + [Migration("7.7.0", 5, Constants.System.UmbracoMigrationName)] [Migration("8.0.0", 2, Constants.System.UmbracoMigrationName)] public class AddUserStartNodeTable : MigrationBase { diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSevenZero/EnsureContentTemplatePermissions.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSevenZero/EnsureContentTemplatePermissions.cs index d5ff6ff067..fa953e87e6 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSevenZero/EnsureContentTemplatePermissions.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSevenZero/EnsureContentTemplatePermissions.cs @@ -6,6 +6,7 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenSevenZe /// /// Ensures the built-in user groups have the blueprint permission by default on upgrade /// + [Migration("7.7.0", 5, Constants.System.UmbracoMigrationName)] [Migration("8.0.0", 6, Constants.System.UmbracoMigrationName)] public class EnsureContentTemplatePermissions : MigrationBase { diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSevenZero/ReduceDictionaryKeyColumnsSize.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSevenZero/ReduceDictionaryKeyColumnsSize.cs index fac6b19026..048d03aeeb 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSevenZero/ReduceDictionaryKeyColumnsSize.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSevenZero/ReduceDictionaryKeyColumnsSize.cs @@ -3,6 +3,7 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenSevenZero { + [Migration("7.7.0", 5, Constants.System.UmbracoMigrationName)] [Migration("8.0.0", 4, Constants.System.UmbracoMigrationName)] public class ReduceDictionaryKeyColumnsSize : MigrationBase { diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSevenZero/UpdateUserTables.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSevenZero/UpdateUserTables.cs index 0030812f32..512404113d 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSevenZero/UpdateUserTables.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSevenZero/UpdateUserTables.cs @@ -6,6 +6,7 @@ using Umbraco.Core.Security; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenSevenZero { + [Migration("7.7.0", 5, Constants.System.UmbracoMigrationName)] [Migration("8.0.0", 0, Constants.System.UmbracoMigrationName)] public class UpdateUserTables : MigrationBase { @@ -30,6 +31,9 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenSevenZe if (columns.Any(x => x.TableName.InvariantEquals("umbracoUser") && x.ColumnName.InvariantEquals("invitedDate")) == false) Create.Column("invitedDate").OnTable("umbracoUser").AsDateTime().Nullable(); + if (columns.Any(x => x.TableName.InvariantEquals("umbracoUser") && x.ColumnName.InvariantEquals("avatar")) == false) + Create.Column("avatar").OnTable("umbracoUser").AsString(500).Nullable(); + if (columns.Any(x => x.TableName.InvariantEquals("umbracoUser") && x.ColumnName.InvariantEquals("passwordConfig")) == false) { Create.Column("passwordConfig").OnTable("umbracoUser").AsString(500).Nullable(); diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs index 318c1f03ee..4fca0c53a1 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs @@ -40,7 +40,7 @@ namespace Umbraco.Core.Persistence.Repositories _publishedQuery = work.Query().Where(x => x.Published); // fixme not used? - _contentByGuidReadRepository = new ContentByGuidReadRepository(this, work, cacheHelper, logger, syntaxProvider); + _contentByGuidReadRepository = new ContentByGuidReadRepository(this, work, cacheHelper, logger); EnsureUniqueNaming = settings.EnsureUniqueNaming; } @@ -760,21 +760,19 @@ namespace Umbraco.Core.Persistence.Repositories return content.HasPublishedVersion; var syntaxUmbracoNode = SqlSyntax.GetQuotedTableName("umbracoNode"); - var syntaxPath = SqlSyntax.GetQuotedColumnName("path"); - var syntaxConcat = SqlSyntax.GetConcat(syntaxUmbracoNode + "." + syntaxPath, "',%'"); + var ids = content.Path.Split(',').Skip(1).Select(int.Parse); var sql = string.Format(@"SELECT COUNT({0}.{1}) FROM {0} JOIN {2} ON ({0}.{1}={2}.{3} AND {2}.{4}=@published) -WHERE (@path LIKE {5})", +WHERE {0}.{1} IN (@ids)", syntaxUmbracoNode, SqlSyntax.GetQuotedColumnName("id"), SqlSyntax.GetQuotedTableName("cmsDocument"), SqlSyntax.GetQuotedColumnName("nodeId"), - SqlSyntax.GetQuotedColumnName("published"), - syntaxConcat); + SqlSyntax.GetQuotedColumnName("published")); - var count = Database.ExecuteScalar(sql, new { @published=true, @path=content.Path }); + var count = Database.ExecuteScalar(sql, new { published=true, ids }); count += 1; // because content does not count return count == content.Level; } @@ -810,54 +808,51 @@ WHERE (@path LIKE {5})", /// TODO: This is ugly and to fix we need to decouple the IRepositoryQueryable -> IRepository -> IReadRepository which should all be separate things! /// Then we can do the same thing with repository instances and we wouldn't need to leave all these methods as not implemented because we wouldn't need to implement them /// - private class ContentByGuidReadRepository : PetaPocoRepositoryBase + private class ContentByGuidReadRepository : NPocoRepositoryBase { private readonly ContentRepository _outerRepo; - public ContentByGuidReadRepository(ContentRepository outerRepo, - IScopeUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) - : base(work, cache, logger, sqlSyntax) + public ContentByGuidReadRepository(ContentRepository outerRepo, IScopeUnitOfWork work, CacheHelper cache, ILogger logger) + : base(work, cache, logger) { _outerRepo = outerRepo; } protected override IContent PerformGet(Guid id) { - var sql = _outerRepo.GetBaseQuery(BaseQueryType.FullSingle) + var sql = _outerRepo.GetBaseQuery(QueryType.Single) .Where(GetBaseWhereClause(), new { Id = id }) - .Where(x => x.Newest, SqlSyntax) - .OrderByDescending(x => x.VersionDate, SqlSyntax); + .Where(x => x.Newest) + .OrderByDescending(x => x.VersionDate); - var dto = Database.Fetch(SqlSyntax.SelectTop(sql, 1)).FirstOrDefault(); + var dto = Database.Fetch(sql.SelectTop(1)).FirstOrDefault(); if (dto == null) return null; - var content = _outerRepo.CreateContentFromDto(dto, sql); + var content = _outerRepo.CreateContentFromDto(dto, dto.ContentVersionDto.VersionId); return content; } protected override IEnumerable PerformGetAll(params Guid[] ids) { - Func translate = s => + Sql Translate(Sql s) { if (ids.Any()) { s.Where("umbracoNode.uniqueID in (@ids)", new { ids }); } //we only want the newest ones with this method - s.Where(x => x.Newest, SqlSyntax); + s.Where(x => x.Newest); return s; - }; + } - var sqlBaseFull = _outerRepo.GetBaseQuery(BaseQueryType.FullMultiple); - var sqlBaseIds = _outerRepo.GetBaseQuery(BaseQueryType.Ids); - - return _outerRepo.ProcessQuery(translate(sqlBaseFull), new PagingSqlQuery(translate(sqlBaseIds))); + var sql = Translate(GetBaseQuery(false)); + return _outerRepo.MapQueryDtos(Database.Fetch(sql), many: true); } - protected override Sql GetBaseQuery(bool isCount) + protected override Sql GetBaseQuery(bool isCount) { return _outerRepo.GetBaseQuery(isCount); } @@ -867,10 +862,7 @@ WHERE (@path LIKE {5})", return "umbracoNode.uniqueID = @Id"; } - protected override Guid NodeObjectTypeId - { - get { return _outerRepo.NodeObjectTypeId; } - } + protected override Guid NodeObjectTypeId => _outerRepo.NodeObjectTypeId; #region Not needed to implement diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs index 48f25c9196..4ac3bb0219 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Umbraco.Core.Models; using Umbraco.Core.Models.Membership; using Umbraco.Core.Persistence.Querying; diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMediaRepository.cs index f88309aa5b..06b812d16e 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMediaRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMediaRepository.cs @@ -1,4 +1,6 @@ -using Umbraco.Core.Models; +using System; +using System.Xml.Linq; +using Umbraco.Core.Models; namespace Umbraco.Core.Persistence.Repositories { @@ -22,6 +24,6 @@ namespace Umbraco.Core.Persistence.Repositories /// /// /// - void AddOrUpdatePreviewXml(IMedia content, Func xml); + void AddOrUpdatePreviewXml(IMedia content, Func xml); } } diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IUserRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IUserRepository.cs index 28835aa56a..1e2c0f8229 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IUserRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IUserRepository.cs @@ -56,7 +56,8 @@ namespace Umbraco.Core.Persistence.Repositories /// IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, Expression> orderBy, Direction orderDirection = Direction.Ascending, - string[] userGroups = null, UserState[] userState = null, IQuery filter = null); + string[] includeUserGroups = null, string[] excludeUserGroups = null, UserState[] userState = null, + IQuery filter = null); /// /// Returns a user by username diff --git a/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs index 44bd3767ce..1eb06a66ef 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs @@ -31,7 +31,7 @@ namespace Umbraco.Core.Persistence.Repositories { _mediaTypeRepository = mediaTypeRepository ?? throw new ArgumentNullException(nameof(mediaTypeRepository)); _tagRepository = tagRepository ?? throw new ArgumentNullException(nameof(tagRepository)); - _mediaByGuidReadRepository = new MediaByGuidReadRepository(this, work, cache, logger, sqlSyntax); + _mediaByGuidReadRepository = new MediaByGuidReadRepository(this, work, cache, logger); EnsureUniqueNaming = contentSection.EnsureUniqueNaming; } @@ -441,13 +441,13 @@ namespace Umbraco.Core.Persistence.Repositories /// TODO: This is ugly and to fix we need to decouple the IRepositoryQueryable -> IRepository -> IReadRepository which should all be separate things! /// Then we can do the same thing with repository instances and we wouldn't need to leave all these methods as not implemented because we wouldn't need to implement them /// - private class MediaByGuidReadRepository : PetaPocoRepositoryBase + private class MediaByGuidReadRepository : NPocoRepositoryBase { private readonly MediaRepository _outerRepo; public MediaByGuidReadRepository(MediaRepository outerRepo, - IScopeUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) - : base(work, cache, logger, sqlSyntax) + IScopeUnitOfWork work, CacheHelper cache, ILogger logger) + : base(work, cache, logger) { _outerRepo = outerRepo; } @@ -456,14 +456,14 @@ namespace Umbraco.Core.Persistence.Repositories { var sql = GetBaseQuery(false); sql.Where(GetBaseWhereClause(), new { Id = id }); - sql.OrderByDescending(x => x.VersionDate, SqlSyntax); + sql.OrderByDescending(x => x.VersionDate); - var dto = Database.Fetch(SqlSyntax.SelectTop(sql, 1)).FirstOrDefault(); + var dto = Database.Fetch(SqlSyntax.SelectTop(sql, 1)).FirstOrDefault(); if (dto == null) return null; - var content = _outerRepo.CreateMediaFromDto(dto, sql); + var content = _outerRepo.CreateMediaFromDto(dto, dto.VersionId); return content; } @@ -476,10 +476,10 @@ namespace Umbraco.Core.Persistence.Repositories sql.Where("umbracoNode.uniqueID in (@ids)", new { ids = ids }); } - return _outerRepo.ProcessQuery(sql, new PagingSqlQuery(sql)); + return _outerRepo.MapQueryDtos(Database.Fetch(sql)); } - protected override Sql GetBaseQuery(bool isCount) + protected override Sql GetBaseQuery(bool isCount) { return _outerRepo.GetBaseQuery(isCount); } @@ -489,10 +489,7 @@ namespace Umbraco.Core.Persistence.Repositories return "umbracoNode.uniqueID = @Id"; } - protected override Guid NodeObjectTypeId - { - get { return _outerRepo.NodeObjectTypeId; } - } + protected override Guid NodeObjectTypeId => _outerRepo.NodeObjectTypeId; #region Not needed to implement diff --git a/src/Umbraco.Core/PropertyEditors/PropertyValueEditor.cs b/src/Umbraco.Core/PropertyEditors/PropertyValueEditor.cs index c51eebd93a..69658bf6e3 100644 --- a/src/Umbraco.Core/PropertyEditors/PropertyValueEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/PropertyValueEditor.cs @@ -294,7 +294,7 @@ namespace Umbraco.Core.PropertyEditors //swallow this exception, we thought it was json but it really isn't so continue returning a string } } - return property.Value.ToString(); + return asString; case DataTypeDatabaseType.Integer: case DataTypeDatabaseType.Decimal: //Decimals need to be formatted with invariant culture (dots, not commas) diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/MultipleTextStringValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/MultipleTextStringValueConverter.cs index 899a145862..6de2951a89 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/MultipleTextStringValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/MultipleTextStringValueConverter.cs @@ -52,7 +52,7 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters // fall back on normal behaviour return values.Any() == false - ? sourceString.Split(Environment.NewLine.ToCharArray()) + ? sourceString.Split(new string[] { Environment.NewLine }, StringSplitOptions.None) : values.ToArray(); } diff --git a/src/Umbraco.Core/Security/BackOfficeUserManager.cs b/src/Umbraco.Core/Security/BackOfficeUserManager.cs index 265e49300d..3593afe18e 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; @@ -6,6 +7,8 @@ using System.Web.Security; using Microsoft.AspNet.Identity; using Microsoft.AspNet.Identity.Owin; using Microsoft.Owin.Security.DataProtection; +using Umbraco.Core.Configuration; +using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Models.Identity; using Umbraco.Core.Services; @@ -23,18 +26,43 @@ namespace Umbraco.Core.Security { } + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Use the constructor specifying all dependencies instead")] public BackOfficeUserManager( IUserStore store, IdentityFactoryOptions options, MembershipProviderBase membershipProvider) + : this(store, options, membershipProvider, UmbracoConfig.For.UmbracoSettings().Content) + { + } + + public BackOfficeUserManager( + IUserStore store, + IdentityFactoryOptions options, + MembershipProviderBase membershipProvider, + IContentSection contentSectionConfig) : base(store) { - if (options == null) throw new ArgumentNullException("options");; - InitUserManager(this, membershipProvider, options); + if (options == null) throw new ArgumentNullException("options"); ; + InitUserManager(this, membershipProvider, contentSectionConfig, options); } #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, + UmbracoConfig.For.UmbracoSettings().Content); + } + /// /// Creates a BackOfficeUserManager instance with all default options and the default BackOfficeUserManager /// @@ -44,6 +72,7 @@ namespace Umbraco.Core.Security /// /// /// + /// /// public static BackOfficeUserManager Create( IdentityFactoryOptions options, @@ -51,7 +80,8 @@ namespace Umbraco.Core.Security IMemberTypeService memberTypeService, IEntityService entityService, IExternalLoginService externalLoginService, - MembershipProviderBase membershipProvider) + MembershipProviderBase membershipProvider, + IContentSection contentSectionConfig) { if (options == null) throw new ArgumentNullException("options"); if (userService == null) throw new ArgumentNullException("userService"); @@ -59,7 +89,18 @@ namespace Umbraco.Core.Security if (externalLoginService == null) throw new ArgumentNullException("externalLoginService"); var manager = new BackOfficeUserManager(new BackOfficeUserStore(userService, memberTypeService, entityService, externalLoginService, membershipProvider)); - manager.InitUserManager(manager, membershipProvider, options); + manager.InitUserManager(manager, membershipProvider, contentSectionConfig, options); + return manager; + } + + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Use the overload specifying all dependencies instead")] + public static BackOfficeUserManager Create( + IdentityFactoryOptions options, + BackOfficeUserStore customUserStore, + MembershipProviderBase membershipProvider) + { + var manager = new BackOfficeUserManager(customUserStore, options, membershipProvider); return manager; } @@ -69,31 +110,45 @@ namespace Umbraco.Core.Security /// /// /// + /// /// public static BackOfficeUserManager Create( - IdentityFactoryOptions options, - BackOfficeUserStore customUserStore, - MembershipProviderBase membershipProvider) + IdentityFactoryOptions options, + BackOfficeUserStore customUserStore, + MembershipProviderBase membershipProvider, + IContentSection contentSectionConfig) { - var manager = new BackOfficeUserManager(customUserStore, options, membershipProvider); + var manager = new BackOfficeUserManager(customUserStore, options, membershipProvider, contentSectionConfig); return manager; } #endregion + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Use the overload specifying all dependencies instead")] + protected void InitUserManager( + BackOfficeUserManager manager, + MembershipProviderBase membershipProvider, + IdentityFactoryOptions options) + { + InitUserManager(manager, membershipProvider, UmbracoConfig.For.UmbracoSettings().Content, options); + } + /// /// Initializes the user manager with the correct options /// /// /// + /// /// /// protected void InitUserManager( BackOfficeUserManager manager, MembershipProviderBase membershipProvider, + IContentSection contentSectionConfig, IdentityFactoryOptions options) { //NOTE: This method is mostly here for backwards compat - base.InitUserManager(manager, membershipProvider, options.DataProtectionProvider); + base.InitUserManager(manager, membershipProvider, options.DataProtectionProvider, contentSectionConfig); } } @@ -138,6 +193,16 @@ namespace Umbraco.Core.Security } #endregion + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Use the overload specifying all dependencies instead")] + protected void InitUserManager( + BackOfficeUserManager manager, + MembershipProviderBase membershipProvider, + IDataProtectionProvider dataProtectionProvider) + { + InitUserManager(manager, membershipProvider, dataProtectionProvider, UmbracoConfig.For.UmbracoSettings().Content); + } + /// /// Initializes the user manager with the correct options /// @@ -146,11 +211,13 @@ namespace Umbraco.Core.Security /// The for the users called UsersMembershipProvider /// /// + /// /// protected void InitUserManager( BackOfficeUserManager manager, MembershipProviderBase membershipProvider, - IDataProtectionProvider dataProtectionProvider) + IDataProtectionProvider dataProtectionProvider, + IContentSection contentSectionConfig) { // Configure validation logic for usernames manager.UserValidator = new BackOfficeUserValidator(manager) @@ -180,7 +247,9 @@ namespace Umbraco.Core.Security //custom identity factory for creating the identity object for which we auth against in the back office manager.ClaimsIdentityFactory = new BackOfficeClaimsIdentityFactory(); - manager.EmailService = new EmailService(); + manager.EmailService = new EmailService( + contentSectionConfig.NotificationEmailAddress, + new EmailSender()); //NOTE: Not implementing these, if people need custom 2 factor auth, they'll need to implement their own UserStore to suport it @@ -266,6 +335,24 @@ namespace Umbraco.Core.Security return password; } + /// + /// Override to check the user approval value as well as the user lock out date, by default this only checks the user's locked out date + /// + /// + /// + /// + /// In the ASP.NET Identity world, there is only one value for being locked out, in Umbraco we have 2 so when checking this for Umbraco we need to check both values + /// + public override async Task IsLockedOutAsync(int userId) + { + var user = await FindByIdAsync(userId); + if (user == null) + throw new InvalidOperationException("No user found by id " + userId); + if (user.IsApproved == false) + return true; + + return await base.IsLockedOutAsync(userId); + } #region Overrides for password logic diff --git a/src/Umbraco.Core/Security/BackOfficeUserStore.cs b/src/Umbraco.Core/Security/BackOfficeUserStore.cs index 89f093bb19..74839d966a 100644 --- a/src/Umbraco.Core/Security/BackOfficeUserStore.cs +++ b/src/Umbraco.Core/Security/BackOfficeUserStore.cs @@ -548,6 +548,9 @@ namespace Umbraco.Core.Security /// /// /// + /// + /// Currently we do not suport a timed lock out, when they are locked out, an admin will have to reset the status + /// public Task SetLockoutEndDateAsync(BackOfficeIdentityUser user, DateTimeOffset lockoutEnd) { if (user == null) throw new ArgumentNullException(nameof(user)); diff --git a/src/Umbraco.Core/Security/EmailService.cs b/src/Umbraco.Core/Security/EmailService.cs index f8f9af10ae..51d1b82207 100644 --- a/src/Umbraco.Core/Security/EmailService.cs +++ b/src/Umbraco.Core/Security/EmailService.cs @@ -1,16 +1,37 @@ -using System.Net.Mail; +using System; +using System.ComponentModel; +using System.Net.Mail; using System.Threading.Tasks; using Microsoft.AspNet.Identity; using Umbraco.Core.Configuration; namespace Umbraco.Core.Security { + /// + /// The implementation for Umbraco + /// public class EmailService : IIdentityMessageService { + private readonly string _notificationEmailAddress; + private readonly IEmailSender _defaultEmailSender; + + public EmailService(string notificationEmailAddress, IEmailSender defaultEmailSender) + { + _notificationEmailAddress = notificationEmailAddress; + _defaultEmailSender = defaultEmailSender; + } + + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Use the constructor specifying all dependencies")] + public EmailService() + : this(UmbracoConfig.For.UmbracoSettings().Content.NotificationEmailAddress, new EmailSender()) + { + } + public async Task SendAsync(IdentityMessage message) { var mailMessage = new MailMessage( - UmbracoConfig.For.UmbracoSettings().Content.NotificationEmailAddress, + _notificationEmailAddress, message.Destination, message.Subject, message.Body) @@ -21,16 +42,15 @@ namespace Umbraco.Core.Security try { - using (var client = new SmtpClient()) + //check if it's a custom message and if so use it's own defined mail sender + var umbMsg = message as UmbracoEmailMessage; + if (umbMsg != null) { - if (client.DeliveryMethod == SmtpDeliveryMethod.Network) - { - await client.SendMailAsync(mailMessage); - } - else - { - client.Send(mailMessage); - } + await umbMsg.MailSender.SendAsync(mailMessage); + } + else + { + await _defaultEmailSender.SendAsync(mailMessage); } } finally diff --git a/src/Umbraco.Core/Security/MembershipProviderBase.cs b/src/Umbraco.Core/Security/MembershipProviderBase.cs index 013e80b020..1c0f407429 100644 --- a/src/Umbraco.Core/Security/MembershipProviderBase.cs +++ b/src/Umbraco.Core/Security/MembershipProviderBase.cs @@ -67,6 +67,19 @@ namespace Umbraco.Core.Security get { return false; } } + /// + /// Returns the raw password value for a given user + /// + /// + /// + /// + /// By default this will return an invalid attempt, inheritors will need to override this to support it + /// + protected virtual Attempt GetRawPassword(string username) + { + return Attempt.Fail(); + } + private string _applicationName; private bool _enablePasswordReset; private bool _enablePasswordRetrieval; @@ -301,7 +314,7 @@ namespace Umbraco.Core.Security /// Processes a request to update the password for a membership user. /// /// The user to update the password for. - /// This property is ignore for this provider + /// Required to change a user password if the user is not new and AllowManuallyChangingPassword is false /// The new password for the specified user. /// /// true if the password was updated successfully; otherwise, false. @@ -311,10 +324,17 @@ namespace Umbraco.Core.Security /// public override bool ChangePassword(string username, string oldPassword, string newPassword) { + string rawPasswordValue = string.Empty; if (oldPassword.IsNullOrWhiteSpace() && AllowManuallyChangingPassword == false) { - //If the old password is empty and AllowManuallyChangingPassword is false, than this provider cannot just arbitrarily change the password - throw new NotSupportedException("This provider does not support manually changing the password"); + //we need to lookup the member since this could be a brand new member without a password set + var rawPassword = GetRawPassword(username); + rawPasswordValue = rawPassword.Success ? rawPassword.Result : string.Empty; + if (rawPassword.Success == false || rawPasswordValue.StartsWith(Constants.Security.EmptyPasswordPrefix) == false) + { + //If the old password is empty and AllowManuallyChangingPassword is false, than this provider cannot just arbitrarily change the password + throw new NotSupportedException("This provider does not support manually changing the password"); + } } var args = new ValidatePasswordEventArgs(username, newPassword, false); @@ -327,10 +347,12 @@ namespace Umbraco.Core.Security throw new MembershipPasswordException("Change password canceled due to password validation failure."); } - //Special case to allow changing password without validating existing credentials - //This is used during installation only - var installing = Current.RuntimeState.Level == RuntimeLevel.Install; - if (AllowManuallyChangingPassword == false && installing && oldPassword == "default") + //Special cases to allow changing password without validating existing credentials + // * the member is new and doesn't have a password set + // * during installation to set the admin password + if (AllowManuallyChangingPassword == false + && (rawPasswordValue.StartsWith(Constants.Security.EmptyPasswordPrefix) + || (installing && oldPassword == "default"))) { return PerformChangePassword(username, oldPassword, newPassword); } @@ -686,12 +708,12 @@ namespace Umbraco.Core.Security { var keyedHashAlgorithm = algorithm; if (keyedHashAlgorithm.Key.Length == saltBytes.Length) - { + { //if the salt bytes is the required key length for the algorithm, use it as-is keyedHashAlgorithm.Key = saltBytes; } else if (keyedHashAlgorithm.Key.Length < saltBytes.Length) - { + { //if the salt bytes is too long for the required key length for the algorithm, reduce it var numArray2 = new byte[keyedHashAlgorithm.Key.Length]; Buffer.BlockCopy(saltBytes, 0, numArray2, 0, numArray2.Length); diff --git a/src/Umbraco.Core/Security/MembershipProviderExtensions.cs b/src/Umbraco.Core/Security/MembershipProviderExtensions.cs index 71581ebeb8..ca01330212 100644 --- a/src/Umbraco.Core/Security/MembershipProviderExtensions.cs +++ b/src/Umbraco.Core/Security/MembershipProviderExtensions.cs @@ -18,6 +18,9 @@ namespace Umbraco.Core.Security /// /// /// + /// + /// An Admin can always reset the password + /// internal static bool CanResetPassword(this MembershipProvider provider, IUserService userService) { if (provider == null) throw new ArgumentNullException("provider"); diff --git a/src/Umbraco.Core/Security/UmbracoEmailMessage.cs b/src/Umbraco.Core/Security/UmbracoEmailMessage.cs new file mode 100644 index 0000000000..9ef6205ebf --- /dev/null +++ b/src/Umbraco.Core/Security/UmbracoEmailMessage.cs @@ -0,0 +1,17 @@ +using Microsoft.AspNet.Identity; + +namespace Umbraco.Core.Security +{ + /// + /// A custom implementation for IdentityMessage that allows the customization of how an email is sent + /// + internal class UmbracoEmailMessage : IdentityMessage + { + public IEmailSender MailSender { get; private set; } + + public UmbracoEmailMessage(IEmailSender mailSender) + { + MailSender = mailSender; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Serialization/KnownTypeUdiJsonConverter.cs b/src/Umbraco.Core/Serialization/KnownTypeUdiJsonConverter.cs new file mode 100644 index 0000000000..e6473e7f8e --- /dev/null +++ b/src/Umbraco.Core/Serialization/KnownTypeUdiJsonConverter.cs @@ -0,0 +1,26 @@ +using System; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Umbraco.Core.Serialization +{ + public class KnownTypeUdiJsonConverter : JsonConverter + { + public override bool CanConvert(Type objectType) + { + return typeof(Udi).IsAssignableFrom(objectType); + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + writer.WriteValue(value.ToString()); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + var jo = JToken.ReadFrom(reader); + var val = jo.ToObject(); + return val == null ? null : Udi.Parse(val, true); + } + } +} diff --git a/src/Umbraco.Core/Serialization/UdiJsonConverter.cs b/src/Umbraco.Core/Serialization/UdiJsonConverter.cs index 8dec6f3919..134faf3d1d 100644 --- a/src/Umbraco.Core/Serialization/UdiJsonConverter.cs +++ b/src/Umbraco.Core/Serialization/UdiJsonConverter.cs @@ -4,12 +4,11 @@ using Newtonsoft.Json.Linq; namespace Umbraco.Core.Serialization { - public class UdiJsonConverter : JsonConverter { public override bool CanConvert(Type objectType) { - return typeof(Udi).IsAssignableFrom(objectType); + return typeof (Udi).IsAssignableFrom(objectType); } public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index 690d4aacf7..92b6d76b07 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -24,15 +24,13 @@ namespace Umbraco.Core.Services { private readonly MediaFileSystem _mediaFileSystem; private IQuery _queryNotTrashed; - private readonly IdkMap _idkMap; #region Constructors - public ContentService(IScopeUnitOfWorkProvider provider, ILogger logger, IEventMessagesFactory eventMessagesFactory, MediaFileSystem mediaFileSystem, IdkMap idkMap) + public ContentService(IScopeUnitOfWorkProvider provider, ILogger logger, IEventMessagesFactory eventMessagesFactory, MediaFileSystem mediaFileSystem) : base(provider, logger, eventMessagesFactory) { _mediaFileSystem = mediaFileSystem; - _idkMap = idkMap; } #endregion @@ -122,8 +120,8 @@ namespace Umbraco.Core.Services repo.AssignEntityPermission(entity, permission, groupIds); uow.Complete(); } - } - + } + /// /// Returns implicit/inherited permissions assigned to the content item for all user groups /// @@ -143,6 +141,26 @@ namespace Umbraco.Core.Services #region Create + /// + /// Creates an object using the alias of the + /// that this Content should based on. + /// + /// + /// Note that using this method will simply return a new IContent without any identity + /// as it has not yet been persisted. It is intended as a shortcut to creating new content objects + /// that does not invoke a save operation against the database. + /// + /// Name of the Content object + /// Id of Parent for the new Content + /// Alias of the + /// Optional id of the user creating the content + /// + public IContent CreateContent(string name, Guid parentId, string contentTypeAlias, int userId = 0) + { + var parent = GetById(parentId); + return CreateContent(name, parent, contentTypeAlias, userId); + } + /// /// Creates an object of a specified content type. /// @@ -316,7 +334,8 @@ namespace Umbraco.Core.Services if (withIdentity) { - if (uow.Events.DispatchCancelable(Saving, this, new SaveEventArgs(content), "Saving")) + var saveEventArgs = new SaveEventArgs(content); + if (uow.Events.DispatchCancelable(Saving, this, saveEventArgs, "Saving")) { content.WasCancelled = true; return; @@ -327,14 +346,15 @@ namespace Umbraco.Core.Services uow.Flush(); // need everything so we can serialize - uow.Events.Dispatch(Saved, this, new SaveEventArgs(content, false), "Saved"); + saveEventArgs.CanCancel = false; + uow.Events.Dispatch(Saved, this, saveEventArgs, "Saved"); uow.Events.Dispatch(TreeChanged, this, new TreeChange(content, TreeChangeTypes.RefreshNode).ToEventArgs()); } uow.Events.Dispatch(Created, this, new NewEventArgs(content, false, content.ContentType.Alias, parent)); if (withIdentity == false) - return; + return; Audit(uow, AuditType.New, $"Content '{content.Name}' was created with Id {content.Id}", content.CreatorId, content.Id); } @@ -376,7 +396,7 @@ namespace Umbraco.Core.Services var index = items.ToDictionary(x => x.Id, x => x); - return idsA.Select(x => index.TryGetValue(x, out IContent c) ? c : null).WhereNotNull(); + return idsA.Select(x => index.TryGetValue(x, out var c) ? c : null).WhereNotNull(); } } @@ -387,13 +407,34 @@ namespace Umbraco.Core.Services /// public IContent GetById(Guid key) { - // the repository implements a cache policy on int identifiers, not guids, - // and we are not changing it now, but we still would like to rely on caching - // instead of running a full query against the database, so relying on the - // id-key map, which is fast. + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) + { + uow.ReadLock(Constants.Locks.ContentTree); + var repository = uow.CreateRepository(); + return repository.Get(key); + } + } - var a = _idkMap.GetIdForKey(key, UmbracoObjectTypes.Document); - return a.Success ? GetById(a.Result) : null; + /// + /// Gets objects by Ids + /// + /// Ids of the Content to retrieve + /// + public IEnumerable GetByIds(IEnumerable ids) + { + var idsA = ids.ToArray(); + if (idsA.Length == 0) return Enumerable.Empty(); + + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) + { + uow.ReadLock(Constants.Locks.ContentTree); + var repository = uow.CreateRepository(); + var items = repository.GetAll(idsA); + + var index = items.ToDictionary(x => x.Key, x => x); + + return idsA.Select(x => index.TryGetValue(x, out var c) ? c : null).WhereNotNull(); + } } /// @@ -626,7 +667,6 @@ namespace Umbraco.Core.Services using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { uow.ReadLock(Constants.Locks.ContentTree); - var repository = uow.CreateRepository(); var filterQuery = filter.IsNullOrWhiteSpace() ? null : uow.Query().Where(x => x.Name.Contains(filter)); @@ -660,7 +700,16 @@ namespace Umbraco.Core.Services var query = uow.Query(); //if the id is System Root, then just get all if (id != Constants.System.Root) - query.Where(x => x.Path.SqlContains($",{id},", TextColumnType.NVarchar)); + { + var entityRepository = uow.CreateRepository(); + var contentPath = entityRepository.GetAllPaths(Constants.ObjectTypes.DocumentGuid, id).ToArray(); + if (contentPath.Length == 0) + { + totalChildren = 0; + return Enumerable.Empty(); + } + query.Where(x => x.Path.SqlStartsWith($"{contentPath[0]},", TextColumnType.NVarchar)); + } return repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, orderBySystemField, filter); } } @@ -895,15 +944,43 @@ namespace Umbraco.Core.Services if (content.Trashed) return false; // trashed content is never publishable // not trashed and has a parent: publishable if the parent is path-published + + int[] ids; + if (content.HasIdentity) + { + // get ids from path (we have identity) + // skip the first one that has to be -1 - and we don't care + // skip the last one that has to be "this" - and it's ok to stop at the parent + ids = content.Path.Split(',').Skip(1).SkipLast().Select(int.Parse).ToArray(); + } + else + { + // no path yet (no identity), have to move up to parent + // skip the first one that has to be -1 - and we don't care + // don't skip the last one that is "parent" + var parent = GetById(content.ParentId); + if (parent == null) return false; + ids = parent.Path.Split(',').Skip(1).Select(int.Parse).ToArray(); + } + if (ids.Length == 0) + return false; + + // if the first one is recycle bin, fail fast + if (ids[0] == Constants.System.RecycleBinContent) + return false; + + // fixme - move to repository? using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { - uow.ReadLock(Constants.Locks.ContentTree); - var repo = uow.CreateRepository(); - var parent = repo.Get(content.ParentId); - if (parent == null) - throw new Exception("Out of sync."); // causes rollback - return repo.IsPathPublished(parent); - } + var sql = uow.Sql(@" + SELECT id + FROM umbracoNode + JOIN cmsDocument ON umbracoNode.id=cmsDocument.nodeId AND cmsDocument.published=@0 + WHERE umbracoNode.trashed=@1 AND umbracoNode.id IN (@2)", + true, false, ids); + var x = uow.Database.Fetch(sql); + return ids.Length == x.Count; + } } public bool IsPathPublished(IContent content) @@ -943,7 +1020,8 @@ namespace Umbraco.Core.Services using (var uow = UowProvider.CreateUnitOfWork()) { - if (raiseEvents && uow.Events.DispatchCancelable(Saving, this, new SaveEventArgs(content, evtMsgs), "Saving")) + var saveEventArgs = new SaveEventArgs(content, evtMsgs); + if (raiseEvents && uow.Events.DispatchCancelable(Saving, this, saveEventArgs, "Saving")) { uow.Complete(); return OperationStatus.Attempt.Cancel(evtMsgs); @@ -971,7 +1049,10 @@ namespace Umbraco.Core.Services repository.AddOrUpdate(content); if (raiseEvents) - uow.Events.Dispatch(Saved, this, new SaveEventArgs(content, false, evtMsgs), "Saved"); + { + saveEventArgs.CanCancel = false; + uow.Events.Dispatch(Saved, this, saveEventArgs, "Saved"); + } var changeType = isNew ? TreeChangeTypes.RefreshBranch : TreeChangeTypes.RefreshNode; uow.Events.Dispatch(TreeChanged, this, new TreeChange(content, changeType).ToEventArgs()); Audit(uow, AuditType.Save, "Save Content performed by user", userId, content.Id); @@ -1010,7 +1091,8 @@ namespace Umbraco.Core.Services using (var uow = UowProvider.CreateUnitOfWork()) { - if (raiseEvents && uow.Events.DispatchCancelable(Saving, this, new SaveEventArgs(contentsA, evtMsgs), "Saving")) + var saveEventArgs = new SaveEventArgs(contentsA, evtMsgs); + if (raiseEvents && uow.Events.DispatchCancelable(Saving, this, saveEventArgs, "Saving")) { uow.Complete(); return OperationStatus.Attempt.Cancel(evtMsgs); @@ -1036,7 +1118,10 @@ namespace Umbraco.Core.Services } if (raiseEvents) - uow.Events.Dispatch(Saved, this, new SaveEventArgs(contentsA, false, evtMsgs), "Saved"); + { + saveEventArgs.CanCancel = false; + uow.Events.Dispatch(Saved, this, saveEventArgs, "Saved"); + } uow.Events.Dispatch(TreeChanged, this, treeChanges.ToEventArgs()); Audit(uow, AuditType.Save, "Bulk Save content performed by user", userId == -1 ? 0 : userId, Constants.System.Root); @@ -1264,7 +1349,8 @@ namespace Umbraco.Core.Services using (var uow = UowProvider.CreateUnitOfWork()) { - if (uow.Events.DispatchCancelable(Deleting, this, new DeleteEventArgs(content, evtMsgs))) + var deleteEventArgs = new DeleteEventArgs(content, evtMsgs); + if (uow.Events.DispatchCancelable(Deleting, this, deleteEventArgs)) { uow.Complete(); return OperationStatus.Attempt.Cancel(evtMsgs); @@ -1338,7 +1424,8 @@ namespace Umbraco.Core.Services { using (var uow = UowProvider.CreateUnitOfWork()) { - if (uow.Events.DispatchCancelable(DeletingVersions, this, new DeleteRevisionsEventArgs(id, dateToRetain: versionDate))) + var deleteRevisionsEventArgs = new DeleteRevisionsEventArgs(id, dateToRetain: versionDate); + if (uow.Events.DispatchCancelable(DeletingVersions, this, deleteRevisionsEventArgs)) { uow.Complete(); return; @@ -1348,7 +1435,8 @@ namespace Umbraco.Core.Services var repository = uow.CreateRepository(); repository.DeleteVersions(id, versionDate); - uow.Events.Dispatch(DeletedVersions, this, new DeleteRevisionsEventArgs(id, false, dateToRetain: versionDate)); + deleteRevisionsEventArgs.CanCancel = false; + uow.Events.Dispatch(DeletedVersions, this, deleteRevisionsEventArgs); Audit(uow, AuditType.Delete, "Delete Content by version date performed by user", userId, Constants.System.Root); uow.Complete(); @@ -1423,7 +1511,9 @@ namespace Umbraco.Core.Services var repository = uow.CreateRepository(); var originalPath = content.Path; - if (uow.Events.DispatchCancelable(Trashing, this, new MoveEventArgs(new MoveEventInfo(content, originalPath, Constants.System.RecycleBinContent)))) + var moveEventInfo = new MoveEventInfo(content, originalPath, Constants.System.RecycleBinContent); + var moveEventArgs = new MoveEventArgs(evtMsgs, moveEventInfo); + if (uow.Events.DispatchCancelable(Trashing, this, moveEventArgs)) { uow.Complete(); return OperationStatus.Attempt.Cancel(evtMsgs); // causes rollback @@ -1442,7 +1532,9 @@ namespace Umbraco.Core.Services .Select(x => new MoveEventInfo(x.Item1, x.Item2, x.Item1.ParentId)) .ToArray(); - uow.Events.Dispatch(Trashed, this, new MoveEventArgs(false, evtMsgs, moveInfo)); + moveEventArgs.CanCancel = false; + moveEventArgs.MoveInfoCollection = moveInfo; + uow.Events.Dispatch(Trashed, this, moveEventArgs); Audit(uow, AuditType.Move, "Move Content to Recycle Bin performed by user", userId, content.Id); uow.Complete(); @@ -1482,7 +1574,9 @@ namespace Umbraco.Core.Services if (parentId != Constants.System.Root && (parent == null || parent.Trashed)) throw new InvalidOperationException("Parent does not exist or is trashed."); // causes rollback - if (uow.Events.DispatchCancelable(Moving, this, new MoveEventArgs(new MoveEventInfo(content, content.Path, parentId)))) + var moveEventInfo = new MoveEventInfo(content, content.Path, parentId); + var moveEventArgs = new MoveEventArgs(moveEventInfo); + if (uow.Events.DispatchCancelable(Moving, this, moveEventArgs)) { uow.Complete(); return; // causes rollback @@ -1511,7 +1605,9 @@ namespace Umbraco.Core.Services .Select(x => new MoveEventInfo(x.Item1, x.Item2, x.Item1.ParentId)) .ToArray(); - uow.Events.Dispatch(Moved, this, new MoveEventArgs(false, moveInfo)); + moveEventArgs.MoveInfoCollection = moveInfo; + moveEventArgs.CanCancel = false; + uow.Events.Dispatch(Moved, this, moveEventArgs); Audit(uow, AuditType.Move, "Move Content performed by user", userId, content.Id); uow.Complete(); @@ -1577,7 +1673,7 @@ namespace Umbraco.Core.Services /// public void EmptyRecycleBin() { - var nodeObjectType = new Guid(Constants.ObjectTypes.Document); + var nodeObjectType = Constants.ObjectTypes.DocumentGuid; var deleted = new List(); var evtMsgs = EventMessagesFactory.Get(); // todo - and then? @@ -1591,7 +1687,8 @@ namespace Umbraco.Core.Services // are managed by Delete, and not here. // no idea what those events are for, keep a simplified version - if (uow.Events.DispatchCancelable(EmptyingRecycleBin, this, new RecycleBinEventArgs(nodeObjectType))) + var recycleBinEventArgs = new RecycleBinEventArgs(nodeObjectType); + if (uow.Events.DispatchCancelable(EmptyingRecycleBin, this, recycleBinEventArgs)) { uow.Complete(); return; // causes rollback @@ -1606,7 +1703,9 @@ namespace Umbraco.Core.Services deleted.Add(content); } - uow.Events.Dispatch(EmptiedRecycleBin, this, new RecycleBinEventArgs(nodeObjectType, true)); + recycleBinEventArgs.CanCancel = false; + recycleBinEventArgs.RecycleBinEmptiedSuccessfully = true; // oh my?! + uow.Events.Dispatch(EmptiedRecycleBin, this, recycleBinEventArgs); uow.Events.Dispatch(TreeChanged, this, deleted.Select(x => new TreeChange(x, TreeChangeTypes.Remove)).ToEventArgs()); Audit(uow, AuditType.Delete, "Empty Content Recycle Bin performed by user", 0, Constants.System.RecycleBinContent); @@ -1649,7 +1748,8 @@ namespace Umbraco.Core.Services using (var uow = UowProvider.CreateUnitOfWork()) { - if (uow.Events.DispatchCancelable(Copying, this, new CopyEventArgs(content, copy, parentId))) + var copyEventArgs = new CopyEventArgs(content, copy, true, parentId, relateToOriginal); + if (uow.Events.DispatchCancelable(Copying, this, copyEventArgs)) { uow.Complete(); return null; @@ -1745,7 +1845,8 @@ namespace Umbraco.Core.Services { using (var uow = UowProvider.CreateUnitOfWork()) { - if (uow.Events.DispatchCancelable(SendingToPublish, this, new SendToPublishEventArgs(content))) + var sendToPublishEventArgs = new SendToPublishEventArgs(content); + if (uow.Events.DispatchCancelable(SendingToPublish, this, sendToPublishEventArgs)) { uow.Complete(); return false; @@ -1755,7 +1856,8 @@ namespace Umbraco.Core.Services // fixme - nesting uow? Save(content, userId); - uow.Events.Dispatch(SentToPublish, this, new SendToPublishEventArgs(content, false)); + sendToPublishEventArgs.CanCancel = false; + uow.Events.Dispatch(SentToPublish, this, sendToPublishEventArgs); Audit(uow, AuditType.SendToPublish, "Send to Publish performed by user", content.WriterId, content.Id); } @@ -1780,7 +1882,8 @@ namespace Umbraco.Core.Services using (var uow = UowProvider.CreateUnitOfWork()) { - if (uow.Events.DispatchCancelable(RollingBack, this, new RollbackEventArgs(content))) + var rollbackEventArgs = new RollbackEventArgs(content); + if (uow.Events.DispatchCancelable(RollingBack, this, rollbackEventArgs)) { uow.Complete(); return content; @@ -1802,7 +1905,8 @@ namespace Umbraco.Core.Services repository.AddOrUpdate(content); - uow.Events.Dispatch(RolledBack, this, new RollbackEventArgs(content, false)); + rollbackEventArgs.CanCancel = false; + uow.Events.Dispatch(RolledBack, this, rollbackEventArgs); uow.Events.Dispatch(TreeChanged, this, new TreeChange(content, TreeChangeTypes.RefreshNode).ToEventArgs()); Audit(uow, AuditType.RollBack, "Content rollback performed by user", content.WriterId, content.Id); @@ -1831,7 +1935,8 @@ namespace Umbraco.Core.Services using (var uow = UowProvider.CreateUnitOfWork()) { - if (raiseEvents && uow.Events.DispatchCancelable(Saving, this, new SaveEventArgs(itemsA), "Saving")) + var saveEventArgs = new SaveEventArgs(itemsA); + if (raiseEvents && uow.Events.DispatchCancelable(Saving, this, saveEventArgs, "Saving")) return false; var published = new List(); @@ -1868,7 +1973,10 @@ namespace Umbraco.Core.Services } if (raiseEvents) - uow.Events.Dispatch(Saved, this, new SaveEventArgs(saved, false), "Saved"); + { + saveEventArgs.CanCancel = false; + uow.Events.Dispatch(Saved, this, saveEventArgs, "Saved"); + } uow.Events.Dispatch(TreeChanged, this, saved.Select(x => new TreeChange(x, TreeChangeTypes.RefreshNode)).ToEventArgs()); @@ -2051,7 +2159,8 @@ namespace Umbraco.Core.Services using (var uow = UowProvider.CreateUnitOfWork()) { - if (raiseEvents && uow.Events.DispatchCancelable(Saving, this, new SaveEventArgs(content), "Saving")) + var saveEventArgs = new SaveEventArgs(content, evtMsgs); + if (raiseEvents && uow.Events.DispatchCancelable(Saving, this, saveEventArgs, "Saving")) { uow.Complete(); return Attempt.Fail(new PublishStatus(PublishStatusType.FailedCancelledByEvent, evtMsgs, content)); @@ -2080,7 +2189,10 @@ namespace Umbraco.Core.Services repository.AddOrUpdate(content); if (raiseEvents) // always - uow.Events.Dispatch(Saved, this, new SaveEventArgs(content, false, evtMsgs), "Saved"); + { + saveEventArgs.CanCancel = false; + uow.Events.Dispatch(Saved, this, saveEventArgs, "Saved"); + } if (status.Success == false) { @@ -2256,7 +2368,7 @@ namespace Umbraco.Core.Services /// Occurs after a blueprint has been deleted. /// public static event TypedEventHandler> DeletedBlueprint; - + #endregion #region Publishing Strategies @@ -2622,16 +2734,16 @@ namespace Umbraco.Core.Services { return GetContentType(uow, contentTypeAlias); } - } - - #endregion - + } + + #endregion + #region Blueprints public IContent GetBlueprintById(int id) { using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) - { + { uow.ReadLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); var blueprint = repository.Get(id); @@ -2643,13 +2755,15 @@ namespace Umbraco.Core.Services public IContent GetBlueprintById(Guid id) { - // the repository implements a cache policy on int identifiers, not guids, - // and we are not changing it now, but we still would like to rely on caching - // instead of running a full query against the database, so relying on the - // id-key map, which is fast. - - var a = _idkMap.GetIdForKey(id, UmbracoObjectTypes.DocumentBlueprint); - return a.Success ? GetBlueprintById(a.Result) : null; + using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) + { + uow.ReadLock(Constants.Locks.ContentTree); + var repository = uow.CreateRepository(); + var blueprint = repository.Get(id); + if (blueprint != null) + ((Content) blueprint).IsBlueprint = true; + return blueprint; + } } public void SaveBlueprint(IContent content, int userId = 0) @@ -2662,7 +2776,7 @@ namespace Umbraco.Core.Services using (var uow = UowProvider.CreateUnitOfWork()) { - uow.WriteLock(Constants.Locks.ContentTree); + uow.WriteLock(Constants.Locks.ContentTree); if (string.IsNullOrWhiteSpace(content.Name)) { @@ -2688,7 +2802,7 @@ namespace Umbraco.Core.Services public void DeleteBlueprint(IContent content, int userId = 0) { using (var uow = UowProvider.CreateUnitOfWork()) - { + { uow.WriteLock(Constants.Locks.ContentTree); var repository = uow.CreateRepository(); repository.Delete(content); diff --git a/src/Umbraco.Core/Services/ContentServiceExtensions.cs b/src/Umbraco.Core/Services/ContentServiceExtensions.cs index 077a9611a5..3d5bbfa4ed 100644 --- a/src/Umbraco.Core/Services/ContentServiceExtensions.cs +++ b/src/Umbraco.Core/Services/ContentServiceExtensions.cs @@ -1,10 +1,48 @@ -using System.Linq; +using System; +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core.Models; using Umbraco.Core.Models.Membership; namespace Umbraco.Core.Services { + /// + /// Content service extension methods + /// public static class ContentServiceExtensions { + public static IEnumerable GetByIds(this IContentService contentService, IEnumerable ids) + { + var guids = new List(); + foreach (var udi in ids) + { + var guidUdi = udi as GuidUdi; + if (guidUdi == null) + throw new InvalidOperationException("The UDI provided isn't of type " + typeof(GuidUdi) + " which is required by content"); + guids.Add(guidUdi); + } + + return contentService.GetByIds(guids.Select(x => x.Guid)); + } + + /// + /// Method to create an IContent object based on the Udi of a parent + /// + /// + /// + /// + /// + /// + /// + public static IContent CreateContent(this IContentService contentService, string name, Udi parentId, string mediaTypeAlias, int userId = 0) + { + var guidUdi = parentId as GuidUdi; + if (guidUdi == null) + throw new InvalidOperationException("The UDI provided isn't of type " + typeof(GuidUdi) + " which is required by content"); + var parent = contentService.GetById(guidUdi.Guid); + return contentService.CreateContent(name, parent, mediaTypeAlias, userId); + } + /// /// Remove all permissions for this user for all nodes /// diff --git a/src/Umbraco.Core/Services/ContentTypeServiceBaseOfTItemTService.cs b/src/Umbraco.Core/Services/ContentTypeServiceBaseOfTItemTService.cs index feb7e66f47..9ba883a441 100644 --- a/src/Umbraco.Core/Services/ContentTypeServiceBaseOfTItemTService.cs +++ b/src/Umbraco.Core/Services/ContentTypeServiceBaseOfTItemTService.cs @@ -112,6 +112,12 @@ namespace Umbraco.Core.Services uow.Events.DispatchCancelable(SavedContainer, This, args); } + protected void OnRenamedContainer(IScopeUnitOfWork uow, SaveEventArgs args) + { + // fixme changing the name of the event?! + uow.Events.DispatchCancelable(SavedContainer, This, args, "RenamedContainer"); + } + // fixme what is this? protected void OnDeletingContainer(IScopeUnitOfWork uow, DeleteEventArgs args) { diff --git a/src/Umbraco.Core/Services/ContentTypeServiceBaseOfTRepositoryTItemTService.cs b/src/Umbraco.Core/Services/ContentTypeServiceBaseOfTRepositoryTItemTService.cs index 8552b0d0e3..34dd2811d9 100644 --- a/src/Umbraco.Core/Services/ContentTypeServiceBaseOfTRepositoryTItemTService.cs +++ b/src/Umbraco.Core/Services/ContentTypeServiceBaseOfTRepositoryTItemTService.cs @@ -385,7 +385,8 @@ namespace Umbraco.Core.Services { using (var uow = UowProvider.CreateUnitOfWork()) { - if (OnSavingCancelled(uow, new SaveEventArgs(item))) + var saveEventArgs = new SaveEventArgs(item); + if (OnSavingCancelled(uow, saveEventArgs)) { uow.Complete(); return; @@ -412,7 +413,8 @@ namespace Umbraco.Core.Services OnUowRefreshedEntity(args); OnChanged(uow, args); - OnSaved(uow, new SaveEventArgs(item, false)); + saveEventArgs.CanCancel = false; + OnSaved(uow, saveEventArgs); Audit(uow, AuditType.Save, $"Save {typeof(TItem).Name} performed by user", userId, item.Id); uow.Complete(); @@ -425,7 +427,8 @@ namespace Umbraco.Core.Services using (var uow = UowProvider.CreateUnitOfWork()) { - if (OnSavingCancelled(uow, new SaveEventArgs(itemsA))) + var saveEventArgs = new SaveEventArgs(itemsA); + if (OnSavingCancelled(uow, saveEventArgs)) { uow.Complete(); return; @@ -454,7 +457,8 @@ namespace Umbraco.Core.Services OnUowRefreshedEntity(args); OnChanged(uow, args); - OnSaved(uow, new SaveEventArgs(itemsA, false)); + saveEventArgs.CanCancel = false; + OnSaved(uow, saveEventArgs); Audit(uow, AuditType.Save, $"Save {typeof(TItem).Name} performed by user", userId, -1); uow.Complete(); @@ -469,7 +473,8 @@ namespace Umbraco.Core.Services { using (var uow = UowProvider.CreateUnitOfWork()) { - if (OnDeletingCancelled(uow, new DeleteEventArgs(item))) + var deleteEventArgs = new DeleteEventArgs(item); + if (OnDeletingCancelled(uow, deleteEventArgs)) { uow.Complete(); return; @@ -511,7 +516,9 @@ namespace Umbraco.Core.Services OnUowRefreshedEntity(args); OnChanged(uow, args); - OnDeleted(uow, new DeleteEventArgs(deleted, false)); + deleteEventArgs.DeletedEntities = deleted.DistinctBy(x => x.Id); + deleteEventArgs.CanCancel = false; + OnDeleted(uow, deleteEventArgs); Audit(uow, AuditType.Delete, $"Delete {typeof(TItem).Name} performed by user", userId, item.Id); uow.Complete(); @@ -524,7 +531,8 @@ namespace Umbraco.Core.Services using (var uow = UowProvider.CreateUnitOfWork()) { - if (OnDeletingCancelled(uow, new DeleteEventArgs(itemsA))) + var deleteEventArgs = new DeleteEventArgs(itemsA); + if (OnDeletingCancelled(uow, deleteEventArgs)) { uow.Complete(); return; @@ -563,7 +571,9 @@ namespace Umbraco.Core.Services OnUowRefreshedEntity(args); OnChanged(uow, args); - OnDeleted(uow, new DeleteEventArgs(deleted, false)); + deleteEventArgs.DeletedEntities = deleted.DistinctBy(x => x.Id); + deleteEventArgs.CanCancel = false; + OnDeleted(uow, deleteEventArgs); Audit(uow, AuditType.Delete, $"Delete {typeof(TItem).Name} performed by user", userId, -1); uow.Complete(); @@ -692,7 +702,9 @@ namespace Umbraco.Core.Services var moveInfo = new List>(); using (var uow = UowProvider.CreateUnitOfWork()) { - if (OnMovingCancelled(uow, new MoveEventArgs(evtMsgs, new MoveEventInfo(moving, moving.Path, containerId)))) + var moveEventInfo = new MoveEventInfo(moving, moving.Path, containerId); + var moveEventArgs = new MoveEventArgs(evtMsgs, moveEventInfo); + if (OnMovingCancelled(uow, moveEventArgs)) { uow.Complete(); return OperationStatus.Attempt.Fail(MoveOperationStatusType.FailedCancelledByEvent, evtMsgs); @@ -725,7 +737,9 @@ namespace Umbraco.Core.Services // has no impact on the published content types - would be entirely different if we were to support // moving a content type under another content type. - OnMoved(uow, new MoveEventArgs(false, evtMsgs, moveInfo.ToArray())); + moveEventArgs.MoveInfoCollection = moveInfo; + moveEventArgs.CanCancel = false; + OnMoved(uow, moveEventArgs); } return OperationStatus.Attempt.Succeed(MoveOperationStatusType.Success, evtMsgs); @@ -756,7 +770,8 @@ namespace Umbraco.Core.Services CreatorId = userId }; - if (OnSavingContainerCancelled(uow, new SaveEventArgs(container, evtMsgs))) + var saveEventArgs = new SaveEventArgs(container, evtMsgs); + if (OnSavingContainerCancelled(uow, saveEventArgs)) { uow.Complete(); return OperationStatus.Attempt.Cancel(evtMsgs, container); @@ -765,7 +780,8 @@ namespace Umbraco.Core.Services repo.AddOrUpdate(container); uow.Complete(); - OnSavedContainer(uow, new SaveEventArgs(container, evtMsgs)); + saveEventArgs.CanCancel = false; + OnSavedContainer(uow, saveEventArgs); //TODO: Audit trail ? return OperationStatus.Attempt.Succeed(evtMsgs, container); @@ -895,7 +911,8 @@ namespace Umbraco.Core.Services return Attempt.Fail(new OperationStatus(OperationStatusType.FailedCannot, evtMsgs)); } - if (OnDeletingContainerCancelled(uow, new DeleteEventArgs(container, evtMsgs))) + var deleteEventArgs = new DeleteEventArgs(container, evtMsgs); + if (OnDeletingContainerCancelled(uow, deleteEventArgs)) { uow.Complete(); return Attempt.Fail(new OperationStatus(OperationStatusType.FailedCancelledByEvent, evtMsgs)); @@ -904,13 +921,45 @@ namespace Umbraco.Core.Services repo.Delete(container); uow.Complete(); - OnDeletedContainer(uow, new DeleteEventArgs(container, evtMsgs)); + deleteEventArgs.CanCancel = false; + OnDeletedContainer(uow, deleteEventArgs); return OperationStatus.Attempt.Succeed(evtMsgs); //TODO: Audit trail ? } } + public Attempt> RenameContainer(int id, string name, int userId = 0) + { + var evtMsgs = EventMessagesFactory.Get(); + using (var uow = UowProvider.CreateUnitOfWork()) + { + uow.WriteLock(WriteLockIds); // also for containers + + var repository = uow.CreateContainerRepository(ContainerObjectType); + try + { + var container = repository.Get(id); + + //throw if null, this will be caught by the catch and a failed returned + if (container == null) + throw new InvalidOperationException("No container found with id " + id); + + container.Name = name; + repository.AddOrUpdate(container); + uow.Complete(); + + OnRenamedContainer(uow, new SaveEventArgs(container, evtMsgs)); + + return OperationStatus.Attempt.Succeed(OperationStatusType.Success, evtMsgs, container); + } + catch (Exception ex) + { + return OperationStatus.Attempt.Fail(evtMsgs, ex); + } + } + } + #endregion #region Audit diff --git a/src/Umbraco.Core/Services/DataTypeService.cs b/src/Umbraco.Core/Services/DataTypeService.cs index 9e0c2cb5c8..3e6b59448e 100644 --- a/src/Umbraco.Core/Services/DataTypeService.cs +++ b/src/Umbraco.Core/Services/DataTypeService.cs @@ -174,6 +174,38 @@ namespace Umbraco.Core.Services return OperationStatus.Attempt.Succeed(evtMsgs); } + public Attempt> RenameContainer(int id, string name, int userId = 0) + { + var evtMsgs = EventMessagesFactory.Get(); + using (var uow = UowProvider.CreateUnitOfWork()) + { + var repository = uow.CreateRepository(); + + try + { + var container = repository.Get(id); + + //throw if null, this will be caught by the catch and a failed returned + if (container == null) + throw new InvalidOperationException("No container found with id " + id); + + container.Name = name; + + repository.AddOrUpdate(container); + uow.Complete(); + + // fixme - triggering SavedContainer with a different name?! + uow.Events.Dispatch(SavedContainer, this, new SaveEventArgs(container, evtMsgs), "RenamedContainer"); + + return OperationStatus.Attempt.Succeed(OperationStatusType.Success, evtMsgs, container); + } + catch (Exception ex) + { + return OperationStatus.Attempt.Fail(evtMsgs, ex); + } + } + } + #endregion /// @@ -301,7 +333,9 @@ namespace Umbraco.Core.Services using (var uow = UowProvider.CreateUnitOfWork()) { - if (uow.Events.DispatchCancelable(Moving, this, new MoveEventArgs(evtMsgs, new MoveEventInfo(toMove, toMove.Path, parentId)))) + var moveEventInfo = new MoveEventInfo(toMove, toMove.Path, parentId); + var moveEventArgs = new MoveEventArgs(evtMsgs, moveEventInfo); + if (uow.Events.DispatchCancelable(Moving, this, moveEventArgs)) { uow.Complete(); return OperationStatus.Attempt.Fail(MoveOperationStatusType.FailedCancelledByEvent, evtMsgs); @@ -321,7 +355,9 @@ namespace Umbraco.Core.Services } moveInfo.AddRange(repository.Move(toMove, container)); - uow.Events.Dispatch(Moved, this, new MoveEventArgs(false, evtMsgs, moveInfo.ToArray())); + moveEventArgs.MoveInfoCollection = moveInfo; + moveEventArgs.CanCancel = false; + uow.Events.Dispatch(Moved, this, moveEventArgs); uow.Complete(); } catch (DataOperationException ex) @@ -345,7 +381,8 @@ namespace Umbraco.Core.Services using (var uow = UowProvider.CreateUnitOfWork()) { - if (uow.Events.DispatchCancelable(Saving, this, new SaveEventArgs(dataTypeDefinition))) + var saveEventArgs = new SaveEventArgs(dataTypeDefinition); + if (uow.Events.DispatchCancelable(Saving, this, saveEventArgs)) { uow.Complete(); return; @@ -359,7 +396,8 @@ namespace Umbraco.Core.Services var repository = uow.CreateRepository(); repository.AddOrUpdate(dataTypeDefinition); - uow.Events.Dispatch(Saved, this, new SaveEventArgs(dataTypeDefinition, false)); + saveEventArgs.CanCancel = false; + uow.Events.Dispatch(Saved, this, saveEventArgs); Audit(uow, AuditType.Save, "Save DataTypeDefinition performed by user", userId, dataTypeDefinition.Id); uow.Complete(); } @@ -384,10 +422,11 @@ namespace Umbraco.Core.Services public void Save(IEnumerable dataTypeDefinitions, int userId, bool raiseEvents) { var dataTypeDefinitionsA = dataTypeDefinitions.ToArray(); + var saveEventArgs = new SaveEventArgs(dataTypeDefinitionsA); using (var uow = UowProvider.CreateUnitOfWork()) { - if (raiseEvents && uow.Events.DispatchCancelable(Saving, this, new SaveEventArgs(dataTypeDefinitionsA))) + if (raiseEvents && uow.Events.DispatchCancelable(Saving, this, saveEventArgs)) { uow.Complete(); return; @@ -401,7 +440,10 @@ namespace Umbraco.Core.Services } if (raiseEvents) - uow.Events.Dispatch(Saved, this, new SaveEventArgs(dataTypeDefinitionsA, false)); + { + saveEventArgs.CanCancel = false; + uow.Events.Dispatch(Saved, this, saveEventArgs); + } Audit(uow, AuditType.Save, "Save DataTypeDefinition performed by user", userId, -1); uow.Complete(); @@ -486,7 +528,8 @@ namespace Umbraco.Core.Services { using (var uow = UowProvider.CreateUnitOfWork()) { - if (uow.Events.DispatchCancelable(Saving, this, new SaveEventArgs(dataTypeDefinition))) + var saveEventArgs = new SaveEventArgs(dataTypeDefinition); + if (uow.Events.DispatchCancelable(Saving, this, saveEventArgs)) { uow.Complete(); return; @@ -502,7 +545,8 @@ namespace Umbraco.Core.Services repository.AddOrUpdate(dataTypeDefinition); // definition repository.AddOrUpdatePreValues(dataTypeDefinition, values); //prevalues - uow.Events.Dispatch(Saved, this, new SaveEventArgs(dataTypeDefinition, false)); + saveEventArgs.CanCancel = false; + uow.Events.Dispatch(Saved, this, saveEventArgs); Audit(uow, AuditType.Save, "Save DataTypeDefinition performed by user", userId, dataTypeDefinition.Id); uow.Complete(); @@ -522,7 +566,8 @@ namespace Umbraco.Core.Services { using (var uow = UowProvider.CreateUnitOfWork()) { - if (uow.Events.DispatchCancelable(Deleting, this, new DeleteEventArgs(dataTypeDefinition))) + var deleteEventArgs = new DeleteEventArgs(dataTypeDefinition); + if (uow.Events.DispatchCancelable(Deleting, this, deleteEventArgs)) { uow.Complete(); return; @@ -531,7 +576,8 @@ namespace Umbraco.Core.Services var repository = uow.CreateRepository(); repository.Delete(dataTypeDefinition); - uow.Events.Dispatch(Deleted, this, new DeleteEventArgs(dataTypeDefinition, false)); + deleteEventArgs.CanCancel = false; + uow.Events.Dispatch(Deleted, this, deleteEventArgs); Audit(uow, AuditType.Delete, "Delete DataTypeDefinition performed by user", userId, dataTypeDefinition.Id); uow.Complete(); diff --git a/src/Umbraco.Core/Services/DomainService.cs b/src/Umbraco.Core/Services/DomainService.cs index 7fa1a391af..b241dbad60 100644 --- a/src/Umbraco.Core/Services/DomainService.cs +++ b/src/Umbraco.Core/Services/DomainService.cs @@ -33,7 +33,8 @@ namespace Umbraco.Core.Services using (var uow = UowProvider.CreateUnitOfWork()) { - if (uow.Events.DispatchCancelable(Deleting, this, new DeleteEventArgs(domain, evtMsgs))) + var deleteEventArgs = new DeleteEventArgs(domain, evtMsgs); + if (uow.Events.DispatchCancelable(Deleting, this, deleteEventArgs)) { uow.Complete(); return OperationStatus.Attempt.Cancel(evtMsgs); @@ -43,8 +44,8 @@ namespace Umbraco.Core.Services repository.Delete(domain); uow.Complete(); - var args = new DeleteEventArgs(domain, false, evtMsgs); - uow.Events.Dispatch(Deleted, this, args); + deleteEventArgs.CanCancel = false; + uow.Events.Dispatch(Deleted, this, deleteEventArgs); } return OperationStatus.Attempt.Succeed(evtMsgs); @@ -92,7 +93,8 @@ namespace Umbraco.Core.Services using (var uow = UowProvider.CreateUnitOfWork()) { - if (uow.Events.DispatchCancelable(Saving, this, new SaveEventArgs(domainEntity, evtMsgs))) + var saveEventArgs = new SaveEventArgs(domainEntity, evtMsgs); + if (uow.Events.DispatchCancelable(Saving, this, saveEventArgs)) { uow.Complete(); return OperationStatus.Attempt.Cancel(evtMsgs); @@ -101,8 +103,8 @@ namespace Umbraco.Core.Services var repository = uow.CreateRepository(); repository.AddOrUpdate(domainEntity); uow.Complete(); - - uow.Events.Dispatch(Saved, this, new SaveEventArgs(domainEntity, false, evtMsgs)); + saveEventArgs.CanCancel = false; + uow.Events.Dispatch(Saved, this, saveEventArgs); } return OperationStatus.Attempt.Succeed(evtMsgs); diff --git a/src/Umbraco.Core/Services/EntityService.cs b/src/Umbraco.Core/Services/EntityService.cs index e910b5504b..a5d570b7d6 100644 --- a/src/Umbraco.Core/Services/EntityService.cs +++ b/src/Umbraco.Core/Services/EntityService.cs @@ -374,11 +374,22 @@ namespace Umbraco.Core.Services using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { var repository = uow.CreateRepository(); - + var query = repository.Query; //if the id is System Root, then just get all if (id != Constants.System.Root) - query.Where(x => x.Path.SqlContains($",{id},", TextColumnType.NVarchar)); + { + //lookup the path so we can use it in the prefix query below + var itemPaths = repository.GetAllPaths(objectTypeId, id).ToArray(); + if (itemPaths.Length == 0) + { + totalRecords = 0; + return Enumerable.Empty(); + } + var itemPath = itemPaths[0].Path; + + query.Where(x => x.Path.SqlStartsWith(itemPath + ",", TextColumnType.NVarchar)); + } IQuery filterQuery = null; if (filter.IsNullOrWhiteSpace() == false) @@ -407,15 +418,30 @@ namespace Umbraco.Core.Services using (var uow = UowProvider.CreateUnitOfWork(readOnly: true)) { var repository = uow.CreateRepository(); - + var query = uow.Query(); if (idsA.All(x => x != Constants.System.Root)) { + //lookup the paths so we can use it in the prefix query below + var itemPaths = repository.GetAllPaths(objectTypeId, idsA).ToArray(); + if (itemPaths.Length == 0) + { + totalRecords = 0; + return Enumerable.Empty(); + } + var clauses = new List>>(); foreach (var id in idsA) { - var qid = id; - clauses.Add(x => x.Path.SqlContains(string.Format(",{0},", qid), TextColumnType.NVarchar) || x.Path.SqlEndsWith(string.Format(",{0}", qid), TextColumnType.NVarchar)); + //if the id is root then don't add any clauses + if (id != Constants.System.Root) + { + var itemPath = itemPaths.FirstOrDefault(x => x.Id == id); + if (itemPath == null) continue; + var path = itemPath.Path; + var qid = id; + clauses.Add(x => x.Path.SqlStartsWith(path + ",", TextColumnType.NVarchar) || x.Path.SqlEndsWith("," + qid, TextColumnType.NVarchar)); + } } query.WhereAny(clauses); } @@ -468,7 +494,7 @@ namespace Umbraco.Core.Services return contents; } } - + /// /// Gets a collection of the entities at the root, which corresponds to the entities with a Parent Id of -1. /// @@ -566,13 +592,13 @@ namespace Umbraco.Core.Services return repository.GetAllPaths(objectTypeId, keys); } } - + /// - /// Gets a collection of + /// Gets a collection of /// /// Guid id of the UmbracoObjectType /// - /// An enumerable list of objects + /// An enumerable list of objects public virtual IEnumerable GetAll(Guid objectTypeId, params int[] ids) { var umbracoObjectType = UmbracoObjectTypesExtensions.GetUmbracoObjectType(objectTypeId); @@ -692,5 +718,34 @@ namespace Umbraco.Core.Services return exists; } } + + /// + public int ReserveId(Guid key) + { + NodeDto node; + using (var scope = UowProvider.ScopeProvider.CreateScope()) + { + var sql = new Sql("SELECT * FROM umbracoNode WHERE uniqueID=@0 AND nodeObjectType=@1", key, Constants.ObjectTypes.IdReservationGuid); + node = scope.Database.SingleOrDefault(sql); + if (node != null) throw new InvalidOperationException("An identifier has already been reserved for this Udi."); + node = new NodeDto + { + UniqueId = key, + Text = "RESERVED.ID", + NodeObjectType = Constants.ObjectTypes.IdReservationGuid, + + CreateDate = DateTime.Now, + UserId = 0, + ParentId = -1, + Level = 1, + Path = "-1", + SortOrder = 0, + Trashed = false + }; + scope.Database.Insert(node); + scope.Complete(); + } + return node.NodeId; + } } } diff --git a/src/Umbraco.Core/Services/FileService.cs b/src/Umbraco.Core/Services/FileService.cs index 976f5b7438..32b32fa401 100644 --- a/src/Umbraco.Core/Services/FileService.cs +++ b/src/Umbraco.Core/Services/FileService.cs @@ -64,7 +64,8 @@ namespace Umbraco.Core.Services { using (var uow = UowProvider.CreateUnitOfWork()) { - if (uow.Events.DispatchCancelable(SavingStylesheet, this, new SaveEventArgs(stylesheet))) + var saveEventArgs = new SaveEventArgs(stylesheet); + if (uow.Events.DispatchCancelable(SavingStylesheet, this, saveEventArgs)) { uow.Complete(); return; @@ -72,8 +73,8 @@ namespace Umbraco.Core.Services var repository = uow.CreateRepository(); repository.AddOrUpdate(stylesheet); - - uow.Events.Dispatch(SavedStylesheet, this, new SaveEventArgs(stylesheet, false)); + saveEventArgs.CanCancel = false; + uow.Events.Dispatch(SavedStylesheet, this, saveEventArgs); Audit(uow, AuditType.Save, "Save Stylesheet performed by user", userId, -1); uow.Complete(); @@ -97,15 +98,16 @@ namespace Umbraco.Core.Services return; } - if (uow.Events.DispatchCancelable(DeletingStylesheet, this, new DeleteEventArgs(stylesheet))) + var deleteEventArgs = new DeleteEventArgs(stylesheet); + if (uow.Events.DispatchCancelable(DeletingStylesheet, this, deleteEventArgs)) { uow.Complete(); return; // causes rollback } repository.Delete(stylesheet); - - uow.Events.Dispatch(DeletedStylesheet, this, new DeleteEventArgs(stylesheet, false)); + deleteEventArgs.CanCancel = false; + uow.Events.Dispatch(DeletedStylesheet, this, deleteEventArgs); Audit(uow, AuditType.Delete, "Delete Stylesheet performed by user", userId, -1); uow.Complete(); @@ -194,7 +196,8 @@ namespace Umbraco.Core.Services { using (var uow = UowProvider.CreateUnitOfWork()) { - if (uow.Events.DispatchCancelable(SavingScript, this, new SaveEventArgs