From 968927d472ffd680c875f24b04cd67ca39d3d4e5 Mon Sep 17 00:00:00 2001 From: AndyButland Date: Sat, 13 Dec 2014 22:00:32 +0100 Subject: [PATCH 01/46] Enabled sorting for custom columns on list view --- .../Repositories/ContentRepository.cs | 7 +- .../Interfaces/IContentRepository.cs | 3 +- .../Interfaces/IMediaRepository.cs | 3 +- .../Interfaces/IMemberRepository.cs | 17 +++-- .../Repositories/MediaRepository.cs | 7 +- .../Repositories/MemberRepository.cs | 19 ++--- .../Repositories/VersionableRepositoryBase.cs | 73 ++++++++++++------- src/Umbraco.Core/Services/ContentService.cs | 10 ++- src/Umbraco.Core/Services/IContentService.cs | 6 +- src/Umbraco.Core/Services/IMediaService.cs | 6 +- src/Umbraco.Core/Services/IMemberService.cs | 9 ++- src/Umbraco.Core/Services/MediaService.cs | 10 ++- src/Umbraco.Core/Services/MemberService.cs | 14 ++-- .../Repositories/ContentRepositoryTest.cs | 12 +-- .../Repositories/MediaRepositoryTest.cs | 14 ++-- .../UmbracoExamine/IndexInitializer.cs | 2 +- .../src/common/resources/content.resource.js | 4 +- .../src/common/resources/media.resource.js | 4 +- .../src/common/resources/member.resource.js | 4 +- .../listview/listview.controller.js | 22 +++--- .../propertyeditors/listview/listview.html | 4 +- src/Umbraco.Web/Editors/ContentController.cs | 3 +- src/Umbraco.Web/Editors/MediaController.cs | 3 +- src/Umbraco.Web/Editors/MemberController.cs | 3 +- src/UmbracoExamine/UmbracoMemberIndexer.cs | 2 +- 25 files changed, 153 insertions(+), 108 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs index 203393391a..ce4f54b2b1 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs @@ -233,7 +233,7 @@ namespace Umbraco.Core.Persistence.Repositories var processed = 0; do { - var descendants = GetPagedResultsByQuery(query, pageIndex, pageSize, out total, "Path", Direction.Ascending); + var descendants = GetPagedResultsByQuery(query, pageIndex, pageSize, out total, "Path", Direction.Ascending, true); var xmlItems = (from descendant in descendants let xml = serializer(descendant) @@ -713,10 +713,11 @@ namespace Umbraco.Core.Persistence.Repositories /// Total records query would return without paging /// Field to order by /// Direction to order by + /// Flag to indicate when ordering by system field /// Search text filter /// An Enumerable list of objects public IEnumerable GetPagedResultsByQuery(IQuery query, int pageIndex, int pageSize, out int totalRecords, - string orderBy, Direction orderDirection, string filter = "") + string orderBy, Direction orderDirection, bool orderBySystemField, string filter = "") { //NOTE: This uses the GetBaseQuery method but that does not take into account the required 'newest' field which is @@ -735,7 +736,7 @@ namespace Umbraco.Core.Persistence.Repositories return GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, new Tuple("cmsDocument", "nodeId"), - ProcessQuery, orderBy, orderDirection, + ProcessQuery, orderBy, orderDirection, orderBySystemField, filterCallback); } diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs index 5cb470b7cd..50adaadd73 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs @@ -92,9 +92,10 @@ namespace Umbraco.Core.Persistence.Repositories /// Total records query would return without paging /// Field to order by /// Direction to order by + /// Flag to indicate when ordering by system field /// Search text filter /// An Enumerable list of objects IEnumerable GetPagedResultsByQuery(IQuery query, int pageIndex, int pageSize, out int totalRecords, - string orderBy, Direction orderDirection, string filter = ""); + string orderBy, Direction orderDirection, bool orderBySystemField, string filter = ""); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMediaRepository.cs index f84844c177..94ac1ed7cb 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMediaRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMediaRepository.cs @@ -33,9 +33,10 @@ namespace Umbraco.Core.Persistence.Repositories /// Total records query would return without paging /// Field to order by /// Direction to order by + /// Flag to indicate when ordering by system field /// Search text filter /// An Enumerable list of objects IEnumerable GetPagedResultsByQuery(IQuery query, int pageIndex, int pageSize, out int totalRecords, - string orderBy, Direction orderDirection, string filter = ""); + string orderBy, Direction orderDirection, bool orderBySystemField, string filter = ""); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMemberRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMemberRepository.cs index 0538fe18fa..ece5e02ba0 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMemberRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMemberRepository.cs @@ -44,16 +44,17 @@ namespace Umbraco.Core.Persistence.Repositories /// /// Gets paged member results /// - /// - /// - /// - /// - /// - /// - /// + /// The query. + /// Index of the page. + /// Size of the page. + /// The total records. + /// The order by column + /// The order direction. + /// Flag to indicate when ordering by system field + /// Search query /// IEnumerable GetPagedResultsByQuery(IQuery query, int pageIndex, int pageSize, out int totalRecords, - string orderBy, Direction orderDirection, string filter = ""); + string orderBy, Direction orderDirection, bool orderBySystemField, string filter = ""); //IEnumerable GetPagedResultsByQuery( // Sql sql, int pageIndex, int pageSize, out int totalRecords, diff --git a/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs index a6db5387f4..5c0be2c2e6 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs @@ -242,7 +242,7 @@ namespace Umbraco.Core.Persistence.Repositories var processed = 0; do { - var descendants = GetPagedResultsByQuery(query, pageIndex, pageSize, out total, "Path", Direction.Ascending); + var descendants = GetPagedResultsByQuery(query, pageIndex, pageSize, out total, "Path", Direction.Ascending, true); var xmlItems = (from descendant in descendants let xml = serializer(descendant) @@ -451,10 +451,11 @@ namespace Umbraco.Core.Persistence.Repositories /// Total records query would return without paging /// Field to order by /// Direction to order by + /// Flag to indicate when ordering by system field /// Search text filter /// An Enumerable list of objects public IEnumerable GetPagedResultsByQuery(IQuery query, int pageIndex, int pageSize, out int totalRecords, - string orderBy, Direction orderDirection, string filter = "") + string orderBy, Direction orderDirection, bool orderBySystemField, string filter = "") { var args = new List(); var sbWhere = new StringBuilder(); @@ -468,7 +469,7 @@ namespace Umbraco.Core.Persistence.Repositories return GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, new Tuple("cmsContentVersion", "contentId"), - ProcessQuery, orderBy, orderDirection, + ProcessQuery, orderBy, orderDirection, orderBySystemField, filterCallback); } diff --git a/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs index 9e3a47efe0..f9ff109879 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs @@ -487,7 +487,7 @@ namespace Umbraco.Core.Persistence.Repositories var processed = 0; do { - var descendants = GetPagedResultsByQuery(query, pageIndex, pageSize, out total, "Path", Direction.Ascending); + var descendants = GetPagedResultsByQuery(query, pageIndex, pageSize, out total, "Path", Direction.Ascending, true); var xmlItems = (from descendant in descendants let xml = serializer(descendant) @@ -643,18 +643,19 @@ namespace Umbraco.Core.Persistence.Repositories /// /// The where clause, if this is null all records are queried /// - /// - /// - /// - /// - /// - /// + /// Index of the page. + /// Size of the page. + /// The total records. + /// The order by column + /// The order direction. + /// Flag to indicate when ordering by system field + /// Search query /// /// /// The query supplied will ONLY work with data specifically on the cmsMember table because we are using PetaPoco paging (SQL paging) /// public IEnumerable GetPagedResultsByQuery(IQuery query, int pageIndex, int pageSize, out int totalRecords, - string orderBy, Direction orderDirection, string filter = "") + string orderBy, Direction orderDirection, bool orderBySystemField, string filter = "") { var args = new List(); var sbWhere = new StringBuilder(); @@ -669,7 +670,7 @@ namespace Umbraco.Core.Persistence.Repositories return GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, new Tuple("cmsMember", "nodeId"), - ProcessQuery, orderBy, orderDirection, + ProcessQuery, orderBy, orderDirection, orderBySystemField, filterCallback); } diff --git a/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs index 3acc06b132..c53937473d 100644 --- a/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs @@ -238,24 +238,54 @@ namespace Umbraco.Core.Persistence.Repositories return filteredSql; } - private Sql GetSortedSqlForPagedResults(Sql sql, Direction orderDirection, string orderBy) + private Sql GetSortedSqlForPagedResults(Sql sql, Direction orderDirection, string orderBy, bool orderBySystemField) { //copy to var so that the original isn't changed var sortedSql = new Sql(sql.SQL, sql.Arguments); - // Apply order according to parameters - if (string.IsNullOrEmpty(orderBy) == false) + + if (orderBySystemField) { - var orderByParams = new[] { GetDatabaseFieldNameForOrderBy(orderBy) }; - if (orderDirection == Direction.Ascending) + // Apply order according to parameters + if (string.IsNullOrEmpty(orderBy) == false) { - sortedSql.OrderBy(orderByParams); + var orderByParams = new[] { GetDatabaseFieldNameForOrderBy(orderBy) }; + if (orderDirection == Direction.Ascending) + { + sortedSql.OrderBy(orderByParams); + } + else + { + sortedSql.OrderByDescending(orderByParams); + } } - else - { - sortedSql.OrderByDescending(orderByParams); - } - return sortedSql; } + else + { + // Sorting by a custom field, so set-up sub-query for ORDER BY clause to pull through valie + // from most recent content version for the given order by field + sortedSql.Append(@"ORDER BY ( + SELECT CASE + WHEN dataInt Is Not Null THEN RIGHT('00000000' + CAST(dataInt AS varchar(8)),8) + WHEN dataDate Is Not Null THEN CONVERT(varchar, dataDate, 102) + ELSE IsNull(dataNvarchar,'') + END + FROM cmsContent c + INNER JOIN cmsContentVersion cv ON cv.ContentId = c.nodeId AND VersionDate = ( + SELECT Max(VersionDate) + FROM cmsContentVersion + WHERE ContentId = c.nodeId + ) + INNER JOIN cmsPropertyType cpt ON cpt.contentTypeId = c.contentType + INNER JOIN cmsPropertyData cpd ON cpd.contentNodeId = c.nodeId + AND cpd.versionId = cv.VersionId + AND cpd.propertytypeId = cpt.id + WHERE c.nodeId = umbracoNode.Id and cpt.Alias = @0)", orderBy); + if (orderDirection == Direction.Descending) + { + sortedSql.Append(" DESC"); + } + } + return sortedSql; } @@ -273,6 +303,7 @@ namespace Umbraco.Core.Persistence.Repositories /// A callback to process the query result /// The order by column /// The order direction. + /// Flag to indicate when ordering by system field /// /// orderBy protected IEnumerable GetPagedResultsByQuery(IQuery query, int pageIndex, int pageSize, out int totalRecords, @@ -280,6 +311,7 @@ namespace Umbraco.Core.Persistence.Repositories Func> processQuery, string orderBy, Direction orderDirection, + bool orderBySystemField, Func> defaultFilter = null) where TContentBase : class, IAggregateRoot, TEntity { @@ -302,7 +334,7 @@ namespace Umbraco.Core.Persistence.Repositories //get sorted and filtered sql var sqlNodeIdsWithSort = GetSortedSqlForPagedResults( GetFilteredSqlForPagedResults(sqlNodeIds, defaultFilter), - orderDirection, orderBy); + orderDirection, orderBy, orderBySystemField); // Get page of results and total count IEnumerable result; @@ -340,22 +372,9 @@ namespace Umbraco.Core.Persistence.Repositories //get sorted and filtered sql var fullQuery = GetSortedSqlForPagedResults( GetFilteredSqlForPagedResults(withInnerJoinSql, defaultFilter), - orderDirection, orderBy); - - var content = processQuery(fullQuery) - .Cast() - .AsQueryable(); + orderDirection, orderBy, orderBySystemField); - // Now we need to ensure this result is also ordered by the same order by clause - var orderByProperty = GetEntityPropertyNameForOrderBy(orderBy); - if (orderDirection == Direction.Ascending) - { - result = content.OrderBy(orderByProperty); - } - else - { - result = content.OrderByDescending(orderByProperty); - } + return processQuery(fullQuery); } else { diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index 057ca928bd..5bebdefec9 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -495,10 +495,11 @@ namespace Umbraco.Core.Services /// Total records query would return without paging /// Field to order by /// Direction to order by + /// Flag to indicate when ordering by system field /// Search text filter /// An Enumerable list of objects public IEnumerable GetPagedChildren(int id, int pageIndex, int pageSize, out int totalChildren, - string orderBy, Direction orderDirection, string filter = "") + string orderBy, Direction orderDirection, bool orderBySystemField = true, string filter = "") { Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); Mandate.ParameterCondition(pageSize > 0, "pageSize"); @@ -511,7 +512,7 @@ namespace Umbraco.Core.Services { query.Where(x => x.ParentId == id); } - var contents = repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, filter); + var contents = repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, orderBySystemField, filter); return contents; } @@ -526,9 +527,10 @@ namespace Umbraco.Core.Services /// Total records query would return without paging /// Field to order by /// Direction to order by + /// Flag to indicate when ordering by system field /// Search text filter /// An Enumerable list of objects - public IEnumerable GetPagedDescendants(int id, int pageIndex, int pageSize, out int totalChildren, string orderBy = "Path", Direction orderDirection = Direction.Ascending, string filter = "") + public IEnumerable GetPagedDescendants(int id, int pageIndex, int pageSize, out int totalChildren, string orderBy = "Path", Direction orderDirection = Direction.Ascending, bool orderBySystemField = true, string filter = "") { Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); Mandate.ParameterCondition(pageSize > 0, "pageSize"); @@ -541,7 +543,7 @@ namespace Umbraco.Core.Services { query.Where(x => x.Path.SqlContains(string.Format(",{0},", id), TextColumnType.NVarchar)); } - var contents = repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, filter); + var contents = repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, orderBySystemField, filter); return contents; } diff --git a/src/Umbraco.Core/Services/IContentService.cs b/src/Umbraco.Core/Services/IContentService.cs index f5579896f4..ce2828e208 100644 --- a/src/Umbraco.Core/Services/IContentService.cs +++ b/src/Umbraco.Core/Services/IContentService.cs @@ -128,10 +128,11 @@ namespace Umbraco.Core.Services /// Total records query would return without paging /// Field to order by /// Direction to order by + /// Flag to indicate when ordering by system field /// Search text filter /// An Enumerable list of objects IEnumerable GetPagedChildren(int id, int pageIndex, int pageSize, out int totalRecords, - string orderBy = "SortOrder", Direction orderDirection = Direction.Ascending, string filter = ""); + string orderBy = "SortOrder", Direction orderDirection = Direction.Ascending, bool orderBySystemField = true, string filter = ""); /// /// Gets a collection of objects by Parent Id @@ -142,10 +143,11 @@ namespace Umbraco.Core.Services /// Total records query would return without paging /// Field to order by /// Direction to order by + /// Flag to indicate when ordering by system field /// Search text filter /// An Enumerable list of objects IEnumerable GetPagedDescendants(int id, int pageIndex, int pageSize, out int totalRecords, - string orderBy = "Path", Direction orderDirection = Direction.Ascending, string filter = ""); + string orderBy = "Path", Direction orderDirection = Direction.Ascending, bool orderBySystemField = true, string filter = ""); /// /// Gets a collection of an objects versions by its Id diff --git a/src/Umbraco.Core/Services/IMediaService.cs b/src/Umbraco.Core/Services/IMediaService.cs index b77046ddc1..05f861a331 100644 --- a/src/Umbraco.Core/Services/IMediaService.cs +++ b/src/Umbraco.Core/Services/IMediaService.cs @@ -80,10 +80,11 @@ namespace Umbraco.Core.Services /// Total records query would return without paging /// Field to order by /// Direction to order by + /// Flag to indicate when ordering by system field /// Search text filter /// An Enumerable list of objects IEnumerable GetPagedChildren(int id, int pageIndex, int pageSize, out int totalRecords, - string orderBy = "SortOrder", Direction orderDirection = Direction.Ascending, string filter = ""); + string orderBy = "SortOrder", Direction orderDirection = Direction.Ascending, bool orderBySystemField = true, string filter = ""); /// /// Gets a collection of objects by Parent Id @@ -94,10 +95,11 @@ namespace Umbraco.Core.Services /// Total records query would return without paging /// Field to order by /// Direction to order by + /// Flag to indicate when ordering by system field /// Search text filter /// An Enumerable list of objects IEnumerable GetPagedDescendants(int id, int pageIndex, int pageSize, out int totalRecords, - string orderBy = "Path", Direction orderDirection = Direction.Ascending, string filter = ""); + string orderBy = "Path", Direction orderDirection = Direction.Ascending, bool orderBySystemField = true, string filter = ""); /// /// Gets descendants of a object by its Id diff --git a/src/Umbraco.Core/Services/IMemberService.cs b/src/Umbraco.Core/Services/IMemberService.cs index 249a744081..b8bb2e69f1 100644 --- a/src/Umbraco.Core/Services/IMemberService.cs +++ b/src/Umbraco.Core/Services/IMemberService.cs @@ -28,13 +28,14 @@ namespace Umbraco.Core.Services /// Current page index /// Size of the page /// Total number of records found (out) - /// - /// + /// Field to order by + /// Direction to order by + /// Flag to indicate when ordering by system field /// - /// + /// Search text filter /// IEnumerable GetAll(int pageIndex, int pageSize, out int totalRecords, - string orderBy, Direction orderDirection, string memberTypeAlias = null, string filter = ""); + string orderBy, Direction orderDirection, bool orderBySystemField = true, string memberTypeAlias = null, string filter = ""); /// /// Creates an object without persisting it diff --git a/src/Umbraco.Core/Services/MediaService.cs b/src/Umbraco.Core/Services/MediaService.cs index 4a42439024..e1ca32d14c 100644 --- a/src/Umbraco.Core/Services/MediaService.cs +++ b/src/Umbraco.Core/Services/MediaService.cs @@ -403,10 +403,11 @@ namespace Umbraco.Core.Services /// Total records query would return without paging /// Field to order by /// Direction to order by + /// Flag to indicate when ordering by system field /// Search text filter /// An Enumerable list of objects public IEnumerable GetPagedChildren(int id, int pageIndex, int pageSize, out int totalChildren, - string orderBy, Direction orderDirection, string filter = "") + string orderBy, Direction orderDirection, bool orderBySystemField = true, string filter = "") { Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); Mandate.ParameterCondition(pageSize > 0, "pageSize"); @@ -418,7 +419,7 @@ namespace Umbraco.Core.Services { query.Where(x => x.ParentId == id); } - var medias = repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, filter); + var medias = repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, orderBySystemField, filter); return medias; } @@ -433,9 +434,10 @@ namespace Umbraco.Core.Services /// Total records query would return without paging /// Field to order by /// Direction to order by + /// Flag to indicate when ordering by system field /// Search text filter /// An Enumerable list of objects - public IEnumerable GetPagedDescendants(int id, int pageIndex, int pageSize, out int totalChildren, string orderBy = "Path", Direction orderDirection = Direction.Ascending, string filter = "") + public IEnumerable GetPagedDescendants(int id, int pageIndex, int pageSize, out int totalChildren, string orderBy = "Path", Direction orderDirection = Direction.Ascending, bool orderBySystemField = true, string filter = "") { Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); Mandate.ParameterCondition(pageSize > 0, "pageSize"); @@ -448,7 +450,7 @@ namespace Umbraco.Core.Services { query.Where(x => x.Path.SqlContains(string.Format(",{0},", id), TextColumnType.NVarchar)); } - var contents = repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, filter); + var contents = repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, orderBySystemField, filter); return contents; } diff --git a/src/Umbraco.Core/Services/MemberService.cs b/src/Umbraco.Core/Services/MemberService.cs index 4bb5e406f9..ea566eca80 100644 --- a/src/Umbraco.Core/Services/MemberService.cs +++ b/src/Umbraco.Core/Services/MemberService.cs @@ -308,7 +308,7 @@ namespace Umbraco.Core.Services throw new ArgumentOutOfRangeException("matchType"); } - return repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, "Name", Direction.Ascending); + return repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, "Name", Direction.Ascending, true); } } @@ -349,7 +349,7 @@ namespace Umbraco.Core.Services throw new ArgumentOutOfRangeException("matchType"); } - return repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, "Email", Direction.Ascending); + return repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, "Email", Direction.Ascending, true); } } @@ -390,7 +390,7 @@ namespace Umbraco.Core.Services throw new ArgumentOutOfRangeException("matchType"); } - return repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, "LoginName", Direction.Ascending); + return repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, "LoginName", Direction.Ascending, true); } } @@ -677,22 +677,22 @@ namespace Umbraco.Core.Services var uow = _uowProvider.GetUnitOfWork(); using (var repository = _repositoryFactory.CreateMemberRepository(uow)) { - return repository.GetPagedResultsByQuery(null, pageIndex, pageSize, out totalRecords, "LoginName", Direction.Ascending); + return repository.GetPagedResultsByQuery(null, pageIndex, pageSize, out totalRecords, "LoginName", Direction.Ascending, true); } } public IEnumerable GetAll(int pageIndex, int pageSize, out int totalRecords, - string orderBy, Direction orderDirection, string memberTypeAlias = null, string filter = "") + string orderBy, Direction orderDirection, bool orderBySystemField = true, string memberTypeAlias = null, string filter = "") { var uow = _uowProvider.GetUnitOfWork(); using (var repository = _repositoryFactory.CreateMemberRepository(uow)) { if (memberTypeAlias == null) { - return repository.GetPagedResultsByQuery(null, pageIndex, pageSize, out totalRecords, orderBy, orderDirection, filter); + return repository.GetPagedResultsByQuery(null, pageIndex, pageSize, out totalRecords, orderBy, orderDirection, orderBySystemField, filter); } var query = new Query().Where(x => x.ContentTypeAlias == memberTypeAlias); - return repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, orderBy, orderDirection, filter); + return repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, orderBy, orderDirection, orderBySystemField, filter); } } diff --git a/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs index f55b185d8b..cacc17bddf 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs @@ -483,7 +483,7 @@ namespace Umbraco.Tests.Persistence.Repositories // Act var query = Query.Builder.Where(x => x.Level == 2); int totalRecords; - var result = repository.GetPagedResultsByQuery(query, 0, 1, out totalRecords, "Name", Direction.Ascending); + var result = repository.GetPagedResultsByQuery(query, 0, 1, out totalRecords, "Name", Direction.Ascending, true); // Assert Assert.That(totalRecords, Is.GreaterThanOrEqualTo(2)); @@ -504,7 +504,7 @@ namespace Umbraco.Tests.Persistence.Repositories // Act var query = Query.Builder.Where(x => x.Level == 2); int totalRecords; - var result = repository.GetPagedResultsByQuery(query, 1, 1, out totalRecords, "Name", Direction.Ascending); + var result = repository.GetPagedResultsByQuery(query, 1, 1, out totalRecords, "Name", Direction.Ascending, true); // Assert Assert.That(totalRecords, Is.GreaterThanOrEqualTo(2)); @@ -525,7 +525,7 @@ namespace Umbraco.Tests.Persistence.Repositories // Act var query = Query.Builder.Where(x => x.Level == 2); int totalRecords; - var result = repository.GetPagedResultsByQuery(query, 0, 2, out totalRecords, "Name", Direction.Ascending); + var result = repository.GetPagedResultsByQuery(query, 0, 2, out totalRecords, "Name", Direction.Ascending, true); // Assert Assert.That(totalRecords, Is.GreaterThanOrEqualTo(2)); @@ -546,7 +546,7 @@ namespace Umbraco.Tests.Persistence.Repositories // Act var query = Query.Builder.Where(x => x.Level == 2); int totalRecords; - var result = repository.GetPagedResultsByQuery(query, 0, 1, out totalRecords, "Name", Direction.Descending); + var result = repository.GetPagedResultsByQuery(query, 0, 1, out totalRecords, "Name", Direction.Descending, true); // Assert Assert.That(totalRecords, Is.GreaterThanOrEqualTo(2)); @@ -567,7 +567,7 @@ namespace Umbraco.Tests.Persistence.Repositories // Act var query = Query.Builder.Where(x => x.Level == 2); int totalRecords; - var result = repository.GetPagedResultsByQuery(query, 0, 1, out totalRecords, "Name", Direction.Ascending, "Page 2"); + var result = repository.GetPagedResultsByQuery(query, 0, 1, out totalRecords, "Name", Direction.Ascending, true, "Page 2"); // Assert Assert.That(totalRecords, Is.EqualTo(1)); @@ -588,7 +588,7 @@ namespace Umbraco.Tests.Persistence.Repositories // Act var query = Query.Builder.Where(x => x.Level == 2); int totalRecords; - var result = repository.GetPagedResultsByQuery(query, 0, 1, out totalRecords, "Name", Direction.Ascending, "Page"); + var result = repository.GetPagedResultsByQuery(query, 0, 1, out totalRecords, "Name", Direction.Ascending, true, "Page"); // Assert Assert.That(totalRecords, Is.EqualTo(2)); diff --git a/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs index b76bdd37bc..dcf7435c3b 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs @@ -332,7 +332,7 @@ namespace Umbraco.Tests.Persistence.Repositories // Act var query = Query.Builder.Where(x => x.Level == 2); int totalRecords; - var result = repository.GetPagedResultsByQuery(query, 0, 1, out totalRecords, "SortOrder", Direction.Ascending); + var result = repository.GetPagedResultsByQuery(query, 0, 1, out totalRecords, "SortOrder", Direction.Ascending, true); // Assert Assert.That(totalRecords, Is.GreaterThanOrEqualTo(2)); @@ -353,7 +353,7 @@ namespace Umbraco.Tests.Persistence.Repositories // Act var query = Query.Builder.Where(x => x.Level == 2); int totalRecords; - var result = repository.GetPagedResultsByQuery(query, 1, 1, out totalRecords, "SortOrder", Direction.Ascending); + var result = repository.GetPagedResultsByQuery(query, 1, 1, out totalRecords, "SortOrder", Direction.Ascending, true); // Assert Assert.That(totalRecords, Is.GreaterThanOrEqualTo(2)); @@ -374,7 +374,7 @@ namespace Umbraco.Tests.Persistence.Repositories // Act var query = Query.Builder.Where(x => x.Level == 2); int totalRecords; - var result = repository.GetPagedResultsByQuery(query, 0, 2, out totalRecords, "SortOrder", Direction.Ascending); + var result = repository.GetPagedResultsByQuery(query, 0, 2, out totalRecords, "SortOrder", Direction.Ascending, true); // Assert Assert.That(totalRecords, Is.GreaterThanOrEqualTo(2)); @@ -395,7 +395,7 @@ namespace Umbraco.Tests.Persistence.Repositories // Act var query = Query.Builder.Where(x => x.Level == 2); int totalRecords; - var result = repository.GetPagedResultsByQuery(query, 0, 1, out totalRecords, "SortOrder", Direction.Descending); + var result = repository.GetPagedResultsByQuery(query, 0, 1, out totalRecords, "SortOrder", Direction.Descending, true); // Assert Assert.That(totalRecords, Is.GreaterThanOrEqualTo(2)); @@ -416,7 +416,7 @@ namespace Umbraco.Tests.Persistence.Repositories // Act var query = Query.Builder.Where(x => x.Level == 2); int totalRecords; - var result = repository.GetPagedResultsByQuery(query, 0, 1, out totalRecords, "Name", Direction.Ascending); + var result = repository.GetPagedResultsByQuery(query, 0, 1, out totalRecords, "Name", Direction.Ascending, true); // Assert Assert.That(totalRecords, Is.GreaterThanOrEqualTo(2)); @@ -437,7 +437,7 @@ namespace Umbraco.Tests.Persistence.Repositories // Act var query = Query.Builder.Where(x => x.Level == 2); int totalRecords; - var result = repository.GetPagedResultsByQuery(query, 0, 1, out totalRecords, "SortOrder", Direction.Ascending, "File"); + var result = repository.GetPagedResultsByQuery(query, 0, 1, out totalRecords, "SortOrder", Direction.Ascending, true, "File"); // Assert Assert.That(totalRecords, Is.EqualTo(1)); @@ -458,7 +458,7 @@ namespace Umbraco.Tests.Persistence.Repositories // Act var query = Query.Builder.Where(x => x.Level == 2); int totalRecords; - var result = repository.GetPagedResultsByQuery(query, 0, 1, out totalRecords, "SortOrder", Direction.Ascending, "Test"); + var result = repository.GetPagedResultsByQuery(query, 0, 1, out totalRecords, "SortOrder", Direction.Ascending, true, "Test"); // Assert Assert.That(totalRecords, Is.EqualTo(2)); diff --git a/src/Umbraco.Tests/UmbracoExamine/IndexInitializer.cs b/src/Umbraco.Tests/UmbracoExamine/IndexInitializer.cs index 7196d59c0c..670fccd07f 100644 --- a/src/Umbraco.Tests/UmbracoExamine/IndexInitializer.cs +++ b/src/Umbraco.Tests/UmbracoExamine/IndexInitializer.cs @@ -74,7 +74,7 @@ namespace Umbraco.Tests.UmbracoExamine mediaService = Mock.Of( x => x.GetPagedDescendants( - It.IsAny(), It.IsAny(), It.IsAny(), out totalRecs, It.IsAny(), It.IsAny(), It.IsAny()) + It.IsAny(), It.IsAny(), It.IsAny(), out totalRecs, It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()) == allRecs); } diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js index 4de66fa240..2823200832 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js @@ -435,7 +435,8 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { pageNumber: 0, filter: '', orderDirection: "Ascending", - orderBy: "SortOrder" + orderBy: "SortOrder", + orderBySystemField: true }; if (options === undefined) { options = {}; @@ -463,6 +464,7 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { { pageSize: options.pageSize }, { orderBy: options.orderBy }, { orderDirection: options.orderDirection }, + { orderBySystemField: options.orderBySystemField }, { filter: options.filter } ])), 'Failed to retrieve children for content item ' + parentId); diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/media.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/media.resource.js index 12963a1ea7..82376ef478 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/media.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/media.resource.js @@ -296,7 +296,8 @@ function mediaResource($q, $http, umbDataFormatter, umbRequestHelper) { pageNumber: 0, filter: '', orderDirection: "Ascending", - orderBy: "SortOrder" + orderBy: "SortOrder", + orderBySystemField: true }; if (options === undefined) { options = {}; @@ -324,6 +325,7 @@ function mediaResource($q, $http, umbDataFormatter, umbRequestHelper) { { pageSize: options.pageSize }, { orderBy: options.orderBy }, { orderDirection: options.orderDirection }, + { orderBySystemField: options.orderBySystemField }, { filter: options.filter } ])), 'Failed to retrieve children for media item ' + parentId); diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/member.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/member.resource.js index 381019fb5b..b0eb8c7bd9 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/member.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/member.resource.js @@ -34,7 +34,8 @@ function memberResource($q, $http, umbDataFormatter, umbRequestHelper) { pageNumber: 1, filter: '', orderDirection: "Ascending", - orderBy: "LoginName" + orderBy: "LoginName", + orderBySystemField: true }; if (options === undefined) { options = {}; @@ -56,6 +57,7 @@ function memberResource($q, $http, umbDataFormatter, umbRequestHelper) { { pageSize: options.pageSize }, { orderBy: options.orderBy }, { orderDirection: options.orderDirection }, + { orderBySystemField: options.orderBySystemField }, { filter: options.filter } ]; if (memberTypeAlias != null) { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js index a0c0c8b517..f83a218a2e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js @@ -66,6 +66,7 @@ function listViewController($rootScope, $scope, $routeParams, $injector, notific { alias: 'updateDate', header: 'Last edited', isSystem: 1 }, { alias: 'updater', header: 'Last edited by', isSystem: 1 } ], + orderBySystemField: true, allowBulkPublish: true, allowBulkUnpublish: true, allowBulkDelete: true, @@ -73,16 +74,15 @@ function listViewController($rootScope, $scope, $routeParams, $injector, notific //update all of the system includeProperties to enable sorting _.each($scope.options.includeProperties, function(e, i) { - - if (e.isSystem) { - //NOTE: special case for contentTypeAlias, it's a system property that cannot be sorted - // to do that, we'd need to update the base query for content to include the content type alias column - // which requires another join and would be slower. BUT We are doing this for members so not sure it makes a diff? - if (e.alias != "contentTypeAlias") { - e.allowSorting = true; - } - + //NOTE: special case for contentTypeAlias, it's a system property that cannot be sorted + // to do that, we'd need to update the base query for content to include the content type alias column + // which requires another join and would be slower. BUT We are doing this for members so not sure it makes a diff? + if (e.alias != "contentTypeAlias") { + e.allowSorting = true; + } + + if (e.isSystem) { //localize the header var key = getLocalizedKey(e.alias); localizationService.localize(key).then(function (v) { @@ -111,7 +111,7 @@ function listViewController($rootScope, $scope, $routeParams, $injector, notific //$location.search("page", $scope.options.pageNumber); }; - $scope.sort = function(field, allow) { + $scope.sort = function(field, allow, isSystem) { if (allow) { $scope.options.orderBy = field; @@ -122,6 +122,8 @@ function listViewController($rootScope, $scope, $routeParams, $injector, notific $scope.options.orderDirection = "desc"; } + $scope.options.orderBySystemField = isSystem; + $scope.reloadView($scope.contentId); } }; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.html index 599d8e5569..9029a2fd65 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.html @@ -48,14 +48,14 @@ - + Name - + diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index d52a65fcb3..4d3c211113 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -155,13 +155,14 @@ namespace Umbraco.Web.Editors int pageSize = 0, string orderBy = "SortOrder", Direction orderDirection = Direction.Ascending, + bool orderBySystemField = true, string filter = "") { int totalChildren; IContent[] children; if (pageNumber > 0 && pageSize > 0) { - children = Services.ContentService.GetPagedChildren(id, (pageNumber - 1), pageSize, out totalChildren, orderBy, orderDirection, filter).ToArray(); + children = Services.ContentService.GetPagedChildren(id, (pageNumber - 1), pageSize, out totalChildren, orderBy, orderDirection, orderBySystemField, filter).ToArray(); } else { diff --git a/src/Umbraco.Web/Editors/MediaController.cs b/src/Umbraco.Web/Editors/MediaController.cs index 7f9d703763..b659676c1d 100644 --- a/src/Umbraco.Web/Editors/MediaController.cs +++ b/src/Umbraco.Web/Editors/MediaController.cs @@ -137,13 +137,14 @@ namespace Umbraco.Web.Editors int pageSize = 0, string orderBy = "SortOrder", Direction orderDirection = Direction.Ascending, + bool orderBySystemField = true, string filter = "") { int totalChildren; IMedia[] children; if (pageNumber > 0 && pageSize > 0) { - children = Services.MediaService.GetPagedChildren(id, (pageNumber - 1), pageSize, out totalChildren, orderBy, orderDirection, filter).ToArray(); + children = Services.MediaService.GetPagedChildren(id, (pageNumber - 1), pageSize, out totalChildren, orderBy, orderDirection, orderBySystemField, filter).ToArray(); } else { diff --git a/src/Umbraco.Web/Editors/MemberController.cs b/src/Umbraco.Web/Editors/MemberController.cs index 30320b278e..cad94b2d1d 100644 --- a/src/Umbraco.Web/Editors/MemberController.cs +++ b/src/Umbraco.Web/Editors/MemberController.cs @@ -77,6 +77,7 @@ namespace Umbraco.Web.Editors int pageSize = 100, string orderBy = "Name", Direction orderDirection = Direction.Ascending, + bool orderBySystemField = true, string filter = "", string memberTypeAlias = null) { @@ -88,7 +89,7 @@ namespace Umbraco.Web.Editors if (MembershipScenario == MembershipScenario.NativeUmbraco) { - var members = Services.MemberService.GetAll((pageNumber - 1), pageSize, out totalRecords, orderBy, orderDirection, memberTypeAlias, filter).ToArray(); + var members = Services.MemberService.GetAll((pageNumber - 1), pageSize, out totalRecords, orderBy, orderDirection, orderBySystemField, memberTypeAlias, filter).ToArray(); if (totalRecords == 0) { return new PagedResult(0, 0, 0); diff --git a/src/UmbracoExamine/UmbracoMemberIndexer.cs b/src/UmbracoExamine/UmbracoMemberIndexer.cs index 1fdfdb97b3..d60cda2840 100644 --- a/src/UmbracoExamine/UmbracoMemberIndexer.cs +++ b/src/UmbracoExamine/UmbracoMemberIndexer.cs @@ -128,7 +128,7 @@ namespace UmbracoExamine do { int total; - var members = _memberService.GetAll(pageIndex, pageSize, out total, "LoginName", Direction.Ascending, nodeType); + var members = _memberService.GetAll(pageIndex, pageSize, out total, "LoginName", Direction.Ascending, true, nodeType).ToArray(); memberCount = 0; foreach (var member in members) { From 5954c370ed0144e522b7a45f8cad6b8e63c6ae97 Mon Sep 17 00:00:00 2001 From: AndyButland Date: Sat, 13 Dec 2014 22:24:06 +0100 Subject: [PATCH 02/46] Fixed issue with sorting on default member list view column of username --- .../Persistence/Repositories/VersionableRepositoryBase.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs index c53937473d..a614f70f58 100644 --- a/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs @@ -532,6 +532,10 @@ WHERE EXISTS( case "OWNER": //TODO: This isn't going to work very nicely because it's going to order by ID, not by letter return "umbracoNode.nodeUser"; + + // Members only + case "USERNAME": + return "cmsMember.LoginName"; default: return orderBy; } From b9ff327536cf56ecc586aae2c330c4502d0e0f10 Mon Sep 17 00:00:00 2001 From: AndyButland Date: Sat, 13 Dec 2014 22:29:32 +0100 Subject: [PATCH 03/46] Removed ability to sort member list view system fields other than those on the base table (others aren't handled so error) --- .../views/propertyeditors/listview/listview.controller.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js index f83a218a2e..63da7b54bf 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js @@ -82,6 +82,11 @@ function listViewController($rootScope, $scope, $routeParams, $injector, notific e.allowSorting = true; } + // Another special case for members, only fields on the base table (cmsMember) can be used for sorting + if (e.isSystem && $scope.entityType == "member") { + e.allowSorting = e.alias == 'username' || e.alias == 'email'; + } + if (e.isSystem) { //localize the header var key = getLocalizedKey(e.alias); From 58f2694a9014af7b057272a3bdbc9404aab827c4 Mon Sep 17 00:00:00 2001 From: AndyButland Date: Sat, 20 Dec 2014 22:05:43 +0100 Subject: [PATCH 04/46] Removed option to sort by media last edited date as currently only base table fields can be sorted and this isn't one. Trying to do so currently triggers an error. --- .../views/propertyeditors/listview/listview.controller.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js index 63da7b54bf..01e20eb6af 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js @@ -82,9 +82,14 @@ function listViewController($rootScope, $scope, $routeParams, $injector, notific e.allowSorting = true; } + // Another special case for lasted edited data/update date for media, again this field isn't available on the base table so we can't sort by it + if (e.isSystem && $scope.entityType == "media") { + e.allowSorting = e.alias != 'updateDate'; + } + // Another special case for members, only fields on the base table (cmsMember) can be used for sorting if (e.isSystem && $scope.entityType == "member") { - e.allowSorting = e.alias == 'username' || e.alias == 'email'; + e.allowSorting = e.alias == 'username' || e.alias == 'email'; } if (e.isSystem) { From a1ae0f70bb6673305bf289e221aa95abd133d9f9 Mon Sep 17 00:00:00 2001 From: AndyButland Date: Thu, 11 Jun 2015 23:46:27 +0200 Subject: [PATCH 05/46] Moved custom column list view order by sql variations into syntax providers --- .../Repositories/VersionableRepositoryBase.cs | 18 +++++++++++++----- .../SqlSyntax/ISqlSyntaxProvider.cs | 4 ++++ .../SqlSyntax/MySqlSyntaxProvider.cs | 4 ++++ .../SqlSyntax/SqlSyntaxProviderBase.cs | 4 ++++ 4 files changed, 25 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs index a614f70f58..ff6b0d81b3 100644 --- a/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs @@ -19,6 +19,8 @@ using Umbraco.Core.Dynamics; namespace Umbraco.Core.Persistence.Repositories { + using SqlSyntax; + internal abstract class VersionableRepositoryBase : PetaPocoRepositoryBase where TEntity : class, IAggregateRoot { @@ -263,11 +265,15 @@ namespace Umbraco.Core.Persistence.Repositories { // Sorting by a custom field, so set-up sub-query for ORDER BY clause to pull through valie // from most recent content version for the given order by field - sortedSql.Append(@"ORDER BY ( + var sortedInt = string.Format(SqlSyntaxContext.SqlSyntaxProvider.ConvertIntegerToOrderableString, "dataInt"); + var sortedDate = string.Format(SqlSyntaxContext.SqlSyntaxProvider.ConvertIntegerToOrderableString, "dataDate"); + var sortedString = string.Format(SqlSyntaxContext.SqlSyntaxProvider.IsNull, "dataNvarchar", "''"); + + var orderBySql = string.Format(@"ORDER BY ( SELECT CASE - WHEN dataInt Is Not Null THEN RIGHT('00000000' + CAST(dataInt AS varchar(8)),8) - WHEN dataDate Is Not Null THEN CONVERT(varchar, dataDate, 102) - ELSE IsNull(dataNvarchar,'') + WHEN dataInt Is Not Null THEN {0} + WHEN dataDate Is Not Null THEN {1} + ELSE {2} END FROM cmsContent c INNER JOIN cmsContentVersion cv ON cv.ContentId = c.nodeId AND VersionDate = ( @@ -279,7 +285,9 @@ namespace Umbraco.Core.Persistence.Repositories INNER JOIN cmsPropertyData cpd ON cpd.contentNodeId = c.nodeId AND cpd.versionId = cv.VersionId AND cpd.propertytypeId = cpt.id - WHERE c.nodeId = umbracoNode.Id and cpt.Alias = @0)", orderBy); + WHERE c.nodeId = umbracoNode.Id and cpt.Alias = @0)", sortedInt, sortedDate, sortedString); + + sortedSql.Append(orderBySql, orderBy); if (orderDirection == Direction.Descending) { sortedSql.Append(" DESC"); diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/ISqlSyntaxProvider.cs b/src/Umbraco.Core/Persistence/SqlSyntax/ISqlSyntaxProvider.cs index 899a462172..8ad8eb0d7a 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/ISqlSyntaxProvider.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/ISqlSyntaxProvider.cs @@ -69,6 +69,10 @@ namespace Umbraco.Core.Persistence.SqlSyntax bool SupportsIdentityInsert(); bool? SupportsCaseInsensitiveQueries(Database db); + string IsNull { get; } + string ConvertIntegerToOrderableString { get; } + string ConvertDateToOrderableString { get; } + IEnumerable GetTablesInSchema(Database db); IEnumerable GetColumnsInSchema(Database db); IEnumerable> GetConstraintsPerTable(Database db); diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/MySqlSyntaxProvider.cs b/src/Umbraco.Core/Persistence/SqlSyntax/MySqlSyntaxProvider.cs index ba6f4dcc7e..bdd2a2ef23 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/MySqlSyntaxProvider.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/MySqlSyntaxProvider.cs @@ -342,6 +342,10 @@ ORDER BY TABLE_NAME, INDEX_NAME", public override string RenameColumn { get { return "ALTER TABLE {0} CHANGE {1} {2}"; } } + public override string IsNull { get { return "IFNULL({0},{1})"; } } + public override string ConvertIntegerToOrderableString { get { return "LPAD({0}, 8, '0')"; } } + public override string ConvertDateToOrderableString { get { return "DATE_FORMAT({0}, '%Y%m%d')"; } } + public override bool? SupportsCaseInsensitiveQueries(Database db) { bool? supportsCaseInsensitiveQueries = null; diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs b/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs index 08d85e0573..81a7750942 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs @@ -501,5 +501,9 @@ namespace Umbraco.Core.Persistence.SqlSyntax public virtual string CreateConstraint { get { return "ALTER TABLE {0} ADD CONSTRAINT {1} {2} ({3})"; } } public virtual string DeleteConstraint { get { return "ALTER TABLE {0} DROP CONSTRAINT {1}"; } } public virtual string CreateForeignKeyConstraint { get { return "ALTER TABLE {0} ADD CONSTRAINT {1} FOREIGN KEY ({2}) REFERENCES {3} ({4}){5}{6}"; } } + + public virtual string IsNull { get { return "ISNULL({0},{1})"; } } + public virtual string ConvertIntegerToOrderableString { get { return "RIGHT('00000000' + CAST({0} AS varchar(8)),8)"; } } + public virtual string ConvertDateToOrderableString { get { return "CONVERT(varchar, {0}, 102)"; } } } } \ No newline at end of file From 7008d6b2fb6ea2c12f1e7100af303affbcc21f3b Mon Sep 17 00:00:00 2001 From: AndyButland Date: Sat, 13 Jun 2015 00:15:51 +0200 Subject: [PATCH 06/46] Fix to issue with ordering by custom date field --- .../Persistence/Repositories/VersionableRepositoryBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs index ff6b0d81b3..9047df31f7 100644 --- a/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs @@ -266,7 +266,7 @@ namespace Umbraco.Core.Persistence.Repositories // Sorting by a custom field, so set-up sub-query for ORDER BY clause to pull through valie // from most recent content version for the given order by field var sortedInt = string.Format(SqlSyntaxContext.SqlSyntaxProvider.ConvertIntegerToOrderableString, "dataInt"); - var sortedDate = string.Format(SqlSyntaxContext.SqlSyntaxProvider.ConvertIntegerToOrderableString, "dataDate"); + var sortedDate = string.Format(SqlSyntaxContext.SqlSyntaxProvider.ConvertDateToOrderableString, "dataDate"); var sortedString = string.Format(SqlSyntaxContext.SqlSyntaxProvider.IsNull, "dataNvarchar", "''"); var orderBySql = string.Format(@"ORDER BY ( From af244917f6b77c57f19de08c1e7ce25f708b323d Mon Sep 17 00:00:00 2001 From: AndyButland Date: Sun, 14 Jun 2015 11:24:03 +0200 Subject: [PATCH 07/46] Fixed issue with ordering by custom property from inherited or composed doc type --- .../Persistence/Repositories/VersionableRepositoryBase.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs index 9047df31f7..2b36853966 100644 --- a/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs @@ -281,10 +281,9 @@ namespace Umbraco.Core.Persistence.Repositories FROM cmsContentVersion WHERE ContentId = c.nodeId ) - INNER JOIN cmsPropertyType cpt ON cpt.contentTypeId = c.contentType INNER JOIN cmsPropertyData cpd ON cpd.contentNodeId = c.nodeId AND cpd.versionId = cv.VersionId - AND cpd.propertytypeId = cpt.id + INNER JOIN cmsPropertyType cpt ON cpt.Id = cpd.propertytypeId WHERE c.nodeId = umbracoNode.Id and cpt.Alias = @0)", sortedInt, sortedDate, sortedString); sortedSql.Append(orderBySql, orderBy); From 4f141e8b27d124d36177da6a848f1cfdecd539b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Ferreira?= Date: Fri, 26 Feb 2016 14:30:32 +0000 Subject: [PATCH 08/46] Fixing column sort on columns with custom headers --- .../Repositories/ContentRepository.cs | 1905 +++---- .../Interfaces/IContentRepository.cs | 143 +- .../Interfaces/IMediaRepository.cs | 59 +- .../Interfaces/IMemberRepository.cs | 115 +- .../Repositories/MediaRepository.cs | 1141 ++-- .../Repositories/MemberRepository.cs | 1487 +++--- .../Repositories/VersionableRepositoryBase.cs | 992 ++-- .../SqlSyntax/ISqlSyntaxProvider.cs | 136 +- .../SqlSyntax/MySqlSyntaxProvider.cs | 651 +-- .../SqlSyntax/SqlSyntaxProviderBase.cs | 1058 ++-- src/Umbraco.Core/Services/ContentService.cs | 4632 +++++++++-------- src/Umbraco.Core/Services/IContentService.cs | 1148 ++-- src/Umbraco.Core/Services/IMediaService.cs | 646 +-- src/Umbraco.Core/Services/IMemberService.cs | 383 +- src/Umbraco.Core/Services/MediaService.cs | 2626 +++++----- src/Umbraco.Core/Services/MemberService.cs | 2584 ++++----- .../Repositories/ContentRepositoryTest.cs | 1582 +++--- .../Repositories/MediaRepositoryTest.cs | 1086 ++-- .../UmbracoExamine/IndexInitializer.cs | 226 +- .../src/common/resources/content.resource.js | 1194 ++--- .../src/common/resources/media.resource.js | 894 ++-- .../src/common/resources/member.resource.js | 430 +- .../list/list.listviewlayout.controller.js | 3 +- .../listview/listview.controller.js | 967 ++-- src/Umbraco.Web/Editors/ContentController.cs | 1353 ++--- src/Umbraco.Web/Editors/MediaController.cs | 1339 ++--- src/Umbraco.Web/Editors/MemberController.cs | 1289 ++--- src/UmbracoExamine/UmbracoMemberIndexer.cs | 353 +- 28 files changed, 15251 insertions(+), 15171 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs index 7d3557a81d..30887281f1 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs @@ -26,956 +26,957 @@ using Umbraco.Core.Persistence.UnitOfWork; namespace Umbraco.Core.Persistence.Repositories { - /// - /// Represents a repository for doing CRUD operations for - /// - internal class ContentRepository : RecycleBinRepository, IContentRepository - { - private readonly IContentTypeRepository _contentTypeRepository; - private readonly ITemplateRepository _templateRepository; - private readonly ITagRepository _tagRepository; - private readonly CacheHelper _cacheHelper; - private readonly ContentPreviewRepository _contentPreviewRepository; - private readonly ContentXmlRepository _contentXmlRepository; - - public ContentRepository(IDatabaseUnitOfWork work, CacheHelper cacheHelper, ILogger logger, ISqlSyntaxProvider syntaxProvider, IContentTypeRepository contentTypeRepository, ITemplateRepository templateRepository, ITagRepository tagRepository, IContentSection contentSection) - : base(work, cacheHelper, logger, syntaxProvider, contentSection) - { - if (contentTypeRepository == null) throw new ArgumentNullException("contentTypeRepository"); - if (templateRepository == null) throw new ArgumentNullException("templateRepository"); - if (tagRepository == null) throw new ArgumentNullException("tagRepository"); - _contentTypeRepository = contentTypeRepository; - _templateRepository = templateRepository; - _tagRepository = tagRepository; - _cacheHelper = cacheHelper; - _contentPreviewRepository = new ContentPreviewRepository(work, CacheHelper.CreateDisabledCacheHelper(), logger, syntaxProvider); - _contentXmlRepository = new ContentXmlRepository(work, CacheHelper.CreateDisabledCacheHelper(), logger, syntaxProvider); - - EnsureUniqueNaming = true; - } - - public bool EnsureUniqueNaming { get; set; } - - #region Overrides of RepositoryBase - - protected override IContent PerformGet(int id) - { - var sql = GetBaseQuery(false) - .Where(GetBaseWhereClause(), new { Id = id }) - .Where(x => x.Newest) - .OrderByDescending(x => x.VersionDate); - - var dto = Database.Fetch(sql).FirstOrDefault(); - - if (dto == null) - return null; - - var content = CreateContentFromDto(dto, dto.ContentVersionDto.VersionId, sql); - - return content; - } - - protected override IEnumerable PerformGetAll(params int[] ids) - { - var sql = GetBaseQuery(false); - if (ids.Any()) - { - sql.Where("umbracoNode.id in (@ids)", new { ids = ids }); - } - - //we only want the newest ones with this method - sql.Where(x => x.Newest); - - return ProcessQuery(sql); - } - - protected override IEnumerable PerformGetByQuery(IQuery query) - { - var sqlClause = GetBaseQuery(false); - var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate() - .Where(x => x.Newest) - .OrderByDescending(x => x.VersionDate) - .OrderBy(x => x.SortOrder); - - return ProcessQuery(sql); - } - - #endregion - - #region Overrides of PetaPocoRepositoryBase - - protected override Sql GetBaseQuery(bool isCount) - { - var sqlx = string.Format("LEFT OUTER JOIN {0} {1} ON ({1}.{2}={0}.{2} AND {1}.{3}=1)", - SqlSyntax.GetQuotedTableName("cmsDocument"), - SqlSyntax.GetQuotedTableName("cmsDocument2"), - SqlSyntax.GetQuotedColumnName("nodeId"), - SqlSyntax.GetQuotedColumnName("published")); - - var sql = new Sql(); - sql.Select(isCount ? "COUNT(*)" : "*") - .From() - .InnerJoin() - .On(left => left.VersionId, right => right.VersionId) - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - - // cannot do this because PetaPoco does not know how to alias the table - //.LeftOuterJoin() - //.On(left => left.NodeId, right => right.NodeId) - // so have to rely on writing our own SQL - .Append(sqlx/*, new { @published = true }*/) - - .Where(x => x.NodeObjectType == NodeObjectTypeId); - return sql; - } - - protected override string GetBaseWhereClause() - { - return "umbracoNode.id = @Id"; - } - - protected override IEnumerable GetDeleteClauses() - { - var list = new List - { - "DELETE FROM cmsTask WHERE nodeId = @Id", - "DELETE FROM umbracoUser2NodeNotify WHERE nodeId = @Id", - "DELETE FROM umbracoUser2NodePermission WHERE nodeId = @Id", - "DELETE FROM umbracoRelation WHERE parentId = @Id", - "DELETE FROM umbracoRelation WHERE childId = @Id", - "DELETE FROM cmsTagRelationship WHERE nodeId = @Id", - "DELETE FROM umbracoDomains WHERE domainRootStructureID = @Id", - "DELETE FROM cmsDocument WHERE nodeId = @Id", - "DELETE FROM cmsPropertyData WHERE contentNodeId = @Id", - "DELETE FROM cmsPreviewXml WHERE nodeId = @Id", - "DELETE FROM cmsContentVersion WHERE ContentId = @Id", - "DELETE FROM cmsContentXml WHERE nodeId = @Id", - "DELETE FROM cmsContent WHERE nodeId = @Id", - "DELETE FROM umbracoAccess WHERE nodeId = @Id", - "DELETE FROM umbracoNode WHERE id = @Id" - }; - return list; - } - - protected override Guid NodeObjectTypeId - { - get { return new Guid(Constants.ObjectTypes.Document); } - } - - #endregion - - #region Overrides of VersionableRepositoryBase - - public void RebuildXmlStructures(Func serializer, int groupSize = 5000, IEnumerable contentTypeIds = null) - { - - //Ok, now we need to remove the data and re-insert it, we'll do this all in one transaction too. - using (var tr = Database.GetTransaction()) - { - //Remove all the data first, if anything fails after this it's no problem the transaction will be reverted - if (contentTypeIds == null) - { - var subQuery = new Sql() - .Select("DISTINCT cmsContentXml.nodeId") - .From() - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId); - - var deleteSql = SqlSyntax.GetDeleteSubquery("cmsContentXml", "nodeId", subQuery); - Database.Execute(deleteSql); - } - else - { - foreach (var id in contentTypeIds) - { - var id1 = id; - var subQuery = new Sql() - .Select("cmsDocument.nodeId") - .From() - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .Where(dto => dto.Published) - .Where(dto => dto.ContentTypeId == id1); - - var deleteSql = SqlSyntax.GetDeleteSubquery("cmsContentXml", "nodeId", subQuery); - Database.Execute(deleteSql); - } - } - - //now insert the data, again if something fails here, the whole transaction is reversed - if (contentTypeIds == null) - { - var query = Query.Builder.Where(x => x.Published == true); - RebuildXmlStructuresProcessQuery(serializer, query, tr, groupSize); - } - else - { - foreach (var contentTypeId in contentTypeIds) - { - //copy local - var id = contentTypeId; - var query = Query.Builder.Where(x => x.Published == true && x.ContentTypeId == id && x.Trashed == false); - RebuildXmlStructuresProcessQuery(serializer, query, tr, groupSize); - } - } - - tr.Complete(); - } - } - - private void RebuildXmlStructuresProcessQuery(Func serializer, IQuery query, Transaction tr, int pageSize) - { - var pageIndex = 0; - var total = long.MinValue; - var processed = 0; - do - { - //NOTE: This is an important call, we cannot simply make a call to: - // GetPagedResultsByQuery(query, pageIndex, pageSize, out total, "Path", Direction.Ascending); - // because that method is used to query 'latest' content items where in this case we don't necessarily - // want latest content items because a pulished content item might not actually be the latest. - // see: http://issues.umbraco.org/issue/U4-6322 & http://issues.umbraco.org/issue/U4-5982 - var descendants = GetPagedResultsByQuery(query, pageIndex, pageSize, out total, - new Tuple("cmsDocument", "nodeId"), - ProcessQuery, "Path", Direction.Ascending); - - var xmlItems = (from descendant in descendants - let xml = serializer(descendant) - select new ContentXmlDto { NodeId = descendant.Id, Xml = xml.ToDataString() }).ToArray(); - - //bulk insert it into the database - Database.BulkInsertRecords(xmlItems, tr); - - processed += xmlItems.Length; - - pageIndex++; - } while (processed < total); - } - - public override IContent GetByVersion(Guid versionId) - { - var sql = GetBaseQuery(false); - sql.Where("cmsContentVersion.VersionId = @VersionId", new { VersionId = versionId }); - sql.OrderByDescending(x => x.VersionDate); - - var dto = Database.Fetch(sql).FirstOrDefault(); - - if (dto == null) - return null; - - var content = CreateContentFromDto(dto, versionId, sql); - - return content; - } - - public override void DeleteVersion(Guid versionId) - { - var sql = new Sql() - .Select("*") - .From() - .InnerJoin().On(left => left.VersionId, right => right.VersionId) - .Where(x => x.VersionId == versionId) - .Where(x => x.Newest != true); - var dto = Database.Fetch(sql).FirstOrDefault(); - - if (dto == null) return; - - using (var transaction = Database.GetTransaction()) - { - PerformDeleteVersion(dto.NodeId, versionId); - - transaction.Complete(); - } - } - - public override void DeleteVersions(int id, DateTime versionDate) - { - var sql = new Sql() - .Select("*") - .From() - .InnerJoin().On(left => left.VersionId, right => right.VersionId) - .Where(x => x.NodeId == id) - .Where(x => x.VersionDate < versionDate) - .Where(x => x.Newest != true); - var list = Database.Fetch(sql); - if (list.Any() == false) return; - - using (var transaction = Database.GetTransaction()) - { - foreach (var dto in list) - { - PerformDeleteVersion(id, dto.VersionId); - } - - transaction.Complete(); - } - } - - protected override void PerformDeleteVersion(int id, Guid versionId) - { - Database.Delete("WHERE nodeId = @Id AND versionId = @VersionId", new { Id = id, VersionId = versionId }); - Database.Delete("WHERE contentNodeId = @Id AND versionId = @VersionId", new { Id = id, VersionId = versionId }); - Database.Delete("WHERE ContentId = @Id AND VersionId = @VersionId", new { Id = id, VersionId = versionId }); - Database.Delete("WHERE nodeId = @Id AND versionId = @VersionId", new { Id = id, VersionId = versionId }); - } - - #endregion - - #region Unit of Work Implementation - - protected override void PersistDeletedItem(IContent entity) - { - //We need to clear out all access rules but we need to do this in a manual way since - // nothing in that table is joined to a content id - var subQuery = new Sql() - .Select("umbracoAccessRule.accessId") - .From(SqlSyntax) - .InnerJoin(SqlSyntax) - .On(SqlSyntax, left => left.AccessId, right => right.Id) - .Where(dto => dto.NodeId == entity.Id); - Database.Execute(SqlSyntax.GetDeleteSubquery("umbracoAccessRule", "accessId", subQuery)); - - //now let the normal delete clauses take care of everything else - base.PersistDeletedItem(entity); - } - - protected override void PersistNewItem(IContent entity) - { - ((Content)entity).AddingEntity(); - - //ensure the default template is assigned - if (entity.Template == null) - { - entity.Template = entity.ContentType.DefaultTemplate; - } - - //Ensure unique name on the same level - entity.Name = EnsureUniqueNodeName(entity.ParentId, entity.Name); - - //Ensure that strings don't contain characters that are invalid in XML - entity.SanitizeEntityPropertiesForXmlStorage(); - - var factory = new ContentFactory(NodeObjectTypeId, entity.Id); - var dto = factory.BuildDto(entity); - - //NOTE Should the logic below have some kind of fallback for empty parent ids ? - //Logic for setting Path, Level and SortOrder - var parent = Database.First("WHERE id = @ParentId", new { ParentId = entity.ParentId }); - var level = parent.Level + 1; - var maxSortOrder = Database.ExecuteScalar( - "SELECT coalesce(max(sortOrder),-1) FROM umbracoNode WHERE parentid = @ParentId AND nodeObjectType = @NodeObjectType", - new { /*ParentId =*/ entity.ParentId, NodeObjectType = NodeObjectTypeId }); - var sortOrder = maxSortOrder + 1; - - //Create the (base) node data - umbracoNode - var nodeDto = dto.ContentVersionDto.ContentDto.NodeDto; - nodeDto.Path = parent.Path; - nodeDto.Level = short.Parse(level.ToString(CultureInfo.InvariantCulture)); - nodeDto.SortOrder = sortOrder; - var o = Database.IsNew(nodeDto) ? Convert.ToInt32(Database.Insert(nodeDto)) : Database.Update(nodeDto); - - //Update with new correct path - nodeDto.Path = string.Concat(parent.Path, ",", nodeDto.NodeId); - Database.Update(nodeDto); - - //Update entity with correct values - entity.Id = nodeDto.NodeId; //Set Id on entity to ensure an Id is set - entity.Path = nodeDto.Path; - entity.SortOrder = sortOrder; - entity.Level = level; - - //Assign the same permissions to it as the parent node - // http://issues.umbraco.org/issue/U4-2161 - var permissionsRepo = new PermissionRepository(UnitOfWork, _cacheHelper, SqlSyntax); - var parentPermissions = permissionsRepo.GetPermissionsForEntity(entity.ParentId).ToArray(); - //if there are parent permissions then assign them, otherwise leave null and permissions will become the - // user's default permissions. - if (parentPermissions.Any()) - { - var userPermissions = ( - from perm in parentPermissions - from p in perm.AssignedPermissions - select new EntityPermissionSet.UserPermission(perm.UserId, p)).ToList(); - - permissionsRepo.ReplaceEntityPermissions(new EntityPermissionSet(entity.Id, userPermissions)); - //flag the entity's permissions changed flag so we can track those changes. - //Currently only used for the cache refreshers to detect if we should refresh all user permissions cache. - ((Content)entity).PermissionsChanged = true; - } - - //Create the Content specific data - cmsContent - var contentDto = dto.ContentVersionDto.ContentDto; - contentDto.NodeId = nodeDto.NodeId; - Database.Insert(contentDto); - - //Create the first version - cmsContentVersion - //Assumes a new Version guid and Version date (modified date) has been set - var contentVersionDto = dto.ContentVersionDto; - contentVersionDto.NodeId = nodeDto.NodeId; - Database.Insert(contentVersionDto); - - //Create the Document specific data for this version - cmsDocument - //Assumes a new Version guid has been generated - dto.NodeId = nodeDto.NodeId; - Database.Insert(dto); - - //Create the PropertyData for this version - cmsPropertyData - var propertyFactory = new PropertyFactory(entity.ContentType.CompositionPropertyTypes.ToArray(), entity.Version, entity.Id); - var propertyDataDtos = propertyFactory.BuildDto(entity.Properties); - var keyDictionary = new Dictionary(); - - //Add Properties - foreach (var propertyDataDto in propertyDataDtos) - { - var primaryKey = Convert.ToInt32(Database.Insert(propertyDataDto)); - keyDictionary.Add(propertyDataDto.PropertyTypeId, primaryKey); - } - - //Update Properties with its newly set Id - foreach (var property in entity.Properties) - { - property.Id = keyDictionary[property.PropertyTypeId]; - } - - //lastly, check if we are a creating a published version , then update the tags table - if (entity.Published) - { - UpdatePropertyTags(entity, _tagRepository); - } - - // published => update published version infos, else leave it blank - if (entity.Published) - { - dto.DocumentPublishedReadOnlyDto = new DocumentPublishedReadOnlyDto - { - VersionId = dto.VersionId, - Newest = true, - NodeId = dto.NodeId, - Published = true - }; - ((Content)entity).PublishedVersionGuid = dto.VersionId; - } - - entity.ResetDirtyProperties(); - } - - protected override void PersistUpdatedItem(IContent entity) - { - var publishedState = ((Content)entity).PublishedState; - - //check if we need to make any database changes at all - if (entity.RequiresSaving(publishedState) == false) - { - entity.ResetDirtyProperties(); - return; - } - - //check if we need to create a new version - bool shouldCreateNewVersion = entity.ShouldCreateNewVersion(publishedState); - if (shouldCreateNewVersion) - { - //Updates Modified date and Version Guid - ((Content)entity).UpdatingEntity(); - } - else - { - entity.UpdateDate = DateTime.Now; - } - - //Ensure unique name on the same level - entity.Name = EnsureUniqueNodeName(entity.ParentId, entity.Name, entity.Id); - - //Ensure that strings don't contain characters that are invalid in XML - entity.SanitizeEntityPropertiesForXmlStorage(); - - //Look up parent to get and set the correct Path and update SortOrder if ParentId has changed - if (entity.IsPropertyDirty("ParentId")) - { - var parent = Database.First("WHERE id = @ParentId", new { ParentId = entity.ParentId }); - entity.Path = string.Concat(parent.Path, ",", entity.Id); - entity.Level = parent.Level + 1; - var maxSortOrder = - Database.ExecuteScalar( - "SELECT coalesce(max(sortOrder),0) FROM umbracoNode WHERE parentid = @ParentId AND nodeObjectType = @NodeObjectType", - new { ParentId = entity.ParentId, NodeObjectType = NodeObjectTypeId }); - entity.SortOrder = maxSortOrder + 1; - - //Question: If we move a node, should we update permissions to inherit from the new parent if the parent has permissions assigned? - // if we do that, then we'd need to propogate permissions all the way downward which might not be ideal for many people. - // Gonna just leave it as is for now, and not re-propogate permissions. - } - - var factory = new ContentFactory(NodeObjectTypeId, entity.Id); - //Look up Content entry to get Primary for updating the DTO - var contentDto = Database.SingleOrDefault("WHERE nodeId = @Id", new { Id = entity.Id }); - factory.SetPrimaryKey(contentDto.PrimaryKey); - var dto = factory.BuildDto(entity); - - //Updates the (base) node data - umbracoNode - var nodeDto = dto.ContentVersionDto.ContentDto.NodeDto; - var o = Database.Update(nodeDto); - - //Only update this DTO if the contentType has actually changed - if (contentDto.ContentTypeId != entity.ContentTypeId) - { - //Create the Content specific data - cmsContent - var newContentDto = dto.ContentVersionDto.ContentDto; - Database.Update(newContentDto); - } - - //a flag that we'll use later to create the tags in the tag db table - var publishedStateChanged = false; - - //If Published state has changed then previous versions should have their publish state reset. - //If state has been changed to unpublished the previous versions publish state should also be reset. - //if (((ICanBeDirty)entity).IsPropertyDirty("Published") && (entity.Published || publishedState == PublishedState.Unpublished)) - if (entity.ShouldClearPublishedFlagForPreviousVersions(publishedState, shouldCreateNewVersion)) - { - var publishedDocs = Database.Fetch("WHERE nodeId = @Id AND published = @IsPublished", new { Id = entity.Id, IsPublished = true }); - foreach (var doc in publishedDocs) - { - var docDto = doc; - docDto.Published = false; - Database.Update(docDto); - } - - //this is a newly published version so we'll update the tags table too (end of this method) - publishedStateChanged = true; - } - - //Look up (newest) entries by id in cmsDocument table to set newest = false - var documentDtos = Database.Fetch("WHERE nodeId = @Id AND newest = @IsNewest", new { Id = entity.Id, IsNewest = true }); - foreach (var documentDto in documentDtos) - { - var docDto = documentDto; - docDto.Newest = false; - Database.Update(docDto); - } - - var contentVersionDto = dto.ContentVersionDto; - if (shouldCreateNewVersion) - { - //Create a new version - cmsContentVersion - //Assumes a new Version guid and Version date (modified date) has been set - Database.Insert(contentVersionDto); - //Create the Document specific data for this version - cmsDocument - //Assumes a new Version guid has been generated - Database.Insert(dto); - } - else - { - //In order to update the ContentVersion we need to retrieve its primary key id - var contentVerDto = Database.SingleOrDefault("WHERE VersionId = @Version", new { Version = entity.Version }); - contentVersionDto.Id = contentVerDto.Id; - - Database.Update(contentVersionDto); - Database.Update(dto); - } - - //Create the PropertyData for this version - cmsPropertyData - var propertyFactory = new PropertyFactory(entity.ContentType.CompositionPropertyTypes.ToArray(), entity.Version, entity.Id); - var propertyDataDtos = propertyFactory.BuildDto(entity.Properties); - var keyDictionary = new Dictionary(); - - //Add Properties - foreach (var propertyDataDto in propertyDataDtos) - { - if (shouldCreateNewVersion == false && propertyDataDto.Id > 0) - { - Database.Update(propertyDataDto); - } - else - { - int primaryKey = Convert.ToInt32(Database.Insert(propertyDataDto)); - keyDictionary.Add(propertyDataDto.PropertyTypeId, primaryKey); - } - } - - //Update Properties with its newly set Id - if (keyDictionary.Any()) - { - foreach (var property in entity.Properties) - { - if (keyDictionary.ContainsKey(property.PropertyTypeId) == false) continue; - - property.Id = keyDictionary[property.PropertyTypeId]; - } - } - - //lastly, check if we are a newly published version and then update the tags table - if (publishedStateChanged && entity.Published) - { - UpdatePropertyTags(entity, _tagRepository); - } - else if (publishedStateChanged && (entity.Trashed || entity.Published == false)) - { - //it's in the trash or not published remove all entity tags - ClearEntityTags(entity, _tagRepository); - } - - // published => update published version infos, - // else if unpublished then clear published version infos - if (entity.Published) - { - dto.DocumentPublishedReadOnlyDto = new DocumentPublishedReadOnlyDto - { - VersionId = dto.VersionId, - Newest = true, - NodeId = dto.NodeId, - Published = true - }; - ((Content)entity).PublishedVersionGuid = dto.VersionId; - } - else if (publishedStateChanged) - { - dto.DocumentPublishedReadOnlyDto = new DocumentPublishedReadOnlyDto - { - VersionId = default(Guid), - Newest = false, - NodeId = dto.NodeId, - Published = false - }; - ((Content)entity).PublishedVersionGuid = default(Guid); - } - - entity.ResetDirtyProperties(); - } - - - #endregion - - #region Implementation of IContentRepository - - public IEnumerable GetByPublishedVersion(IQuery query) - { - // we WANT to return contents in top-down order, ie parents should come before children - // ideal would be pure xml "document order" which can be achieved with: - // ORDER BY substring(path, 1, len(path) - charindex(',', reverse(path))), sortOrder - // but that's probably an overkill - sorting by level,sortOrder should be enough - - var sqlClause = GetBaseQuery(false); - var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate() - .Where(x => x.Published) - .OrderBy(x => x.Level, SqlSyntax) - .OrderBy(x => x.SortOrder, SqlSyntax); - - //NOTE: This doesn't allow properties to be part of the query - var dtos = Database.Fetch(sql); - - foreach (var dto in dtos) - { - //Check in the cache first. If it exists there AND it is published - // then we can use that entity. Otherwise if it is not published (which can be the case - // because we only store the 'latest' entries in the cache which might not be the published - // version) - var fromCache = RuntimeCache.GetCacheItem(GetCacheIdKey(dto.NodeId)); - //var fromCache = TryGetFromCache(dto.NodeId); - if (fromCache != null && fromCache.Published) - { - yield return fromCache; - } - else - { - yield return CreateContentFromDto(dto, dto.VersionId, sql); - } - } - } - - public int CountPublished() - { - var sql = GetBaseQuery(true).Where(x => x.Trashed == false) - .Where(x => x.Published == true); - return Database.ExecuteScalar(sql); - } - - public void ReplaceContentPermissions(EntityPermissionSet permissionSet) - { - var repo = new PermissionRepository(UnitOfWork, _cacheHelper, SqlSyntax); - repo.ReplaceEntityPermissions(permissionSet); - } - - public void ClearPublished(IContent content) - { - // race cond! - var documentDtos = Database.Fetch("WHERE nodeId=@id AND published=@published", new { id = content.Id, published = true }); - foreach (var documentDto in documentDtos) - { - documentDto.Published = false; - Database.Update(documentDto); - } - } - - /// - /// Assigns a single permission to the current content item for the specified user ids - /// - /// - /// - /// - public void AssignEntityPermission(IContent entity, char permission, IEnumerable userIds) - { - var repo = new PermissionRepository(UnitOfWork, _cacheHelper, SqlSyntax); - repo.AssignEntityPermission(entity, permission, userIds); - } - - public IEnumerable GetPermissionsForEntity(int entityId) - { - var repo = new PermissionRepository(UnitOfWork, _cacheHelper, SqlSyntax); - return repo.GetPermissionsForEntity(entityId); - } - - /// - /// Adds/updates content/published xml - /// - /// - /// - public void AddOrUpdateContentXml(IContent content, Func xml) - { - _contentXmlRepository.AddOrUpdate(new ContentXmlEntity(content, xml)); - } - - /// - /// Used to remove the content xml for a content item - /// - /// - public void DeleteContentXml(IContent content) - { - _contentXmlRepository.Delete(new ContentXmlEntity(content)); - } - - /// - /// Adds/updates preview xml - /// - /// - /// - public void AddOrUpdatePreviewXml(IContent content, Func xml) - { - _contentPreviewRepository.AddOrUpdate(new ContentPreviewEntity(content, xml)); - } - - /// - /// Gets paged content results - /// - /// Query to excute - /// Page number - /// Page size - /// Total records query would return without paging - /// Field to order by - /// Direction to order by - /// Search text filter - /// An Enumerable list of objects - public IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, - string orderBy, Direction orderDirection, string filter = "") - { - - //NOTE: This uses the GetBaseQuery method but that does not take into account the required 'newest' field which is - // what we always require for a paged result, so we'll ensure it's included in the filter - - var args = new List(); - var sbWhere = new StringBuilder("AND (cmsDocument.newest = 1)"); - - if (filter.IsNullOrWhiteSpace() == false) - { - sbWhere.Append(" AND (cmsDocument." + SqlSyntax.GetQuotedColumnName("text") + " LIKE @" + args.Count + ")"); - args.Add("%" + filter + "%"); - } - - Func> filterCallback = () => new Tuple(sbWhere.ToString(), args.ToArray()); - - return GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, - new Tuple("cmsDocument", "nodeId"), - ProcessQuery, orderBy, orderDirection, - filterCallback); - - } - - #endregion - - #region IRecycleBinRepository members - - protected override int RecycleBinId - { - get { return Constants.System.RecycleBinContent; } - } - - #endregion - - protected override string GetDatabaseFieldNameForOrderBy(string orderBy) - { - //Some custom ones - switch (orderBy.ToUpperInvariant()) - { - case "NAME": - return "cmsDocument.text"; - case "UPDATER": - //TODO: This isn't going to work very nicely because it's going to order by ID, not by letter - return "cmsDocument.documentUser"; - } - - return base.GetDatabaseFieldNameForOrderBy(orderBy); - } - - private IEnumerable ProcessQuery(Sql sql) - { - //NOTE: This doesn't allow properties to be part of the query - var dtos = Database.Fetch(sql); - - //nothing found - if (dtos.Any() == false) return Enumerable.Empty(); - - //content types - //NOTE: This should be ok for an SQL 'IN' statement, there shouldn't be an insane amount of content types - var contentTypes = _contentTypeRepository.GetAll(dtos.Select(x => x.ContentVersionDto.ContentDto.ContentTypeId).ToArray()) - .ToArray(); - - - var ids = dtos - .Where(dto => dto.TemplateId.HasValue && dto.TemplateId.Value > 0) - .Select(x => x.TemplateId.Value).ToArray(); - - //NOTE: This should be ok for an SQL 'IN' statement, there shouldn't be an insane amount of content types - var templates = ids.Length == 0 ? Enumerable.Empty() : _templateRepository.GetAll(ids).ToArray(); - - var dtosWithContentTypes = dtos - //This select into and null check are required because we don't have a foreign damn key on the contentType column - // http://issues.umbraco.org/issue/U4-5503 - .Select(x => new { dto = x, contentType = contentTypes.FirstOrDefault(ct => ct.Id == x.ContentVersionDto.ContentDto.ContentTypeId) }) - .Where(x => x.contentType != null) - .ToArray(); - - //Go get the property data for each document - var docDefs = dtosWithContentTypes.Select(d => new DocumentDefinition( - d.dto.NodeId, - d.dto.VersionId, - d.dto.ContentVersionDto.VersionDate, - d.dto.ContentVersionDto.ContentDto.NodeDto.CreateDate, - d.contentType)); - - var propertyData = GetPropertyCollection(sql, docDefs); - - return dtosWithContentTypes.Select(d => CreateContentFromDto( - d.dto, - contentTypes.First(ct => ct.Id == d.dto.ContentVersionDto.ContentDto.ContentTypeId), - templates.FirstOrDefault(tem => tem.Id == (d.dto.TemplateId.HasValue ? d.dto.TemplateId.Value : -1)), - propertyData[d.dto.NodeId])); - } - - /// - /// Private method to create a content object from a DocumentDto, which is used by Get and GetByVersion. - /// - /// - /// - /// - /// - /// - private IContent CreateContentFromDto(DocumentDto dto, - IContentType contentType, - ITemplate template, - Models.PropertyCollection propCollection) - { - var factory = new ContentFactory(contentType, NodeObjectTypeId, dto.NodeId); - var content = factory.BuildEntity(dto); - - //Check if template id is set on DocumentDto, and get ITemplate if it is. - if (dto.TemplateId.HasValue && dto.TemplateId.Value > 0) - { - content.Template = template ?? _templateRepository.Get(dto.TemplateId.Value); - } - else - { - //ensure there isn't one set. - content.Template = null; - } - - content.Properties = propCollection; - - //on initial construction we don't want to have dirty properties tracked - // http://issues.umbraco.org/issue/U4-1946 - ((Entity)content).ResetDirtyProperties(false); - return content; - } - - /// - /// Private method to create a content object from a DocumentDto, which is used by Get and GetByVersion. - /// - /// - /// - /// - /// - private IContent CreateContentFromDto(DocumentDto dto, Guid versionId, Sql docSql) - { - var contentType = _contentTypeRepository.Get(dto.ContentVersionDto.ContentDto.ContentTypeId); - - var factory = new ContentFactory(contentType, NodeObjectTypeId, dto.NodeId); - var content = factory.BuildEntity(dto); - - //Check if template id is set on DocumentDto, and get ITemplate if it is. - if (dto.TemplateId.HasValue && dto.TemplateId.Value > 0) - { - content.Template = _templateRepository.Get(dto.TemplateId.Value); - } - - var docDef = new DocumentDefinition(dto.NodeId, versionId, content.UpdateDate, content.CreateDate, contentType); - - var properties = GetPropertyCollection(docSql, new[] { docDef }); - - content.Properties = properties[dto.NodeId]; - - //on initial construction we don't want to have dirty properties tracked - // http://issues.umbraco.org/issue/U4-1946 - ((Entity)content).ResetDirtyProperties(false); - return content; - } - - private string EnsureUniqueNodeName(int parentId, string nodeName, int id = 0) - { - if (EnsureUniqueNaming == false) - return nodeName; - - var sql = new Sql(); - sql.Select("*") - .From() - .Where(x => x.NodeObjectType == NodeObjectTypeId && x.ParentId == parentId && 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; - } - - /// - /// Dispose disposable properties - /// - /// - /// Ensure the unit of work is disposed - /// - protected override void DisposeResources() - { - _contentTypeRepository.Dispose(); - _templateRepository.Dispose(); - _tagRepository.Dispose(); - _contentPreviewRepository.Dispose(); - _contentXmlRepository.Dispose(); - } - } + /// + /// Represents a repository for doing CRUD operations for + /// + internal class ContentRepository : RecycleBinRepository, IContentRepository + { + private readonly IContentTypeRepository _contentTypeRepository; + private readonly ITemplateRepository _templateRepository; + private readonly ITagRepository _tagRepository; + private readonly CacheHelper _cacheHelper; + private readonly ContentPreviewRepository _contentPreviewRepository; + private readonly ContentXmlRepository _contentXmlRepository; + + public ContentRepository(IDatabaseUnitOfWork work, CacheHelper cacheHelper, ILogger logger, ISqlSyntaxProvider syntaxProvider, IContentTypeRepository contentTypeRepository, ITemplateRepository templateRepository, ITagRepository tagRepository, IContentSection contentSection) + : base(work, cacheHelper, logger, syntaxProvider, contentSection) + { + if (contentTypeRepository == null) throw new ArgumentNullException("contentTypeRepository"); + if (templateRepository == null) throw new ArgumentNullException("templateRepository"); + if (tagRepository == null) throw new ArgumentNullException("tagRepository"); + _contentTypeRepository = contentTypeRepository; + _templateRepository = templateRepository; + _tagRepository = tagRepository; + _cacheHelper = cacheHelper; + _contentPreviewRepository = new ContentPreviewRepository(work, CacheHelper.CreateDisabledCacheHelper(), logger, syntaxProvider); + _contentXmlRepository = new ContentXmlRepository(work, CacheHelper.CreateDisabledCacheHelper(), logger, syntaxProvider); + + EnsureUniqueNaming = true; + } + + public bool EnsureUniqueNaming { get; set; } + + #region Overrides of RepositoryBase + + protected override IContent PerformGet(int id) + { + var sql = GetBaseQuery(false) + .Where(GetBaseWhereClause(), new { Id = id }) + .Where(x => x.Newest) + .OrderByDescending(x => x.VersionDate); + + var dto = Database.Fetch(sql).FirstOrDefault(); + + if (dto == null) + return null; + + var content = CreateContentFromDto(dto, dto.ContentVersionDto.VersionId, sql); + + return content; + } + + protected override IEnumerable PerformGetAll(params int[] ids) + { + var sql = GetBaseQuery(false); + if (ids.Any()) + { + sql.Where("umbracoNode.id in (@ids)", new { ids = ids }); + } + + //we only want the newest ones with this method + sql.Where(x => x.Newest); + + return ProcessQuery(sql); + } + + protected override IEnumerable PerformGetByQuery(IQuery query) + { + var sqlClause = GetBaseQuery(false); + var translator = new SqlTranslator(sqlClause, query); + var sql = translator.Translate() + .Where(x => x.Newest) + .OrderByDescending(x => x.VersionDate) + .OrderBy(x => x.SortOrder); + + return ProcessQuery(sql); + } + + #endregion + + #region Overrides of PetaPocoRepositoryBase + + protected override Sql GetBaseQuery(bool isCount) + { + var sqlx = string.Format("LEFT OUTER JOIN {0} {1} ON ({1}.{2}={0}.{2} AND {1}.{3}=1)", + SqlSyntax.GetQuotedTableName("cmsDocument"), + SqlSyntax.GetQuotedTableName("cmsDocument2"), + SqlSyntax.GetQuotedColumnName("nodeId"), + SqlSyntax.GetQuotedColumnName("published")); + + var sql = new Sql(); + sql.Select(isCount ? "COUNT(*)" : "*") + .From() + .InnerJoin() + .On(left => left.VersionId, right => right.VersionId) + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + + // cannot do this because PetaPoco does not know how to alias the table + //.LeftOuterJoin() + //.On(left => left.NodeId, right => right.NodeId) + // so have to rely on writing our own SQL + .Append(sqlx/*, new { @published = true }*/) + + .Where(x => x.NodeObjectType == NodeObjectTypeId); + return sql; + } + + protected override string GetBaseWhereClause() + { + return "umbracoNode.id = @Id"; + } + + protected override IEnumerable GetDeleteClauses() + { + var list = new List + { + "DELETE FROM cmsTask WHERE nodeId = @Id", + "DELETE FROM umbracoUser2NodeNotify WHERE nodeId = @Id", + "DELETE FROM umbracoUser2NodePermission WHERE nodeId = @Id", + "DELETE FROM umbracoRelation WHERE parentId = @Id", + "DELETE FROM umbracoRelation WHERE childId = @Id", + "DELETE FROM cmsTagRelationship WHERE nodeId = @Id", + "DELETE FROM umbracoDomains WHERE domainRootStructureID = @Id", + "DELETE FROM cmsDocument WHERE nodeId = @Id", + "DELETE FROM cmsPropertyData WHERE contentNodeId = @Id", + "DELETE FROM cmsPreviewXml WHERE nodeId = @Id", + "DELETE FROM cmsContentVersion WHERE ContentId = @Id", + "DELETE FROM cmsContentXml WHERE nodeId = @Id", + "DELETE FROM cmsContent WHERE nodeId = @Id", + "DELETE FROM umbracoAccess WHERE nodeId = @Id", + "DELETE FROM umbracoNode WHERE id = @Id" + }; + return list; + } + + protected override Guid NodeObjectTypeId + { + get { return new Guid(Constants.ObjectTypes.Document); } + } + + #endregion + + #region Overrides of VersionableRepositoryBase + + public void RebuildXmlStructures(Func serializer, int groupSize = 5000, IEnumerable contentTypeIds = null) + { + + //Ok, now we need to remove the data and re-insert it, we'll do this all in one transaction too. + using (var tr = Database.GetTransaction()) + { + //Remove all the data first, if anything fails after this it's no problem the transaction will be reverted + if (contentTypeIds == null) + { + var subQuery = new Sql() + .Select("DISTINCT cmsContentXml.nodeId") + .From() + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId); + + var deleteSql = SqlSyntax.GetDeleteSubquery("cmsContentXml", "nodeId", subQuery); + Database.Execute(deleteSql); + } + else + { + foreach (var id in contentTypeIds) + { + var id1 = id; + var subQuery = new Sql() + .Select("cmsDocument.nodeId") + .From() + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .Where(dto => dto.Published) + .Where(dto => dto.ContentTypeId == id1); + + var deleteSql = SqlSyntax.GetDeleteSubquery("cmsContentXml", "nodeId", subQuery); + Database.Execute(deleteSql); + } + } + + //now insert the data, again if something fails here, the whole transaction is reversed + if (contentTypeIds == null) + { + var query = Query.Builder.Where(x => x.Published == true); + RebuildXmlStructuresProcessQuery(serializer, query, tr, groupSize); + } + else + { + foreach (var contentTypeId in contentTypeIds) + { + //copy local + var id = contentTypeId; + var query = Query.Builder.Where(x => x.Published == true && x.ContentTypeId == id && x.Trashed == false); + RebuildXmlStructuresProcessQuery(serializer, query, tr, groupSize); + } + } + + tr.Complete(); + } + } + + private void RebuildXmlStructuresProcessQuery(Func serializer, IQuery query, Transaction tr, int pageSize) + { + var pageIndex = 0; + var total = long.MinValue; + var processed = 0; + do + { + //NOTE: This is an important call, we cannot simply make a call to: + // GetPagedResultsByQuery(query, pageIndex, pageSize, out total, "Path", Direction.Ascending); + // because that method is used to query 'latest' content items where in this case we don't necessarily + // want latest content items because a pulished content item might not actually be the latest. + // see: http://issues.umbraco.org/issue/U4-6322 & http://issues.umbraco.org/issue/U4-5982 + var descendants = GetPagedResultsByQuery(query, pageIndex, pageSize, out total, + new Tuple("cmsDocument", "nodeId"), + ProcessQuery, "Path", Direction.Ascending, orderBySystemField: true); + + var xmlItems = (from descendant in descendants + let xml = serializer(descendant) + select new ContentXmlDto { NodeId = descendant.Id, Xml = xml.ToDataString() }).ToArray(); + + //bulk insert it into the database + Database.BulkInsertRecords(xmlItems, tr); + + processed += xmlItems.Length; + + pageIndex++; + } while (processed < total); + } + + public override IContent GetByVersion(Guid versionId) + { + var sql = GetBaseQuery(false); + sql.Where("cmsContentVersion.VersionId = @VersionId", new { VersionId = versionId }); + sql.OrderByDescending(x => x.VersionDate); + + var dto = Database.Fetch(sql).FirstOrDefault(); + + if (dto == null) + return null; + + var content = CreateContentFromDto(dto, versionId, sql); + + return content; + } + + public override void DeleteVersion(Guid versionId) + { + var sql = new Sql() + .Select("*") + .From() + .InnerJoin().On(left => left.VersionId, right => right.VersionId) + .Where(x => x.VersionId == versionId) + .Where(x => x.Newest != true); + var dto = Database.Fetch(sql).FirstOrDefault(); + + if (dto == null) return; + + using (var transaction = Database.GetTransaction()) + { + PerformDeleteVersion(dto.NodeId, versionId); + + transaction.Complete(); + } + } + + public override void DeleteVersions(int id, DateTime versionDate) + { + var sql = new Sql() + .Select("*") + .From() + .InnerJoin().On(left => left.VersionId, right => right.VersionId) + .Where(x => x.NodeId == id) + .Where(x => x.VersionDate < versionDate) + .Where(x => x.Newest != true); + var list = Database.Fetch(sql); + if (list.Any() == false) return; + + using (var transaction = Database.GetTransaction()) + { + foreach (var dto in list) + { + PerformDeleteVersion(id, dto.VersionId); + } + + transaction.Complete(); + } + } + + protected override void PerformDeleteVersion(int id, Guid versionId) + { + Database.Delete("WHERE nodeId = @Id AND versionId = @VersionId", new { Id = id, VersionId = versionId }); + Database.Delete("WHERE contentNodeId = @Id AND versionId = @VersionId", new { Id = id, VersionId = versionId }); + Database.Delete("WHERE ContentId = @Id AND VersionId = @VersionId", new { Id = id, VersionId = versionId }); + Database.Delete("WHERE nodeId = @Id AND versionId = @VersionId", new { Id = id, VersionId = versionId }); + } + + #endregion + + #region Unit of Work Implementation + + protected override void PersistDeletedItem(IContent entity) + { + //We need to clear out all access rules but we need to do this in a manual way since + // nothing in that table is joined to a content id + var subQuery = new Sql() + .Select("umbracoAccessRule.accessId") + .From(SqlSyntax) + .InnerJoin(SqlSyntax) + .On(SqlSyntax, left => left.AccessId, right => right.Id) + .Where(dto => dto.NodeId == entity.Id); + Database.Execute(SqlSyntax.GetDeleteSubquery("umbracoAccessRule", "accessId", subQuery)); + + //now let the normal delete clauses take care of everything else + base.PersistDeletedItem(entity); + } + + protected override void PersistNewItem(IContent entity) + { + ((Content)entity).AddingEntity(); + + //ensure the default template is assigned + if (entity.Template == null) + { + entity.Template = entity.ContentType.DefaultTemplate; + } + + //Ensure unique name on the same level + entity.Name = EnsureUniqueNodeName(entity.ParentId, entity.Name); + + //Ensure that strings don't contain characters that are invalid in XML + entity.SanitizeEntityPropertiesForXmlStorage(); + + var factory = new ContentFactory(NodeObjectTypeId, entity.Id); + var dto = factory.BuildDto(entity); + + //NOTE Should the logic below have some kind of fallback for empty parent ids ? + //Logic for setting Path, Level and SortOrder + var parent = Database.First("WHERE id = @ParentId", new { ParentId = entity.ParentId }); + var level = parent.Level + 1; + var maxSortOrder = Database.ExecuteScalar( + "SELECT coalesce(max(sortOrder),-1) FROM umbracoNode WHERE parentid = @ParentId AND nodeObjectType = @NodeObjectType", + new { /*ParentId =*/ entity.ParentId, NodeObjectType = NodeObjectTypeId }); + var sortOrder = maxSortOrder + 1; + + //Create the (base) node data - umbracoNode + var nodeDto = dto.ContentVersionDto.ContentDto.NodeDto; + nodeDto.Path = parent.Path; + nodeDto.Level = short.Parse(level.ToString(CultureInfo.InvariantCulture)); + nodeDto.SortOrder = sortOrder; + var o = Database.IsNew(nodeDto) ? Convert.ToInt32(Database.Insert(nodeDto)) : Database.Update(nodeDto); + + //Update with new correct path + nodeDto.Path = string.Concat(parent.Path, ",", nodeDto.NodeId); + Database.Update(nodeDto); + + //Update entity with correct values + entity.Id = nodeDto.NodeId; //Set Id on entity to ensure an Id is set + entity.Path = nodeDto.Path; + entity.SortOrder = sortOrder; + entity.Level = level; + + //Assign the same permissions to it as the parent node + // http://issues.umbraco.org/issue/U4-2161 + var permissionsRepo = new PermissionRepository(UnitOfWork, _cacheHelper, SqlSyntax); + var parentPermissions = permissionsRepo.GetPermissionsForEntity(entity.ParentId).ToArray(); + //if there are parent permissions then assign them, otherwise leave null and permissions will become the + // user's default permissions. + if (parentPermissions.Any()) + { + var userPermissions = ( + from perm in parentPermissions + from p in perm.AssignedPermissions + select new EntityPermissionSet.UserPermission(perm.UserId, p)).ToList(); + + permissionsRepo.ReplaceEntityPermissions(new EntityPermissionSet(entity.Id, userPermissions)); + //flag the entity's permissions changed flag so we can track those changes. + //Currently only used for the cache refreshers to detect if we should refresh all user permissions cache. + ((Content)entity).PermissionsChanged = true; + } + + //Create the Content specific data - cmsContent + var contentDto = dto.ContentVersionDto.ContentDto; + contentDto.NodeId = nodeDto.NodeId; + Database.Insert(contentDto); + + //Create the first version - cmsContentVersion + //Assumes a new Version guid and Version date (modified date) has been set + var contentVersionDto = dto.ContentVersionDto; + contentVersionDto.NodeId = nodeDto.NodeId; + Database.Insert(contentVersionDto); + + //Create the Document specific data for this version - cmsDocument + //Assumes a new Version guid has been generated + dto.NodeId = nodeDto.NodeId; + Database.Insert(dto); + + //Create the PropertyData for this version - cmsPropertyData + var propertyFactory = new PropertyFactory(entity.ContentType.CompositionPropertyTypes.ToArray(), entity.Version, entity.Id); + var propertyDataDtos = propertyFactory.BuildDto(entity.Properties); + var keyDictionary = new Dictionary(); + + //Add Properties + foreach (var propertyDataDto in propertyDataDtos) + { + var primaryKey = Convert.ToInt32(Database.Insert(propertyDataDto)); + keyDictionary.Add(propertyDataDto.PropertyTypeId, primaryKey); + } + + //Update Properties with its newly set Id + foreach (var property in entity.Properties) + { + property.Id = keyDictionary[property.PropertyTypeId]; + } + + //lastly, check if we are a creating a published version , then update the tags table + if (entity.Published) + { + UpdatePropertyTags(entity, _tagRepository); + } + + // published => update published version infos, else leave it blank + if (entity.Published) + { + dto.DocumentPublishedReadOnlyDto = new DocumentPublishedReadOnlyDto + { + VersionId = dto.VersionId, + Newest = true, + NodeId = dto.NodeId, + Published = true + }; + ((Content)entity).PublishedVersionGuid = dto.VersionId; + } + + entity.ResetDirtyProperties(); + } + + protected override void PersistUpdatedItem(IContent entity) + { + var publishedState = ((Content)entity).PublishedState; + + //check if we need to make any database changes at all + if (entity.RequiresSaving(publishedState) == false) + { + entity.ResetDirtyProperties(); + return; + } + + //check if we need to create a new version + bool shouldCreateNewVersion = entity.ShouldCreateNewVersion(publishedState); + if (shouldCreateNewVersion) + { + //Updates Modified date and Version Guid + ((Content)entity).UpdatingEntity(); + } + else + { + entity.UpdateDate = DateTime.Now; + } + + //Ensure unique name on the same level + entity.Name = EnsureUniqueNodeName(entity.ParentId, entity.Name, entity.Id); + + //Ensure that strings don't contain characters that are invalid in XML + entity.SanitizeEntityPropertiesForXmlStorage(); + + //Look up parent to get and set the correct Path and update SortOrder if ParentId has changed + if (entity.IsPropertyDirty("ParentId")) + { + var parent = Database.First("WHERE id = @ParentId", new { ParentId = entity.ParentId }); + entity.Path = string.Concat(parent.Path, ",", entity.Id); + entity.Level = parent.Level + 1; + var maxSortOrder = + Database.ExecuteScalar( + "SELECT coalesce(max(sortOrder),0) FROM umbracoNode WHERE parentid = @ParentId AND nodeObjectType = @NodeObjectType", + new { ParentId = entity.ParentId, NodeObjectType = NodeObjectTypeId }); + entity.SortOrder = maxSortOrder + 1; + + //Question: If we move a node, should we update permissions to inherit from the new parent if the parent has permissions assigned? + // if we do that, then we'd need to propogate permissions all the way downward which might not be ideal for many people. + // Gonna just leave it as is for now, and not re-propogate permissions. + } + + var factory = new ContentFactory(NodeObjectTypeId, entity.Id); + //Look up Content entry to get Primary for updating the DTO + var contentDto = Database.SingleOrDefault("WHERE nodeId = @Id", new { Id = entity.Id }); + factory.SetPrimaryKey(contentDto.PrimaryKey); + var dto = factory.BuildDto(entity); + + //Updates the (base) node data - umbracoNode + var nodeDto = dto.ContentVersionDto.ContentDto.NodeDto; + var o = Database.Update(nodeDto); + + //Only update this DTO if the contentType has actually changed + if (contentDto.ContentTypeId != entity.ContentTypeId) + { + //Create the Content specific data - cmsContent + var newContentDto = dto.ContentVersionDto.ContentDto; + Database.Update(newContentDto); + } + + //a flag that we'll use later to create the tags in the tag db table + var publishedStateChanged = false; + + //If Published state has changed then previous versions should have their publish state reset. + //If state has been changed to unpublished the previous versions publish state should also be reset. + //if (((ICanBeDirty)entity).IsPropertyDirty("Published") && (entity.Published || publishedState == PublishedState.Unpublished)) + if (entity.ShouldClearPublishedFlagForPreviousVersions(publishedState, shouldCreateNewVersion)) + { + var publishedDocs = Database.Fetch("WHERE nodeId = @Id AND published = @IsPublished", new { Id = entity.Id, IsPublished = true }); + foreach (var doc in publishedDocs) + { + var docDto = doc; + docDto.Published = false; + Database.Update(docDto); + } + + //this is a newly published version so we'll update the tags table too (end of this method) + publishedStateChanged = true; + } + + //Look up (newest) entries by id in cmsDocument table to set newest = false + var documentDtos = Database.Fetch("WHERE nodeId = @Id AND newest = @IsNewest", new { Id = entity.Id, IsNewest = true }); + foreach (var documentDto in documentDtos) + { + var docDto = documentDto; + docDto.Newest = false; + Database.Update(docDto); + } + + var contentVersionDto = dto.ContentVersionDto; + if (shouldCreateNewVersion) + { + //Create a new version - cmsContentVersion + //Assumes a new Version guid and Version date (modified date) has been set + Database.Insert(contentVersionDto); + //Create the Document specific data for this version - cmsDocument + //Assumes a new Version guid has been generated + Database.Insert(dto); + } + else + { + //In order to update the ContentVersion we need to retrieve its primary key id + var contentVerDto = Database.SingleOrDefault("WHERE VersionId = @Version", new { Version = entity.Version }); + contentVersionDto.Id = contentVerDto.Id; + + Database.Update(contentVersionDto); + Database.Update(dto); + } + + //Create the PropertyData for this version - cmsPropertyData + var propertyFactory = new PropertyFactory(entity.ContentType.CompositionPropertyTypes.ToArray(), entity.Version, entity.Id); + var propertyDataDtos = propertyFactory.BuildDto(entity.Properties); + var keyDictionary = new Dictionary(); + + //Add Properties + foreach (var propertyDataDto in propertyDataDtos) + { + if (shouldCreateNewVersion == false && propertyDataDto.Id > 0) + { + Database.Update(propertyDataDto); + } + else + { + int primaryKey = Convert.ToInt32(Database.Insert(propertyDataDto)); + keyDictionary.Add(propertyDataDto.PropertyTypeId, primaryKey); + } + } + + //Update Properties with its newly set Id + if (keyDictionary.Any()) + { + foreach (var property in entity.Properties) + { + if (keyDictionary.ContainsKey(property.PropertyTypeId) == false) continue; + + property.Id = keyDictionary[property.PropertyTypeId]; + } + } + + //lastly, check if we are a newly published version and then update the tags table + if (publishedStateChanged && entity.Published) + { + UpdatePropertyTags(entity, _tagRepository); + } + else if (publishedStateChanged && (entity.Trashed || entity.Published == false)) + { + //it's in the trash or not published remove all entity tags + ClearEntityTags(entity, _tagRepository); + } + + // published => update published version infos, + // else if unpublished then clear published version infos + if (entity.Published) + { + dto.DocumentPublishedReadOnlyDto = new DocumentPublishedReadOnlyDto + { + VersionId = dto.VersionId, + Newest = true, + NodeId = dto.NodeId, + Published = true + }; + ((Content)entity).PublishedVersionGuid = dto.VersionId; + } + else if (publishedStateChanged) + { + dto.DocumentPublishedReadOnlyDto = new DocumentPublishedReadOnlyDto + { + VersionId = default(Guid), + Newest = false, + NodeId = dto.NodeId, + Published = false + }; + ((Content)entity).PublishedVersionGuid = default(Guid); + } + + entity.ResetDirtyProperties(); + } + + + #endregion + + #region Implementation of IContentRepository + + public IEnumerable GetByPublishedVersion(IQuery query) + { + // we WANT to return contents in top-down order, ie parents should come before children + // ideal would be pure xml "document order" which can be achieved with: + // ORDER BY substring(path, 1, len(path) - charindex(',', reverse(path))), sortOrder + // but that's probably an overkill - sorting by level,sortOrder should be enough + + var sqlClause = GetBaseQuery(false); + var translator = new SqlTranslator(sqlClause, query); + var sql = translator.Translate() + .Where(x => x.Published) + .OrderBy(x => x.Level, SqlSyntax) + .OrderBy(x => x.SortOrder, SqlSyntax); + + //NOTE: This doesn't allow properties to be part of the query + var dtos = Database.Fetch(sql); + + foreach (var dto in dtos) + { + //Check in the cache first. If it exists there AND it is published + // then we can use that entity. Otherwise if it is not published (which can be the case + // because we only store the 'latest' entries in the cache which might not be the published + // version) + var fromCache = RuntimeCache.GetCacheItem(GetCacheIdKey(dto.NodeId)); + //var fromCache = TryGetFromCache(dto.NodeId); + if (fromCache != null && fromCache.Published) + { + yield return fromCache; + } + else + { + yield return CreateContentFromDto(dto, dto.VersionId, sql); + } + } + } + + public int CountPublished() + { + var sql = GetBaseQuery(true).Where(x => x.Trashed == false) + .Where(x => x.Published == true); + return Database.ExecuteScalar(sql); + } + + public void ReplaceContentPermissions(EntityPermissionSet permissionSet) + { + var repo = new PermissionRepository(UnitOfWork, _cacheHelper, SqlSyntax); + repo.ReplaceEntityPermissions(permissionSet); + } + + public void ClearPublished(IContent content) + { + // race cond! + var documentDtos = Database.Fetch("WHERE nodeId=@id AND published=@published", new { id = content.Id, published = true }); + foreach (var documentDto in documentDtos) + { + documentDto.Published = false; + Database.Update(documentDto); + } + } + + /// + /// Assigns a single permission to the current content item for the specified user ids + /// + /// + /// + /// + public void AssignEntityPermission(IContent entity, char permission, IEnumerable userIds) + { + var repo = new PermissionRepository(UnitOfWork, _cacheHelper, SqlSyntax); + repo.AssignEntityPermission(entity, permission, userIds); + } + + public IEnumerable GetPermissionsForEntity(int entityId) + { + var repo = new PermissionRepository(UnitOfWork, _cacheHelper, SqlSyntax); + return repo.GetPermissionsForEntity(entityId); + } + + /// + /// Adds/updates content/published xml + /// + /// + /// + public void AddOrUpdateContentXml(IContent content, Func xml) + { + _contentXmlRepository.AddOrUpdate(new ContentXmlEntity(content, xml)); + } + + /// + /// Used to remove the content xml for a content item + /// + /// + public void DeleteContentXml(IContent content) + { + _contentXmlRepository.Delete(new ContentXmlEntity(content)); + } + + /// + /// Adds/updates preview xml + /// + /// + /// + public void AddOrUpdatePreviewXml(IContent content, Func xml) + { + _contentPreviewRepository.AddOrUpdate(new ContentPreviewEntity(content, xml)); + } + + /// + /// Gets paged content results + /// + /// Query to excute + /// Page number + /// Page size + /// Total records query would return without paging + /// Field to order by + /// Direction to order by + /// Flag to indicate when ordering by system field + /// Search text filter + /// An Enumerable list of objects + public IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, + string orderBy, Direction orderDirection, bool orderBySystemField, string filter = "") + { + + //NOTE: This uses the GetBaseQuery method but that does not take into account the required 'newest' field which is + // what we always require for a paged result, so we'll ensure it's included in the filter + + var args = new List(); + var sbWhere = new StringBuilder("AND (cmsDocument.newest = 1)"); + + if (filter.IsNullOrWhiteSpace() == false) + { + sbWhere.Append(" AND (cmsDocument." + SqlSyntax.GetQuotedColumnName("text") + " LIKE @" + args.Count + ")"); + args.Add("%" + filter + "%"); + } + + Func> filterCallback = () => new Tuple(sbWhere.ToString(), args.ToArray()); + + return GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, + new Tuple("cmsDocument", "nodeId"), + ProcessQuery, orderBy, orderDirection, orderBySystemField, + filterCallback); + + } + + #endregion + + #region IRecycleBinRepository members + + protected override int RecycleBinId + { + get { return Constants.System.RecycleBinContent; } + } + + #endregion + + protected override string GetDatabaseFieldNameForOrderBy(string orderBy) + { + //Some custom ones + switch (orderBy.ToUpperInvariant()) + { + case "NAME": + return "cmsDocument.text"; + case "UPDATER": + //TODO: This isn't going to work very nicely because it's going to order by ID, not by letter + return "cmsDocument.documentUser"; + } + + return base.GetDatabaseFieldNameForOrderBy(orderBy); + } + + private IEnumerable ProcessQuery(Sql sql) + { + //NOTE: This doesn't allow properties to be part of the query + var dtos = Database.Fetch(sql); + + //nothing found + if (dtos.Any() == false) return Enumerable.Empty(); + + //content types + //NOTE: This should be ok for an SQL 'IN' statement, there shouldn't be an insane amount of content types + var contentTypes = _contentTypeRepository.GetAll(dtos.Select(x => x.ContentVersionDto.ContentDto.ContentTypeId).ToArray()) + .ToArray(); + + + var ids = dtos + .Where(dto => dto.TemplateId.HasValue && dto.TemplateId.Value > 0) + .Select(x => x.TemplateId.Value).ToArray(); + + //NOTE: This should be ok for an SQL 'IN' statement, there shouldn't be an insane amount of content types + var templates = ids.Length == 0 ? Enumerable.Empty() : _templateRepository.GetAll(ids).ToArray(); + + var dtosWithContentTypes = dtos + //This select into and null check are required because we don't have a foreign damn key on the contentType column + // http://issues.umbraco.org/issue/U4-5503 + .Select(x => new { dto = x, contentType = contentTypes.FirstOrDefault(ct => ct.Id == x.ContentVersionDto.ContentDto.ContentTypeId) }) + .Where(x => x.contentType != null) + .ToArray(); + + //Go get the property data for each document + var docDefs = dtosWithContentTypes.Select(d => new DocumentDefinition( + d.dto.NodeId, + d.dto.VersionId, + d.dto.ContentVersionDto.VersionDate, + d.dto.ContentVersionDto.ContentDto.NodeDto.CreateDate, + d.contentType)); + + var propertyData = GetPropertyCollection(sql, docDefs); + + return dtosWithContentTypes.Select(d => CreateContentFromDto( + d.dto, + contentTypes.First(ct => ct.Id == d.dto.ContentVersionDto.ContentDto.ContentTypeId), + templates.FirstOrDefault(tem => tem.Id == (d.dto.TemplateId.HasValue ? d.dto.TemplateId.Value : -1)), + propertyData[d.dto.NodeId])); + } + + /// + /// Private method to create a content object from a DocumentDto, which is used by Get and GetByVersion. + /// + /// + /// + /// + /// + /// + private IContent CreateContentFromDto(DocumentDto dto, + IContentType contentType, + ITemplate template, + Models.PropertyCollection propCollection) + { + var factory = new ContentFactory(contentType, NodeObjectTypeId, dto.NodeId); + var content = factory.BuildEntity(dto); + + //Check if template id is set on DocumentDto, and get ITemplate if it is. + if (dto.TemplateId.HasValue && dto.TemplateId.Value > 0) + { + content.Template = template ?? _templateRepository.Get(dto.TemplateId.Value); + } + else + { + //ensure there isn't one set. + content.Template = null; + } + + content.Properties = propCollection; + + //on initial construction we don't want to have dirty properties tracked + // http://issues.umbraco.org/issue/U4-1946 + ((Entity)content).ResetDirtyProperties(false); + return content; + } + + /// + /// Private method to create a content object from a DocumentDto, which is used by Get and GetByVersion. + /// + /// + /// + /// + /// + private IContent CreateContentFromDto(DocumentDto dto, Guid versionId, Sql docSql) + { + var contentType = _contentTypeRepository.Get(dto.ContentVersionDto.ContentDto.ContentTypeId); + + var factory = new ContentFactory(contentType, NodeObjectTypeId, dto.NodeId); + var content = factory.BuildEntity(dto); + + //Check if template id is set on DocumentDto, and get ITemplate if it is. + if (dto.TemplateId.HasValue && dto.TemplateId.Value > 0) + { + content.Template = _templateRepository.Get(dto.TemplateId.Value); + } + + var docDef = new DocumentDefinition(dto.NodeId, versionId, content.UpdateDate, content.CreateDate, contentType); + + var properties = GetPropertyCollection(docSql, new[] { docDef }); + + content.Properties = properties[dto.NodeId]; + + //on initial construction we don't want to have dirty properties tracked + // http://issues.umbraco.org/issue/U4-1946 + ((Entity)content).ResetDirtyProperties(false); + return content; + } + + private string EnsureUniqueNodeName(int parentId, string nodeName, int id = 0) + { + if (EnsureUniqueNaming == false) + return nodeName; + + var sql = new Sql(); + sql.Select("*") + .From() + .Where(x => x.NodeObjectType == NodeObjectTypeId && x.ParentId == parentId && 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; + } + + /// + /// Dispose disposable properties + /// + /// + /// Ensure the unit of work is disposed + /// + protected override void DisposeResources() + { + _contentTypeRepository.Dispose(); + _templateRepository.Dispose(); + _tagRepository.Dispose(); + _contentPreviewRepository.Dispose(); + _contentXmlRepository.Dispose(); + } + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs index 9d3fcbb40b..0662581b15 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs @@ -9,84 +9,85 @@ using Umbraco.Core.Persistence.Querying; namespace Umbraco.Core.Persistence.Repositories { - public interface IContentRepository : IRepositoryVersionable, IRecycleBinRepository, IDeleteMediaFilesRepository - { - /// - /// Get the count of published items - /// - /// - /// - /// We require this on the repo because the IQuery{IContent} cannot supply the 'newest' parameter - /// - int CountPublished(); + public interface IContentRepository : IRepositoryVersionable, IRecycleBinRepository, IDeleteMediaFilesRepository + { + /// + /// Get the count of published items + /// + /// + /// + /// We require this on the repo because the IQuery{IContent} cannot supply the 'newest' parameter + /// + int CountPublished(); - /// - /// Used to bulk update the permissions set for a content item. This will replace all permissions - /// assigned to an entity with a list of user id & permission pairs. - /// - /// - void ReplaceContentPermissions(EntityPermissionSet permissionSet); + /// + /// Used to bulk update the permissions set for a content item. This will replace all permissions + /// assigned to an entity with a list of user id & permission pairs. + /// + /// + void ReplaceContentPermissions(EntityPermissionSet permissionSet); - /// - /// Clears the published flag for a content. - /// - /// - void ClearPublished(IContent content); + /// + /// Clears the published flag for a content. + /// + /// + void ClearPublished(IContent content); - /// - /// Gets all published Content by the specified query - /// - /// Query to execute against published versions - /// An enumerable list of - IEnumerable GetByPublishedVersion(IQuery query); + /// + /// Gets all published Content by the specified query + /// + /// Query to execute against published versions + /// An enumerable list of + IEnumerable GetByPublishedVersion(IQuery query); - /// - /// Assigns a single permission to the current content item for the specified user ids - /// - /// - /// - /// - void AssignEntityPermission(IContent entity, char permission, IEnumerable userIds); + /// + /// Assigns a single permission to the current content item for the specified user ids + /// + /// + /// + /// + void AssignEntityPermission(IContent entity, char permission, IEnumerable userIds); - /// - /// Gets the list of permissions for the content item - /// - /// - /// - IEnumerable GetPermissionsForEntity(int entityId); + /// + /// Gets the list of permissions for the content item + /// + /// + /// + IEnumerable GetPermissionsForEntity(int entityId); - /// - /// Used to add/update published xml for the content item - /// - /// - /// - void AddOrUpdateContentXml(IContent content, Func xml); + /// + /// Used to add/update published xml for the content item + /// + /// + /// + void AddOrUpdateContentXml(IContent content, Func xml); - /// - /// Used to remove the content xml for a content item - /// - /// - void DeleteContentXml(IContent content); + /// + /// Used to remove the content xml for a content item + /// + /// + void DeleteContentXml(IContent content); - /// - /// Used to add/update preview xml for the content item - /// - /// - /// - void AddOrUpdatePreviewXml(IContent content, Func xml); + /// + /// Used to add/update preview xml for the content item + /// + /// + /// + void AddOrUpdatePreviewXml(IContent content, Func xml); - /// - /// Gets paged content results - /// - /// Query to excute - /// Page number - /// Page size - /// Total records query would return without paging - /// Field to order by - /// Direction to order by - /// Search text filter - /// An Enumerable list of objects - IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, - string orderBy, Direction orderDirection, string filter = ""); - } + /// + /// Gets paged content results + /// + /// Query to excute + /// Page number + /// Page size + /// Total records query would return without paging + /// Field to order by + /// Direction to order by + /// Flag to indicate when ordering by system field + /// Search text filter + /// An Enumerable list of objects + IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, + string orderBy, Direction orderDirection, bool orderBySystemField, string filter = ""); + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMediaRepository.cs index 46bce6a03c..358a099ea0 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMediaRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMediaRepository.cs @@ -7,35 +7,36 @@ using Umbraco.Core.Persistence.Querying; namespace Umbraco.Core.Persistence.Repositories { - public interface IMediaRepository : IRepositoryVersionable, IRecycleBinRepository, IDeleteMediaFilesRepository - { - - /// - /// Used to add/update published xml for the media item - /// - /// - /// - void AddOrUpdateContentXml(IMedia content, Func xml); + public interface IMediaRepository : IRepositoryVersionable, IRecycleBinRepository, IDeleteMediaFilesRepository + { - /// - /// Used to add/update preview xml for the content item - /// - /// - /// - void AddOrUpdatePreviewXml(IMedia content, Func xml); + /// + /// Used to add/update published xml for the media item + /// + /// + /// + void AddOrUpdateContentXml(IMedia content, Func xml); - /// - /// Gets paged media results - /// - /// Query to excute - /// Page number - /// Page size - /// Total records query would return without paging - /// Field to order by - /// Direction to order by - /// Search text filter - /// An Enumerable list of objects - IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, - string orderBy, Direction orderDirection, string filter = ""); - } + /// + /// Used to add/update preview xml for the content item + /// + /// + /// + void AddOrUpdatePreviewXml(IMedia content, Func xml); + + /// + /// Gets paged media results + /// + /// Query to excute + /// Page number + /// Page size + /// Total records query would return without paging + /// Field to order by + /// Direction to order by + /// Flag to indicate when ordering by system field + /// Search text filter + /// An Enumerable list of objects + IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, + string orderBy, Direction orderDirection, bool orderBySystemField, string filter = ""); + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMemberRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMemberRepository.cs index 9cb74d1806..c59bf5d192 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMemberRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMemberRepository.cs @@ -9,69 +9,70 @@ using Umbraco.Core.Persistence.Querying; namespace Umbraco.Core.Persistence.Repositories { - public interface IMemberRepository : IRepositoryVersionable, IDeleteMediaFilesRepository - { - /// - /// Finds members in a given role - /// - /// - /// - /// - /// - IEnumerable FindMembersInRole(string roleName, string usernameToMatch, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith); + public interface IMemberRepository : IRepositoryVersionable, IDeleteMediaFilesRepository + { + /// + /// Finds members in a given role + /// + /// + /// + /// + /// + IEnumerable FindMembersInRole(string roleName, string usernameToMatch, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith); - /// - /// Get all members in a specific group - /// - /// - /// - IEnumerable GetByMemberGroup(string groupName); + /// + /// Get all members in a specific group + /// + /// + /// + IEnumerable GetByMemberGroup(string groupName); - /// - /// Checks if a member with the username exists - /// - /// - /// - bool Exists(string username); + /// + /// Checks if a member with the username exists + /// + /// + /// + bool Exists(string username); - /// - /// Gets the count of items based on a complex query - /// - /// - /// - int GetCountByQuery(IQuery query); + /// + /// Gets the count of items based on a complex query + /// + /// + /// + int GetCountByQuery(IQuery query); - /// - /// Gets paged member results - /// - /// - /// - /// - /// - /// - /// - /// - /// - IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, - string orderBy, Direction orderDirection, string filter = ""); + /// + /// Gets paged member results + /// + /// The query. + /// Index of the page. + /// Size of the page. + /// The total records. + /// The order by column + /// The order direction. + /// Flag to indicate when ordering by system field + /// Search query + /// + IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, + string orderBy, Direction orderDirection, bool orderBySystemField, string filter = ""); - //IEnumerable GetPagedResultsByQuery( - // Sql sql, int pageIndex, int pageSize, out int totalRecords, - // Func, int[]> resolveIds); + //IEnumerable GetPagedResultsByQuery( + // Sql sql, int pageIndex, int pageSize, out int totalRecords, + // Func, int[]> resolveIds); - /// - /// Used to add/update published xml for the media item - /// - /// - /// - void AddOrUpdateContentXml(IMember content, Func xml); + /// + /// Used to add/update published xml for the media item + /// + /// + /// + void AddOrUpdateContentXml(IMember content, Func xml); - /// - /// Used to add/update preview xml for the content item - /// - /// - /// - void AddOrUpdatePreviewXml(IMember content, Func xml); + /// + /// Used to add/update preview xml for the content item + /// + /// + /// + void AddOrUpdatePreviewXml(IMember content, Func xml); - } + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs index 162ad88ca0..d425f0afda 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs @@ -22,574 +22,575 @@ using Umbraco.Core.Services; namespace Umbraco.Core.Persistence.Repositories { - /// - /// Represents a repository for doing CRUD operations for - /// - internal class MediaRepository : RecycleBinRepository, IMediaRepository - { - private readonly IMediaTypeRepository _mediaTypeRepository; - private readonly ITagRepository _tagRepository; - private readonly ContentXmlRepository _contentXmlRepository; - private readonly ContentPreviewRepository _contentPreviewRepository; - - public MediaRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax, IMediaTypeRepository mediaTypeRepository, ITagRepository tagRepository, IContentSection contentSection) - : base(work, cache, logger, sqlSyntax, contentSection) - { - if (mediaTypeRepository == null) throw new ArgumentNullException("mediaTypeRepository"); - if (tagRepository == null) throw new ArgumentNullException("tagRepository"); - _mediaTypeRepository = mediaTypeRepository; - _tagRepository = tagRepository; - _contentXmlRepository = new ContentXmlRepository(work, CacheHelper.CreateDisabledCacheHelper(), logger, sqlSyntax); - _contentPreviewRepository = new ContentPreviewRepository(work, CacheHelper.CreateDisabledCacheHelper(), logger, sqlSyntax); - EnsureUniqueNaming = contentSection.EnsureUniqueNaming; - } - - public bool EnsureUniqueNaming { get; private set; } - - #region Overrides of RepositoryBase - - protected override IMedia PerformGet(int id) - { - var sql = GetBaseQuery(false); - sql.Where(GetBaseWhereClause(), new { Id = id }); - sql.OrderByDescending(x => x.VersionDate); - - var dto = Database.Fetch(sql).FirstOrDefault(); - - if (dto == null) - return null; - - var content = CreateMediaFromDto(dto, dto.VersionId, sql); - - return content; - } - - protected override IEnumerable PerformGetAll(params int[] ids) - { - var sql = GetBaseQuery(false); - if (ids.Any()) - { - sql.Where("umbracoNode.id in (@ids)", new { ids = ids }); - } - - return ProcessQuery(sql); - } - - protected override IEnumerable PerformGetByQuery(IQuery query) - { - var sqlClause = GetBaseQuery(false); - var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate() - .OrderBy(x => x.SortOrder); - - return ProcessQuery(sql); - } - - #endregion - - #region Overrides of PetaPocoRepositoryBase - - protected override Sql GetBaseQuery(bool isCount) - { - var sql = new Sql(); - sql.Select(isCount ? "COUNT(*)" : "*") - .From() - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .Where(x => x.NodeObjectType == NodeObjectTypeId); - return sql; - } - - protected override string GetBaseWhereClause() - { - return "umbracoNode.id = @Id"; - } - - protected override IEnumerable GetDeleteClauses() - { - var list = new List - { - "DELETE FROM cmsTask WHERE nodeId = @Id", - "DELETE FROM umbracoUser2NodeNotify WHERE nodeId = @Id", - "DELETE FROM umbracoUser2NodePermission WHERE nodeId = @Id", - "DELETE FROM umbracoRelation WHERE parentId = @Id", - "DELETE FROM umbracoRelation WHERE childId = @Id", - "DELETE FROM cmsTagRelationship WHERE nodeId = @Id", - "DELETE FROM cmsDocument WHERE nodeId = @Id", - "DELETE FROM cmsPropertyData WHERE contentNodeId = @Id", - "DELETE FROM cmsPreviewXml WHERE nodeId = @Id", - "DELETE FROM cmsContentVersion WHERE ContentId = @Id", - "DELETE FROM cmsContentXml WHERE nodeId = @Id", - "DELETE FROM cmsContent WHERE nodeId = @Id", - "DELETE FROM umbracoNode WHERE id = @Id" - }; - return list; - } - - protected override Guid NodeObjectTypeId - { - get { return new Guid(Constants.ObjectTypes.Media); } - } - - #endregion - - #region Overrides of VersionableRepositoryBase - - public override IMedia GetByVersion(Guid versionId) - { - var sql = GetBaseQuery(false); - sql.Where("cmsContentVersion.VersionId = @VersionId", new { VersionId = versionId }); - sql.OrderByDescending(x => x.VersionDate); - - var dto = Database.Fetch(sql).FirstOrDefault(); - - if (dto == null) - return null; - - var mediaType = _mediaTypeRepository.Get(dto.ContentDto.ContentTypeId); - - var factory = new MediaFactory(mediaType, NodeObjectTypeId, dto.NodeId); - var media = factory.BuildEntity(dto); - - var properties = GetPropertyCollection(sql, new[] { new DocumentDefinition(dto.NodeId, dto.VersionId, media.UpdateDate, media.CreateDate, mediaType) }); - - media.Properties = properties[dto.NodeId]; - - //on initial construction we don't want to have dirty properties tracked - // http://issues.umbraco.org/issue/U4-1946 - ((Entity)media).ResetDirtyProperties(false); - return media; - } - - public void RebuildXmlStructures(Func serializer, int groupSize = 5000, IEnumerable contentTypeIds = null) - { - - //Ok, now we need to remove the data and re-insert it, we'll do this all in one transaction too. - using (var tr = Database.GetTransaction()) - { - //Remove all the data first, if anything fails after this it's no problem the transaction will be reverted - if (contentTypeIds == null) - { - var mediaObjectType = Guid.Parse(Constants.ObjectTypes.Media); - var subQuery = new Sql() - .Select("DISTINCT cmsContentXml.nodeId") - .From() - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .Where(dto => dto.NodeObjectType == mediaObjectType); - - var deleteSql = SqlSyntax.GetDeleteSubquery("cmsContentXml", "nodeId", subQuery); - Database.Execute(deleteSql); - } - else - { - foreach (var id in contentTypeIds) - { - var id1 = id; - var mediaObjectType = Guid.Parse(Constants.ObjectTypes.Media); - var subQuery = new Sql() - .Select("DISTINCT cmsContentXml.nodeId") - .From() - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .Where(dto => dto.NodeObjectType == mediaObjectType) - .Where(dto => dto.ContentTypeId == id1); - - var deleteSql = SqlSyntax.GetDeleteSubquery("cmsContentXml", "nodeId", subQuery); - Database.Execute(deleteSql); - } - } - - //now insert the data, again if something fails here, the whole transaction is reversed - if (contentTypeIds == null) - { - var query = Query.Builder; - RebuildXmlStructuresProcessQuery(serializer, query, tr, groupSize); - } - else - { - foreach (var contentTypeId in contentTypeIds) - { - //copy local - var id = contentTypeId; - var query = Query.Builder.Where(x => x.ContentTypeId == id && x.Trashed == false); - RebuildXmlStructuresProcessQuery(serializer, query, tr, groupSize); - } - } - - tr.Complete(); - } - } - - private void RebuildXmlStructuresProcessQuery(Func serializer, IQuery query, Transaction tr, int pageSize) - { - var pageIndex = 0; - var total = long.MinValue; - var processed = 0; - do - { - var descendants = GetPagedResultsByQuery(query, pageIndex, pageSize, out total, "Path", Direction.Ascending); - - var xmlItems = (from descendant in descendants - let xml = serializer(descendant) - select new ContentXmlDto { NodeId = descendant.Id, Xml = xml.ToDataString() }).ToArray(); - - //bulk insert it into the database - Database.BulkInsertRecords(xmlItems, tr); - - processed += xmlItems.Length; - - pageIndex++; - } while (processed < total); - } - - public void AddOrUpdateContentXml(IMedia content, Func xml) - { - _contentXmlRepository.AddOrUpdate(new ContentXmlEntity(content, xml)); - } - - public void AddOrUpdatePreviewXml(IMedia content, Func xml) - { - _contentPreviewRepository.AddOrUpdate(new ContentPreviewEntity(content, xml)); - } - - protected override void PerformDeleteVersion(int id, Guid versionId) - { - Database.Delete("WHERE nodeId = @Id AND versionId = @VersionId", new { Id = id, VersionId = versionId }); - Database.Delete("WHERE contentNodeId = @Id AND versionId = @VersionId", new { Id = id, VersionId = versionId }); - Database.Delete("WHERE ContentId = @Id AND VersionId = @VersionId", new { Id = id, VersionId = versionId }); - } - - #endregion - - #region Unit of Work Implementation - - protected override void PersistNewItem(IMedia entity) - { - ((Models.Media)entity).AddingEntity(); - - //Ensure unique name on the same level - entity.Name = EnsureUniqueNodeName(entity.ParentId, entity.Name); - - //Ensure that strings don't contain characters that are invalid in XML - entity.SanitizeEntityPropertiesForXmlStorage(); - - var factory = new MediaFactory(NodeObjectTypeId, entity.Id); - var dto = factory.BuildDto(entity); - - //NOTE Should the logic below have some kind of fallback for empty parent ids ? - //Logic for setting Path, Level and SortOrder - var parent = Database.First("WHERE id = @ParentId", new { ParentId = entity.ParentId }); - var level = parent.Level + 1; - var maxSortOrder = Database.ExecuteScalar( - "SELECT coalesce(max(sortOrder),-1) FROM umbracoNode WHERE parentid = @ParentId AND nodeObjectType = @NodeObjectType", - new { /*ParentId =*/ entity.ParentId, NodeObjectType = NodeObjectTypeId }); - var sortOrder = maxSortOrder + 1; - - //Create the (base) node data - umbracoNode - var nodeDto = dto.ContentDto.NodeDto; - nodeDto.Path = parent.Path; - nodeDto.Level = short.Parse(level.ToString(CultureInfo.InvariantCulture)); - nodeDto.SortOrder = sortOrder; - var o = Database.IsNew(nodeDto) ? Convert.ToInt32(Database.Insert(nodeDto)) : Database.Update(nodeDto); - - //Update with new correct path - nodeDto.Path = string.Concat(parent.Path, ",", nodeDto.NodeId); - Database.Update(nodeDto); - - //Update entity with correct values - entity.Id = nodeDto.NodeId; //Set Id on entity to ensure an Id is set - entity.Path = nodeDto.Path; - entity.SortOrder = sortOrder; - entity.Level = level; - - //Create the Content specific data - cmsContent - var contentDto = dto.ContentDto; - contentDto.NodeId = nodeDto.NodeId; - Database.Insert(contentDto); - - //Create the first version - cmsContentVersion - //Assumes a new Version guid and Version date (modified date) has been set - dto.NodeId = nodeDto.NodeId; - Database.Insert(dto); - - //Create the PropertyData for this version - cmsPropertyData - var propertyFactory = new PropertyFactory(entity.ContentType.CompositionPropertyTypes.ToArray(), entity.Version, entity.Id); - var propertyDataDtos = propertyFactory.BuildDto(entity.Properties); - var keyDictionary = new Dictionary(); - - //Add Properties - foreach (var propertyDataDto in propertyDataDtos) - { - var primaryKey = Convert.ToInt32(Database.Insert(propertyDataDto)); - keyDictionary.Add(propertyDataDto.PropertyTypeId, primaryKey); - } - - //Update Properties with its newly set Id - foreach (var property in entity.Properties) - { - property.Id = keyDictionary[property.PropertyTypeId]; - } - - UpdatePropertyTags(entity, _tagRepository); - - entity.ResetDirtyProperties(); - } - - protected override void PersistUpdatedItem(IMedia entity) - { - //Updates Modified date - ((Models.Media)entity).UpdatingEntity(); - - //Ensure unique name on the same level - entity.Name = EnsureUniqueNodeName(entity.ParentId, entity.Name, entity.Id); - - //Ensure that strings don't contain characters that are invalid in XML - entity.SanitizeEntityPropertiesForXmlStorage(); - - //Look up parent to get and set the correct Path and update SortOrder if ParentId has changed - if (entity.IsPropertyDirty("ParentId")) - { - var parent = Database.First("WHERE id = @ParentId", new { ParentId = entity.ParentId }); - entity.Path = string.Concat(parent.Path, ",", entity.Id); - entity.Level = parent.Level + 1; - var maxSortOrder = - Database.ExecuteScalar( - "SELECT coalesce(max(sortOrder),0) FROM umbracoNode WHERE parentid = @ParentId AND nodeObjectType = @NodeObjectType", - new { ParentId = entity.ParentId, NodeObjectType = NodeObjectTypeId }); - entity.SortOrder = maxSortOrder + 1; - } - - var factory = new MediaFactory(NodeObjectTypeId, entity.Id); - //Look up Content entry to get Primary for updating the DTO - var contentDto = Database.SingleOrDefault("WHERE nodeId = @Id", new { Id = entity.Id }); - factory.SetPrimaryKey(contentDto.PrimaryKey); - var dto = factory.BuildDto(entity); - - //Updates the (base) node data - umbracoNode - var nodeDto = dto.ContentDto.NodeDto; - var o = Database.Update(nodeDto); - - //Only update this DTO if the contentType has actually changed - if (contentDto.ContentTypeId != entity.ContentTypeId) - { - //Create the Content specific data - cmsContent - var newContentDto = dto.ContentDto; - Database.Update(newContentDto); - } - - //In order to update the ContentVersion we need to retrieve its primary key id - var contentVerDto = Database.SingleOrDefault("WHERE VersionId = @Version", new { Version = entity.Version }); - dto.Id = contentVerDto.Id; - //Updates the current version - cmsContentVersion - //Assumes a Version guid exists and Version date (modified date) has been set/updated - Database.Update(dto); - - //Create the PropertyData for this version - cmsPropertyData - var propertyFactory = new PropertyFactory(entity.ContentType.CompositionPropertyTypes.ToArray(), entity.Version, entity.Id); - var propertyDataDtos = propertyFactory.BuildDto(entity.Properties); - var keyDictionary = new Dictionary(); - - //Add Properties - foreach (var propertyDataDto in propertyDataDtos) - { - if (propertyDataDto.Id > 0) - { - Database.Update(propertyDataDto); - } - else - { - int primaryKey = Convert.ToInt32(Database.Insert(propertyDataDto)); - keyDictionary.Add(propertyDataDto.PropertyTypeId, primaryKey); - } - } - - //Update Properties with its newly set Id - if (keyDictionary.Any()) - { - foreach (var property in entity.Properties) - { - property.Id = keyDictionary[property.PropertyTypeId]; - } - } - - UpdatePropertyTags(entity, _tagRepository); - - entity.ResetDirtyProperties(); - } - - #endregion - - #region IRecycleBinRepository members - - protected override int RecycleBinId - { - get { return Constants.System.RecycleBinMedia; } - } - - #endregion - - /// - /// Gets paged media results - /// - /// Query to excute - /// Page number - /// Page size - /// Total records query would return without paging - /// Field to order by - /// Direction to order by - /// Search text filter - /// An Enumerable list of objects - public IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, - string orderBy, Direction orderDirection, string filter = "") - { - var args = new List(); - var sbWhere = new StringBuilder(); - Func> filterCallback = null; - if (filter.IsNullOrWhiteSpace() == false) - { - sbWhere.Append("AND (umbracoNode." + SqlSyntax.GetQuotedColumnName("text") + " LIKE @" + args.Count + ")"); - args.Add("%" + filter + "%"); - filterCallback = () => new Tuple(sbWhere.ToString().Trim(), args.ToArray()); - } - - return GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, - new Tuple("cmsContentVersion", "contentId"), - ProcessQuery, orderBy, orderDirection, - filterCallback); - - } - - private IEnumerable ProcessQuery(Sql sql) - { - //NOTE: This doesn't allow properties to be part of the query - var dtos = Database.Fetch(sql); - - var ids = dtos.Select(x => x.ContentDto.ContentTypeId).ToArray(); - - //content types - var contentTypes = ids.Length == 0 ? Enumerable.Empty() : _mediaTypeRepository.GetAll(ids).ToArray(); - - var dtosWithContentTypes = dtos - //This select into and null check are required because we don't have a foreign damn key on the contentType column - // http://issues.umbraco.org/issue/U4-5503 - .Select(x => new { dto = x, contentType = contentTypes.FirstOrDefault(ct => ct.Id == x.ContentDto.ContentTypeId) }) - .Where(x => x.contentType != null) - .ToArray(); - - //Go get the property data for each document - var docDefs = dtosWithContentTypes.Select(d => new DocumentDefinition( - d.dto.NodeId, - d.dto.VersionId, - d.dto.VersionDate, - d.dto.ContentDto.NodeDto.CreateDate, - d.contentType)) - .ToArray(); - - var propertyData = GetPropertyCollection(sql, docDefs); - - return dtosWithContentTypes.Select(d => CreateMediaFromDto( - d.dto, - contentTypes.First(ct => ct.Id == d.dto.ContentDto.ContentTypeId), - propertyData[d.dto.NodeId])); - } - - /// - /// Private method to create a media object from a ContentDto - /// - /// - /// - /// - /// - private IMedia CreateMediaFromDto(ContentVersionDto dto, - IMediaType contentType, - PropertyCollection propCollection) - { - var factory = new MediaFactory(contentType, NodeObjectTypeId, dto.NodeId); - var media = factory.BuildEntity(dto); - - media.Properties = propCollection; - - //on initial construction we don't want to have dirty properties tracked - // http://issues.umbraco.org/issue/U4-1946 - ((Entity)media).ResetDirtyProperties(false); - return media; - } - - /// - /// Private method to create a media object from a ContentDto - /// - /// - /// - /// - /// - private IMedia CreateMediaFromDto(ContentVersionDto dto, Guid versionId, Sql docSql) - { - var contentType = _mediaTypeRepository.Get(dto.ContentDto.ContentTypeId); - - var factory = new MediaFactory(contentType, NodeObjectTypeId, dto.NodeId); - var media = factory.BuildEntity(dto); - - var docDef = new DocumentDefinition(dto.NodeId, versionId, media.UpdateDate, media.CreateDate, contentType); - - var properties = GetPropertyCollection(docSql, new[] { docDef }); - - media.Properties = properties[dto.NodeId]; - - //on initial construction we don't want to have dirty properties tracked - // http://issues.umbraco.org/issue/U4-1946 - ((Entity)media).ResetDirtyProperties(false); - return media; - } - - private string EnsureUniqueNodeName(int parentId, string nodeName, int id = 0) - { - if (EnsureUniqueNaming == false) - return nodeName; - - var sql = new Sql(); - sql.Select("*") - .From() - .Where(x => x.NodeObjectType == NodeObjectTypeId && x.ParentId == parentId && 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; - } - - /// - /// Dispose disposable properties - /// - /// - /// Ensure the unit of work is disposed - /// - protected override void DisposeResources() - { - _mediaTypeRepository.Dispose(); - _tagRepository.Dispose(); - _contentXmlRepository.Dispose(); - _contentPreviewRepository.Dispose(); - } - } + /// + /// Represents a repository for doing CRUD operations for + /// + internal class MediaRepository : RecycleBinRepository, IMediaRepository + { + private readonly IMediaTypeRepository _mediaTypeRepository; + private readonly ITagRepository _tagRepository; + private readonly ContentXmlRepository _contentXmlRepository; + private readonly ContentPreviewRepository _contentPreviewRepository; + + public MediaRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax, IMediaTypeRepository mediaTypeRepository, ITagRepository tagRepository, IContentSection contentSection) + : base(work, cache, logger, sqlSyntax, contentSection) + { + if (mediaTypeRepository == null) throw new ArgumentNullException("mediaTypeRepository"); + if (tagRepository == null) throw new ArgumentNullException("tagRepository"); + _mediaTypeRepository = mediaTypeRepository; + _tagRepository = tagRepository; + _contentXmlRepository = new ContentXmlRepository(work, CacheHelper.CreateDisabledCacheHelper(), logger, sqlSyntax); + _contentPreviewRepository = new ContentPreviewRepository(work, CacheHelper.CreateDisabledCacheHelper(), logger, sqlSyntax); + EnsureUniqueNaming = contentSection.EnsureUniqueNaming; + } + + public bool EnsureUniqueNaming { get; private set; } + + #region Overrides of RepositoryBase + + protected override IMedia PerformGet(int id) + { + var sql = GetBaseQuery(false); + sql.Where(GetBaseWhereClause(), new { Id = id }); + sql.OrderByDescending(x => x.VersionDate); + + var dto = Database.Fetch(sql).FirstOrDefault(); + + if (dto == null) + return null; + + var content = CreateMediaFromDto(dto, dto.VersionId, sql); + + return content; + } + + protected override IEnumerable PerformGetAll(params int[] ids) + { + var sql = GetBaseQuery(false); + if (ids.Any()) + { + sql.Where("umbracoNode.id in (@ids)", new { ids = ids }); + } + + return ProcessQuery(sql); + } + + protected override IEnumerable PerformGetByQuery(IQuery query) + { + var sqlClause = GetBaseQuery(false); + var translator = new SqlTranslator(sqlClause, query); + var sql = translator.Translate() + .OrderBy(x => x.SortOrder); + + return ProcessQuery(sql); + } + + #endregion + + #region Overrides of PetaPocoRepositoryBase + + protected override Sql GetBaseQuery(bool isCount) + { + var sql = new Sql(); + sql.Select(isCount ? "COUNT(*)" : "*") + .From() + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .Where(x => x.NodeObjectType == NodeObjectTypeId); + return sql; + } + + protected override string GetBaseWhereClause() + { + return "umbracoNode.id = @Id"; + } + + protected override IEnumerable GetDeleteClauses() + { + var list = new List + { + "DELETE FROM cmsTask WHERE nodeId = @Id", + "DELETE FROM umbracoUser2NodeNotify WHERE nodeId = @Id", + "DELETE FROM umbracoUser2NodePermission WHERE nodeId = @Id", + "DELETE FROM umbracoRelation WHERE parentId = @Id", + "DELETE FROM umbracoRelation WHERE childId = @Id", + "DELETE FROM cmsTagRelationship WHERE nodeId = @Id", + "DELETE FROM cmsDocument WHERE nodeId = @Id", + "DELETE FROM cmsPropertyData WHERE contentNodeId = @Id", + "DELETE FROM cmsPreviewXml WHERE nodeId = @Id", + "DELETE FROM cmsContentVersion WHERE ContentId = @Id", + "DELETE FROM cmsContentXml WHERE nodeId = @Id", + "DELETE FROM cmsContent WHERE nodeId = @Id", + "DELETE FROM umbracoNode WHERE id = @Id" + }; + return list; + } + + protected override Guid NodeObjectTypeId + { + get { return new Guid(Constants.ObjectTypes.Media); } + } + + #endregion + + #region Overrides of VersionableRepositoryBase + + public override IMedia GetByVersion(Guid versionId) + { + var sql = GetBaseQuery(false); + sql.Where("cmsContentVersion.VersionId = @VersionId", new { VersionId = versionId }); + sql.OrderByDescending(x => x.VersionDate); + + var dto = Database.Fetch(sql).FirstOrDefault(); + + if (dto == null) + return null; + + var mediaType = _mediaTypeRepository.Get(dto.ContentDto.ContentTypeId); + + var factory = new MediaFactory(mediaType, NodeObjectTypeId, dto.NodeId); + var media = factory.BuildEntity(dto); + + var properties = GetPropertyCollection(sql, new[] { new DocumentDefinition(dto.NodeId, dto.VersionId, media.UpdateDate, media.CreateDate, mediaType) }); + + media.Properties = properties[dto.NodeId]; + + //on initial construction we don't want to have dirty properties tracked + // http://issues.umbraco.org/issue/U4-1946 + ((Entity)media).ResetDirtyProperties(false); + return media; + } + + public void RebuildXmlStructures(Func serializer, int groupSize = 5000, IEnumerable contentTypeIds = null) + { + + //Ok, now we need to remove the data and re-insert it, we'll do this all in one transaction too. + using (var tr = Database.GetTransaction()) + { + //Remove all the data first, if anything fails after this it's no problem the transaction will be reverted + if (contentTypeIds == null) + { + var mediaObjectType = Guid.Parse(Constants.ObjectTypes.Media); + var subQuery = new Sql() + .Select("DISTINCT cmsContentXml.nodeId") + .From() + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .Where(dto => dto.NodeObjectType == mediaObjectType); + + var deleteSql = SqlSyntax.GetDeleteSubquery("cmsContentXml", "nodeId", subQuery); + Database.Execute(deleteSql); + } + else + { + foreach (var id in contentTypeIds) + { + var id1 = id; + var mediaObjectType = Guid.Parse(Constants.ObjectTypes.Media); + var subQuery = new Sql() + .Select("DISTINCT cmsContentXml.nodeId") + .From() + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .Where(dto => dto.NodeObjectType == mediaObjectType) + .Where(dto => dto.ContentTypeId == id1); + + var deleteSql = SqlSyntax.GetDeleteSubquery("cmsContentXml", "nodeId", subQuery); + Database.Execute(deleteSql); + } + } + + //now insert the data, again if something fails here, the whole transaction is reversed + if (contentTypeIds == null) + { + var query = Query.Builder; + RebuildXmlStructuresProcessQuery(serializer, query, tr, groupSize); + } + else + { + foreach (var contentTypeId in contentTypeIds) + { + //copy local + var id = contentTypeId; + var query = Query.Builder.Where(x => x.ContentTypeId == id && x.Trashed == false); + RebuildXmlStructuresProcessQuery(serializer, query, tr, groupSize); + } + } + + tr.Complete(); + } + } + + private void RebuildXmlStructuresProcessQuery(Func serializer, IQuery query, Transaction tr, int pageSize) + { + var pageIndex = 0; + var total = long.MinValue; + var processed = 0; + do + { + var descendants = GetPagedResultsByQuery(query, pageIndex, pageSize, out total, "Path", Direction.Ascending, orderBySystemField: true); + + var xmlItems = (from descendant in descendants + let xml = serializer(descendant) + select new ContentXmlDto { NodeId = descendant.Id, Xml = xml.ToDataString() }).ToArray(); + + //bulk insert it into the database + Database.BulkInsertRecords(xmlItems, tr); + + processed += xmlItems.Length; + + pageIndex++; + } while (processed < total); + } + + public void AddOrUpdateContentXml(IMedia content, Func xml) + { + _contentXmlRepository.AddOrUpdate(new ContentXmlEntity(content, xml)); + } + + public void AddOrUpdatePreviewXml(IMedia content, Func xml) + { + _contentPreviewRepository.AddOrUpdate(new ContentPreviewEntity(content, xml)); + } + + protected override void PerformDeleteVersion(int id, Guid versionId) + { + Database.Delete("WHERE nodeId = @Id AND versionId = @VersionId", new { Id = id, VersionId = versionId }); + Database.Delete("WHERE contentNodeId = @Id AND versionId = @VersionId", new { Id = id, VersionId = versionId }); + Database.Delete("WHERE ContentId = @Id AND VersionId = @VersionId", new { Id = id, VersionId = versionId }); + } + + #endregion + + #region Unit of Work Implementation + + protected override void PersistNewItem(IMedia entity) + { + ((Models.Media)entity).AddingEntity(); + + //Ensure unique name on the same level + entity.Name = EnsureUniqueNodeName(entity.ParentId, entity.Name); + + //Ensure that strings don't contain characters that are invalid in XML + entity.SanitizeEntityPropertiesForXmlStorage(); + + var factory = new MediaFactory(NodeObjectTypeId, entity.Id); + var dto = factory.BuildDto(entity); + + //NOTE Should the logic below have some kind of fallback for empty parent ids ? + //Logic for setting Path, Level and SortOrder + var parent = Database.First("WHERE id = @ParentId", new { ParentId = entity.ParentId }); + var level = parent.Level + 1; + var maxSortOrder = Database.ExecuteScalar( + "SELECT coalesce(max(sortOrder),-1) FROM umbracoNode WHERE parentid = @ParentId AND nodeObjectType = @NodeObjectType", + new { /*ParentId =*/ entity.ParentId, NodeObjectType = NodeObjectTypeId }); + var sortOrder = maxSortOrder + 1; + + //Create the (base) node data - umbracoNode + var nodeDto = dto.ContentDto.NodeDto; + nodeDto.Path = parent.Path; + nodeDto.Level = short.Parse(level.ToString(CultureInfo.InvariantCulture)); + nodeDto.SortOrder = sortOrder; + var o = Database.IsNew(nodeDto) ? Convert.ToInt32(Database.Insert(nodeDto)) : Database.Update(nodeDto); + + //Update with new correct path + nodeDto.Path = string.Concat(parent.Path, ",", nodeDto.NodeId); + Database.Update(nodeDto); + + //Update entity with correct values + entity.Id = nodeDto.NodeId; //Set Id on entity to ensure an Id is set + entity.Path = nodeDto.Path; + entity.SortOrder = sortOrder; + entity.Level = level; + + //Create the Content specific data - cmsContent + var contentDto = dto.ContentDto; + contentDto.NodeId = nodeDto.NodeId; + Database.Insert(contentDto); + + //Create the first version - cmsContentVersion + //Assumes a new Version guid and Version date (modified date) has been set + dto.NodeId = nodeDto.NodeId; + Database.Insert(dto); + + //Create the PropertyData for this version - cmsPropertyData + var propertyFactory = new PropertyFactory(entity.ContentType.CompositionPropertyTypes.ToArray(), entity.Version, entity.Id); + var propertyDataDtos = propertyFactory.BuildDto(entity.Properties); + var keyDictionary = new Dictionary(); + + //Add Properties + foreach (var propertyDataDto in propertyDataDtos) + { + var primaryKey = Convert.ToInt32(Database.Insert(propertyDataDto)); + keyDictionary.Add(propertyDataDto.PropertyTypeId, primaryKey); + } + + //Update Properties with its newly set Id + foreach (var property in entity.Properties) + { + property.Id = keyDictionary[property.PropertyTypeId]; + } + + UpdatePropertyTags(entity, _tagRepository); + + entity.ResetDirtyProperties(); + } + + protected override void PersistUpdatedItem(IMedia entity) + { + //Updates Modified date + ((Models.Media)entity).UpdatingEntity(); + + //Ensure unique name on the same level + entity.Name = EnsureUniqueNodeName(entity.ParentId, entity.Name, entity.Id); + + //Ensure that strings don't contain characters that are invalid in XML + entity.SanitizeEntityPropertiesForXmlStorage(); + + //Look up parent to get and set the correct Path and update SortOrder if ParentId has changed + if (entity.IsPropertyDirty("ParentId")) + { + var parent = Database.First("WHERE id = @ParentId", new { ParentId = entity.ParentId }); + entity.Path = string.Concat(parent.Path, ",", entity.Id); + entity.Level = parent.Level + 1; + var maxSortOrder = + Database.ExecuteScalar( + "SELECT coalesce(max(sortOrder),0) FROM umbracoNode WHERE parentid = @ParentId AND nodeObjectType = @NodeObjectType", + new { ParentId = entity.ParentId, NodeObjectType = NodeObjectTypeId }); + entity.SortOrder = maxSortOrder + 1; + } + + var factory = new MediaFactory(NodeObjectTypeId, entity.Id); + //Look up Content entry to get Primary for updating the DTO + var contentDto = Database.SingleOrDefault("WHERE nodeId = @Id", new { Id = entity.Id }); + factory.SetPrimaryKey(contentDto.PrimaryKey); + var dto = factory.BuildDto(entity); + + //Updates the (base) node data - umbracoNode + var nodeDto = dto.ContentDto.NodeDto; + var o = Database.Update(nodeDto); + + //Only update this DTO if the contentType has actually changed + if (contentDto.ContentTypeId != entity.ContentTypeId) + { + //Create the Content specific data - cmsContent + var newContentDto = dto.ContentDto; + Database.Update(newContentDto); + } + + //In order to update the ContentVersion we need to retrieve its primary key id + var contentVerDto = Database.SingleOrDefault("WHERE VersionId = @Version", new { Version = entity.Version }); + dto.Id = contentVerDto.Id; + //Updates the current version - cmsContentVersion + //Assumes a Version guid exists and Version date (modified date) has been set/updated + Database.Update(dto); + + //Create the PropertyData for this version - cmsPropertyData + var propertyFactory = new PropertyFactory(entity.ContentType.CompositionPropertyTypes.ToArray(), entity.Version, entity.Id); + var propertyDataDtos = propertyFactory.BuildDto(entity.Properties); + var keyDictionary = new Dictionary(); + + //Add Properties + foreach (var propertyDataDto in propertyDataDtos) + { + if (propertyDataDto.Id > 0) + { + Database.Update(propertyDataDto); + } + else + { + int primaryKey = Convert.ToInt32(Database.Insert(propertyDataDto)); + keyDictionary.Add(propertyDataDto.PropertyTypeId, primaryKey); + } + } + + //Update Properties with its newly set Id + if (keyDictionary.Any()) + { + foreach (var property in entity.Properties) + { + property.Id = keyDictionary[property.PropertyTypeId]; + } + } + + UpdatePropertyTags(entity, _tagRepository); + + entity.ResetDirtyProperties(); + } + + #endregion + + #region IRecycleBinRepository members + + protected override int RecycleBinId + { + get { return Constants.System.RecycleBinMedia; } + } + + #endregion + + /// + /// Gets paged media results + /// + /// Query to excute + /// Page number + /// Page size + /// Total records query would return without paging + /// Field to order by + /// Direction to order by + /// Flag to indicate when ordering by system field + /// Search text filter + /// An Enumerable list of objects + public IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, + string orderBy, Direction orderDirection, bool orderBySystemField, string filter = "") + { + var args = new List(); + var sbWhere = new StringBuilder(); + Func> filterCallback = null; + if (filter.IsNullOrWhiteSpace() == false) + { + sbWhere.Append("AND (umbracoNode." + SqlSyntax.GetQuotedColumnName("text") + " LIKE @" + args.Count + ")"); + args.Add("%" + filter + "%"); + filterCallback = () => new Tuple(sbWhere.ToString().Trim(), args.ToArray()); + } + + return GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, + new Tuple("cmsContentVersion", "contentId"), + ProcessQuery, orderBy, orderDirection, orderBySystemField, + filterCallback); + + } + + private IEnumerable ProcessQuery(Sql sql) + { + //NOTE: This doesn't allow properties to be part of the query + var dtos = Database.Fetch(sql); + + var ids = dtos.Select(x => x.ContentDto.ContentTypeId).ToArray(); + + //content types + var contentTypes = ids.Length == 0 ? Enumerable.Empty() : _mediaTypeRepository.GetAll(ids).ToArray(); + + var dtosWithContentTypes = dtos + //This select into and null check are required because we don't have a foreign damn key on the contentType column + // http://issues.umbraco.org/issue/U4-5503 + .Select(x => new { dto = x, contentType = contentTypes.FirstOrDefault(ct => ct.Id == x.ContentDto.ContentTypeId) }) + .Where(x => x.contentType != null) + .ToArray(); + + //Go get the property data for each document + var docDefs = dtosWithContentTypes.Select(d => new DocumentDefinition( + d.dto.NodeId, + d.dto.VersionId, + d.dto.VersionDate, + d.dto.ContentDto.NodeDto.CreateDate, + d.contentType)) + .ToArray(); + + var propertyData = GetPropertyCollection(sql, docDefs); + + return dtosWithContentTypes.Select(d => CreateMediaFromDto( + d.dto, + contentTypes.First(ct => ct.Id == d.dto.ContentDto.ContentTypeId), + propertyData[d.dto.NodeId])); + } + + /// + /// Private method to create a media object from a ContentDto + /// + /// + /// + /// + /// + private IMedia CreateMediaFromDto(ContentVersionDto dto, + IMediaType contentType, + PropertyCollection propCollection) + { + var factory = new MediaFactory(contentType, NodeObjectTypeId, dto.NodeId); + var media = factory.BuildEntity(dto); + + media.Properties = propCollection; + + //on initial construction we don't want to have dirty properties tracked + // http://issues.umbraco.org/issue/U4-1946 + ((Entity)media).ResetDirtyProperties(false); + return media; + } + + /// + /// Private method to create a media object from a ContentDto + /// + /// + /// + /// + /// + private IMedia CreateMediaFromDto(ContentVersionDto dto, Guid versionId, Sql docSql) + { + var contentType = _mediaTypeRepository.Get(dto.ContentDto.ContentTypeId); + + var factory = new MediaFactory(contentType, NodeObjectTypeId, dto.NodeId); + var media = factory.BuildEntity(dto); + + var docDef = new DocumentDefinition(dto.NodeId, versionId, media.UpdateDate, media.CreateDate, contentType); + + var properties = GetPropertyCollection(docSql, new[] { docDef }); + + media.Properties = properties[dto.NodeId]; + + //on initial construction we don't want to have dirty properties tracked + // http://issues.umbraco.org/issue/U4-1946 + ((Entity)media).ResetDirtyProperties(false); + return media; + } + + private string EnsureUniqueNodeName(int parentId, string nodeName, int id = 0) + { + if (EnsureUniqueNaming == false) + return nodeName; + + var sql = new Sql(); + sql.Select("*") + .From() + .Where(x => x.NodeObjectType == NodeObjectTypeId && x.ParentId == parentId && 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; + } + + /// + /// Dispose disposable properties + /// + /// + /// Ensure the unit of work is disposed + /// + protected override void DisposeResources() + { + _mediaTypeRepository.Dispose(); + _tagRepository.Dispose(); + _contentXmlRepository.Dispose(); + _contentPreviewRepository.Dispose(); + } + } } diff --git a/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs index ac5529b523..c103d24b2d 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs @@ -23,747 +23,748 @@ using Umbraco.Core.Dynamics; namespace Umbraco.Core.Persistence.Repositories { - /// - /// Represents a repository for doing CRUD operations for - /// - internal class MemberRepository : VersionableRepositoryBase, IMemberRepository - { - private readonly IMemberTypeRepository _memberTypeRepository; - private readonly ITagRepository _tagRepository; - private readonly IMemberGroupRepository _memberGroupRepository; - private readonly ContentXmlRepository _contentXmlRepository; - private readonly ContentPreviewRepository _contentPreviewRepository; - - public MemberRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax, IMemberTypeRepository memberTypeRepository, IMemberGroupRepository memberGroupRepository, ITagRepository tagRepository, IContentSection contentSection) - : base(work, cache, logger, sqlSyntax, contentSection) - { - if (memberTypeRepository == null) throw new ArgumentNullException("memberTypeRepository"); - if (tagRepository == null) throw new ArgumentNullException("tagRepository"); - _memberTypeRepository = memberTypeRepository; - _tagRepository = tagRepository; - _memberGroupRepository = memberGroupRepository; - _contentXmlRepository = new ContentXmlRepository(work, CacheHelper.CreateDisabledCacheHelper(), logger, sqlSyntax); - _contentPreviewRepository = new ContentPreviewRepository(work, CacheHelper.CreateDisabledCacheHelper(), logger, sqlSyntax); - } - - #region Overrides of RepositoryBase - - protected override IMember PerformGet(int id) - { - var sql = GetBaseQuery(false); - sql.Where(GetBaseWhereClause(), new { Id = id }); - sql.OrderByDescending(x => x.VersionDate); - - var dto = Database.Fetch(sql).FirstOrDefault(); - - if (dto == null) - return null; - - var content = CreateMemberFromDto(dto, dto.ContentVersionDto.VersionId, sql); - - return content; - - } - - protected override IEnumerable PerformGetAll(params int[] ids) - { - var sql = GetBaseQuery(false); - if (ids.Any()) - { - sql.Where("umbracoNode.id in (@ids)", new { ids = ids }); - } - - return ProcessQuery(sql); - - } - - protected override IEnumerable PerformGetByQuery(IQuery query) - { - var baseQuery = GetBaseQuery(false); - - //check if the query is based on properties or not - - var wheres = query.GetWhereClauses(); - //this is a pretty rudimentary check but wil work, we just need to know if this query requires property - // level queries - if (wheres.Any(x => x.Item1.Contains("cmsPropertyType"))) - { - var sqlWithProps = GetNodeIdQueryWithPropertyData(); - var translator = new SqlTranslator(sqlWithProps, query); - var sql = translator.Translate(); - - baseQuery.Append(new Sql("WHERE umbracoNode.id IN (" + sql.SQL + ")", sql.Arguments)) - .OrderBy(x => x.SortOrder); - - return ProcessQuery(baseQuery); - } - else - { - var translator = new SqlTranslator(baseQuery, query); - var sql = translator.Translate() - .OrderBy(x => x.SortOrder); - - return ProcessQuery(sql); - } - - } - - #endregion - - #region Overrides of PetaPocoRepositoryBase - - protected override Sql GetBaseQuery(bool isCount) - { - var sql = new Sql(); - sql.Select(isCount ? "COUNT(*)" : "*") - .From() - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - //We're joining the type so we can do a query against the member type - not sure if this adds much overhead or not? - // the execution plan says it doesn't so we'll go with that and in that case, it might be worth joining the content - // types by default on the document and media repo's so we can query by content type there too. - .InnerJoin().On(left => left.NodeId, right => right.ContentTypeId) - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .Where(x => x.NodeObjectType == NodeObjectTypeId); - return sql; - - } - - protected override string GetBaseWhereClause() - { - return "umbracoNode.id = @Id"; - } - - protected Sql GetNodeIdQueryWithPropertyData() - { - var sql = new Sql(); - sql.Select("DISTINCT(umbracoNode.id)") - .From() - .InnerJoin().On(left => left.NodeId, right => right.NodeId) - .InnerJoin().On(left => left.NodeId, right => right.ContentTypeId) - .InnerJoin().On(left => left.NodeId, right => right.NodeId) - .InnerJoin().On(left => left.NodeId, right => right.NodeId) - .LeftJoin().On(left => left.ContentTypeId, right => right.ContentTypeId) - .LeftJoin().On(left => left.DataTypeId, right => right.DataTypeId) - .LeftJoin().On(left => left.PropertyTypeId, right => right.Id) - .Append("AND cmsPropertyData.versionId = cmsContentVersion.VersionId") - .Where(x => x.NodeObjectType == NodeObjectTypeId); - return sql; - } - - protected override IEnumerable GetDeleteClauses() - { - var list = new List - { - "DELETE FROM cmsTask WHERE nodeId = @Id", - "DELETE FROM umbracoUser2NodeNotify WHERE nodeId = @Id", - "DELETE FROM umbracoUser2NodePermission WHERE nodeId = @Id", - "DELETE FROM umbracoRelation WHERE parentId = @Id", - "DELETE FROM umbracoRelation WHERE childId = @Id", - "DELETE FROM cmsTagRelationship WHERE nodeId = @Id", - "DELETE FROM cmsPropertyData WHERE contentNodeId = @Id", - "DELETE FROM cmsMember2MemberGroup WHERE Member = @Id", - "DELETE FROM cmsMember WHERE nodeId = @Id", - "DELETE FROM cmsContentVersion WHERE ContentId = @Id", - "DELETE FROM cmsContentXml WHERE nodeId = @Id", - "DELETE FROM cmsContent WHERE nodeId = @Id", - "DELETE FROM umbracoNode WHERE id = @Id" - }; - return list; - } - - protected override Guid NodeObjectTypeId - { - get { return new Guid(Constants.ObjectTypes.Member); } - } - - #endregion - - #region Unit of Work Implementation - - protected override void PersistNewItem(IMember entity) - { - ((Member)entity).AddingEntity(); - - //Ensure that strings don't contain characters that are invalid in XML - entity.SanitizeEntityPropertiesForXmlStorage(); - - var factory = new MemberFactory(NodeObjectTypeId, entity.Id); - var dto = factory.BuildDto(entity); - - //NOTE Should the logic below have some kind of fallback for empty parent ids ? - //Logic for setting Path, Level and SortOrder - var parent = Database.First("WHERE id = @ParentId", new { ParentId = ((IUmbracoEntity)entity).ParentId }); - int level = parent.Level + 1; - int sortOrder = - Database.ExecuteScalar("SELECT COUNT(*) FROM umbracoNode WHERE parentID = @ParentId AND nodeObjectType = @NodeObjectType", - new { ParentId = ((IUmbracoEntity)entity).ParentId, NodeObjectType = NodeObjectTypeId }); - - //Create the (base) node data - umbracoNode - var nodeDto = dto.ContentVersionDto.ContentDto.NodeDto; - nodeDto.Path = parent.Path; - nodeDto.Level = short.Parse(level.ToString(CultureInfo.InvariantCulture)); - nodeDto.SortOrder = sortOrder; - var o = Database.IsNew(nodeDto) ? Convert.ToInt32(Database.Insert(nodeDto)) : Database.Update(nodeDto); - - //Update with new correct path - nodeDto.Path = string.Concat(parent.Path, ",", nodeDto.NodeId); - Database.Update(nodeDto); - - //Update entity with correct values - entity.Id = nodeDto.NodeId; //Set Id on entity to ensure an Id is set - entity.Path = nodeDto.Path; - entity.SortOrder = sortOrder; - entity.Level = level; - - //Create the Content specific data - cmsContent - var contentDto = dto.ContentVersionDto.ContentDto; - contentDto.NodeId = nodeDto.NodeId; - Database.Insert(contentDto); - - //Create the first version - cmsContentVersion - //Assumes a new Version guid and Version date (modified date) has been set - dto.ContentVersionDto.NodeId = nodeDto.NodeId; - Database.Insert(dto.ContentVersionDto); - - //Create the first entry in cmsMember - dto.NodeId = nodeDto.NodeId; - Database.Insert(dto); - - //Create the PropertyData for this version - cmsPropertyData - var propertyFactory = new PropertyFactory(entity.ContentType.CompositionPropertyTypes.ToArray(), entity.Version, entity.Id); - //Add Properties - // - don't try to save the property if it doesn't exist (or doesn't have an ID) on the content type - // - this can occur if the member type doesn't contain the built-in properties that the - // - member object contains. - var propsToPersist = entity.Properties.Where(x => x.PropertyType.HasIdentity).ToArray(); - var propertyDataDtos = propertyFactory.BuildDto(propsToPersist); - var keyDictionary = new Dictionary(); - - //Add Properties - foreach (var propertyDataDto in propertyDataDtos) - { - var primaryKey = Convert.ToInt32(Database.Insert(propertyDataDto)); - keyDictionary.Add(propertyDataDto.PropertyTypeId, primaryKey); - } - - //Update Properties with its newly set Id - foreach (var property in propsToPersist) - { - property.Id = keyDictionary[property.PropertyTypeId]; - } - - UpdatePropertyTags(entity, _tagRepository); - - ((Member)entity).ResetDirtyProperties(); - } - - protected override void PersistUpdatedItem(IMember entity) - { - //Updates Modified date - ((Member)entity).UpdatingEntity(); - - //Ensure that strings don't contain characters that are invalid in XML - entity.SanitizeEntityPropertiesForXmlStorage(); - - var dirtyEntity = (ICanBeDirty) entity; - - //Look up parent to get and set the correct Path and update SortOrder if ParentId has changed - if (dirtyEntity.IsPropertyDirty("ParentId")) - { - var parent = Database.First("WHERE id = @ParentId", new { ParentId = ((IUmbracoEntity)entity).ParentId }); - ((IUmbracoEntity)entity).Path = string.Concat(parent.Path, ",", entity.Id); - ((IUmbracoEntity)entity).Level = parent.Level + 1; - var maxSortOrder = - Database.ExecuteScalar( - "SELECT coalesce(max(sortOrder),0) FROM umbracoNode WHERE parentid = @ParentId AND nodeObjectType = @NodeObjectType", - new { ParentId = ((IUmbracoEntity)entity).ParentId, NodeObjectType = NodeObjectTypeId }); - ((IUmbracoEntity)entity).SortOrder = maxSortOrder + 1; - } - - var factory = new MemberFactory(NodeObjectTypeId, entity.Id); - //Look up Content entry to get Primary for updating the DTO - var contentDto = Database.SingleOrDefault("WHERE nodeId = @Id", new { Id = entity.Id }); - factory.SetPrimaryKey(contentDto.PrimaryKey); - var dto = factory.BuildDto(entity); - - //Updates the (base) node data - umbracoNode - var nodeDto = dto.ContentVersionDto.ContentDto.NodeDto; - var o = Database.Update(nodeDto); - - //Only update this DTO if the contentType has actually changed - if (contentDto.ContentTypeId != ((Member)entity).ContentTypeId) - { - //Create the Content specific data - cmsContent - var newContentDto = dto.ContentVersionDto.ContentDto; - Database.Update(newContentDto); - } - - //In order to update the ContentVersion we need to retrieve its primary key id - var contentVerDto = Database.SingleOrDefault("WHERE VersionId = @Version", new { Version = entity.Version }); - dto.ContentVersionDto.Id = contentVerDto.Id; - //Updates the current version - cmsContentVersion - //Assumes a Version guid exists and Version date (modified date) has been set/updated - Database.Update(dto.ContentVersionDto); - - //Updates the cmsMember entry if it has changed - - //NOTE: these cols are the REAL column names in the db - var changedCols = new List(); - - if (dirtyEntity.IsPropertyDirty("Email")) - { - changedCols.Add("Email"); - } - if (dirtyEntity.IsPropertyDirty("Username")) - { - changedCols.Add("LoginName"); - } - // DO NOT update the password if it has not changed or if it is null or empty - if (dirtyEntity.IsPropertyDirty("RawPasswordValue") && entity.RawPasswordValue.IsNullOrWhiteSpace() == false) - { - changedCols.Add("Password"); - } - //only update the changed cols - if (changedCols.Count > 0) - { - Database.Update(dto, changedCols); - } - - //TODO ContentType for the Member entity - - //Create the PropertyData for this version - cmsPropertyData - var propertyFactory = new PropertyFactory(entity.ContentType.CompositionPropertyTypes.ToArray(), entity.Version, entity.Id); - var keyDictionary = new Dictionary(); - - //Add Properties - // - don't try to save the property if it doesn't exist (or doesn't have an ID) on the content type - // - this can occur if the member type doesn't contain the built-in properties that the - // - member object contains. - var propsToPersist = entity.Properties.Where(x => x.PropertyType.HasIdentity).ToArray(); - - var propertyDataDtos = propertyFactory.BuildDto(propsToPersist); - - foreach (var propertyDataDto in propertyDataDtos) - { - if (propertyDataDto.Id > 0) - { - Database.Update(propertyDataDto); - } - else - { - int primaryKey = Convert.ToInt32(Database.Insert(propertyDataDto)); - keyDictionary.Add(propertyDataDto.PropertyTypeId, primaryKey); - } - } - - //Update Properties with its newly set Id - if (keyDictionary.Any()) - { - foreach (var property in ((Member)entity).Properties) - { - property.Id = keyDictionary[property.PropertyTypeId]; - } - } - - UpdatePropertyTags(entity, _tagRepository); - - dirtyEntity.ResetDirtyProperties(); - } - - #endregion - - #region Overrides of VersionableRepositoryBase - - public void RebuildXmlStructures(Func serializer, int groupSize = 5000, IEnumerable contentTypeIds = null) - { - - //Ok, now we need to remove the data and re-insert it, we'll do this all in one transaction too. - using (var tr = Database.GetTransaction()) - { - //Remove all the data first, if anything fails after this it's no problem the transaction will be reverted - if (contentTypeIds == null) - { - var memberObjectType = Guid.Parse(Constants.ObjectTypes.Member); - var subQuery = new Sql() - .Select("DISTINCT cmsContentXml.nodeId") - .From() - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .Where(dto => dto.NodeObjectType == memberObjectType); - - var deleteSql = SqlSyntax.GetDeleteSubquery("cmsContentXml", "nodeId", subQuery); - Database.Execute(deleteSql); - } - else - { - foreach (var id in contentTypeIds) - { - var id1 = id; - var memberObjectType = Guid.Parse(Constants.ObjectTypes.Member); - var subQuery = new Sql() - .Select("DISTINCT cmsContentXml.nodeId") - .From() - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .Where(dto => dto.NodeObjectType == memberObjectType) - .Where(dto => dto.ContentTypeId == id1); - - var deleteSql = SqlSyntax.GetDeleteSubquery("cmsContentXml", "nodeId", subQuery); - Database.Execute(deleteSql); - } - } - - //now insert the data, again if something fails here, the whole transaction is reversed - if (contentTypeIds == null) - { - var query = Query.Builder; - RebuildXmlStructuresProcessQuery(serializer, query, tr, groupSize); - } - else - { - foreach (var contentTypeId in contentTypeIds) - { - //copy local - var id = contentTypeId; - var query = Query.Builder.Where(x => x.ContentTypeId == id && x.Trashed == false); - RebuildXmlStructuresProcessQuery(serializer, query, tr, groupSize); - } - } - - tr.Complete(); - } - } - - private void RebuildXmlStructuresProcessQuery(Func serializer, IQuery query, Transaction tr, int pageSize) - { - var pageIndex = 0; - var total = long.MinValue; - var processed = 0; - do - { - var descendants = GetPagedResultsByQuery(query, pageIndex, pageSize, out total, "Path", Direction.Ascending); - - var xmlItems = (from descendant in descendants - let xml = serializer(descendant) - select new ContentXmlDto { NodeId = descendant.Id, Xml = xml.ToDataString() }).ToArray(); - - //bulk insert it into the database - Database.BulkInsertRecords(xmlItems, tr); - - processed += xmlItems.Length; - - pageIndex++; - } while (processed < total); - } - - public override IMember GetByVersion(Guid versionId) - { - var sql = GetBaseQuery(false); - sql.Where("cmsContentVersion.VersionId = @VersionId", new { VersionId = versionId }); - sql.OrderByDescending(x => x.VersionDate); - - var dto = Database.Fetch(sql).FirstOrDefault(); - - if (dto == null) - return null; - - var memberType = _memberTypeRepository.Get(dto.ContentVersionDto.ContentDto.ContentTypeId); - - var factory = new MemberFactory(memberType, NodeObjectTypeId, dto.NodeId); - var media = factory.BuildEntity(dto); - - var properties = GetPropertyCollection(sql, new[] { new DocumentDefinition(dto.NodeId, dto.ContentVersionDto.VersionId, media.UpdateDate, media.CreateDate, memberType) }); - - media.Properties = properties[dto.NodeId]; - - //on initial construction we don't want to have dirty properties tracked - // http://issues.umbraco.org/issue/U4-1946 - ((Entity)media).ResetDirtyProperties(false); - return media; - - } - - protected override void PerformDeleteVersion(int id, Guid versionId) - { - Database.Delete("WHERE nodeId = @Id AND versionId = @VersionId", new { Id = id, VersionId = versionId }); - Database.Delete("WHERE contentNodeId = @Id AND versionId = @VersionId", new { Id = id, VersionId = versionId }); - Database.Delete("WHERE ContentId = @Id AND VersionId = @VersionId", new { Id = id, VersionId = versionId }); - } - - #endregion - - public IEnumerable FindMembersInRole(string roleName, string usernameToMatch, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith) - { - //get the group id - var grpQry = new Query().Where(group => group.Name.Equals(roleName)); - var memberGroup = _memberGroupRepository.GetByQuery(grpQry).FirstOrDefault(); - if (memberGroup == null) return Enumerable.Empty(); - - // get the members by username - var query = new Query(); - switch (matchType) - { - case StringPropertyMatchType.Exact: - query.Where(member => member.Username.Equals(usernameToMatch)); - break; - case StringPropertyMatchType.Contains: - query.Where(member => member.Username.Contains(usernameToMatch)); - break; - case StringPropertyMatchType.StartsWith: - query.Where(member => member.Username.StartsWith(usernameToMatch)); - break; - case StringPropertyMatchType.EndsWith: - query.Where(member => member.Username.EndsWith(usernameToMatch)); - break; - case StringPropertyMatchType.Wildcard: - query.Where(member => member.Username.SqlWildcard(usernameToMatch, TextColumnType.NVarchar)); - break; - default: - throw new ArgumentOutOfRangeException("matchType"); - } - var matchedMembers = GetByQuery(query).ToArray(); - - var membersInGroup = new List(); - //then we need to filter the matched members that are in the role - //since the max sql params are 2100 on sql server, we'll reduce that to be safe for potentially other servers and run the queries in batches - var inGroups = matchedMembers.InGroupsOf(1000); - foreach (var batch in inGroups) - { - var memberIdBatch = batch.Select(x => x.Id); - var sql = new Sql().Select("*").From() - .Where(dto => dto.MemberGroup == memberGroup.Id) - .Where("Member IN (@memberIds)", new { memberIds = memberIdBatch }); - var memberIdsInGroup = Database.Fetch(sql) - .Select(x => x.Member).ToArray(); - - membersInGroup.AddRange(matchedMembers.Where(x => memberIdsInGroup.Contains(x.Id))); - } - - return membersInGroup; - - } - - /// - /// Get all members in a specific group - /// - /// - /// - public IEnumerable GetByMemberGroup(string groupName) - { - var grpQry = new Query().Where(group => group.Name.Equals(groupName)); - var memberGroup = _memberGroupRepository.GetByQuery(grpQry).FirstOrDefault(); - if (memberGroup == null) return Enumerable.Empty(); - - var subQuery = new Sql().Select("Member").From().Where(dto => dto.MemberGroup == memberGroup.Id); - - var sql = GetBaseQuery(false) - //TODO: An inner join would be better, though I've read that the query optimizer will always turn a - // subquery with an IN clause into an inner join anyways. - .Append(new Sql("WHERE umbracoNode.id IN (" + subQuery.SQL + ")", subQuery.Arguments)) - .OrderByDescending(x => x.VersionDate) - .OrderBy(x => x.SortOrder); - - return ProcessQuery(sql); - - } - - public bool Exists(string username) - { - var sql = new Sql(); - - sql.Select("COUNT(*)") - .From() - .Where(x => x.LoginName == username); - - return Database.ExecuteScalar(sql) > 0; - } - - public int GetCountByQuery(IQuery query) - { - var sqlWithProps = GetNodeIdQueryWithPropertyData(); - var translator = new SqlTranslator(sqlWithProps, query); - var sql = translator.Translate(); - - //get the COUNT base query - var fullSql = GetBaseQuery(true) - .Append(new Sql("WHERE umbracoNode.id IN (" + sql.SQL + ")", sql.Arguments)); - - return Database.ExecuteScalar(fullSql); - } - - /// - /// Gets paged member results - /// - /// - /// The where clause, if this is null all records are queried - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// The query supplied will ONLY work with data specifically on the cmsMember table because we are using PetaPoco paging (SQL paging) - /// - public IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, - string orderBy, Direction orderDirection, string filter = "") - { - var args = new List(); - var sbWhere = new StringBuilder(); - Func> filterCallback = null; - if (filter.IsNullOrWhiteSpace() == false) - { - sbWhere.Append("AND ((umbracoNode. " + SqlSyntax.GetQuotedColumnName("text") + " LIKE @" + args.Count + ") " + - "OR (cmsMember.LoginName LIKE @0" + args.Count + "))"); - args.Add("%" + filter + "%"); - filterCallback = () => new Tuple(sbWhere.ToString().Trim(), args.ToArray()); - } - - return GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, - new Tuple("cmsMember", "nodeId"), - ProcessQuery, orderBy, orderDirection, - filterCallback); - } - - public void AddOrUpdateContentXml(IMember content, Func xml) - { - _contentXmlRepository.AddOrUpdate(new ContentXmlEntity(content, xml)); - } - - public void AddOrUpdatePreviewXml(IMember content, Func xml) - { - _contentPreviewRepository.AddOrUpdate(new ContentPreviewEntity(content, xml)); - } - - protected override string GetDatabaseFieldNameForOrderBy(string orderBy) - { - //Some custom ones - switch (orderBy.ToUpperInvariant()) - { - case "EMAIL": - return "cmsMember.Email"; - case "LOGINNAME": - return "cmsMember.LoginName"; - } - - return base.GetDatabaseFieldNameForOrderBy(orderBy); - } - - protected override string GetEntityPropertyNameForOrderBy(string orderBy) - { - //Some custom ones - switch (orderBy.ToUpperInvariant()) - { - case "LOGINNAME": - return "Username"; - } - - return base.GetEntityPropertyNameForOrderBy(orderBy); - } - - private IEnumerable ProcessQuery(Sql sql) - { - //NOTE: This doesn't allow properties to be part of the query - var dtos = Database.Fetch(sql); - - var ids = dtos.Select(x => x.ContentVersionDto.ContentDto.ContentTypeId).ToArray(); - - //content types - var contentTypes = ids.Length == 0 ? Enumerable.Empty() : _memberTypeRepository.GetAll(ids).ToArray(); - - var dtosWithContentTypes = dtos - //This select into and null check are required because we don't have a foreign damn key on the contentType column - // http://issues.umbraco.org/issue/U4-5503 - .Select(x => new {dto = x, contentType = contentTypes.FirstOrDefault(ct => ct.Id == x.ContentVersionDto.ContentDto.ContentTypeId)}) - .Where(x => x.contentType != null) - .ToArray(); - - //Go get the property data for each document - IEnumerable docDefs = dtosWithContentTypes.Select(d => new DocumentDefinition( - d.dto.NodeId, - d.dto.ContentVersionDto.VersionId, - d.dto.ContentVersionDto.VersionDate, - d.dto.ContentVersionDto.ContentDto.NodeDto.CreateDate, - d.contentType)); - - var propertyData = GetPropertyCollection(sql, docDefs); - - return dtosWithContentTypes.Select(d => CreateMemberFromDto( - d.dto, - contentTypes.First(ct => ct.Id == d.dto.ContentVersionDto.ContentDto.ContentTypeId), - propertyData[d.dto.NodeId])); - } - - /// - /// Private method to create a member object from a MemberDto - /// - /// - /// - /// - /// - private IMember CreateMemberFromDto(MemberDto dto, - IMemberType contentType, - PropertyCollection propCollection) - { - var factory = new MemberFactory(contentType, NodeObjectTypeId, dto.ContentVersionDto.NodeId); - var member = factory.BuildEntity(dto); - - member.Properties = propCollection; - - //on initial construction we don't want to have dirty properties tracked - // http://issues.umbraco.org/issue/U4-1946 - ((Entity)member).ResetDirtyProperties(false); - return member; - } - - /// - /// Private method to create a member object from a MemberDto - /// - /// - /// - /// - /// - private IMember CreateMemberFromDto(MemberDto dto, Guid versionId, Sql docSql) - { - var memberType = _memberTypeRepository.Get(dto.ContentVersionDto.ContentDto.ContentTypeId); - - var factory = new MemberFactory(memberType, NodeObjectTypeId, dto.ContentVersionDto.NodeId); - var member = factory.BuildEntity(dto); - - var docDef = new DocumentDefinition(dto.ContentVersionDto.NodeId, versionId, member.UpdateDate, member.CreateDate, memberType); - - var properties = GetPropertyCollection(docSql, new[] { docDef }); - - member.Properties = properties[dto.ContentVersionDto.NodeId]; - - //on initial construction we don't want to have dirty properties tracked - // http://issues.umbraco.org/issue/U4-1946 - ((Entity)member).ResetDirtyProperties(false); - return member; - } - - /// - /// Dispose disposable properties - /// - /// - /// Ensure the unit of work is disposed - /// - protected override void DisposeResources() - { - _memberTypeRepository.Dispose(); - _tagRepository.Dispose(); - _memberGroupRepository.Dispose(); - _contentXmlRepository.Dispose(); - _contentPreviewRepository.Dispose(); - } - - } + /// + /// Represents a repository for doing CRUD operations for + /// + internal class MemberRepository : VersionableRepositoryBase, IMemberRepository + { + private readonly IMemberTypeRepository _memberTypeRepository; + private readonly ITagRepository _tagRepository; + private readonly IMemberGroupRepository _memberGroupRepository; + private readonly ContentXmlRepository _contentXmlRepository; + private readonly ContentPreviewRepository _contentPreviewRepository; + + public MemberRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax, IMemberTypeRepository memberTypeRepository, IMemberGroupRepository memberGroupRepository, ITagRepository tagRepository, IContentSection contentSection) + : base(work, cache, logger, sqlSyntax, contentSection) + { + if (memberTypeRepository == null) throw new ArgumentNullException("memberTypeRepository"); + if (tagRepository == null) throw new ArgumentNullException("tagRepository"); + _memberTypeRepository = memberTypeRepository; + _tagRepository = tagRepository; + _memberGroupRepository = memberGroupRepository; + _contentXmlRepository = new ContentXmlRepository(work, CacheHelper.CreateDisabledCacheHelper(), logger, sqlSyntax); + _contentPreviewRepository = new ContentPreviewRepository(work, CacheHelper.CreateDisabledCacheHelper(), logger, sqlSyntax); + } + + #region Overrides of RepositoryBase + + protected override IMember PerformGet(int id) + { + var sql = GetBaseQuery(false); + sql.Where(GetBaseWhereClause(), new { Id = id }); + sql.OrderByDescending(x => x.VersionDate); + + var dto = Database.Fetch(sql).FirstOrDefault(); + + if (dto == null) + return null; + + var content = CreateMemberFromDto(dto, dto.ContentVersionDto.VersionId, sql); + + return content; + + } + + protected override IEnumerable PerformGetAll(params int[] ids) + { + var sql = GetBaseQuery(false); + if (ids.Any()) + { + sql.Where("umbracoNode.id in (@ids)", new { ids = ids }); + } + + return ProcessQuery(sql); + + } + + protected override IEnumerable PerformGetByQuery(IQuery query) + { + var baseQuery = GetBaseQuery(false); + + //check if the query is based on properties or not + + var wheres = query.GetWhereClauses(); + //this is a pretty rudimentary check but wil work, we just need to know if this query requires property + // level queries + if (wheres.Any(x => x.Item1.Contains("cmsPropertyType"))) + { + var sqlWithProps = GetNodeIdQueryWithPropertyData(); + var translator = new SqlTranslator(sqlWithProps, query); + var sql = translator.Translate(); + + baseQuery.Append(new Sql("WHERE umbracoNode.id IN (" + sql.SQL + ")", sql.Arguments)) + .OrderBy(x => x.SortOrder); + + return ProcessQuery(baseQuery); + } + else + { + var translator = new SqlTranslator(baseQuery, query); + var sql = translator.Translate() + .OrderBy(x => x.SortOrder); + + return ProcessQuery(sql); + } + + } + + #endregion + + #region Overrides of PetaPocoRepositoryBase + + protected override Sql GetBaseQuery(bool isCount) + { + var sql = new Sql(); + sql.Select(isCount ? "COUNT(*)" : "*") + .From() + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + //We're joining the type so we can do a query against the member type - not sure if this adds much overhead or not? + // the execution plan says it doesn't so we'll go with that and in that case, it might be worth joining the content + // types by default on the document and media repo's so we can query by content type there too. + .InnerJoin().On(left => left.NodeId, right => right.ContentTypeId) + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .Where(x => x.NodeObjectType == NodeObjectTypeId); + return sql; + + } + + protected override string GetBaseWhereClause() + { + return "umbracoNode.id = @Id"; + } + + protected Sql GetNodeIdQueryWithPropertyData() + { + var sql = new Sql(); + sql.Select("DISTINCT(umbracoNode.id)") + .From() + .InnerJoin().On(left => left.NodeId, right => right.NodeId) + .InnerJoin().On(left => left.NodeId, right => right.ContentTypeId) + .InnerJoin().On(left => left.NodeId, right => right.NodeId) + .InnerJoin().On(left => left.NodeId, right => right.NodeId) + .LeftJoin().On(left => left.ContentTypeId, right => right.ContentTypeId) + .LeftJoin().On(left => left.DataTypeId, right => right.DataTypeId) + .LeftJoin().On(left => left.PropertyTypeId, right => right.Id) + .Append("AND cmsPropertyData.versionId = cmsContentVersion.VersionId") + .Where(x => x.NodeObjectType == NodeObjectTypeId); + return sql; + } + + protected override IEnumerable GetDeleteClauses() + { + var list = new List + { + "DELETE FROM cmsTask WHERE nodeId = @Id", + "DELETE FROM umbracoUser2NodeNotify WHERE nodeId = @Id", + "DELETE FROM umbracoUser2NodePermission WHERE nodeId = @Id", + "DELETE FROM umbracoRelation WHERE parentId = @Id", + "DELETE FROM umbracoRelation WHERE childId = @Id", + "DELETE FROM cmsTagRelationship WHERE nodeId = @Id", + "DELETE FROM cmsPropertyData WHERE contentNodeId = @Id", + "DELETE FROM cmsMember2MemberGroup WHERE Member = @Id", + "DELETE FROM cmsMember WHERE nodeId = @Id", + "DELETE FROM cmsContentVersion WHERE ContentId = @Id", + "DELETE FROM cmsContentXml WHERE nodeId = @Id", + "DELETE FROM cmsContent WHERE nodeId = @Id", + "DELETE FROM umbracoNode WHERE id = @Id" + }; + return list; + } + + protected override Guid NodeObjectTypeId + { + get { return new Guid(Constants.ObjectTypes.Member); } + } + + #endregion + + #region Unit of Work Implementation + + protected override void PersistNewItem(IMember entity) + { + ((Member)entity).AddingEntity(); + + //Ensure that strings don't contain characters that are invalid in XML + entity.SanitizeEntityPropertiesForXmlStorage(); + + var factory = new MemberFactory(NodeObjectTypeId, entity.Id); + var dto = factory.BuildDto(entity); + + //NOTE Should the logic below have some kind of fallback for empty parent ids ? + //Logic for setting Path, Level and SortOrder + var parent = Database.First("WHERE id = @ParentId", new { ParentId = ((IUmbracoEntity)entity).ParentId }); + int level = parent.Level + 1; + int sortOrder = + Database.ExecuteScalar("SELECT COUNT(*) FROM umbracoNode WHERE parentID = @ParentId AND nodeObjectType = @NodeObjectType", + new { ParentId = ((IUmbracoEntity)entity).ParentId, NodeObjectType = NodeObjectTypeId }); + + //Create the (base) node data - umbracoNode + var nodeDto = dto.ContentVersionDto.ContentDto.NodeDto; + nodeDto.Path = parent.Path; + nodeDto.Level = short.Parse(level.ToString(CultureInfo.InvariantCulture)); + nodeDto.SortOrder = sortOrder; + var o = Database.IsNew(nodeDto) ? Convert.ToInt32(Database.Insert(nodeDto)) : Database.Update(nodeDto); + + //Update with new correct path + nodeDto.Path = string.Concat(parent.Path, ",", nodeDto.NodeId); + Database.Update(nodeDto); + + //Update entity with correct values + entity.Id = nodeDto.NodeId; //Set Id on entity to ensure an Id is set + entity.Path = nodeDto.Path; + entity.SortOrder = sortOrder; + entity.Level = level; + + //Create the Content specific data - cmsContent + var contentDto = dto.ContentVersionDto.ContentDto; + contentDto.NodeId = nodeDto.NodeId; + Database.Insert(contentDto); + + //Create the first version - cmsContentVersion + //Assumes a new Version guid and Version date (modified date) has been set + dto.ContentVersionDto.NodeId = nodeDto.NodeId; + Database.Insert(dto.ContentVersionDto); + + //Create the first entry in cmsMember + dto.NodeId = nodeDto.NodeId; + Database.Insert(dto); + + //Create the PropertyData for this version - cmsPropertyData + var propertyFactory = new PropertyFactory(entity.ContentType.CompositionPropertyTypes.ToArray(), entity.Version, entity.Id); + //Add Properties + // - don't try to save the property if it doesn't exist (or doesn't have an ID) on the content type + // - this can occur if the member type doesn't contain the built-in properties that the + // - member object contains. + var propsToPersist = entity.Properties.Where(x => x.PropertyType.HasIdentity).ToArray(); + var propertyDataDtos = propertyFactory.BuildDto(propsToPersist); + var keyDictionary = new Dictionary(); + + //Add Properties + foreach (var propertyDataDto in propertyDataDtos) + { + var primaryKey = Convert.ToInt32(Database.Insert(propertyDataDto)); + keyDictionary.Add(propertyDataDto.PropertyTypeId, primaryKey); + } + + //Update Properties with its newly set Id + foreach (var property in propsToPersist) + { + property.Id = keyDictionary[property.PropertyTypeId]; + } + + UpdatePropertyTags(entity, _tagRepository); + + ((Member)entity).ResetDirtyProperties(); + } + + protected override void PersistUpdatedItem(IMember entity) + { + //Updates Modified date + ((Member)entity).UpdatingEntity(); + + //Ensure that strings don't contain characters that are invalid in XML + entity.SanitizeEntityPropertiesForXmlStorage(); + + var dirtyEntity = (ICanBeDirty)entity; + + //Look up parent to get and set the correct Path and update SortOrder if ParentId has changed + if (dirtyEntity.IsPropertyDirty("ParentId")) + { + var parent = Database.First("WHERE id = @ParentId", new { ParentId = ((IUmbracoEntity)entity).ParentId }); + ((IUmbracoEntity)entity).Path = string.Concat(parent.Path, ",", entity.Id); + ((IUmbracoEntity)entity).Level = parent.Level + 1; + var maxSortOrder = + Database.ExecuteScalar( + "SELECT coalesce(max(sortOrder),0) FROM umbracoNode WHERE parentid = @ParentId AND nodeObjectType = @NodeObjectType", + new { ParentId = ((IUmbracoEntity)entity).ParentId, NodeObjectType = NodeObjectTypeId }); + ((IUmbracoEntity)entity).SortOrder = maxSortOrder + 1; + } + + var factory = new MemberFactory(NodeObjectTypeId, entity.Id); + //Look up Content entry to get Primary for updating the DTO + var contentDto = Database.SingleOrDefault("WHERE nodeId = @Id", new { Id = entity.Id }); + factory.SetPrimaryKey(contentDto.PrimaryKey); + var dto = factory.BuildDto(entity); + + //Updates the (base) node data - umbracoNode + var nodeDto = dto.ContentVersionDto.ContentDto.NodeDto; + var o = Database.Update(nodeDto); + + //Only update this DTO if the contentType has actually changed + if (contentDto.ContentTypeId != ((Member)entity).ContentTypeId) + { + //Create the Content specific data - cmsContent + var newContentDto = dto.ContentVersionDto.ContentDto; + Database.Update(newContentDto); + } + + //In order to update the ContentVersion we need to retrieve its primary key id + var contentVerDto = Database.SingleOrDefault("WHERE VersionId = @Version", new { Version = entity.Version }); + dto.ContentVersionDto.Id = contentVerDto.Id; + //Updates the current version - cmsContentVersion + //Assumes a Version guid exists and Version date (modified date) has been set/updated + Database.Update(dto.ContentVersionDto); + + //Updates the cmsMember entry if it has changed + + //NOTE: these cols are the REAL column names in the db + var changedCols = new List(); + + if (dirtyEntity.IsPropertyDirty("Email")) + { + changedCols.Add("Email"); + } + if (dirtyEntity.IsPropertyDirty("Username")) + { + changedCols.Add("LoginName"); + } + // DO NOT update the password if it has not changed or if it is null or empty + if (dirtyEntity.IsPropertyDirty("RawPasswordValue") && entity.RawPasswordValue.IsNullOrWhiteSpace() == false) + { + changedCols.Add("Password"); + } + //only update the changed cols + if (changedCols.Count > 0) + { + Database.Update(dto, changedCols); + } + + //TODO ContentType for the Member entity + + //Create the PropertyData for this version - cmsPropertyData + var propertyFactory = new PropertyFactory(entity.ContentType.CompositionPropertyTypes.ToArray(), entity.Version, entity.Id); + var keyDictionary = new Dictionary(); + + //Add Properties + // - don't try to save the property if it doesn't exist (or doesn't have an ID) on the content type + // - this can occur if the member type doesn't contain the built-in properties that the + // - member object contains. + var propsToPersist = entity.Properties.Where(x => x.PropertyType.HasIdentity).ToArray(); + + var propertyDataDtos = propertyFactory.BuildDto(propsToPersist); + + foreach (var propertyDataDto in propertyDataDtos) + { + if (propertyDataDto.Id > 0) + { + Database.Update(propertyDataDto); + } + else + { + int primaryKey = Convert.ToInt32(Database.Insert(propertyDataDto)); + keyDictionary.Add(propertyDataDto.PropertyTypeId, primaryKey); + } + } + + //Update Properties with its newly set Id + if (keyDictionary.Any()) + { + foreach (var property in ((Member)entity).Properties) + { + property.Id = keyDictionary[property.PropertyTypeId]; + } + } + + UpdatePropertyTags(entity, _tagRepository); + + dirtyEntity.ResetDirtyProperties(); + } + + #endregion + + #region Overrides of VersionableRepositoryBase + + public void RebuildXmlStructures(Func serializer, int groupSize = 5000, IEnumerable contentTypeIds = null) + { + + //Ok, now we need to remove the data and re-insert it, we'll do this all in one transaction too. + using (var tr = Database.GetTransaction()) + { + //Remove all the data first, if anything fails after this it's no problem the transaction will be reverted + if (contentTypeIds == null) + { + var memberObjectType = Guid.Parse(Constants.ObjectTypes.Member); + var subQuery = new Sql() + .Select("DISTINCT cmsContentXml.nodeId") + .From() + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .Where(dto => dto.NodeObjectType == memberObjectType); + + var deleteSql = SqlSyntax.GetDeleteSubquery("cmsContentXml", "nodeId", subQuery); + Database.Execute(deleteSql); + } + else + { + foreach (var id in contentTypeIds) + { + var id1 = id; + var memberObjectType = Guid.Parse(Constants.ObjectTypes.Member); + var subQuery = new Sql() + .Select("DISTINCT cmsContentXml.nodeId") + .From() + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .Where(dto => dto.NodeObjectType == memberObjectType) + .Where(dto => dto.ContentTypeId == id1); + + var deleteSql = SqlSyntax.GetDeleteSubquery("cmsContentXml", "nodeId", subQuery); + Database.Execute(deleteSql); + } + } + + //now insert the data, again if something fails here, the whole transaction is reversed + if (contentTypeIds == null) + { + var query = Query.Builder; + RebuildXmlStructuresProcessQuery(serializer, query, tr, groupSize); + } + else + { + foreach (var contentTypeId in contentTypeIds) + { + //copy local + var id = contentTypeId; + var query = Query.Builder.Where(x => x.ContentTypeId == id && x.Trashed == false); + RebuildXmlStructuresProcessQuery(serializer, query, tr, groupSize); + } + } + + tr.Complete(); + } + } + + private void RebuildXmlStructuresProcessQuery(Func serializer, IQuery query, Transaction tr, int pageSize) + { + var pageIndex = 0; + var total = long.MinValue; + var processed = 0; + do + { + var descendants = GetPagedResultsByQuery(query, pageIndex, pageSize, out total, "Path", Direction.Ascending, orderBySystemField: true); + + var xmlItems = (from descendant in descendants + let xml = serializer(descendant) + select new ContentXmlDto { NodeId = descendant.Id, Xml = xml.ToDataString() }).ToArray(); + + //bulk insert it into the database + Database.BulkInsertRecords(xmlItems, tr); + + processed += xmlItems.Length; + + pageIndex++; + } while (processed < total); + } + + public override IMember GetByVersion(Guid versionId) + { + var sql = GetBaseQuery(false); + sql.Where("cmsContentVersion.VersionId = @VersionId", new { VersionId = versionId }); + sql.OrderByDescending(x => x.VersionDate); + + var dto = Database.Fetch(sql).FirstOrDefault(); + + if (dto == null) + return null; + + var memberType = _memberTypeRepository.Get(dto.ContentVersionDto.ContentDto.ContentTypeId); + + var factory = new MemberFactory(memberType, NodeObjectTypeId, dto.NodeId); + var media = factory.BuildEntity(dto); + + var properties = GetPropertyCollection(sql, new[] { new DocumentDefinition(dto.NodeId, dto.ContentVersionDto.VersionId, media.UpdateDate, media.CreateDate, memberType) }); + + media.Properties = properties[dto.NodeId]; + + //on initial construction we don't want to have dirty properties tracked + // http://issues.umbraco.org/issue/U4-1946 + ((Entity)media).ResetDirtyProperties(false); + return media; + + } + + protected override void PerformDeleteVersion(int id, Guid versionId) + { + Database.Delete("WHERE nodeId = @Id AND versionId = @VersionId", new { Id = id, VersionId = versionId }); + Database.Delete("WHERE contentNodeId = @Id AND versionId = @VersionId", new { Id = id, VersionId = versionId }); + Database.Delete("WHERE ContentId = @Id AND VersionId = @VersionId", new { Id = id, VersionId = versionId }); + } + + #endregion + + public IEnumerable FindMembersInRole(string roleName, string usernameToMatch, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith) + { + //get the group id + var grpQry = new Query().Where(group => group.Name.Equals(roleName)); + var memberGroup = _memberGroupRepository.GetByQuery(grpQry).FirstOrDefault(); + if (memberGroup == null) return Enumerable.Empty(); + + // get the members by username + var query = new Query(); + switch (matchType) + { + case StringPropertyMatchType.Exact: + query.Where(member => member.Username.Equals(usernameToMatch)); + break; + case StringPropertyMatchType.Contains: + query.Where(member => member.Username.Contains(usernameToMatch)); + break; + case StringPropertyMatchType.StartsWith: + query.Where(member => member.Username.StartsWith(usernameToMatch)); + break; + case StringPropertyMatchType.EndsWith: + query.Where(member => member.Username.EndsWith(usernameToMatch)); + break; + case StringPropertyMatchType.Wildcard: + query.Where(member => member.Username.SqlWildcard(usernameToMatch, TextColumnType.NVarchar)); + break; + default: + throw new ArgumentOutOfRangeException("matchType"); + } + var matchedMembers = GetByQuery(query).ToArray(); + + var membersInGroup = new List(); + //then we need to filter the matched members that are in the role + //since the max sql params are 2100 on sql server, we'll reduce that to be safe for potentially other servers and run the queries in batches + var inGroups = matchedMembers.InGroupsOf(1000); + foreach (var batch in inGroups) + { + var memberIdBatch = batch.Select(x => x.Id); + var sql = new Sql().Select("*").From() + .Where(dto => dto.MemberGroup == memberGroup.Id) + .Where("Member IN (@memberIds)", new { memberIds = memberIdBatch }); + var memberIdsInGroup = Database.Fetch(sql) + .Select(x => x.Member).ToArray(); + + membersInGroup.AddRange(matchedMembers.Where(x => memberIdsInGroup.Contains(x.Id))); + } + + return membersInGroup; + + } + + /// + /// Get all members in a specific group + /// + /// + /// + public IEnumerable GetByMemberGroup(string groupName) + { + var grpQry = new Query().Where(group => group.Name.Equals(groupName)); + var memberGroup = _memberGroupRepository.GetByQuery(grpQry).FirstOrDefault(); + if (memberGroup == null) return Enumerable.Empty(); + + var subQuery = new Sql().Select("Member").From().Where(dto => dto.MemberGroup == memberGroup.Id); + + var sql = GetBaseQuery(false) + //TODO: An inner join would be better, though I've read that the query optimizer will always turn a + // subquery with an IN clause into an inner join anyways. + .Append(new Sql("WHERE umbracoNode.id IN (" + subQuery.SQL + ")", subQuery.Arguments)) + .OrderByDescending(x => x.VersionDate) + .OrderBy(x => x.SortOrder); + + return ProcessQuery(sql); + + } + + public bool Exists(string username) + { + var sql = new Sql(); + + sql.Select("COUNT(*)") + .From() + .Where(x => x.LoginName == username); + + return Database.ExecuteScalar(sql) > 0; + } + + public int GetCountByQuery(IQuery query) + { + var sqlWithProps = GetNodeIdQueryWithPropertyData(); + var translator = new SqlTranslator(sqlWithProps, query); + var sql = translator.Translate(); + + //get the COUNT base query + var fullSql = GetBaseQuery(true) + .Append(new Sql("WHERE umbracoNode.id IN (" + sql.SQL + ")", sql.Arguments)); + + return Database.ExecuteScalar(fullSql); + } + + /// + /// Gets paged member results + /// + /// + /// The where clause, if this is null all records are queried + /// + /// Index of the page. + /// Size of the page. + /// The total records. + /// The order by column + /// The order direction. + /// Flag to indicate when ordering by system field + /// Search query + /// + /// + /// The query supplied will ONLY work with data specifically on the cmsMember table because we are using PetaPoco paging (SQL paging) + /// + public IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, + string orderBy, Direction orderDirection, bool orderBySystemField, string filter = "") + { + var args = new List(); + var sbWhere = new StringBuilder(); + Func> filterCallback = null; + if (filter.IsNullOrWhiteSpace() == false) + { + sbWhere.Append("AND ((umbracoNode. " + SqlSyntax.GetQuotedColumnName("text") + " LIKE @" + args.Count + ") " + + "OR (cmsMember.LoginName LIKE @0" + args.Count + "))"); + args.Add("%" + filter + "%"); + filterCallback = () => new Tuple(sbWhere.ToString().Trim(), args.ToArray()); + } + + return GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, + new Tuple("cmsMember", "nodeId"), + ProcessQuery, orderBy, orderDirection, orderBySystemField, + filterCallback); + } + + public void AddOrUpdateContentXml(IMember content, Func xml) + { + _contentXmlRepository.AddOrUpdate(new ContentXmlEntity(content, xml)); + } + + public void AddOrUpdatePreviewXml(IMember content, Func xml) + { + _contentPreviewRepository.AddOrUpdate(new ContentPreviewEntity(content, xml)); + } + + protected override string GetDatabaseFieldNameForOrderBy(string orderBy) + { + //Some custom ones + switch (orderBy.ToUpperInvariant()) + { + case "EMAIL": + return "cmsMember.Email"; + case "LOGINNAME": + return "cmsMember.LoginName"; + } + + return base.GetDatabaseFieldNameForOrderBy(orderBy); + } + + protected override string GetEntityPropertyNameForOrderBy(string orderBy) + { + //Some custom ones + switch (orderBy.ToUpperInvariant()) + { + case "LOGINNAME": + return "Username"; + } + + return base.GetEntityPropertyNameForOrderBy(orderBy); + } + + private IEnumerable ProcessQuery(Sql sql) + { + //NOTE: This doesn't allow properties to be part of the query + var dtos = Database.Fetch(sql); + + var ids = dtos.Select(x => x.ContentVersionDto.ContentDto.ContentTypeId).ToArray(); + + //content types + var contentTypes = ids.Length == 0 ? Enumerable.Empty() : _memberTypeRepository.GetAll(ids).ToArray(); + + var dtosWithContentTypes = dtos + //This select into and null check are required because we don't have a foreign damn key on the contentType column + // http://issues.umbraco.org/issue/U4-5503 + .Select(x => new { dto = x, contentType = contentTypes.FirstOrDefault(ct => ct.Id == x.ContentVersionDto.ContentDto.ContentTypeId) }) + .Where(x => x.contentType != null) + .ToArray(); + + //Go get the property data for each document + IEnumerable docDefs = dtosWithContentTypes.Select(d => new DocumentDefinition( + d.dto.NodeId, + d.dto.ContentVersionDto.VersionId, + d.dto.ContentVersionDto.VersionDate, + d.dto.ContentVersionDto.ContentDto.NodeDto.CreateDate, + d.contentType)); + + var propertyData = GetPropertyCollection(sql, docDefs); + + return dtosWithContentTypes.Select(d => CreateMemberFromDto( + d.dto, + contentTypes.First(ct => ct.Id == d.dto.ContentVersionDto.ContentDto.ContentTypeId), + propertyData[d.dto.NodeId])); + } + + /// + /// Private method to create a member object from a MemberDto + /// + /// + /// + /// + /// + private IMember CreateMemberFromDto(MemberDto dto, + IMemberType contentType, + PropertyCollection propCollection) + { + var factory = new MemberFactory(contentType, NodeObjectTypeId, dto.ContentVersionDto.NodeId); + var member = factory.BuildEntity(dto); + + member.Properties = propCollection; + + //on initial construction we don't want to have dirty properties tracked + // http://issues.umbraco.org/issue/U4-1946 + ((Entity)member).ResetDirtyProperties(false); + return member; + } + + /// + /// Private method to create a member object from a MemberDto + /// + /// + /// + /// + /// + private IMember CreateMemberFromDto(MemberDto dto, Guid versionId, Sql docSql) + { + var memberType = _memberTypeRepository.Get(dto.ContentVersionDto.ContentDto.ContentTypeId); + + var factory = new MemberFactory(memberType, NodeObjectTypeId, dto.ContentVersionDto.NodeId); + var member = factory.BuildEntity(dto); + + var docDef = new DocumentDefinition(dto.ContentVersionDto.NodeId, versionId, member.UpdateDate, member.CreateDate, memberType); + + var properties = GetPropertyCollection(docSql, new[] { docDef }); + + member.Properties = properties[dto.ContentVersionDto.NodeId]; + + //on initial construction we don't want to have dirty properties tracked + // http://issues.umbraco.org/issue/U4-1946 + ((Entity)member).ResetDirtyProperties(false); + return member; + } + + /// + /// Dispose disposable properties + /// + /// + /// Ensure the unit of work is disposed + /// + protected override void DisposeResources() + { + _memberTypeRepository.Dispose(); + _tagRepository.Dispose(); + _memberGroupRepository.Dispose(); + _contentXmlRepository.Dispose(); + _contentPreviewRepository.Dispose(); + } + + } } diff --git a/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs index 56b0b63ad5..4fd1c83436 100644 --- a/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs @@ -25,368 +25,391 @@ using Umbraco.Core.IO; namespace Umbraco.Core.Persistence.Repositories { - internal abstract class VersionableRepositoryBase : PetaPocoRepositoryBase - where TEntity : class, IAggregateRoot - { - private readonly IContentSection _contentSection; + using SqlSyntax; + internal abstract class VersionableRepositoryBase : PetaPocoRepositoryBase + where TEntity : class, IAggregateRoot + { + private readonly IContentSection _contentSection; - protected VersionableRepositoryBase(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax, IContentSection contentSection) - : base(work, cache, logger, sqlSyntax) - { - _contentSection = contentSection; - } + protected VersionableRepositoryBase(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax, IContentSection contentSection) + : base(work, cache, logger, sqlSyntax) + { + _contentSection = contentSection; + } - #region IRepositoryVersionable Implementation + #region IRepositoryVersionable Implementation - public virtual IEnumerable GetAllVersions(int id) - { - var sql = new Sql(); - sql.Select("*") - .From(SqlSyntax) - .InnerJoin(SqlSyntax) - .On(SqlSyntax, left => left.NodeId, right => right.NodeId) - .InnerJoin(SqlSyntax) - .On(SqlSyntax, left => left.NodeId, right => right.NodeId) - .Where(x => x.NodeObjectType == NodeObjectTypeId) - .Where(x => x.NodeId == id) - .OrderByDescending(x => x.VersionDate, SqlSyntax); + public virtual IEnumerable GetAllVersions(int id) + { + var sql = new Sql(); + sql.Select("*") + .From(SqlSyntax) + .InnerJoin(SqlSyntax) + .On(SqlSyntax, left => left.NodeId, right => right.NodeId) + .InnerJoin(SqlSyntax) + .On(SqlSyntax, left => left.NodeId, right => right.NodeId) + .Where(x => x.NodeObjectType == NodeObjectTypeId) + .Where(x => x.NodeId == id) + .OrderByDescending(x => x.VersionDate, SqlSyntax); - var dtos = Database.Fetch(sql); - foreach (var dto in dtos) - { - yield return GetByVersion(dto.VersionId); - } - } + var dtos = Database.Fetch(sql); + foreach (var dto in dtos) + { + yield return GetByVersion(dto.VersionId); + } + } - public virtual void DeleteVersion(Guid versionId) - { - var dto = Database.FirstOrDefault("WHERE versionId = @VersionId", new { VersionId = versionId }); - if(dto == null) return; + public virtual void DeleteVersion(Guid versionId) + { + var dto = Database.FirstOrDefault("WHERE versionId = @VersionId", new { VersionId = versionId }); + if (dto == null) return; - //Ensure that the lastest version is not deleted - var latestVersionDto = Database.FirstOrDefault("WHERE ContentId = @Id ORDER BY VersionDate DESC", new { Id = dto.NodeId }); - if(latestVersionDto.VersionId == dto.VersionId) - return; + //Ensure that the lastest version is not deleted + var latestVersionDto = Database.FirstOrDefault("WHERE ContentId = @Id ORDER BY VersionDate DESC", new { Id = dto.NodeId }); + if (latestVersionDto.VersionId == dto.VersionId) + return; - using (var transaction = Database.GetTransaction()) - { - PerformDeleteVersion(dto.NodeId, versionId); + using (var transaction = Database.GetTransaction()) + { + PerformDeleteVersion(dto.NodeId, versionId); - transaction.Complete(); - } - } + transaction.Complete(); + } + } - public virtual void DeleteVersions(int id, DateTime versionDate) - { - //Ensure that the latest version is not part of the versions being deleted - var latestVersionDto = Database.FirstOrDefault("WHERE ContentId = @Id ORDER BY VersionDate DESC", new { Id = id }); - var list = - Database.Fetch( - "WHERE versionId <> @VersionId AND (ContentId = @Id AND VersionDate < @VersionDate)", - new {VersionId = latestVersionDto.VersionId, Id = id, VersionDate = versionDate}); - if (list.Any() == false) return; + public virtual void DeleteVersions(int id, DateTime versionDate) + { + //Ensure that the latest version is not part of the versions being deleted + var latestVersionDto = Database.FirstOrDefault("WHERE ContentId = @Id ORDER BY VersionDate DESC", new { Id = id }); + var list = + Database.Fetch( + "WHERE versionId <> @VersionId AND (ContentId = @Id AND VersionDate < @VersionDate)", + new { VersionId = latestVersionDto.VersionId, Id = id, VersionDate = versionDate }); + if (list.Any() == false) return; - using (var transaction = Database.GetTransaction()) - { - foreach (var dto in list) - { - PerformDeleteVersion(id, dto.VersionId); - } + using (var transaction = Database.GetTransaction()) + { + foreach (var dto in list) + { + PerformDeleteVersion(id, dto.VersionId); + } - transaction.Complete(); - } - } + transaction.Complete(); + } + } - public abstract TEntity GetByVersion(Guid versionId); + public abstract TEntity GetByVersion(Guid versionId); - /// - /// Protected method to execute the delete statements for removing a single version for a TEntity item. - /// - /// Id of the to delete a version from - /// Guid id of the version to delete - protected abstract void PerformDeleteVersion(int id, Guid versionId); + /// + /// Protected method to execute the delete statements for removing a single version for a TEntity item. + /// + /// Id of the to delete a version from + /// Guid id of the version to delete + protected abstract void PerformDeleteVersion(int id, Guid versionId); - #endregion + #endregion - public int CountDescendants(int parentId, string contentTypeAlias = null) - { - var pathMatch = parentId == -1 - ? "-1," - : "," + parentId + ","; - var sql = new Sql(); - if (contentTypeAlias.IsNullOrWhiteSpace()) - { - sql.Select("COUNT(*)") - .From() - .Where(x => x.NodeObjectType == NodeObjectTypeId) - .Where(x => x.Path.Contains(pathMatch)); - } - else - { - sql.Select("COUNT(*)") - .From() - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .InnerJoin() - .On(left => left.NodeId, right => right.ContentTypeId) - .Where(x => x.NodeObjectType == NodeObjectTypeId) - .Where(x => x.Path.Contains(pathMatch)) - .Where(x => x.Alias == contentTypeAlias); - } + public int CountDescendants(int parentId, string contentTypeAlias = null) + { + var pathMatch = parentId == -1 + ? "-1," + : "," + parentId + ","; + var sql = new Sql(); + if (contentTypeAlias.IsNullOrWhiteSpace()) + { + sql.Select("COUNT(*)") + .From() + .Where(x => x.NodeObjectType == NodeObjectTypeId) + .Where(x => x.Path.Contains(pathMatch)); + } + else + { + sql.Select("COUNT(*)") + .From() + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .InnerJoin() + .On(left => left.NodeId, right => right.ContentTypeId) + .Where(x => x.NodeObjectType == NodeObjectTypeId) + .Where(x => x.Path.Contains(pathMatch)) + .Where(x => x.Alias == contentTypeAlias); + } - return Database.ExecuteScalar(sql); - } + return Database.ExecuteScalar(sql); + } - public int CountChildren(int parentId, string contentTypeAlias = null) - { - var sql = new Sql(); - if (contentTypeAlias.IsNullOrWhiteSpace()) - { - sql.Select("COUNT(*)") - .From() - .Where(x => x.NodeObjectType == NodeObjectTypeId) - .Where(x => x.ParentId == parentId); - } - else - { - sql.Select("COUNT(*)") - .From() - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .InnerJoin() - .On(left => left.NodeId, right => right.ContentTypeId) - .Where(x => x.NodeObjectType == NodeObjectTypeId) - .Where(x => x.ParentId == parentId) - .Where(x => x.Alias == contentTypeAlias); - } + public int CountChildren(int parentId, string contentTypeAlias = null) + { + var sql = new Sql(); + if (contentTypeAlias.IsNullOrWhiteSpace()) + { + sql.Select("COUNT(*)") + .From() + .Where(x => x.NodeObjectType == NodeObjectTypeId) + .Where(x => x.ParentId == parentId); + } + else + { + sql.Select("COUNT(*)") + .From() + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .InnerJoin() + .On(left => left.NodeId, right => right.ContentTypeId) + .Where(x => x.NodeObjectType == NodeObjectTypeId) + .Where(x => x.ParentId == parentId) + .Where(x => x.Alias == contentTypeAlias); + } - return Database.ExecuteScalar(sql); - } + return Database.ExecuteScalar(sql); + } - /// - /// Get the total count of entities - /// - /// - /// - public int Count(string contentTypeAlias = null) - { - var sql = new Sql(); - if (contentTypeAlias.IsNullOrWhiteSpace()) - { - sql.Select("COUNT(*)") - .From() - .Where(x => x.NodeObjectType == NodeObjectTypeId); - } - else - { - sql.Select("COUNT(*)") - .From() - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .InnerJoin() - .On(left => left.NodeId, right => right.ContentTypeId) - .Where(x => x.NodeObjectType == NodeObjectTypeId) - .Where(x => x.Alias == contentTypeAlias); - } + /// + /// Get the total count of entities + /// + /// + /// + public int Count(string contentTypeAlias = null) + { + var sql = new Sql(); + if (contentTypeAlias.IsNullOrWhiteSpace()) + { + sql.Select("COUNT(*)") + .From() + .Where(x => x.NodeObjectType == NodeObjectTypeId); + } + else + { + sql.Select("COUNT(*)") + .From() + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .InnerJoin() + .On(left => left.NodeId, right => right.ContentTypeId) + .Where(x => x.NodeObjectType == NodeObjectTypeId) + .Where(x => x.Alias == contentTypeAlias); + } - return Database.ExecuteScalar(sql); - } + return Database.ExecuteScalar(sql); + } - /// - /// This removes associated tags from the entity - used generally when an entity is recycled - /// - /// - /// - protected void ClearEntityTags(IContentBase entity, ITagRepository tagRepo) - { - tagRepo.ClearTagsFromEntity(entity.Id); - } + /// + /// This removes associated tags from the entity - used generally when an entity is recycled + /// + /// + /// + protected void ClearEntityTags(IContentBase entity, ITagRepository tagRepo) + { + tagRepo.ClearTagsFromEntity(entity.Id); + } - /// - /// Updates the tag repository with any tag enabled properties and their values - /// - /// - /// - protected void UpdatePropertyTags(IContentBase entity, ITagRepository tagRepo) - { - foreach (var tagProp in entity.Properties.Where(x => x.TagSupport.Enable)) - { - if (tagProp.TagSupport.Behavior == PropertyTagBehavior.Remove) - { - //remove the specific tags - tagRepo.RemoveTagsFromProperty( - entity.Id, - tagProp.PropertyTypeId, - tagProp.TagSupport.Tags.Select(x => new Tag { Text = x.Item1, Group = x.Item2 })); - } - else - { - //assign the tags - tagRepo.AssignTagsToProperty( - entity.Id, - tagProp.PropertyTypeId, - tagProp.TagSupport.Tags.Select(x => new Tag { Text = x.Item1, Group = x.Item2 }), - tagProp.TagSupport.Behavior == PropertyTagBehavior.Replace); - } - } - } + /// + /// Updates the tag repository with any tag enabled properties and their values + /// + /// + /// + protected void UpdatePropertyTags(IContentBase entity, ITagRepository tagRepo) + { + foreach (var tagProp in entity.Properties.Where(x => x.TagSupport.Enable)) + { + if (tagProp.TagSupport.Behavior == PropertyTagBehavior.Remove) + { + //remove the specific tags + tagRepo.RemoveTagsFromProperty( + entity.Id, + tagProp.PropertyTypeId, + tagProp.TagSupport.Tags.Select(x => new Tag { Text = x.Item1, Group = x.Item2 })); + } + else + { + //assign the tags + tagRepo.AssignTagsToProperty( + entity.Id, + tagProp.PropertyTypeId, + tagProp.TagSupport.Tags.Select(x => new Tag { Text = x.Item1, Group = x.Item2 }), + tagProp.TagSupport.Behavior == PropertyTagBehavior.Replace); + } + } + } - private Sql GetFilteredSqlForPagedResults(Sql sql, Func> defaultFilter = null) - { - //copy to var so that the original isn't changed - var filteredSql = new Sql(sql.SQL, sql.Arguments); - // Apply filter - if (defaultFilter != null) - { - var filterResult = defaultFilter(); - filteredSql.Append(filterResult.Item1, filterResult.Item2); - } - return filteredSql; - } + private Sql GetFilteredSqlForPagedResults(Sql sql, Func> defaultFilter = null) + { + //copy to var so that the original isn't changed + var filteredSql = new Sql(sql.SQL, sql.Arguments); + // Apply filter + if (defaultFilter != null) + { + var filterResult = defaultFilter(); + filteredSql.Append(filterResult.Item1, filterResult.Item2); + } + return filteredSql; + } - private Sql GetSortedSqlForPagedResults(Sql sql, Direction orderDirection, string orderBy) - { - //copy to var so that the original isn't changed - var sortedSql = new Sql(sql.SQL, sql.Arguments); - // Apply order according to parameters - if (string.IsNullOrEmpty(orderBy) == false) - { - var orderByParams = new[] { GetDatabaseFieldNameForOrderBy(orderBy) }; - if (orderDirection == Direction.Ascending) - { - sortedSql.OrderBy(orderByParams); - } - else - { - sortedSql.OrderByDescending(orderByParams); - } - return sortedSql; - } - return sortedSql; - } + private Sql GetSortedSqlForPagedResults(Sql sql, Direction orderDirection, string orderBy, bool orderBySystemField) + { + //copy to var so that the original isn't changed + var sortedSql = new Sql(sql.SQL, sql.Arguments); - /// - /// A helper method for inheritors to get the paged results by query in a way that minimizes queries - /// - /// The type of the d. - /// The 'true' entity type (i.e. Content, Member, etc...) - /// The query. - /// Index of the page. - /// Size of the page. - /// The total records. - /// The tablename + column name for the SELECT statement fragment to return the node id from the query - /// A callback to create the default filter to be applied if there is one - /// A callback to process the query result - /// The order by column - /// The order direction. - /// - /// orderBy - protected IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, - Tuple nodeIdSelect, - Func> processQuery, - string orderBy, - Direction orderDirection, - Func> defaultFilter = null) - where TContentBase : class, IAggregateRoot, TEntity - { - if (orderBy == null) throw new ArgumentNullException("orderBy"); + if (orderBySystemField) + { + // Apply order according to parameters + if (string.IsNullOrEmpty(orderBy) == false) + { + var orderByParams = new[] { GetDatabaseFieldNameForOrderBy(orderBy) }; + if (orderDirection == Direction.Ascending) + { + sortedSql.OrderBy(orderByParams); + } + else + { + sortedSql.OrderByDescending(orderByParams); + } + } + } + else + { + // Sorting by a custom field, so set-up sub-query for ORDER BY clause to pull through valie + // from most recent content version for the given order by field + var sortedInt = string.Format(SqlSyntaxContext.SqlSyntaxProvider.ConvertIntegerToOrderableString, "dataInt"); + var sortedDate = string.Format(SqlSyntaxContext.SqlSyntaxProvider.ConvertDateToOrderableString, "dataDate"); + var sortedString = string.Format(SqlSyntaxContext.SqlSyntaxProvider.IsNull, "dataNvarchar", "''"); - // Get base query - var sqlBase = GetBaseQuery(false); + var orderBySql = string.Format(@"ORDER BY ( + SELECT CASE + WHEN dataInt Is Not Null THEN {0} + WHEN dataDate Is Not Null THEN {1} + ELSE {2} + END + FROM cmsContent c + INNER JOIN cmsContentVersion cv ON cv.ContentId = c.nodeId AND VersionDate = ( + SELECT Max(VersionDate) + FROM cmsContentVersion + WHERE ContentId = c.nodeId + ) + INNER JOIN cmsPropertyData cpd ON cpd.contentNodeId = c.nodeId + AND cpd.versionId = cv.VersionId + INNER JOIN cmsPropertyType cpt ON cpt.Id = cpd.propertytypeId + WHERE c.nodeId = umbracoNode.Id and cpt.Alias = @0)", sortedInt, sortedDate, sortedString); - if (query == null) query = new Query(); - var translator = new SqlTranslator(sqlBase, query); - var sqlQuery = translator.Translate(); - - // Note we can't do multi-page for several DTOs like we can multi-fetch and are doing in PerformGetByQuery, - // but actually given we are doing a Get on each one (again as in PerformGetByQuery), we only need the node Id. - // So we'll modify the SQL. - var sqlNodeIds = new Sql( - sqlQuery.SQL.Replace("SELECT *", string.Format("SELECT {0}.{1}",nodeIdSelect.Item1, nodeIdSelect.Item2)), - sqlQuery.Arguments); - - //get sorted and filtered sql - var sqlNodeIdsWithSort = GetSortedSqlForPagedResults( - GetFilteredSqlForPagedResults(sqlNodeIds, defaultFilter), - orderDirection, orderBy); + sortedSql.Append(orderBySql, orderBy); + if (orderDirection == Direction.Descending) + { + sortedSql.Append(" DESC"); + } + } + return sortedSql; + } - // Get page of results and total count - IEnumerable result; - var pagedResult = Database.Page(pageIndex + 1, pageSize, sqlNodeIdsWithSort); - totalRecords = Convert.ToInt32(pagedResult.TotalItems); + /// + /// A helper method for inheritors to get the paged results by query in a way that minimizes queries + /// + /// The type of the d. + /// The 'true' entity type (i.e. Content, Member, etc...) + /// The query. + /// Index of the page. + /// Size of the page. + /// The total records. + /// The tablename + column name for the SELECT statement fragment to return the node id from the query + /// A callback to create the default filter to be applied if there is one + /// A callback to process the query result + /// The order by column + /// The order direction. + /// Flag to indicate when ordering by system field + /// + /// orderBy + protected IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, + Tuple nodeIdSelect, + Func> processQuery, + string orderBy, + Direction orderDirection, + bool orderBySystemField, + Func> defaultFilter = null) + where TContentBase : class, IAggregateRoot, TEntity + { + if (orderBy == null) throw new ArgumentNullException("orderBy"); - //NOTE: We need to check the actual items returned, not the 'totalRecords', that is because if you request a page number - // that doesn't actually have any data on it, the totalRecords will still indicate there are records but there are none in - // the pageResult, then the GetAll will actually return ALL records in the db. - if (pagedResult.Items.Any()) - { - //Crete the inner paged query that was used above to get the paged result, we'll use that as the inner sub query - var args = sqlNodeIdsWithSort.Arguments; - string sqlStringCount, sqlStringPage; - Database.BuildPageQueries(pageIndex * pageSize, pageSize, sqlNodeIdsWithSort.SQL, ref args, out sqlStringCount, out sqlStringPage); - - //if this is for sql server, the sqlPage will start with a SELECT * but we don't want that, we only want to return the nodeId - sqlStringPage = sqlStringPage - .Replace("SELECT *", - //This ensures we only take the field name of the node id select and not the table name - since the resulting select - // will ony work with the field name. - "SELECT " + nodeIdSelect.Item2); + // Get base query + var sqlBase = GetBaseQuery(false); - //We need to make this an inner join on the paged query - var splitQuery = sqlQuery.SQL.Split(new[] {"WHERE "}, StringSplitOptions.None); - var withInnerJoinSql = new Sql(splitQuery[0]) - .Append("INNER JOIN (") - //join the paged query with the paged query arguments - .Append(sqlStringPage, args) - .Append(") temp ") - .Append(string.Format("ON {0}.{1} = temp.{1}", nodeIdSelect.Item1, nodeIdSelect.Item2)) - //add the original where clause back with the original arguments - .Where(splitQuery[1], sqlQuery.Arguments); + if (query == null) query = new Query(); + var translator = new SqlTranslator(sqlBase, query); + var sqlQuery = translator.Translate(); - //get sorted and filtered sql - var fullQuery = GetSortedSqlForPagedResults( - GetFilteredSqlForPagedResults(withInnerJoinSql, defaultFilter), - orderDirection, orderBy); - - var content = processQuery(fullQuery) - .Cast() - .AsQueryable(); + // Note we can't do multi-page for several DTOs like we can multi-fetch and are doing in PerformGetByQuery, + // but actually given we are doing a Get on each one (again as in PerformGetByQuery), we only need the node Id. + // So we'll modify the SQL. + var sqlNodeIds = new Sql( + sqlQuery.SQL.Replace("SELECT *", string.Format("SELECT {0}.{1}", nodeIdSelect.Item1, nodeIdSelect.Item2)), + sqlQuery.Arguments); - // Now we need to ensure this result is also ordered by the same order by clause - var orderByProperty = GetEntityPropertyNameForOrderBy(orderBy); - if (orderDirection == Direction.Ascending) - { - result = content.OrderBy(orderByProperty); - } - else - { - result = content.OrderByDescending(orderByProperty); - } - } - else - { - result = Enumerable.Empty(); - } + //get sorted and filtered sql + var sqlNodeIdsWithSort = GetSortedSqlForPagedResults( + GetFilteredSqlForPagedResults(sqlNodeIds, defaultFilter), + orderDirection, orderBy, orderBySystemField); - return result; - } + // Get page of results and total count + IEnumerable result; + var pagedResult = Database.Page(pageIndex + 1, pageSize, sqlNodeIdsWithSort); + totalRecords = Convert.ToInt32(pagedResult.TotalItems); - protected IDictionary GetPropertyCollection( - Sql docSql, - IEnumerable documentDefs) - { - if (documentDefs.Any() == false) return new Dictionary(); + //NOTE: We need to check the actual items returned, not the 'totalRecords', that is because if you request a page number + // that doesn't actually have any data on it, the totalRecords will still indicate there are records but there are none in + // the pageResult, then the GetAll will actually return ALL records in the db. + if (pagedResult.Items.Any()) + { + //Crete the inner paged query that was used above to get the paged result, we'll use that as the inner sub query + var args = sqlNodeIdsWithSort.Arguments; + string sqlStringCount, sqlStringPage; + Database.BuildPageQueries(pageIndex * pageSize, pageSize, sqlNodeIdsWithSort.SQL, ref args, out sqlStringCount, out sqlStringPage); - //we need to parse the original SQL statement and reduce the columns to just cmsContent.nodeId, cmsContentVersion.VersionId so that we can use - // the statement to go get the property data for all of the items by using an inner join - var parsedOriginalSql = "SELECT {0} " + docSql.SQL.Substring(docSql.SQL.IndexOf("FROM", StringComparison.Ordinal)); - //now remove everything from an Orderby clause and beyond - if (parsedOriginalSql.InvariantContains("ORDER BY ")) - { - parsedOriginalSql = parsedOriginalSql.Substring(0, parsedOriginalSql.LastIndexOf("ORDER BY ", StringComparison.Ordinal)); - } + //if this is for sql server, the sqlPage will start with a SELECT * but we don't want that, we only want to return the nodeId + sqlStringPage = sqlStringPage + .Replace("SELECT *", + //This ensures we only take the field name of the node id select and not the table name - since the resulting select + // will ony work with the field name. + "SELECT " + nodeIdSelect.Item2); - var propSql = new Sql(@"SELECT cmsPropertyData.* + //We need to make this an inner join on the paged query + var splitQuery = sqlQuery.SQL.Split(new[] { "WHERE " }, StringSplitOptions.None); + var withInnerJoinSql = new Sql(splitQuery[0]) + .Append("INNER JOIN (") + //join the paged query with the paged query arguments + .Append(sqlStringPage, args) + .Append(") temp ") + .Append(string.Format("ON {0}.{1} = temp.{1}", nodeIdSelect.Item1, nodeIdSelect.Item2)) + //add the original where clause back with the original arguments + .Where(splitQuery[1], sqlQuery.Arguments); + + //get sorted and filtered sql + var fullQuery = GetSortedSqlForPagedResults( + GetFilteredSqlForPagedResults(withInnerJoinSql, defaultFilter), + orderDirection, orderBy, orderBySystemField); + return processQuery(fullQuery); + } + else + { + result = Enumerable.Empty(); + } + + return result; + } + + protected IDictionary GetPropertyCollection( + Sql docSql, + IEnumerable documentDefs) + { + if (documentDefs.Any() == false) return new Dictionary(); + + //we need to parse the original SQL statement and reduce the columns to just cmsContent.nodeId, cmsContentVersion.VersionId so that we can use + // the statement to go get the property data for all of the items by using an inner join + var parsedOriginalSql = "SELECT {0} " + docSql.SQL.Substring(docSql.SQL.IndexOf("FROM", StringComparison.Ordinal)); + //now remove everything from an Orderby clause and beyond + if (parsedOriginalSql.InvariantContains("ORDER BY ")) + { + parsedOriginalSql = parsedOriginalSql.Substring(0, parsedOriginalSql.LastIndexOf("ORDER BY ", StringComparison.Ordinal)); + } + + var propSql = new Sql(@"SELECT cmsPropertyData.* FROM cmsPropertyData INNER JOIN cmsPropertyType ON cmsPropertyData.propertytypeid = cmsPropertyType.id @@ -394,15 +417,15 @@ INNER JOIN (" + string.Format(parsedOriginalSql, "cmsContent.nodeId, cmsContentVersion.VersionId") + @") as docData ON cmsPropertyData.versionId = docData.VersionId AND cmsPropertyData.contentNodeId = docData.nodeId LEFT OUTER JOIN cmsDataTypePreValues -ON cmsPropertyType.dataTypeId = cmsDataTypePreValues.datatypeNodeId", docSql.Arguments); +ON cmsPropertyType.dataTypeId = cmsDataTypePreValues.datatypeNodeId", docSql.Arguments); - var allPropertyData = Database.Fetch(propSql); + var allPropertyData = Database.Fetch(propSql); - //This is a lazy access call to get all prevalue data for the data types that make up all of these properties which we use - // below if any property requires tag support - var allPreValues = new Lazy>(() => - { - var preValsSql = new Sql(@"SELECT a.id, a.value, a.sortorder, a.alias, a.datatypeNodeId + //This is a lazy access call to get all prevalue data for the data types that make up all of these properties which we use + // below if any property requires tag support + var allPreValues = new Lazy>(() => + { + var preValsSql = new Sql(@"SELECT a.id, a.value, a.sortorder, a.alias, a.datatypeNodeId FROM cmsDataTypePreValues a WHERE EXISTS( SELECT DISTINCT b.id as preValIdInner @@ -414,182 +437,185 @@ WHERE EXISTS( ON cmsPropertyType.contentTypeId = docData.contentType WHERE a.id = b.id)", docSql.Arguments); - return Database.Fetch(preValsSql); - }); + return Database.Fetch(preValsSql); + }); - var result = new Dictionary(); + var result = new Dictionary(); - var propertiesWithTagSupport = new Dictionary(); + var propertiesWithTagSupport = new Dictionary(); - //iterate each definition grouped by it's content type - this will mean less property type iterations while building - // up the property collections - foreach (var compositionGroup in documentDefs.GroupBy(x => x.Composition)) - { - var compositionProperties = compositionGroup.Key.CompositionPropertyTypes.ToArray(); + //iterate each definition grouped by it's content type - this will mean less property type iterations while building + // up the property collections + foreach (var compositionGroup in documentDefs.GroupBy(x => x.Composition)) + { + var compositionProperties = compositionGroup.Key.CompositionPropertyTypes.ToArray(); - foreach (var def in compositionGroup) - { - var propertyDataDtos = allPropertyData.Where(x => x.NodeId == def.Id).Distinct(); + foreach (var def in compositionGroup) + { + var propertyDataDtos = allPropertyData.Where(x => x.NodeId == def.Id).Distinct(); - var propertyFactory = new PropertyFactory(compositionProperties, def.Version, def.Id, def.CreateDate, def.VersionDate); - var properties = propertyFactory.BuildEntity(propertyDataDtos.ToArray()).ToArray(); - - var newProperties = properties.Where(x => x.HasIdentity == false && x.PropertyType.HasIdentity); + var propertyFactory = new PropertyFactory(compositionProperties, def.Version, def.Id, def.CreateDate, def.VersionDate); + var properties = propertyFactory.BuildEntity(propertyDataDtos.ToArray()).ToArray(); - foreach (var property in newProperties) - { - var propertyDataDto = new PropertyDataDto { NodeId = def.Id, PropertyTypeId = property.PropertyTypeId, VersionId = def.Version }; - int primaryKey = Convert.ToInt32(Database.Insert(propertyDataDto)); + var newProperties = properties.Where(x => x.HasIdentity == false && x.PropertyType.HasIdentity); - property.Version = def.Version; - property.Id = primaryKey; - } + foreach (var property in newProperties) + { + var propertyDataDto = new PropertyDataDto { NodeId = def.Id, PropertyTypeId = property.PropertyTypeId, VersionId = def.Version }; + int primaryKey = Convert.ToInt32(Database.Insert(propertyDataDto)); - foreach (var property in properties) - { - //NOTE: The benchmarks run with and without the following code show very little change so this is not a perf bottleneck - var editor = PropertyEditorResolver.Current.GetByAlias(property.PropertyType.PropertyEditorAlias); + property.Version = def.Version; + property.Id = primaryKey; + } - var tagSupport = propertiesWithTagSupport.ContainsKey(property.PropertyType.PropertyEditorAlias) - ? propertiesWithTagSupport[property.PropertyType.PropertyEditorAlias] - : TagExtractor.GetAttribute(editor); + foreach (var property in properties) + { + //NOTE: The benchmarks run with and without the following code show very little change so this is not a perf bottleneck + var editor = PropertyEditorResolver.Current.GetByAlias(property.PropertyType.PropertyEditorAlias); - if (tagSupport != null) - { - //add to local cache so we don't need to reflect next time for this property editor alias - propertiesWithTagSupport[property.PropertyType.PropertyEditorAlias] = tagSupport; + var tagSupport = propertiesWithTagSupport.ContainsKey(property.PropertyType.PropertyEditorAlias) + ? propertiesWithTagSupport[property.PropertyType.PropertyEditorAlias] + : TagExtractor.GetAttribute(editor); - //this property has tags, so we need to extract them and for that we need the prevals which we've already looked up - var preValData = allPreValues.Value.Where(x => x.DataTypeNodeId == property.PropertyType.DataTypeDefinitionId) - .Distinct() - .ToArray(); + if (tagSupport != null) + { + //add to local cache so we don't need to reflect next time for this property editor alias + propertiesWithTagSupport[property.PropertyType.PropertyEditorAlias] = tagSupport; - var asDictionary = preValData.ToDictionary(x => x.Alias, x => new PreValue(x.Id, x.Value, x.SortOrder)); + //this property has tags, so we need to extract them and for that we need the prevals which we've already looked up + var preValData = allPreValues.Value.Where(x => x.DataTypeNodeId == property.PropertyType.DataTypeDefinitionId) + .Distinct() + .ToArray(); - var preVals = new PreValueCollection(asDictionary); + var asDictionary = preValData.ToDictionary(x => x.Alias, x => new PreValue(x.Id, x.Value, x.SortOrder)); - var contentPropData = new ContentPropertyData(property.Value, - preVals, - new Dictionary()); + var preVals = new PreValueCollection(asDictionary); - TagExtractor.SetPropertyTags(property, contentPropData, property.Value, tagSupport); - } - } + var contentPropData = new ContentPropertyData(property.Value, + preVals, + new Dictionary()); - if (result.ContainsKey(def.Id)) - { - Logger.Warn>("The query returned multiple property sets for document definition " + def.Id + ", " + def.Composition.Name); - } - result[def.Id] = new PropertyCollection(properties); - } - } + TagExtractor.SetPropertyTags(property, contentPropData, property.Value, tagSupport); + } + } - return result; - } + if (result.ContainsKey(def.Id)) + { + Logger.Warn>("The query returned multiple property sets for document definition " + def.Id + ", " + def.Composition.Name); + } + result[def.Id] = new PropertyCollection(properties); + } + } - public class DocumentDefinition - { - /// - /// Initializes a new instance of the class. - /// - public DocumentDefinition(int id, Guid version, DateTime versionDate, DateTime createDate, IContentTypeComposition composition) - { - Id = id; - Version = version; - VersionDate = versionDate; - CreateDate = createDate; - Composition = composition; - } + return result; + } - public int Id { get; set; } - public Guid Version { get; set; } - public DateTime VersionDate { get; set; } - public DateTime CreateDate { get; set; } - public IContentTypeComposition Composition { get; set; } - } + public class DocumentDefinition + { + /// + /// Initializes a new instance of the class. + /// + public DocumentDefinition(int id, Guid version, DateTime versionDate, DateTime createDate, IContentTypeComposition composition) + { + Id = id; + Version = version; + VersionDate = versionDate; + CreateDate = createDate; + Composition = composition; + } - protected virtual string GetDatabaseFieldNameForOrderBy(string orderBy) - { - // Translate the passed order by field (which were originally defined for in-memory object sorting - // of ContentItemBasic instances) to the database field names. - switch (orderBy.ToUpperInvariant()) - { - case "UPDATEDATE": - return "cmsContentVersion.VersionDate"; - case "NAME": - return "umbracoNode.text"; - case "OWNER": - //TODO: This isn't going to work very nicely because it's going to order by ID, not by letter - return "umbracoNode.nodeUser"; - default: - //ensure invalid SQL cannot be submitted - return Regex.Replace(orderBy, @"[^\w\.,`\[\]@-]", ""); - } - } + public int Id { get; set; } + public Guid Version { get; set; } + public DateTime VersionDate { get; set; } + public DateTime CreateDate { get; set; } + public IContentTypeComposition Composition { get; set; } + } - protected virtual string GetEntityPropertyNameForOrderBy(string orderBy) - { - // Translate the passed order by field (which were originally defined for in-memory object sorting - // of ContentItemBasic instances) to the IMedia property names. - switch (orderBy.ToUpperInvariant()) - { - case "OWNER": - //TODO: This isn't going to work very nicely because it's going to order by ID, not by letter - return "CreatorId"; - case "UPDATER": - //TODO: This isn't going to work very nicely because it's going to order by ID, not by letter - return "WriterId"; - case "VERSIONDATE": - return "UpdateDate"; - default: - //ensure invalid SQL cannot be submitted - return Regex.Replace(orderBy, @"[^\w\.,`\[\]@-]", ""); - } - } + protected virtual string GetDatabaseFieldNameForOrderBy(string orderBy) + { + // Translate the passed order by field (which were originally defined for in-memory object sorting + // of ContentItemBasic instances) to the database field names. + switch (orderBy.ToUpperInvariant()) + { + case "UPDATEDATE": + return "cmsContentVersion.VersionDate"; + case "NAME": + return "umbracoNode.text"; + case "OWNER": + //TODO: This isn't going to work very nicely because it's going to order by ID, not by letter + return "umbracoNode.nodeUser"; + // Members only + case "USERNAME": + return "cmsMember.LoginName"; + default: + //ensure invalid SQL cannot be submitted + return Regex.Replace(orderBy, @"[^\w\.,`\[\]@-]", ""); + } + } - /// - /// Deletes all media files passed in. - /// - /// - /// - public virtual bool DeleteMediaFiles(IEnumerable files) - { - //ensure duplicates are removed - files = files.Distinct(); + protected virtual string GetEntityPropertyNameForOrderBy(string orderBy) + { + // Translate the passed order by field (which were originally defined for in-memory object sorting + // of ContentItemBasic instances) to the IMedia property names. + switch (orderBy.ToUpperInvariant()) + { + case "OWNER": + //TODO: This isn't going to work very nicely because it's going to order by ID, not by letter + return "CreatorId"; + case "UPDATER": + //TODO: This isn't going to work very nicely because it's going to order by ID, not by letter + return "WriterId"; + case "VERSIONDATE": + return "UpdateDate"; + default: + //ensure invalid SQL cannot be submitted + return Regex.Replace(orderBy, @"[^\w\.,`\[\]@-]", ""); + } + } - var allsuccess = true; + /// + /// Deletes all media files passed in. + /// + /// + /// + public virtual bool DeleteMediaFiles(IEnumerable files) + { + //ensure duplicates are removed + files = files.Distinct(); - var fs = FileSystemProviderManager.Current.GetFileSystemProvider(); - Parallel.ForEach(files, file => - { - try - { - if (file.IsNullOrWhiteSpace()) return; + var allsuccess = true; - var relativeFilePath = fs.GetRelativePath(file); - if (fs.FileExists(relativeFilePath) == false) return; + var fs = FileSystemProviderManager.Current.GetFileSystemProvider(); + Parallel.ForEach(files, file => + { + try + { + if (file.IsNullOrWhiteSpace()) return; - var parentDirectory = System.IO.Path.GetDirectoryName(relativeFilePath); + var relativeFilePath = fs.GetRelativePath(file); + if (fs.FileExists(relativeFilePath) == false) return; - // don't want to delete the media folder if not using directories. - if (_contentSection.UploadAllowDirectories && parentDirectory != fs.GetRelativePath("/")) - { - //issue U4-771: if there is a parent directory the recursive parameter should be true - fs.DeleteDirectory(parentDirectory, String.IsNullOrEmpty(parentDirectory) == false); - } - else - { - fs.DeleteFile(file, true); - } - } - catch (Exception e) - { - Logger.Error>("An error occurred while deleting file attached to nodes: " + file, e); - allsuccess = false; - } - }); + var parentDirectory = System.IO.Path.GetDirectoryName(relativeFilePath); - return allsuccess; - } - } + // don't want to delete the media folder if not using directories. + if (_contentSection.UploadAllowDirectories && parentDirectory != fs.GetRelativePath("/")) + { + //issue U4-771: if there is a parent directory the recursive parameter should be true + fs.DeleteDirectory(parentDirectory, String.IsNullOrEmpty(parentDirectory) == false); + } + else + { + fs.DeleteFile(file, true); + } + } + catch (Exception e) + { + Logger.Error>("An error occurred while deleting file attached to nodes: " + file, e); + allsuccess = false; + } + }); + + return allsuccess; + } + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/ISqlSyntaxProvider.cs b/src/Umbraco.Core/Persistence/SqlSyntax/ISqlSyntaxProvider.cs index 056d9c1d9a..ef4601f1e5 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/ISqlSyntaxProvider.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/ISqlSyntaxProvider.cs @@ -6,75 +6,79 @@ using Umbraco.Core.Persistence.Querying; namespace Umbraco.Core.Persistence.SqlSyntax { - /// - /// Defines an SqlSyntaxProvider - /// - public interface ISqlSyntaxProvider - { - string EscapeString(string val); + /// + /// Defines an SqlSyntaxProvider + /// + public interface ISqlSyntaxProvider + { + string EscapeString(string val); - string GetWildcardPlaceholder(); - string GetStringColumnEqualComparison(string column, int paramIndex, TextColumnType columnType); - string GetStringColumnWildcardComparison(string column, int paramIndex, TextColumnType columnType); + string GetWildcardPlaceholder(); + string GetStringColumnEqualComparison(string column, int paramIndex, TextColumnType columnType); + string GetStringColumnWildcardComparison(string column, int paramIndex, TextColumnType columnType); - [Obsolete("Use the overload with the parameter index instead")] - string GetStringColumnEqualComparison(string column, string value, TextColumnType columnType); - [Obsolete("Use the overload with the parameter index instead")] - string GetStringColumnStartsWithComparison(string column, string value, TextColumnType columnType); - [Obsolete("Use the overload with the parameter index instead")] - string GetStringColumnEndsWithComparison(string column, string value, TextColumnType columnType); - [Obsolete("Use the overload with the parameter index instead")] - string GetStringColumnContainsComparison(string column, string value, TextColumnType columnType); - [Obsolete("Use the overload with the parameter index instead")] - string GetStringColumnWildcardComparison(string column, string value, TextColumnType columnType); + [Obsolete("Use the overload with the parameter index instead")] + string GetStringColumnEqualComparison(string column, string value, TextColumnType columnType); + [Obsolete("Use the overload with the parameter index instead")] + string GetStringColumnStartsWithComparison(string column, string value, TextColumnType columnType); + [Obsolete("Use the overload with the parameter index instead")] + string GetStringColumnEndsWithComparison(string column, string value, TextColumnType columnType); + [Obsolete("Use the overload with the parameter index instead")] + string GetStringColumnContainsComparison(string column, string value, TextColumnType columnType); + [Obsolete("Use the overload with the parameter index instead")] + string GetStringColumnWildcardComparison(string column, string value, TextColumnType columnType); - string GetQuotedTableName(string tableName); - string GetQuotedColumnName(string columnName); - string GetQuotedName(string name); - bool DoesTableExist(Database db, string tableName); - string GetIndexType(IndexTypes indexTypes); - string GetSpecialDbType(SpecialDbTypes dbTypes); - string CreateTable { get; } - string DropTable { get; } - string AddColumn { get; } - string DropColumn { get; } - string AlterColumn { get; } - string RenameColumn { get; } - string RenameTable { get; } - string CreateSchema { get; } - string AlterSchema { get; } - string DropSchema { get; } - string CreateIndex { get; } - string DropIndex { get; } - string InsertData { get; } - string UpdateData { get; } - string DeleteData { get; } - string TruncateTable { get; } - string CreateConstraint { get; } - string DeleteConstraint { get; } - string CreateForeignKeyConstraint { get; } - string DeleteDefaultConstraint { get; } - string FormatDateTime(DateTime date, bool includeTime = true); - string Format(TableDefinition table); - string Format(IEnumerable columns); - List Format(IEnumerable indexes); - List Format(IEnumerable foreignKeys); - string FormatPrimaryKey(TableDefinition table); - string GetQuotedValue(string value); - string Format(ColumnDefinition column); - string Format(IndexDefinition index); - string Format(ForeignKeyDefinition foreignKey); - string FormatColumnRename(string tableName, string oldName, string newName); - string FormatTableRename(string oldName, string newName); - bool SupportsClustered(); - bool SupportsIdentityInsert(); - bool? SupportsCaseInsensitiveQueries(Database db); + string GetQuotedTableName(string tableName); + string GetQuotedColumnName(string columnName); + string GetQuotedName(string name); + bool DoesTableExist(Database db, string tableName); + string GetIndexType(IndexTypes indexTypes); + string GetSpecialDbType(SpecialDbTypes dbTypes); + string CreateTable { get; } + string DropTable { get; } + string AddColumn { get; } + string DropColumn { get; } + string AlterColumn { get; } + string RenameColumn { get; } + string RenameTable { get; } + string CreateSchema { get; } + string AlterSchema { get; } + string DropSchema { get; } + string CreateIndex { get; } + string DropIndex { get; } + string InsertData { get; } + string UpdateData { get; } + string DeleteData { get; } + string TruncateTable { get; } + string CreateConstraint { get; } + string DeleteConstraint { get; } + string CreateForeignKeyConstraint { get; } + string DeleteDefaultConstraint { get; } + string FormatDateTime(DateTime date, bool includeTime = true); + string Format(TableDefinition table); + string Format(IEnumerable columns); + List Format(IEnumerable indexes); + List Format(IEnumerable foreignKeys); + string FormatPrimaryKey(TableDefinition table); + string GetQuotedValue(string value); + string Format(ColumnDefinition column); + string Format(IndexDefinition index); + string Format(ForeignKeyDefinition foreignKey); + string FormatColumnRename(string tableName, string oldName, string newName); + string FormatTableRename(string oldName, string newName); + bool SupportsClustered(); + bool SupportsIdentityInsert(); + bool? SupportsCaseInsensitiveQueries(Database db); - IEnumerable GetTablesInSchema(Database db); - IEnumerable GetColumnsInSchema(Database db); - IEnumerable> GetConstraintsPerTable(Database db); - IEnumerable> GetConstraintsPerColumn(Database db); + string IsNull { get; } + string ConvertIntegerToOrderableString { get; } + string ConvertDateToOrderableString { get; } - IEnumerable> GetDefinedIndexes(Database db); - } + IEnumerable GetTablesInSchema(Database db); + IEnumerable GetColumnsInSchema(Database db); + IEnumerable> GetConstraintsPerTable(Database db); + IEnumerable> GetConstraintsPerColumn(Database db); + + IEnumerable> GetDefinedIndexes(Database db); + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/MySqlSyntaxProvider.cs b/src/Umbraco.Core/Persistence/SqlSyntax/MySqlSyntaxProvider.cs index c18a5f8477..c29afa7d32 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/MySqlSyntaxProvider.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/MySqlSyntaxProvider.cs @@ -7,390 +7,393 @@ using Umbraco.Core.Persistence.DatabaseModelDefinitions; namespace Umbraco.Core.Persistence.SqlSyntax { - /// - /// Represents an SqlSyntaxProvider for MySql - /// - [SqlSyntaxProviderAttribute("MySql.Data.MySqlClient")] - public class MySqlSyntaxProvider : SqlSyntaxProviderBase - { - private readonly ILogger _logger; + /// + /// Represents an SqlSyntaxProvider for MySql + /// + [SqlSyntaxProviderAttribute("MySql.Data.MySqlClient")] + public class MySqlSyntaxProvider : SqlSyntaxProviderBase + { + private readonly ILogger _logger; - public MySqlSyntaxProvider(ILogger logger) - { - _logger = logger; - - AutoIncrementDefinition = "AUTO_INCREMENT"; - IntColumnDefinition = "int(11)"; - BoolColumnDefinition = "tinyint(1)"; - DateTimeColumnDefinition = "TIMESTAMP"; - TimeColumnDefinition = "time"; - DecimalColumnDefinition = "decimal(38,6)"; - GuidColumnDefinition = "char(36)"; - - DefaultValueFormat = "DEFAULT {0}"; + public MySqlSyntaxProvider(ILogger logger) + { + _logger = logger; + + AutoIncrementDefinition = "AUTO_INCREMENT"; + IntColumnDefinition = "int(11)"; + BoolColumnDefinition = "tinyint(1)"; + DateTimeColumnDefinition = "TIMESTAMP"; + TimeColumnDefinition = "time"; + DecimalColumnDefinition = "decimal(38,6)"; + GuidColumnDefinition = "char(36)"; + + DefaultValueFormat = "DEFAULT {0}"; InitColumnTypeMap(); - } + } - public override IEnumerable GetTablesInSchema(Database db) - { - List list; - try - { - //needs to be open to read the schema name - db.OpenSharedConnection(); + public override IEnumerable GetTablesInSchema(Database db) + { + List list; + try + { + //needs to be open to read the schema name + db.OpenSharedConnection(); - var items = - db.Fetch( - "SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = @TableSchema", - new { TableSchema = db.Connection.Database }); - list = items.Select(x => x.TABLE_NAME).Cast().ToList(); - } - finally - { - db.CloseSharedConnection(); - } - return list; - } + var items = + db.Fetch( + "SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = @TableSchema", + new { TableSchema = db.Connection.Database }); + list = items.Select(x => x.TABLE_NAME).Cast().ToList(); + } + finally + { + db.CloseSharedConnection(); + } + return list; + } - public override IEnumerable GetColumnsInSchema(Database db) - { - List list; - try - { - //needs to be open to read the schema name - db.OpenSharedConnection(); + public override IEnumerable GetColumnsInSchema(Database db) + { + List list; + try + { + //needs to be open to read the schema name + db.OpenSharedConnection(); - var items = - db.Fetch( - "SELECT TABLE_NAME, COLUMN_NAME, ORDINAL_POSITION, COLUMN_DEFAULT, IS_NULLABLE, DATA_TYPE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = @TableSchema", - new { TableSchema = db.Connection.Database }); - list = - items.Select( - item => - new ColumnInfo(item.TABLE_NAME, item.COLUMN_NAME, int.Parse(item.ORDINAL_POSITION.ToString()), item.COLUMN_DEFAULT ?? "", - item.IS_NULLABLE, item.DATA_TYPE)).ToList(); - } - finally - { - db.CloseSharedConnection(); - } - return list; - } + var items = + db.Fetch( + "SELECT TABLE_NAME, COLUMN_NAME, ORDINAL_POSITION, COLUMN_DEFAULT, IS_NULLABLE, DATA_TYPE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = @TableSchema", + new { TableSchema = db.Connection.Database }); + list = + items.Select( + item => + new ColumnInfo(item.TABLE_NAME, item.COLUMN_NAME, int.Parse(item.ORDINAL_POSITION.ToString()), item.COLUMN_DEFAULT ?? "", + item.IS_NULLABLE, item.DATA_TYPE)).ToList(); + } + finally + { + db.CloseSharedConnection(); + } + return list; + } - public override IEnumerable> GetConstraintsPerTable(Database db) - { - List> list; - try - { - //needs to be open to read the schema name - db.OpenSharedConnection(); + public override IEnumerable> GetConstraintsPerTable(Database db) + { + List> list; + try + { + //needs to be open to read the schema name + db.OpenSharedConnection(); - //Does not include indexes and constraints are named differently - var items = - db.Fetch( - "SELECT TABLE_NAME, CONSTRAINT_NAME FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE TABLE_SCHEMA = @TableSchema", - new { TableSchema = db.Connection.Database }); - list = items.Select(item => new Tuple(item.TABLE_NAME, item.CONSTRAINT_NAME)).ToList(); - } - finally - { - db.CloseSharedConnection(); - } - return list; - } + //Does not include indexes and constraints are named differently + var items = + db.Fetch( + "SELECT TABLE_NAME, CONSTRAINT_NAME FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE TABLE_SCHEMA = @TableSchema", + new { TableSchema = db.Connection.Database }); + list = items.Select(item => new Tuple(item.TABLE_NAME, item.CONSTRAINT_NAME)).ToList(); + } + finally + { + db.CloseSharedConnection(); + } + return list; + } - public override IEnumerable> GetConstraintsPerColumn(Database db) - { - List> list; - try - { - //needs to be open to read the schema name - db.OpenSharedConnection(); + public override IEnumerable> GetConstraintsPerColumn(Database db) + { + List> list; + try + { + //needs to be open to read the schema name + db.OpenSharedConnection(); - //Does not include indexes and constraints are named differently - var items = - db.Fetch( - "SELECT TABLE_NAME, COLUMN_NAME, CONSTRAINT_NAME FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE WHERE TABLE_SCHEMA = @TableSchema", - new { TableSchema = db.Connection.Database }); - list = - items.Select( - item => - new Tuple(item.TABLE_NAME, item.COLUMN_NAME, item.CONSTRAINT_NAME)) - .ToList(); - } - finally - { - db.CloseSharedConnection(); - } - return list; - } + //Does not include indexes and constraints are named differently + var items = + db.Fetch( + "SELECT TABLE_NAME, COLUMN_NAME, CONSTRAINT_NAME FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE WHERE TABLE_SCHEMA = @TableSchema", + new { TableSchema = db.Connection.Database }); + list = + items.Select( + item => + new Tuple(item.TABLE_NAME, item.COLUMN_NAME, item.CONSTRAINT_NAME)) + .ToList(); + } + finally + { + db.CloseSharedConnection(); + } + return list; + } - public override IEnumerable> GetDefinedIndexes(Database db) - { - List> list; - try - { - //needs to be open to read the schema name - db.OpenSharedConnection(); + public override IEnumerable> GetDefinedIndexes(Database db) + { + List> list; + try + { + //needs to be open to read the schema name + db.OpenSharedConnection(); - var indexes = - db.Fetch(@"SELECT DISTINCT + var indexes = + db.Fetch(@"SELECT DISTINCT TABLE_NAME, INDEX_NAME, COLUMN_NAME, CASE NON_UNIQUE WHEN 1 THEN 0 ELSE 1 END AS `UNIQUE` FROM INFORMATION_SCHEMA.STATISTICS WHERE TABLE_SCHEMA = @TableSchema AND INDEX_NAME <> COLUMN_NAME AND INDEX_NAME <> 'PRIMARY' ORDER BY TABLE_NAME, INDEX_NAME", - new { TableSchema = db.Connection.Database }); - list = - indexes.Select( - item => - new Tuple(item.TABLE_NAME, item.INDEX_NAME, item.COLUMN_NAME, item.UNIQUE == 1)) - .ToList(); - } - finally - { - db.CloseSharedConnection(); - } - return list; - } + new { TableSchema = db.Connection.Database }); + list = + indexes.Select( + item => + new Tuple(item.TABLE_NAME, item.INDEX_NAME, item.COLUMN_NAME, item.UNIQUE == 1)) + .ToList(); + } + finally + { + db.CloseSharedConnection(); + } + return list; + } - public override bool DoesTableExist(Database db, string tableName) - { - long result; - try - { - //needs to be open to read the schema name - db.OpenSharedConnection(); + public override bool DoesTableExist(Database db, string tableName) + { + long result; + try + { + //needs to be open to read the schema name + db.OpenSharedConnection(); - result = - db.ExecuteScalar("SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES " + - "WHERE TABLE_NAME = @TableName AND " + - "TABLE_SCHEMA = @TableSchema", - new { TableName = tableName, TableSchema = db.Connection.Database }); + result = + db.ExecuteScalar("SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES " + + "WHERE TABLE_NAME = @TableName AND " + + "TABLE_SCHEMA = @TableSchema", + new { TableName = tableName, TableSchema = db.Connection.Database }); - } - finally - { - db.CloseSharedConnection(); - } + } + finally + { + db.CloseSharedConnection(); + } - return result > 0; - } + return result > 0; + } - public override bool SupportsClustered() - { - return true; - } + public override bool SupportsClustered() + { + return true; + } - public override bool SupportsIdentityInsert() - { - return false; - } + public override bool SupportsIdentityInsert() + { + return false; + } - /// - /// This is used ONLY if we need to format datetime without using SQL parameters (i.e. during migrations) - /// - /// - /// - /// - /// - /// MySQL has a DateTime standard that is unambiguous and works on all servers: - /// YYYYMMDDHHMMSS - /// - public override string FormatDateTime(DateTime date, bool includeTime = true) - { - return includeTime ? date.ToString("yyyyMMddHHmmss") : date.ToString("yyyyMMdd"); - } + /// + /// This is used ONLY if we need to format datetime without using SQL parameters (i.e. during migrations) + /// + /// + /// + /// + /// + /// MySQL has a DateTime standard that is unambiguous and works on all servers: + /// YYYYMMDDHHMMSS + /// + public override string FormatDateTime(DateTime date, bool includeTime = true) + { + return includeTime ? date.ToString("yyyyMMddHHmmss") : date.ToString("yyyyMMdd"); + } - public override string GetQuotedTableName(string tableName) - { - return string.Format("`{0}`", tableName); - } + public override string GetQuotedTableName(string tableName) + { + return string.Format("`{0}`", tableName); + } - public override string GetQuotedColumnName(string columnName) - { - return string.Format("`{0}`", columnName); - } + public override string GetQuotedColumnName(string columnName) + { + return string.Format("`{0}`", columnName); + } - public override string GetQuotedName(string name) - { - return string.Format("`{0}`", name); - } + public override string GetQuotedName(string name) + { + return string.Format("`{0}`", name); + } - public override string GetSpecialDbType(SpecialDbTypes dbTypes) - { - if (dbTypes == SpecialDbTypes.NCHAR) - { - return "CHAR"; - } - else if (dbTypes == SpecialDbTypes.NTEXT) - return "LONGTEXT"; + public override string GetSpecialDbType(SpecialDbTypes dbTypes) + { + if (dbTypes == SpecialDbTypes.NCHAR) + { + return "CHAR"; + } + else if (dbTypes == SpecialDbTypes.NTEXT) + return "LONGTEXT"; - return "NVARCHAR"; - } + return "NVARCHAR"; + } - public override string Format(TableDefinition table) - { - string primaryKey = string.Empty; - var columnDefinition = table.Columns.FirstOrDefault(x => x.IsPrimaryKey); - if (columnDefinition != null) - { - string columns = string.IsNullOrEmpty(columnDefinition.PrimaryKeyColumns) - ? GetQuotedColumnName(columnDefinition.Name) - : string.Join(", ", columnDefinition.PrimaryKeyColumns - .Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries) - .Select(GetQuotedColumnName)); + public override string Format(TableDefinition table) + { + string primaryKey = string.Empty; + var columnDefinition = table.Columns.FirstOrDefault(x => x.IsPrimaryKey); + if (columnDefinition != null) + { + string columns = string.IsNullOrEmpty(columnDefinition.PrimaryKeyColumns) + ? GetQuotedColumnName(columnDefinition.Name) + : string.Join(", ", columnDefinition.PrimaryKeyColumns + .Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries) + .Select(GetQuotedColumnName)); - primaryKey = string.Format(", \nPRIMARY KEY {0} ({1})", columnDefinition.IsIndexed ? "CLUSTERED" : "NONCLUSTERED", columns); - } + primaryKey = string.Format(", \nPRIMARY KEY {0} ({1})", columnDefinition.IsIndexed ? "CLUSTERED" : "NONCLUSTERED", columns); + } - var statement = string.Format(CreateTable, GetQuotedTableName(table.Name), Format(table.Columns), primaryKey); + var statement = string.Format(CreateTable, GetQuotedTableName(table.Name), Format(table.Columns), primaryKey); - return statement; - } + return statement; + } - public override string Format(IndexDefinition index) - { - string name = string.IsNullOrEmpty(index.Name) - ? string.Format("IX_{0}_{1}", index.TableName, index.ColumnName) - : index.Name; + public override string Format(IndexDefinition index) + { + string name = string.IsNullOrEmpty(index.Name) + ? string.Format("IX_{0}_{1}", index.TableName, index.ColumnName) + : index.Name; - string columns = index.Columns.Any() - ? string.Join(",", index.Columns.Select(x => GetQuotedColumnName(x.Name))) - : GetQuotedColumnName(index.ColumnName); + string columns = index.Columns.Any() + ? string.Join(",", index.Columns.Select(x => GetQuotedColumnName(x.Name))) + : GetQuotedColumnName(index.ColumnName); - return string.Format(CreateIndex, - GetQuotedName(name), - GetQuotedTableName(index.TableName), - columns); - } + return string.Format(CreateIndex, + GetQuotedName(name), + GetQuotedTableName(index.TableName), + columns); + } - public override string Format(ForeignKeyDefinition foreignKey) - { - return string.Format(CreateForeignKeyConstraint, - GetQuotedTableName(foreignKey.ForeignTable), - GetQuotedColumnName(foreignKey.ForeignColumns.First()), - GetQuotedTableName(foreignKey.PrimaryTable), - GetQuotedColumnName(foreignKey.PrimaryColumns.First()), - FormatCascade("DELETE", foreignKey.OnDelete), - FormatCascade("UPDATE", foreignKey.OnUpdate)); - } + public override string Format(ForeignKeyDefinition foreignKey) + { + return string.Format(CreateForeignKeyConstraint, + GetQuotedTableName(foreignKey.ForeignTable), + GetQuotedColumnName(foreignKey.ForeignColumns.First()), + GetQuotedTableName(foreignKey.PrimaryTable), + GetQuotedColumnName(foreignKey.PrimaryColumns.First()), + FormatCascade("DELETE", foreignKey.OnDelete), + FormatCascade("UPDATE", foreignKey.OnUpdate)); + } - public override string FormatPrimaryKey(TableDefinition table) - { - return string.Empty; - } + public override string FormatPrimaryKey(TableDefinition table) + { + return string.Empty; + } - protected override string FormatConstraint(ColumnDefinition column) - { - return string.Empty; - } + protected override string FormatConstraint(ColumnDefinition column) + { + return string.Empty; + } - protected override string FormatIdentity(ColumnDefinition column) - { - return column.IsIdentity ? AutoIncrementDefinition : string.Empty; - } + protected override string FormatIdentity(ColumnDefinition column) + { + return column.IsIdentity ? AutoIncrementDefinition : string.Empty; + } - protected override string FormatDefaultValue(ColumnDefinition column) - { - if (column.DefaultValue == null) - return string.Empty; + protected override string FormatDefaultValue(ColumnDefinition column) + { + if (column.DefaultValue == null) + return string.Empty; - //hack - probably not needed with latest changes - if (column.DefaultValue.ToString().ToLower().Equals("getdate()".ToLower())) - column.DefaultValue = SystemMethods.CurrentDateTime; + //hack - probably not needed with latest changes + if (column.DefaultValue.ToString().ToLower().Equals("getdate()".ToLower())) + column.DefaultValue = SystemMethods.CurrentDateTime; - // see if this is for a system method - if (column.DefaultValue is SystemMethods) - { - string method = FormatSystemMethods((SystemMethods)column.DefaultValue); - if (string.IsNullOrEmpty(method)) - return string.Empty; + // see if this is for a system method + if (column.DefaultValue is SystemMethods) + { + string method = FormatSystemMethods((SystemMethods)column.DefaultValue); + if (string.IsNullOrEmpty(method)) + return string.Empty; - return string.Format(DefaultValueFormat, method); - } + return string.Format(DefaultValueFormat, method); + } - //needs quote - return string.Format(DefaultValueFormat, string.Format("'{0}'", column.DefaultValue)); - } + //needs quote + return string.Format(DefaultValueFormat, string.Format("'{0}'", column.DefaultValue)); + } - protected override string FormatPrimaryKey(ColumnDefinition column) - { - return string.Empty; - } + protected override string FormatPrimaryKey(ColumnDefinition column) + { + return string.Empty; + } - protected override string FormatSystemMethods(SystemMethods systemMethod) - { - switch (systemMethod) - { - case SystemMethods.NewGuid: - return null; // NOT SUPPORTED! - //return "NEWID()"; - case SystemMethods.CurrentDateTime: - return "CURRENT_TIMESTAMP"; - //case SystemMethods.NewSequentialId: - // return "NEWSEQUENTIALID()"; - //case SystemMethods.CurrentUTCDateTime: - // return "GETUTCDATE()"; - } + protected override string FormatSystemMethods(SystemMethods systemMethod) + { + switch (systemMethod) + { + case SystemMethods.NewGuid: + return null; // NOT SUPPORTED! + //return "NEWID()"; + case SystemMethods.CurrentDateTime: + return "CURRENT_TIMESTAMP"; + //case SystemMethods.NewSequentialId: + // return "NEWSEQUENTIALID()"; + //case SystemMethods.CurrentUTCDateTime: + // return "GETUTCDATE()"; + } - return null; - } + return null; + } - public override string DeleteDefaultConstraint - { - get - { - return "ALTER TABLE {0} ALTER COLUMN {1} DROP DEFAULT"; - } - } + public override string DeleteDefaultConstraint + { + get + { + return "ALTER TABLE {0} ALTER COLUMN {1} DROP DEFAULT"; + } + } - public override string AlterColumn { get { return "ALTER TABLE {0} MODIFY COLUMN {1}"; } } + public override string AlterColumn { get { return "ALTER TABLE {0} MODIFY COLUMN {1}"; } } - //CREATE TABLE {0} ({1}) ENGINE = INNODB versus CREATE TABLE {0} ({1}) ENGINE = MYISAM ? - public override string CreateTable { get { return "CREATE TABLE {0} ({1}{2})"; } } + //CREATE TABLE {0} ({1}) ENGINE = INNODB versus CREATE TABLE {0} ({1}) ENGINE = MYISAM ? + public override string CreateTable { get { return "CREATE TABLE {0} ({1}{2})"; } } - public override string CreateIndex { get { return "CREATE INDEX {0} ON {1} ({2})"; } } + public override string CreateIndex { get { return "CREATE INDEX {0} ON {1} ({2})"; } } - public override string CreateForeignKeyConstraint { get { return "ALTER TABLE {0} ADD FOREIGN KEY ({1}) REFERENCES {2} ({3}){4}{5}"; } } + public override string CreateForeignKeyConstraint { get { return "ALTER TABLE {0} ADD FOREIGN KEY ({1}) REFERENCES {2} ({3}){4}{5}"; } } - public override string DeleteConstraint { get { return "ALTER TABLE {0} DROP {1} {2}"; } } + public override string DeleteConstraint { get { return "ALTER TABLE {0} DROP {1} {2}"; } } - public override string DropIndex { get { return "DROP INDEX {0} ON {1}"; } } + public override string DropIndex { get { return "DROP INDEX {0} ON {1}"; } } - public override string RenameColumn { get { return "ALTER TABLE {0} CHANGE {1} {2}"; } } + public override string RenameColumn { get { return "ALTER TABLE {0} CHANGE {1} {2}"; } } + public override string IsNull { get { return "IFNULL({0},{1})"; } } + public override string ConvertIntegerToOrderableString { get { return "LPAD({0}, 8, '0')"; } } + public override string ConvertDateToOrderableString { get { return "DATE_FORMAT({0}, '%Y%m%d')"; } } - public override bool? SupportsCaseInsensitiveQueries(Database db) - { - bool? supportsCaseInsensitiveQueries = null; + public override bool? SupportsCaseInsensitiveQueries(Database db) + { + bool? supportsCaseInsensitiveQueries = null; - try - { - db.OpenSharedConnection(); - // Need 4 @ signs as it is regarded as a parameter, @@ escapes it once, @@@@ escapes it twice - var lowerCaseTableNames = db.Fetch("SELECT @@@@Global.lower_case_table_names"); + try + { + db.OpenSharedConnection(); + // Need 4 @ signs as it is regarded as a parameter, @@ escapes it once, @@@@ escapes it twice + var lowerCaseTableNames = db.Fetch("SELECT @@@@Global.lower_case_table_names"); - if (lowerCaseTableNames.Any()) - supportsCaseInsensitiveQueries = lowerCaseTableNames.First() == 1; - } - catch (Exception ex) - { - _logger.Error("Error querying for lower_case support", ex); - } - finally - { - db.CloseSharedConnection(); - } + if (lowerCaseTableNames.Any()) + supportsCaseInsensitiveQueries = lowerCaseTableNames.First() == 1; + } + catch (Exception ex) + { + _logger.Error("Error querying for lower_case support", ex); + } + finally + { + db.CloseSharedConnection(); + } - // Could return null, which means testing failed, - // add message to check with their hosting provider - return supportsCaseInsensitiveQueries; - } + // Could return null, which means testing failed, + // add message to check with their hosting provider + return supportsCaseInsensitiveQueries; + } - public override string EscapeString(string val) - { - return PetaPocoExtensions.EscapeAtSymbols(MySql.Data.MySqlClient.MySqlHelper.EscapeString(val)); - } - } + public override string EscapeString(string val) + { + return PetaPocoExtensions.EscapeAtSymbols(MySql.Data.MySqlClient.MySqlHelper.EscapeString(val)); + } + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs b/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs index c2b81aa753..3b3fe103d0 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs @@ -10,531 +10,535 @@ using Umbraco.Core.Persistence.Querying; namespace Umbraco.Core.Persistence.SqlSyntax { - /// - /// Represents the Base Sql Syntax provider implementation. - /// - /// - /// All Sql Syntax provider implementations should derive from this abstract class. - /// - /// - public abstract class SqlSyntaxProviderBase : ISqlSyntaxProvider - where TSyntax : ISqlSyntaxProvider - { - protected SqlSyntaxProviderBase() - { - ClauseOrder = new List> - { - FormatString, - FormatType, - FormatNullable, - FormatConstraint, - FormatDefaultValue, - FormatPrimaryKey, - FormatIdentity - }; - - //defaults for all providers - StringLengthColumnDefinitionFormat = StringLengthUnicodeColumnDefinitionFormat; - StringColumnDefinition = string.Format(StringLengthColumnDefinitionFormat, DefaultStringLength); - DecimalColumnDefinition = string.Format(DecimalColumnDefinitionFormat, DefaultDecimalPrecision, DefaultDecimalScale); - - InitColumnTypeMap(); - } - - public string GetWildcardPlaceholder() - { - return "%"; - } - - public string StringLengthNonUnicodeColumnDefinitionFormat = "VARCHAR({0})"; - public string StringLengthUnicodeColumnDefinitionFormat = "NVARCHAR({0})"; - public string DecimalColumnDefinitionFormat = "DECIMAL({0},{1})"; - - public string DefaultValueFormat = "DEFAULT ({0})"; - public int DefaultStringLength = 255; - public int DefaultDecimalPrecision = 20; - public int DefaultDecimalScale = 9; - - //Set by Constructor - public string StringColumnDefinition; - public string StringLengthColumnDefinitionFormat; - - public string AutoIncrementDefinition = "AUTOINCREMENT"; - public string IntColumnDefinition = "INTEGER"; - public string LongColumnDefinition = "BIGINT"; - public string GuidColumnDefinition = "GUID"; - public string BoolColumnDefinition = "BOOL"; - public string RealColumnDefinition = "DOUBLE"; - public string DecimalColumnDefinition; - public string BlobColumnDefinition = "BLOB"; - public string DateTimeColumnDefinition = "DATETIME"; - public string TimeColumnDefinition = "DATETIME"; - - protected IList> ClauseOrder { get; set; } - - protected DbTypes DbTypeMap = new DbTypes(); - protected void InitColumnTypeMap() - { - DbTypeMap.Set(DbType.String, StringColumnDefinition); - DbTypeMap.Set(DbType.StringFixedLength, StringColumnDefinition); - DbTypeMap.Set(DbType.StringFixedLength, StringColumnDefinition); - DbTypeMap.Set(DbType.String, StringColumnDefinition); - DbTypeMap.Set(DbType.Boolean, BoolColumnDefinition); - DbTypeMap.Set(DbType.Boolean, BoolColumnDefinition); - DbTypeMap.Set(DbType.Guid, GuidColumnDefinition); - DbTypeMap.Set(DbType.Guid, GuidColumnDefinition); - DbTypeMap.Set(DbType.DateTime, DateTimeColumnDefinition); - DbTypeMap.Set(DbType.DateTime, DateTimeColumnDefinition); - DbTypeMap.Set(DbType.Time, TimeColumnDefinition); - DbTypeMap.Set(DbType.Time, TimeColumnDefinition); - DbTypeMap.Set(DbType.Time, TimeColumnDefinition); - DbTypeMap.Set(DbType.Time, TimeColumnDefinition); - - DbTypeMap.Set(DbType.Byte, IntColumnDefinition); - DbTypeMap.Set(DbType.Byte, IntColumnDefinition); - DbTypeMap.Set(DbType.SByte, IntColumnDefinition); - DbTypeMap.Set(DbType.SByte, IntColumnDefinition); - DbTypeMap.Set(DbType.Int16, IntColumnDefinition); - DbTypeMap.Set(DbType.Int16, IntColumnDefinition); - DbTypeMap.Set(DbType.UInt16, IntColumnDefinition); - DbTypeMap.Set(DbType.UInt16, IntColumnDefinition); - DbTypeMap.Set(DbType.Int32, IntColumnDefinition); - DbTypeMap.Set(DbType.Int32, IntColumnDefinition); - DbTypeMap.Set(DbType.UInt32, IntColumnDefinition); - DbTypeMap.Set(DbType.UInt32, IntColumnDefinition); - - DbTypeMap.Set(DbType.Int64, LongColumnDefinition); - DbTypeMap.Set(DbType.Int64, LongColumnDefinition); - DbTypeMap.Set(DbType.UInt64, LongColumnDefinition); - DbTypeMap.Set(DbType.UInt64, LongColumnDefinition); - - DbTypeMap.Set(DbType.Single, RealColumnDefinition); - DbTypeMap.Set(DbType.Single, RealColumnDefinition); - DbTypeMap.Set(DbType.Double, RealColumnDefinition); - DbTypeMap.Set(DbType.Double, RealColumnDefinition); - - DbTypeMap.Set(DbType.Decimal, DecimalColumnDefinition); - DbTypeMap.Set(DbType.Decimal, DecimalColumnDefinition); - - DbTypeMap.Set(DbType.Binary, BlobColumnDefinition); - } - - public virtual string EscapeString(string val) - { - return PetaPocoExtensions.EscapeAtSymbols(val.Replace("'", "''")); - } - - public virtual string GetStringColumnEqualComparison(string column, int paramIndex, TextColumnType columnType) - { - //use the 'upper' method to always ensure strings are matched without case sensitivity no matter what the db setting. - return string.Format("upper({0}) = upper(@{1})", column, paramIndex); - } - - public virtual string GetStringColumnWildcardComparison(string column, int paramIndex, TextColumnType columnType) - { - //use the 'upper' method to always ensure strings are matched without case sensitivity no matter what the db setting. - return string.Format("upper({0}) LIKE upper(@{1})", column, paramIndex); - } - - [Obsolete("Use the overload with the parameter index instead")] - public virtual string GetStringColumnEqualComparison(string column, string value, TextColumnType columnType) - { - //use the 'upper' method to always ensure strings are matched without case sensitivity no matter what the db setting. - return string.Format("upper({0}) = '{1}'", column, value.ToUpper()); - } - - [Obsolete("Use the overload with the parameter index instead")] - public virtual string GetStringColumnStartsWithComparison(string column, string value, TextColumnType columnType) - { - //use the 'upper' method to always ensure strings are matched without case sensitivity no matter what the db setting. - return string.Format("upper({0}) LIKE '{1}%'", column, value.ToUpper()); - } - - [Obsolete("Use the overload with the parameter index instead")] - public virtual string GetStringColumnEndsWithComparison(string column, string value, TextColumnType columnType) - { - //use the 'upper' method to always ensure strings are matched without case sensitivity no matter what the db setting. - return string.Format("upper({0}) LIKE '%{1}'", column, value.ToUpper()); - } - - [Obsolete("Use the overload with the parameter index instead")] - public virtual string GetStringColumnContainsComparison(string column, string value, TextColumnType columnType) - { - //use the 'upper' method to always ensure strings are matched without case sensitivity no matter what the db setting. - return string.Format("upper({0}) LIKE '%{1}%'", column, value.ToUpper()); - } - - [Obsolete("Use the overload with the parameter index instead")] - public virtual string GetStringColumnWildcardComparison(string column, string value, TextColumnType columnType) - { - //use the 'upper' method to always ensure strings are matched without case sensitivity no matter what the db setting. - return string.Format("upper({0}) LIKE '{1}'", column, value.ToUpper()); - } - - public virtual string GetQuotedTableName(string tableName) - { - return string.Format("\"{0}\"", tableName); - } - - public virtual string GetQuotedColumnName(string columnName) - { - return string.Format("\"{0}\"", columnName); - } - - public virtual string GetQuotedName(string name) - { - return string.Format("\"{0}\"", name); - } - - public virtual string GetQuotedValue(string value) - { - return string.Format("'{0}'", value); - } - - public virtual string GetIndexType(IndexTypes indexTypes) - { - string indexType; - - if (indexTypes == IndexTypes.Clustered) - { - indexType = "CLUSTERED"; - } - else - { - indexType = indexTypes == IndexTypes.NonClustered - ? "NONCLUSTERED" - : "UNIQUE NONCLUSTERED"; - } - return indexType; - } - - public virtual string GetSpecialDbType(SpecialDbTypes dbTypes) - { - if (dbTypes == SpecialDbTypes.NCHAR) - { - return "NCHAR"; - } - else if (dbTypes == SpecialDbTypes.NTEXT) - return "NTEXT"; - - return "NVARCHAR"; - } - - public virtual bool? SupportsCaseInsensitiveQueries(Database db) - { - return true; - } - - public virtual IEnumerable GetTablesInSchema(Database db) - { - return new List(); - } - - public virtual IEnumerable GetColumnsInSchema(Database db) - { - return new List(); - } - - public virtual IEnumerable> GetConstraintsPerTable(Database db) - { - return new List>(); - } - - public virtual IEnumerable> GetConstraintsPerColumn(Database db) - { - return new List>(); - } - - public abstract IEnumerable> GetDefinedIndexes(Database db); - - public virtual bool DoesTableExist(Database db, string tableName) - { - return false; - } - - public virtual bool SupportsClustered() - { - return true; - } - - public virtual bool SupportsIdentityInsert() - { - return true; - } - - /// - /// This is used ONLY if we need to format datetime without using SQL parameters (i.e. during migrations) - /// - /// - /// - /// - /// - /// MSSQL has a DateTime standard that is unambiguous and works on all servers: - /// YYYYMMDD HH:mm:ss - /// - public virtual string FormatDateTime(DateTime date, bool includeTime = true) - { - // need CultureInfo.InvariantCulture because ":" here is the "time separator" and - // may be converted to something else in different cultures (eg "." in DK). - return date.ToString(includeTime ? "yyyyMMdd HH:mm:ss" : "yyyyMMdd", CultureInfo.InvariantCulture); - } - - public virtual string Format(TableDefinition table) - { - var statement = string.Format(CreateTable, GetQuotedTableName(table.Name), Format(table.Columns)); - - return statement; - } - - public virtual List Format(IEnumerable indexes) - { - return indexes.Select(Format).ToList(); - } - - public virtual string Format(IndexDefinition index) - { - string name = string.IsNullOrEmpty(index.Name) - ? string.Format("IX_{0}_{1}", index.TableName, index.ColumnName) - : index.Name; - - string columns = index.Columns.Any() - ? string.Join(",", index.Columns.Select(x => GetQuotedColumnName(x.Name))) - : GetQuotedColumnName(index.ColumnName); - - return string.Format(CreateIndex, GetIndexType(index.IndexType), " ", GetQuotedName(name), - GetQuotedTableName(index.TableName), columns); - } - - public virtual List Format(IEnumerable foreignKeys) - { - return foreignKeys.Select(Format).ToList(); - } - - public virtual string Format(ForeignKeyDefinition foreignKey) - { - string constraintName = string.IsNullOrEmpty(foreignKey.Name) - ? string.Format("FK_{0}_{1}_{2}", foreignKey.ForeignTable, foreignKey.PrimaryTable, foreignKey.PrimaryColumns.First()) - : foreignKey.Name; - - return string.Format(CreateForeignKeyConstraint, - GetQuotedTableName(foreignKey.ForeignTable), - GetQuotedName(constraintName), - GetQuotedColumnName(foreignKey.ForeignColumns.First()), - GetQuotedTableName(foreignKey.PrimaryTable), - GetQuotedColumnName(foreignKey.PrimaryColumns.First()), - FormatCascade("DELETE", foreignKey.OnDelete), - FormatCascade("UPDATE", foreignKey.OnUpdate)); - } - - public virtual string Format(IEnumerable columns) - { - var sb = new StringBuilder(); - foreach (var column in columns) - { - sb.Append(Format(column) +",\n"); - } - return sb.ToString().TrimEnd(",\n"); - } - - public virtual string Format(ColumnDefinition column) - { - var clauses = new List(); - - foreach (var action in ClauseOrder) - { - string clause = action(column); - if (!string.IsNullOrEmpty(clause)) - clauses.Add(clause); - } - - return string.Join(" ", clauses.ToArray()); - } - - public virtual string FormatPrimaryKey(TableDefinition table) - { - var columnDefinition = table.Columns.FirstOrDefault(x => x.IsPrimaryKey); - if (columnDefinition == null) - return string.Empty; - - string constraintName = string.IsNullOrEmpty(columnDefinition.PrimaryKeyName) - ? string.Format("PK_{0}", table.Name) - : columnDefinition.PrimaryKeyName; - - string columns = string.IsNullOrEmpty(columnDefinition.PrimaryKeyColumns) - ? GetQuotedColumnName(columnDefinition.Name) - : string.Join(", ", columnDefinition.PrimaryKeyColumns - .Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries) - .Select(GetQuotedColumnName)); - - string primaryKeyPart = string.Concat("PRIMARY KEY", columnDefinition.IsIndexed ? " CLUSTERED" : " NONCLUSTERED"); - - return string.Format(CreateConstraint, - GetQuotedTableName(table.Name), - GetQuotedName(constraintName), - primaryKeyPart, - columns); - } - - public virtual string FormatColumnRename(string tableName, string oldName, string newName) - { - return string.Format(RenameColumn, - GetQuotedTableName(tableName), - GetQuotedColumnName(oldName), - GetQuotedColumnName(newName)); - } - - public virtual string FormatTableRename(string oldName, string newName) - { - return string.Format(RenameTable, GetQuotedTableName(oldName), GetQuotedTableName(newName)); - } - - protected virtual string FormatCascade(string onWhat, Rule rule) - { - string action = "NO ACTION"; - switch (rule) - { - case Rule.None: - return ""; - case Rule.Cascade: - action = "CASCADE"; - break; - case Rule.SetNull: - action = "SET NULL"; - break; - case Rule.SetDefault: - action = "SET DEFAULT"; - break; - } - - return string.Format(" ON {0} {1}", onWhat, action); - } - - protected virtual string FormatString(ColumnDefinition column) - { - return GetQuotedColumnName(column.Name); - } - - protected virtual string FormatType(ColumnDefinition column) - { - if (column.Type.HasValue == false && string.IsNullOrEmpty(column.CustomType) == false) - return column.CustomType; - - if (column.HasSpecialDbType) - { - if (column.Size != default(int)) - { - return string.Format("{0}({1})", - GetSpecialDbType(column.DbType), - column.Size); - } - - return GetSpecialDbType(column.DbType); - } - - Type type = column.Type.HasValue - ? DbTypeMap.ColumnDbTypeMap.First(x => x.Value == column.Type.Value).Key - : column.PropertyType; - - if (type == typeof (string)) - { - var valueOrDefault = column.Size != default(int) ? column.Size : DefaultStringLength; - return string.Format(StringLengthColumnDefinitionFormat, valueOrDefault); - } - - if (type == typeof(decimal)) - { - var precision = column.Size != default(int) ? column.Size : DefaultDecimalPrecision; - var scale = column.Precision != default(int) ? column.Precision : DefaultDecimalScale; - return string.Format(DecimalColumnDefinitionFormat, precision, scale); - } - - string definition = DbTypeMap.ColumnTypeMap.First(x => x.Key == type).Value; - string dbTypeDefinition = column.Size != default(int) - ? string.Format("{0}({1})", definition, column.Size) - : definition; - //NOTE Percision is left out - return dbTypeDefinition; - } - - protected virtual string FormatNullable(ColumnDefinition column) - { - return column.IsNullable ? "NULL" : "NOT NULL"; - } - - protected virtual string FormatConstraint(ColumnDefinition column) - { - if (string.IsNullOrEmpty(column.ConstraintName) && column.DefaultValue == null) - return string.Empty; - - return string.Format("CONSTRAINT {0}", - string.IsNullOrEmpty(column.ConstraintName) - ? GetQuotedName(string.Format("DF_{0}_{1}", column.TableName, column.Name)) - : column.ConstraintName); - } - - protected virtual string FormatDefaultValue(ColumnDefinition column) - { - if (column.DefaultValue == null) - return string.Empty; - - //hack - probably not needed with latest changes - if (column.DefaultValue.ToString().ToLower().Equals("getdate()".ToLower())) - column.DefaultValue = SystemMethods.CurrentDateTime; - - // see if this is for a system method - if (column.DefaultValue is SystemMethods) - { - string method = FormatSystemMethods((SystemMethods)column.DefaultValue); - if (string.IsNullOrEmpty(method)) - return string.Empty; - - return string.Format(DefaultValueFormat, method); - } - - return string.Format(DefaultValueFormat, GetQuotedValue(column.DefaultValue.ToString())); - } - - protected virtual string FormatPrimaryKey(ColumnDefinition column) - { - return string.Empty; - } - - protected abstract string FormatSystemMethods(SystemMethods systemMethod); - - protected abstract string FormatIdentity(ColumnDefinition column); - - public virtual string DeleteDefaultConstraint - { - get - { - throw new NotSupportedException("Default constraints are not supported"); - } - } - - public virtual string CreateTable { get { return "CREATE TABLE {0} ({1})"; } } - public virtual string DropTable { get { return "DROP TABLE {0}"; } } - - public virtual string AddColumn { get { return "ALTER TABLE {0} ADD COLUMN {1}"; } } - public virtual string DropColumn { get { return "ALTER TABLE {0} DROP COLUMN {1}"; } } - public virtual string AlterColumn { get { return "ALTER TABLE {0} ALTER COLUMN {1}"; } } - public virtual string RenameColumn { get { return "ALTER TABLE {0} RENAME COLUMN {1} TO {2}"; } } - - public virtual string RenameTable { get { return "RENAME TABLE {0} TO {1}"; } } - - public virtual string CreateSchema { get { return "CREATE SCHEMA {0}"; } } - public virtual string AlterSchema { get { return "ALTER SCHEMA {0} TRANSFER {1}.{2}"; } } - public virtual string DropSchema { get { return "DROP SCHEMA {0}"; } } - - public virtual string CreateIndex { get { return "CREATE {0}{1}INDEX {2} ON {3} ({4})"; } } - public virtual string DropIndex { get { return "DROP INDEX {0}"; } } - - public virtual string InsertData { get { return "INSERT INTO {0} ({1}) VALUES ({2})"; } } - public virtual string UpdateData { get { return "UPDATE {0} SET {1} WHERE {2}"; } } - public virtual string DeleteData { get { return "DELETE FROM {0} WHERE {1}"; } } - public virtual string TruncateTable { get { return "TRUNCATE TABLE {0}"; } } - - public virtual string CreateConstraint { get { return "ALTER TABLE {0} ADD CONSTRAINT {1} {2} ({3})"; } } - public virtual string DeleteConstraint { get { return "ALTER TABLE {0} DROP CONSTRAINT {1}"; } } - public virtual string CreateForeignKeyConstraint { get { return "ALTER TABLE {0} ADD CONSTRAINT {1} FOREIGN KEY ({2}) REFERENCES {3} ({4}){5}{6}"; } } - } + /// + /// Represents the Base Sql Syntax provider implementation. + /// + /// + /// All Sql Syntax provider implementations should derive from this abstract class. + /// + /// + public abstract class SqlSyntaxProviderBase : ISqlSyntaxProvider + where TSyntax : ISqlSyntaxProvider + { + protected SqlSyntaxProviderBase() + { + ClauseOrder = new List> + { + FormatString, + FormatType, + FormatNullable, + FormatConstraint, + FormatDefaultValue, + FormatPrimaryKey, + FormatIdentity + }; + + //defaults for all providers + StringLengthColumnDefinitionFormat = StringLengthUnicodeColumnDefinitionFormat; + StringColumnDefinition = string.Format(StringLengthColumnDefinitionFormat, DefaultStringLength); + DecimalColumnDefinition = string.Format(DecimalColumnDefinitionFormat, DefaultDecimalPrecision, DefaultDecimalScale); + + InitColumnTypeMap(); + } + + public string GetWildcardPlaceholder() + { + return "%"; + } + + public string StringLengthNonUnicodeColumnDefinitionFormat = "VARCHAR({0})"; + public string StringLengthUnicodeColumnDefinitionFormat = "NVARCHAR({0})"; + public string DecimalColumnDefinitionFormat = "DECIMAL({0},{1})"; + + public string DefaultValueFormat = "DEFAULT ({0})"; + public int DefaultStringLength = 255; + public int DefaultDecimalPrecision = 20; + public int DefaultDecimalScale = 9; + + //Set by Constructor + public string StringColumnDefinition; + public string StringLengthColumnDefinitionFormat; + + public string AutoIncrementDefinition = "AUTOINCREMENT"; + public string IntColumnDefinition = "INTEGER"; + public string LongColumnDefinition = "BIGINT"; + public string GuidColumnDefinition = "GUID"; + public string BoolColumnDefinition = "BOOL"; + public string RealColumnDefinition = "DOUBLE"; + public string DecimalColumnDefinition; + public string BlobColumnDefinition = "BLOB"; + public string DateTimeColumnDefinition = "DATETIME"; + public string TimeColumnDefinition = "DATETIME"; + + protected IList> ClauseOrder { get; set; } + + protected DbTypes DbTypeMap = new DbTypes(); + protected void InitColumnTypeMap() + { + DbTypeMap.Set(DbType.String, StringColumnDefinition); + DbTypeMap.Set(DbType.StringFixedLength, StringColumnDefinition); + DbTypeMap.Set(DbType.StringFixedLength, StringColumnDefinition); + DbTypeMap.Set(DbType.String, StringColumnDefinition); + DbTypeMap.Set(DbType.Boolean, BoolColumnDefinition); + DbTypeMap.Set(DbType.Boolean, BoolColumnDefinition); + DbTypeMap.Set(DbType.Guid, GuidColumnDefinition); + DbTypeMap.Set(DbType.Guid, GuidColumnDefinition); + DbTypeMap.Set(DbType.DateTime, DateTimeColumnDefinition); + DbTypeMap.Set(DbType.DateTime, DateTimeColumnDefinition); + DbTypeMap.Set(DbType.Time, TimeColumnDefinition); + DbTypeMap.Set(DbType.Time, TimeColumnDefinition); + DbTypeMap.Set(DbType.Time, TimeColumnDefinition); + DbTypeMap.Set(DbType.Time, TimeColumnDefinition); + + DbTypeMap.Set(DbType.Byte, IntColumnDefinition); + DbTypeMap.Set(DbType.Byte, IntColumnDefinition); + DbTypeMap.Set(DbType.SByte, IntColumnDefinition); + DbTypeMap.Set(DbType.SByte, IntColumnDefinition); + DbTypeMap.Set(DbType.Int16, IntColumnDefinition); + DbTypeMap.Set(DbType.Int16, IntColumnDefinition); + DbTypeMap.Set(DbType.UInt16, IntColumnDefinition); + DbTypeMap.Set(DbType.UInt16, IntColumnDefinition); + DbTypeMap.Set(DbType.Int32, IntColumnDefinition); + DbTypeMap.Set(DbType.Int32, IntColumnDefinition); + DbTypeMap.Set(DbType.UInt32, IntColumnDefinition); + DbTypeMap.Set(DbType.UInt32, IntColumnDefinition); + + DbTypeMap.Set(DbType.Int64, LongColumnDefinition); + DbTypeMap.Set(DbType.Int64, LongColumnDefinition); + DbTypeMap.Set(DbType.UInt64, LongColumnDefinition); + DbTypeMap.Set(DbType.UInt64, LongColumnDefinition); + + DbTypeMap.Set(DbType.Single, RealColumnDefinition); + DbTypeMap.Set(DbType.Single, RealColumnDefinition); + DbTypeMap.Set(DbType.Double, RealColumnDefinition); + DbTypeMap.Set(DbType.Double, RealColumnDefinition); + + DbTypeMap.Set(DbType.Decimal, DecimalColumnDefinition); + DbTypeMap.Set(DbType.Decimal, DecimalColumnDefinition); + + DbTypeMap.Set(DbType.Binary, BlobColumnDefinition); + } + + public virtual string EscapeString(string val) + { + return PetaPocoExtensions.EscapeAtSymbols(val.Replace("'", "''")); + } + + public virtual string GetStringColumnEqualComparison(string column, int paramIndex, TextColumnType columnType) + { + //use the 'upper' method to always ensure strings are matched without case sensitivity no matter what the db setting. + return string.Format("upper({0}) = upper(@{1})", column, paramIndex); + } + + public virtual string GetStringColumnWildcardComparison(string column, int paramIndex, TextColumnType columnType) + { + //use the 'upper' method to always ensure strings are matched without case sensitivity no matter what the db setting. + return string.Format("upper({0}) LIKE upper(@{1})", column, paramIndex); + } + + [Obsolete("Use the overload with the parameter index instead")] + public virtual string GetStringColumnEqualComparison(string column, string value, TextColumnType columnType) + { + //use the 'upper' method to always ensure strings are matched without case sensitivity no matter what the db setting. + return string.Format("upper({0}) = '{1}'", column, value.ToUpper()); + } + + [Obsolete("Use the overload with the parameter index instead")] + public virtual string GetStringColumnStartsWithComparison(string column, string value, TextColumnType columnType) + { + //use the 'upper' method to always ensure strings are matched without case sensitivity no matter what the db setting. + return string.Format("upper({0}) LIKE '{1}%'", column, value.ToUpper()); + } + + [Obsolete("Use the overload with the parameter index instead")] + public virtual string GetStringColumnEndsWithComparison(string column, string value, TextColumnType columnType) + { + //use the 'upper' method to always ensure strings are matched without case sensitivity no matter what the db setting. + return string.Format("upper({0}) LIKE '%{1}'", column, value.ToUpper()); + } + + [Obsolete("Use the overload with the parameter index instead")] + public virtual string GetStringColumnContainsComparison(string column, string value, TextColumnType columnType) + { + //use the 'upper' method to always ensure strings are matched without case sensitivity no matter what the db setting. + return string.Format("upper({0}) LIKE '%{1}%'", column, value.ToUpper()); + } + + [Obsolete("Use the overload with the parameter index instead")] + public virtual string GetStringColumnWildcardComparison(string column, string value, TextColumnType columnType) + { + //use the 'upper' method to always ensure strings are matched without case sensitivity no matter what the db setting. + return string.Format("upper({0}) LIKE '{1}'", column, value.ToUpper()); + } + + public virtual string GetQuotedTableName(string tableName) + { + return string.Format("\"{0}\"", tableName); + } + + public virtual string GetQuotedColumnName(string columnName) + { + return string.Format("\"{0}\"", columnName); + } + + public virtual string GetQuotedName(string name) + { + return string.Format("\"{0}\"", name); + } + + public virtual string GetQuotedValue(string value) + { + return string.Format("'{0}'", value); + } + + public virtual string GetIndexType(IndexTypes indexTypes) + { + string indexType; + + if (indexTypes == IndexTypes.Clustered) + { + indexType = "CLUSTERED"; + } + else + { + indexType = indexTypes == IndexTypes.NonClustered + ? "NONCLUSTERED" + : "UNIQUE NONCLUSTERED"; + } + return indexType; + } + + public virtual string GetSpecialDbType(SpecialDbTypes dbTypes) + { + if (dbTypes == SpecialDbTypes.NCHAR) + { + return "NCHAR"; + } + else if (dbTypes == SpecialDbTypes.NTEXT) + return "NTEXT"; + + return "NVARCHAR"; + } + + public virtual bool? SupportsCaseInsensitiveQueries(Database db) + { + return true; + } + + public virtual IEnumerable GetTablesInSchema(Database db) + { + return new List(); + } + + public virtual IEnumerable GetColumnsInSchema(Database db) + { + return new List(); + } + + public virtual IEnumerable> GetConstraintsPerTable(Database db) + { + return new List>(); + } + + public virtual IEnumerable> GetConstraintsPerColumn(Database db) + { + return new List>(); + } + + public abstract IEnumerable> GetDefinedIndexes(Database db); + + public virtual bool DoesTableExist(Database db, string tableName) + { + return false; + } + + public virtual bool SupportsClustered() + { + return true; + } + + public virtual bool SupportsIdentityInsert() + { + return true; + } + + /// + /// This is used ONLY if we need to format datetime without using SQL parameters (i.e. during migrations) + /// + /// + /// + /// + /// + /// MSSQL has a DateTime standard that is unambiguous and works on all servers: + /// YYYYMMDD HH:mm:ss + /// + public virtual string FormatDateTime(DateTime date, bool includeTime = true) + { + // need CultureInfo.InvariantCulture because ":" here is the "time separator" and + // may be converted to something else in different cultures (eg "." in DK). + return date.ToString(includeTime ? "yyyyMMdd HH:mm:ss" : "yyyyMMdd", CultureInfo.InvariantCulture); + } + + public virtual string Format(TableDefinition table) + { + var statement = string.Format(CreateTable, GetQuotedTableName(table.Name), Format(table.Columns)); + + return statement; + } + + public virtual List Format(IEnumerable indexes) + { + return indexes.Select(Format).ToList(); + } + + public virtual string Format(IndexDefinition index) + { + string name = string.IsNullOrEmpty(index.Name) + ? string.Format("IX_{0}_{1}", index.TableName, index.ColumnName) + : index.Name; + + string columns = index.Columns.Any() + ? string.Join(",", index.Columns.Select(x => GetQuotedColumnName(x.Name))) + : GetQuotedColumnName(index.ColumnName); + + return string.Format(CreateIndex, GetIndexType(index.IndexType), " ", GetQuotedName(name), + GetQuotedTableName(index.TableName), columns); + } + + public virtual List Format(IEnumerable foreignKeys) + { + return foreignKeys.Select(Format).ToList(); + } + + public virtual string Format(ForeignKeyDefinition foreignKey) + { + string constraintName = string.IsNullOrEmpty(foreignKey.Name) + ? string.Format("FK_{0}_{1}_{2}", foreignKey.ForeignTable, foreignKey.PrimaryTable, foreignKey.PrimaryColumns.First()) + : foreignKey.Name; + + return string.Format(CreateForeignKeyConstraint, + GetQuotedTableName(foreignKey.ForeignTable), + GetQuotedName(constraintName), + GetQuotedColumnName(foreignKey.ForeignColumns.First()), + GetQuotedTableName(foreignKey.PrimaryTable), + GetQuotedColumnName(foreignKey.PrimaryColumns.First()), + FormatCascade("DELETE", foreignKey.OnDelete), + FormatCascade("UPDATE", foreignKey.OnUpdate)); + } + + public virtual string Format(IEnumerable columns) + { + var sb = new StringBuilder(); + foreach (var column in columns) + { + sb.Append(Format(column) + ",\n"); + } + return sb.ToString().TrimEnd(",\n"); + } + + public virtual string Format(ColumnDefinition column) + { + var clauses = new List(); + + foreach (var action in ClauseOrder) + { + string clause = action(column); + if (!string.IsNullOrEmpty(clause)) + clauses.Add(clause); + } + + return string.Join(" ", clauses.ToArray()); + } + + public virtual string FormatPrimaryKey(TableDefinition table) + { + var columnDefinition = table.Columns.FirstOrDefault(x => x.IsPrimaryKey); + if (columnDefinition == null) + return string.Empty; + + string constraintName = string.IsNullOrEmpty(columnDefinition.PrimaryKeyName) + ? string.Format("PK_{0}", table.Name) + : columnDefinition.PrimaryKeyName; + + string columns = string.IsNullOrEmpty(columnDefinition.PrimaryKeyColumns) + ? GetQuotedColumnName(columnDefinition.Name) + : string.Join(", ", columnDefinition.PrimaryKeyColumns + .Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries) + .Select(GetQuotedColumnName)); + + string primaryKeyPart = string.Concat("PRIMARY KEY", columnDefinition.IsIndexed ? " CLUSTERED" : " NONCLUSTERED"); + + return string.Format(CreateConstraint, + GetQuotedTableName(table.Name), + GetQuotedName(constraintName), + primaryKeyPart, + columns); + } + + public virtual string FormatColumnRename(string tableName, string oldName, string newName) + { + return string.Format(RenameColumn, + GetQuotedTableName(tableName), + GetQuotedColumnName(oldName), + GetQuotedColumnName(newName)); + } + + public virtual string FormatTableRename(string oldName, string newName) + { + return string.Format(RenameTable, GetQuotedTableName(oldName), GetQuotedTableName(newName)); + } + + protected virtual string FormatCascade(string onWhat, Rule rule) + { + string action = "NO ACTION"; + switch (rule) + { + case Rule.None: + return ""; + case Rule.Cascade: + action = "CASCADE"; + break; + case Rule.SetNull: + action = "SET NULL"; + break; + case Rule.SetDefault: + action = "SET DEFAULT"; + break; + } + + return string.Format(" ON {0} {1}", onWhat, action); + } + + protected virtual string FormatString(ColumnDefinition column) + { + return GetQuotedColumnName(column.Name); + } + + protected virtual string FormatType(ColumnDefinition column) + { + if (column.Type.HasValue == false && string.IsNullOrEmpty(column.CustomType) == false) + return column.CustomType; + + if (column.HasSpecialDbType) + { + if (column.Size != default(int)) + { + return string.Format("{0}({1})", + GetSpecialDbType(column.DbType), + column.Size); + } + + return GetSpecialDbType(column.DbType); + } + + Type type = column.Type.HasValue + ? DbTypeMap.ColumnDbTypeMap.First(x => x.Value == column.Type.Value).Key + : column.PropertyType; + + if (type == typeof(string)) + { + var valueOrDefault = column.Size != default(int) ? column.Size : DefaultStringLength; + return string.Format(StringLengthColumnDefinitionFormat, valueOrDefault); + } + + if (type == typeof(decimal)) + { + var precision = column.Size != default(int) ? column.Size : DefaultDecimalPrecision; + var scale = column.Precision != default(int) ? column.Precision : DefaultDecimalScale; + return string.Format(DecimalColumnDefinitionFormat, precision, scale); + } + + string definition = DbTypeMap.ColumnTypeMap.First(x => x.Key == type).Value; + string dbTypeDefinition = column.Size != default(int) + ? string.Format("{0}({1})", definition, column.Size) + : definition; + //NOTE Percision is left out + return dbTypeDefinition; + } + + protected virtual string FormatNullable(ColumnDefinition column) + { + return column.IsNullable ? "NULL" : "NOT NULL"; + } + + protected virtual string FormatConstraint(ColumnDefinition column) + { + if (string.IsNullOrEmpty(column.ConstraintName) && column.DefaultValue == null) + return string.Empty; + + return string.Format("CONSTRAINT {0}", + string.IsNullOrEmpty(column.ConstraintName) + ? GetQuotedName(string.Format("DF_{0}_{1}", column.TableName, column.Name)) + : column.ConstraintName); + } + + protected virtual string FormatDefaultValue(ColumnDefinition column) + { + if (column.DefaultValue == null) + return string.Empty; + + //hack - probably not needed with latest changes + if (column.DefaultValue.ToString().ToLower().Equals("getdate()".ToLower())) + column.DefaultValue = SystemMethods.CurrentDateTime; + + // see if this is for a system method + if (column.DefaultValue is SystemMethods) + { + string method = FormatSystemMethods((SystemMethods)column.DefaultValue); + if (string.IsNullOrEmpty(method)) + return string.Empty; + + return string.Format(DefaultValueFormat, method); + } + + return string.Format(DefaultValueFormat, GetQuotedValue(column.DefaultValue.ToString())); + } + + protected virtual string FormatPrimaryKey(ColumnDefinition column) + { + return string.Empty; + } + + protected abstract string FormatSystemMethods(SystemMethods systemMethod); + + protected abstract string FormatIdentity(ColumnDefinition column); + + public virtual string DeleteDefaultConstraint + { + get + { + throw new NotSupportedException("Default constraints are not supported"); + } + } + + public virtual string CreateTable { get { return "CREATE TABLE {0} ({1})"; } } + public virtual string DropTable { get { return "DROP TABLE {0}"; } } + + public virtual string AddColumn { get { return "ALTER TABLE {0} ADD COLUMN {1}"; } } + public virtual string DropColumn { get { return "ALTER TABLE {0} DROP COLUMN {1}"; } } + public virtual string AlterColumn { get { return "ALTER TABLE {0} ALTER COLUMN {1}"; } } + public virtual string RenameColumn { get { return "ALTER TABLE {0} RENAME COLUMN {1} TO {2}"; } } + + public virtual string RenameTable { get { return "RENAME TABLE {0} TO {1}"; } } + + public virtual string CreateSchema { get { return "CREATE SCHEMA {0}"; } } + public virtual string AlterSchema { get { return "ALTER SCHEMA {0} TRANSFER {1}.{2}"; } } + public virtual string DropSchema { get { return "DROP SCHEMA {0}"; } } + + public virtual string CreateIndex { get { return "CREATE {0}{1}INDEX {2} ON {3} ({4})"; } } + public virtual string DropIndex { get { return "DROP INDEX {0}"; } } + + public virtual string InsertData { get { return "INSERT INTO {0} ({1}) VALUES ({2})"; } } + public virtual string UpdateData { get { return "UPDATE {0} SET {1} WHERE {2}"; } } + public virtual string DeleteData { get { return "DELETE FROM {0} WHERE {1}"; } } + public virtual string TruncateTable { get { return "TRUNCATE TABLE {0}"; } } + + public virtual string CreateConstraint { get { return "ALTER TABLE {0} ADD CONSTRAINT {1} {2} ({3})"; } } + public virtual string DeleteConstraint { get { return "ALTER TABLE {0} DROP CONSTRAINT {1}"; } } + public virtual string CreateForeignKeyConstraint { get { return "ALTER TABLE {0} ADD CONSTRAINT {1} FOREIGN KEY ({2}) REFERENCES {3} ({4}){5}{6}"; } } + + public virtual string IsNull { get { return "ISNULL({0},{1})"; } } + public virtual string ConvertIntegerToOrderableString { get { return "RIGHT('00000000' + CAST({0} AS varchar(8)),8)"; } } + public virtual string ConvertDateToOrderableString { get { return "CONVERT(varchar, {0}, 102)"; } } + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index e7298a5c6e..0e5b55866f 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -23,2319 +23,2321 @@ using Umbraco.Core.Publishing; namespace Umbraco.Core.Services { - /// - /// Represents the Content Service, which is an easy access to operations involving - /// - public class ContentService : RepositoryService, IContentService, IContentServiceOperations - { - private readonly IPublishingStrategy _publishingStrategy; - private readonly EntityXmlSerializer _entitySerializer = new EntityXmlSerializer(); - private readonly IDataTypeService _dataTypeService; - private readonly IUserService _userService; - - //Support recursive locks because some of the methods that require locking call other methods that require locking. - //for example, the Move method needs to be locked but this calls the Save method which also needs to be locked. - private static readonly ReaderWriterLockSlim Locker = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion); - - public ContentService( - IDatabaseUnitOfWorkProvider provider, - RepositoryFactory repositoryFactory, - ILogger logger, - IEventMessagesFactory eventMessagesFactory, - IPublishingStrategy publishingStrategy, - IDataTypeService dataTypeService, - IUserService userService) - : base(provider, repositoryFactory, logger, eventMessagesFactory) - { - if (publishingStrategy == null) throw new ArgumentNullException("publishingStrategy"); - if (dataTypeService == null) throw new ArgumentNullException("dataTypeService"); - if (userService == null) throw new ArgumentNullException("userService"); - _publishingStrategy = publishingStrategy; - _dataTypeService = dataTypeService; - _userService = userService; - } - - public int CountPublished(string contentTypeAlias = null) - { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateContentRepository(uow)) - { - return repository.CountPublished(); - } - } - - public int Count(string contentTypeAlias = null) - { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateContentRepository(uow)) - { - return repository.Count(contentTypeAlias); - } - } - - public int CountChildren(int parentId, string contentTypeAlias = null) - { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateContentRepository(uow)) - { - return repository.CountChildren(parentId, contentTypeAlias); - } - } - - public int CountDescendants(int parentId, string contentTypeAlias = null) - { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateContentRepository(uow)) - { - return repository.CountDescendants(parentId, contentTypeAlias); - } - } - - /// - /// Used to bulk update the permissions set for a content item. This will replace all permissions - /// assigned to an entity with a list of user id & permission pairs. - /// - /// - public void ReplaceContentPermissions(EntityPermissionSet permissionSet) - { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateContentRepository(uow)) - { - repository.ReplaceContentPermissions(permissionSet); - } - } - - /// - /// Assigns a single permission to the current content item for the specified user ids - /// - /// - /// - /// - public void AssignContentPermission(IContent entity, char permission, IEnumerable userIds) - { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateContentRepository(uow)) - { - repository.AssignEntityPermission(entity, permission, userIds); - } - } - - /// - /// Gets the list of permissions for the content item - /// - /// - /// - public IEnumerable GetPermissionsForEntity(IContent content) - { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateContentRepository(uow)) - { - return repository.GetPermissionsForEntity(content.Id); - } - } - - /// - /// 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, int parentId, string contentTypeAlias, int userId = 0) - { - var contentType = FindContentTypeByAlias(contentTypeAlias); - var content = new Content(name, parentId, contentType); - var parent = GetById(content.ParentId); - content.Path = string.Concat(parent.IfNotNull(x => x.Path, content.ParentId.ToString()), ",", content.Id); - - - if (Creating.IsRaisedEventCancelled(new NewEventArgs(content, contentTypeAlias, parentId), this)) - { - content.WasCancelled = true; - return content; - } - - content.CreatorId = userId; - content.WriterId = userId; - - Created.RaiseEvent(new NewEventArgs(content, false, contentTypeAlias, parentId), this); - - var uow = UowProvider.GetUnitOfWork(); - using (var auditRepo = RepositoryFactory.CreateAuditRepository(uow)) - { - auditRepo.AddOrUpdate(new AuditItem(content.Id, string.Format("Content '{0}' was created", name), AuditType.New, content.CreatorId)); - uow.Commit(); - } - - return content; - } - - /// - /// 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 - /// Parent object for the new Content - /// Alias of the - /// Optional id of the user creating the content - /// - public IContent CreateContent(string name, IContent parent, string contentTypeAlias, int userId = 0) - { - if (parent == null) throw new ArgumentNullException("parent"); - - var contentType = FindContentTypeByAlias(contentTypeAlias); - var content = new Content(name, parent, contentType); - content.Path = string.Concat(parent.Path, ",", content.Id); - - if (Creating.IsRaisedEventCancelled(new NewEventArgs(content, contentTypeAlias, parent), this)) - { - content.WasCancelled = true; - return content; - } - - content.CreatorId = userId; - content.WriterId = userId; - - Created.RaiseEvent(new NewEventArgs(content, false, contentTypeAlias, parent), this); - - Audit(AuditType.New, string.Format("Content '{0}' was created", name), content.CreatorId, content.Id); - - return content; - } - - /// - /// Creates and saves an object using the alias of the - /// that this Content should based on. - /// - /// - /// This method returns an object that has been persisted to the database - /// and therefor has an identity. - /// - /// 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 CreateContentWithIdentity(string name, int parentId, string contentTypeAlias, int userId = 0) - { - var contentType = FindContentTypeByAlias(contentTypeAlias); - var content = new Content(name, parentId, contentType); - - //NOTE: I really hate the notion of these Creating/Created events - they are so inconsistent, I've only just found - // out that in these 'WithIdentity' methods, the Saving/Saved events were not fired, wtf. Anyways, they're added now. - if (Creating.IsRaisedEventCancelled(new NewEventArgs(content, contentTypeAlias, parentId), this)) - { - content.WasCancelled = true; - return content; - } - - if (Saving.IsRaisedEventCancelled(new SaveEventArgs(content), this)) - { - content.WasCancelled = true; - return content; - } - - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateContentRepository(uow)) - { - content.CreatorId = userId; - content.WriterId = userId; - repository.AddOrUpdate(content); - //Generate a new preview - repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); - uow.Commit(); - } - - Saved.RaiseEvent(new SaveEventArgs(content, false), this); - - Created.RaiseEvent(new NewEventArgs(content, false, contentTypeAlias, parentId), this); - - Audit(AuditType.New, string.Format("Content '{0}' was created with Id {1}", name, content.Id), content.CreatorId, content.Id); - - return content; - } - - /// - /// Creates and saves an object using the alias of the - /// that this Content should based on. - /// - /// - /// This method returns an object that has been persisted to the database - /// and therefor has an identity. - /// - /// Name of the Content object - /// Parent object for the new Content - /// Alias of the - /// Optional id of the user creating the content - /// - public IContent CreateContentWithIdentity(string name, IContent parent, string contentTypeAlias, int userId = 0) - { - if (parent == null) throw new ArgumentNullException("parent"); - - var contentType = FindContentTypeByAlias(contentTypeAlias); - var content = new Content(name, parent, contentType); - - //NOTE: I really hate the notion of these Creating/Created events - they are so inconsistent, I've only just found - // out that in these 'WithIdentity' methods, the Saving/Saved events were not fired, wtf. Anyways, they're added now. - if (Creating.IsRaisedEventCancelled(new NewEventArgs(content, contentTypeAlias, parent), this)) - { - content.WasCancelled = true; - return content; - } - - if (Saving.IsRaisedEventCancelled(new SaveEventArgs(content), this)) - { - content.WasCancelled = true; - return content; - } - - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateContentRepository(uow)) - { - content.CreatorId = userId; - content.WriterId = userId; - repository.AddOrUpdate(content); - //Generate a new preview - repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); - uow.Commit(); - } - - Saved.RaiseEvent(new SaveEventArgs(content, false), this); - - Created.RaiseEvent(new NewEventArgs(content, false, contentTypeAlias, parent), this); - - Audit(AuditType.New, string.Format("Content '{0}' was created with Id {1}", name, content.Id), content.CreatorId, content.Id); - - return content; - } - - /// - /// Gets an object by Id - /// - /// Id of the Content to retrieve - /// - public IContent GetById(int id) - { - using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) - { - return repository.Get(id); - } - } - - /// - /// Gets an object by Id - /// - /// Ids of the Content to retrieve - /// - public IEnumerable GetByIds(IEnumerable ids) - { - if (ids.Any() == false) return Enumerable.Empty(); - - using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) - { - return repository.GetAll(ids.ToArray()); - } - } - - /// - /// Gets an object by its 'UniqueId' - /// - /// Guid key of the Content to retrieve - /// - public IContent GetById(Guid key) - { - using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) - { - var query = Query.Builder.Where(x => x.Key == key); - var contents = repository.GetByQuery(query); - return contents.SingleOrDefault(); - } - } - - /// - /// Gets a collection of objects by the Id of the - /// - /// Id of the - /// An Enumerable list of objects - public IEnumerable GetContentOfContentType(int id) - { - using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) - { - var query = Query.Builder.Where(x => x.ContentTypeId == id); - var contents = repository.GetByQuery(query); - - return contents; - } - } - - internal IEnumerable GetPublishedContentOfContentType(int id) - { - using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) - { - var query = Query.Builder.Where(x => x.ContentTypeId == id); - var contents = repository.GetByPublishedVersion(query); - - return contents; - } - } - - /// - /// Gets a collection of objects by Level - /// - /// The level to retrieve Content from - /// An Enumerable list of objects - public IEnumerable GetByLevel(int level) - { - using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) - { - var query = Query.Builder.Where(x => x.Level == level && !x.Path.StartsWith(Constants.System.RecycleBinContent.ToInvariantString())); - var contents = repository.GetByQuery(query); - - return contents; - } - } - - /// - /// Gets a specific version of an item. - /// - /// Id of the version to retrieve - /// An item - public IContent GetByVersion(Guid versionId) - { - using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) - { - return repository.GetByVersion(versionId); - } - } - - - /// - /// Gets a collection of an objects versions by Id - /// - /// - /// An Enumerable list of objects - public IEnumerable GetVersions(int id) - { - using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) - { - var versions = repository.GetAllVersions(id); - return versions; - } - } - - /// - /// Gets a collection of objects, which are ancestors of the current content. - /// - /// Id of the to retrieve ancestors for - /// An Enumerable list of objects - public IEnumerable GetAncestors(int id) - { - var content = GetById(id); - return GetAncestors(content); - } - - /// - /// Gets a collection of objects, which are ancestors of the current content. - /// - /// to retrieve ancestors for - /// An Enumerable list of objects - public IEnumerable GetAncestors(IContent content) - { - //null check otherwise we get exceptions - if (content.Path.IsNullOrWhiteSpace()) return Enumerable.Empty(); - - var ids = content.Path.Split(',').Where(x => x != Constants.System.Root.ToInvariantString() && x != content.Id.ToString(CultureInfo.InvariantCulture)).Select(int.Parse).ToArray(); - if (ids.Any() == false) - return new List(); - - using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) - { - return repository.GetAll(ids); - } - } - - /// - /// Gets a collection of objects by Parent Id - /// - /// Id of the Parent to retrieve Children from - /// An Enumerable list of objects - public IEnumerable GetChildren(int id) - { - using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) - { - var query = Query.Builder.Where(x => x.ParentId == id); - var contents = repository.GetByQuery(query).OrderBy(x => x.SortOrder); - - return contents; - } - } - - [Obsolete("Use the overload with 'long' parameter types instead")] - [EditorBrowsable(EditorBrowsableState.Never)] - public IEnumerable GetPagedChildren(int id, int pageIndex, int pageSize, out int totalChildren, - string orderBy, Direction orderDirection, string filter = "") - { - Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); - Mandate.ParameterCondition(pageSize > 0, "pageSize"); - using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) - { - - var query = Query.Builder; - //if the id is System Root, then just get all - if (id != Constants.System.Root) - { - query.Where(x => x.ParentId == id); - } - long total; - var contents = repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out total, orderBy, orderDirection, filter); - totalChildren = Convert.ToInt32(total); - return contents; - } - } - - /// - /// Gets a collection of objects by Parent Id - /// - /// Id of the Parent to retrieve Children from - /// Page index (zero based) - /// Page size - /// Total records query would return without paging - /// Field to order by - /// Direction to order by - /// Search text filter - /// An Enumerable list of objects - public IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize, out long totalChildren, - string orderBy, Direction orderDirection, string filter = "") - { - Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); - Mandate.ParameterCondition(pageSize > 0, "pageSize"); - using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) - { - - var query = Query.Builder; - //if the id is System Root, then just get all - if (id != Constants.System.Root) - { - query.Where(x => x.ParentId == id); - } - var contents = repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, filter); - - return contents; - } - } - - [Obsolete("Use the overload with 'long' parameter types instead")] - [EditorBrowsable(EditorBrowsableState.Never)] - public IEnumerable GetPagedDescendants(int id, int pageIndex, int pageSize, out int totalChildren, string orderBy = "Path", Direction orderDirection = Direction.Ascending, string filter = "") - { - Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); - Mandate.ParameterCondition(pageSize > 0, "pageSize"); - using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) - { - - var query = Query.Builder; - //if the id is System Root, then just get all - if (id != Constants.System.Root) - { - query.Where(x => x.Path.SqlContains(string.Format(",{0},", id), TextColumnType.NVarchar)); - } - long total; - var contents = repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out total, orderBy, orderDirection, filter); - totalChildren = Convert.ToInt32(total); - return contents; - } - } - - /// - /// Gets a collection of objects by Parent Id - /// - /// Id of the Parent to retrieve Descendants from - /// Page number - /// Page size - /// Total records query would return without paging - /// Field to order by - /// Direction to order by - /// Search text filter - /// An Enumerable list of objects - public IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalChildren, string orderBy = "Path", Direction orderDirection = Direction.Ascending, string filter = "") - { - Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); - Mandate.ParameterCondition(pageSize > 0, "pageSize"); - using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) - { - - var query = Query.Builder; - //if the id is System Root, then just get all - if (id != Constants.System.Root) - { - query.Where(x => x.Path.SqlContains(string.Format(",{0},", id), TextColumnType.NVarchar)); - } - var contents = repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, filter); - - return contents; - } - } - - /// - /// Gets a collection of objects by its name or partial name - /// - /// Id of the Parent to retrieve Children from - /// Full or partial name of the children - /// An Enumerable list of objects - public IEnumerable GetChildrenByName(int parentId, string name) - { - using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) - { - var query = Query.Builder.Where(x => x.ParentId == parentId && x.Name.Contains(name)); - var contents = repository.GetByQuery(query); - - return contents; - } - } - - /// - /// Gets a collection of objects by Parent Id - /// - /// Id of the Parent to retrieve Descendants from - /// An Enumerable list of objects - public IEnumerable GetDescendants(int id) - { - var content = GetById(id); - if (content == null) - { - return Enumerable.Empty(); - } - return GetDescendants(content); - } - - /// - /// Gets a collection of objects by Parent Id - /// - /// item to retrieve Descendants from - /// An Enumerable list of objects - public IEnumerable GetDescendants(IContent content) - { - using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) - { - var pathMatch = content.Path + ","; - var query = Query.Builder.Where(x => x.Path.StartsWith(pathMatch) && x.Id != content.Id); - var contents = repository.GetByQuery(query); - - return contents; - } - } - - /// - /// Gets the parent of the current content as an item. - /// - /// Id of the to retrieve the parent from - /// Parent object - public IContent GetParent(int id) - { - var content = GetById(id); - return GetParent(content); - } - - /// - /// Gets the parent of the current content as an item. - /// - /// to retrieve the parent from - /// Parent object - public IContent GetParent(IContent content) - { - if (content.ParentId == Constants.System.Root || content.ParentId == Constants.System.RecycleBinContent) - return null; - - return GetById(content.ParentId); - } - - /// - /// Gets the published version of an item - /// - /// Id of the to retrieve version from - /// An item - public IContent GetPublishedVersion(int id) - { - var version = GetVersions(id); - return version.FirstOrDefault(x => x.Published == true); - } - - /// - /// Gets the published version of a item. - /// - /// The content item. - /// The published version, if any; otherwise, null. - public IContent GetPublishedVersion(IContent content) - { - if (content.Published) return content; - return content.HasPublishedVersion - ? GetByVersion(content.PublishedVersionGuid) - : null; - } - - /// - /// Gets a collection of objects, which reside at the first level / root - /// - /// An Enumerable list of objects - public IEnumerable GetRootContent() - { - using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) - { - var query = Query.Builder.Where(x => x.ParentId == Constants.System.Root); - var contents = repository.GetByQuery(query); - - return contents; - } - } - - /// - /// Gets all published content items - /// - /// - internal IEnumerable GetAllPublished() - { - using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) - { - var query = Query.Builder.Where(x => x.Trashed == false); - return repository.GetByPublishedVersion(query); - } - } - - /// - /// Gets a collection of objects, which has an expiration date less than or equal to today. - /// - /// An Enumerable list of objects - public IEnumerable GetContentForExpiration() - { - using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) - { - var query = Query.Builder.Where(x => x.Published == true && x.ExpireDate <= DateTime.Now); - var contents = repository.GetByQuery(query); - - return contents; - } - } - - /// - /// Gets a collection of objects, which has a release date less than or equal to today. - /// - /// An Enumerable list of objects - public IEnumerable GetContentForRelease() - { - using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) - { - var query = Query.Builder.Where(x => x.Published == false && x.ReleaseDate <= DateTime.Now); - var contents = repository.GetByQuery(query); - - return contents; - } - } - - /// - /// Gets a collection of an objects, which resides in the Recycle Bin - /// - /// An Enumerable list of objects - public IEnumerable GetContentInRecycleBin() - { - using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) - { - var query = Query.Builder.Where(x => x.Path.Contains(Constants.System.RecycleBinContent.ToInvariantString())); - var contents = repository.GetByQuery(query); - - return contents; - } - } - - - - /// - /// Checks whether an item has any children - /// - /// Id of the - /// True if the content has any children otherwise False - public bool HasChildren(int id) - { - return CountChildren(id) > 0; - } - - internal int CountChildren(int id) - { - using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) - { - var query = Query.Builder.Where(x => x.ParentId == id); - var count = repository.Count(query); - return count; - } - } - - /// - /// Checks whether an item has any published versions - /// - /// Id of the - /// True if the content has any published version otherwise False - public bool HasPublishedVersion(int id) - { - using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) - { - var query = Query.Builder.Where(x => x.Published == true && x.Id == id && x.Trashed == false); - int count = repository.Count(query); - return count > 0; - } - } - - /// - /// Checks if the passed in can be published based on the anscestors publish state. - /// - /// to check if anscestors are published - /// True if the Content can be published, otherwise False - public bool IsPublishable(IContent content) - { - //If the passed in content has yet to be saved we "fallback" to checking the Parent - //because if the Parent is publishable then the current content can be Saved and Published - if (content.HasIdentity == false) - { - IContent parent = GetById(content.ParentId); - return IsPublishable(parent, true); - } - - return IsPublishable(content, false); - } - - /// - /// This will rebuild the xml structures for content in the database. - /// - /// This is not used for anything - /// True if publishing succeeded, otherwise False - /// - /// This is used for when a document type alias or a document type property is changed, the xml will need to - /// be regenerated. - /// - public bool RePublishAll(int userId = 0) - { - try - { - RebuildXmlStructures(); - return true; - } - catch (Exception ex) - { - Logger.Error("An error occurred executing RePublishAll", ex); - return false; - } - } - - /// - /// This will rebuild the xml structures for content in the database. - /// - /// - /// If specified will only rebuild the xml for the content type's specified, otherwise will update the structure - /// for all published content. - /// - internal void RePublishAll(params int[] contentTypeIds) - { - try - { - RebuildXmlStructures(contentTypeIds); - } - catch (Exception ex) - { - Logger.Error("An error occurred executing RePublishAll", ex); - } - } - - /// - /// Publishes a single object - /// - /// The to publish - /// Optional Id of the User issueing the publishing - /// True if publishing succeeded, otherwise False - public bool Publish(IContent content, int userId = 0) - { - var result = SaveAndPublishDo(content, userId); - Logger.Info("Call was made to ContentService.Publish, use PublishWithStatus instead since that method will provide more detailed information on the outcome"); - return result.Success; - } - - /// - /// Publishes a object and all its children - /// - /// The to publish along with its children - /// Optional Id of the User issueing the publishing - /// - /// The list of statuses for all published items - IEnumerable> IContentServiceOperations.PublishWithChildren(IContent content, int userId, bool includeUnpublished) - { - return PublishWithChildrenDo(content, userId, includeUnpublished); - } - - /// - /// Saves and Publishes a single object - /// - /// The to save and publish - /// Optional Id of the User issueing the publishing - /// Optional boolean indicating whether or not to raise save events. - /// True if publishing succeeded, otherwise False - Attempt IContentServiceOperations.SaveAndPublish(IContent content, int userId, bool raiseEvents) - { - return SaveAndPublishDo(content, userId, raiseEvents); - } - - /// - /// Deletes an object by moving it to the Recycle Bin - /// - /// Move an item to the Recycle Bin will result in the item being unpublished - /// The to delete - /// Optional Id of the User deleting the Content - Attempt IContentServiceOperations.MoveToRecycleBin(IContent content, int userId) - { - var evtMsgs = EventMessagesFactory.Get(); - - using (new WriteLock(Locker)) - { - var originalPath = content.Path; - - if (Trashing.IsRaisedEventCancelled( - new MoveEventArgs(evtMsgs, new MoveEventInfo(content, originalPath, Constants.System.RecycleBinContent)), - this)) - { - return OperationStatus.Cancelled(evtMsgs); - } - - var moveInfo = new List> - { - new MoveEventInfo(content, originalPath, Constants.System.RecycleBinContent) - }; - - //Make sure that published content is unpublished before being moved to the Recycle Bin - if (HasPublishedVersion(content.Id)) - { - //TODO: this shouldn't be a 'sub operation', and if it needs to be it cannot raise events and cannot be cancelled! - UnPublish(content, userId); - } - - //Unpublish descendents of the content item that is being moved to trash - var descendants = GetDescendants(content).OrderBy(x => x.Level).ToList(); - foreach (var descendant in descendants) - { - //TODO: this shouldn't be a 'sub operation', and if it needs to be it cannot raise events and cannot be cancelled! - UnPublish(descendant, userId); - } - - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateContentRepository(uow)) - { - content.WriterId = userId; - content.ChangeTrashedState(true); - repository.AddOrUpdate(content); - - //Loop through descendants to update their trash state, but ensuring structure by keeping the ParentId - foreach (var descendant in descendants) - { - moveInfo.Add(new MoveEventInfo(descendant, descendant.Path, descendant.ParentId)); - - descendant.WriterId = userId; - descendant.ChangeTrashedState(true, descendant.ParentId); - repository.AddOrUpdate(descendant); - } - - uow.Commit(); - } - - Trashed.RaiseEvent(new MoveEventArgs(false, evtMsgs, moveInfo.ToArray()), this); - - Audit(AuditType.Move, "Move Content to Recycle Bin performed by user", userId, content.Id); - - return OperationStatus.Success(evtMsgs); - } - } - - /// - /// UnPublishes a single object - /// - /// The to publish - /// Optional Id of the User issueing the publishing - /// True if unpublishing succeeded, otherwise False - Attempt IContentServiceOperations.UnPublish(IContent content, int userId) - { - return UnPublishDo(content, false, userId); - } - - /// - /// Publishes a single object - /// - /// The to publish - /// Optional Id of the User issueing the publishing - /// True if publishing succeeded, otherwise False - public Attempt PublishWithStatus(IContent content, int userId = 0) - { - return ((IContentServiceOperations)this).Publish(content, userId); - } - - /// - /// Publishes a object and all its children - /// - /// The to publish along with its children - /// Optional Id of the User issueing the publishing - /// True if publishing succeeded, otherwise False - [Obsolete("Use PublishWithChildrenWithStatus instead, that method will provide more detailed information on the outcome and also allows the includeUnpublished flag")] - public bool PublishWithChildren(IContent content, int userId = 0) - { - var result = PublishWithChildrenDo(content, userId, true); - - //This used to just return false only when the parent content failed, otherwise would always return true so we'll - // do the same thing for the moment - if (result.All(x => x.Result.ContentItem.Id != content.Id)) - return false; - - return result.Single(x => x.Result.ContentItem.Id == content.Id).Success; - } - - /// - /// Publishes a object and all its children - /// - /// The to publish along with its children - /// Optional Id of the User issueing the publishing - /// set to true if you want to also publish children that are currently unpublished - /// True if publishing succeeded, otherwise False - public IEnumerable> PublishWithChildrenWithStatus(IContent content, int userId = 0, bool includeUnpublished = false) - { - return ((IContentServiceOperations)this).PublishWithChildren(content, userId, includeUnpublished); - } - - /// - /// UnPublishes a single object - /// - /// The to publish - /// Optional Id of the User issueing the publishing - /// True if unpublishing succeeded, otherwise False - public bool UnPublish(IContent content, int userId = 0) - { - return ((IContentServiceOperations) this).UnPublish(content, userId).Success; - } - - /// - /// Saves and Publishes a single object - /// - /// The to save and publish - /// Optional Id of the User issueing the publishing - /// Optional boolean indicating whether or not to raise save events. - /// True if publishing succeeded, otherwise False - [Obsolete("Use SaveAndPublishWithStatus instead, that method will provide more detailed information on the outcome")] - public bool SaveAndPublish(IContent content, int userId = 0, bool raiseEvents = true) - { - var result = SaveAndPublishDo(content, userId, raiseEvents); - return result.Success; - } - - /// - /// Saves and Publishes a single object - /// - /// The to save and publish - /// Optional Id of the User issueing the publishing - /// Optional boolean indicating whether or not to raise save events. - /// True if publishing succeeded, otherwise False - public Attempt SaveAndPublishWithStatus(IContent content, int userId = 0, bool raiseEvents = true) - { - return ((IContentServiceOperations)this).SaveAndPublish(content, userId, raiseEvents); - } - - /// - /// Saves a single object - /// - /// The to save - /// Optional Id of the User saving the Content - /// Optional boolean indicating whether or not to raise events. - public void Save(IContent content, int userId = 0, bool raiseEvents = true) - { - ((IContentServiceOperations)this).Save(content, userId, raiseEvents); - } - - /// - /// Saves a collection of objects. - /// - /// Collection of to save - /// Optional Id of the User saving the Content - /// Optional boolean indicating whether or not to raise events. - Attempt IContentServiceOperations.Save(IEnumerable contents, int userId, bool raiseEvents) - { - var asArray = contents.ToArray(); - - var evtMsgs = EventMessagesFactory.Get(); - - if (raiseEvents) - { - if (Saving.IsRaisedEventCancelled( - new SaveEventArgs(asArray, evtMsgs), - this)) - { - return OperationStatus.Cancelled(evtMsgs); - } - } - using (new WriteLock(Locker)) - { - var containsNew = asArray.Any(x => x.HasIdentity == false); - - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateContentRepository(uow)) - { - if (containsNew) - { - foreach (var content in asArray) - { - content.WriterId = userId; - - //Only change the publish state if the "previous" version was actually published - if (content.Published) - content.ChangePublishedState(PublishedState.Saved); - - repository.AddOrUpdate(content); - //add or update preview - repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); - } - } - else - { - foreach (var content in asArray) - { - content.WriterId = userId; - repository.AddOrUpdate(content); - //add or update preview - repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); - } - } - - uow.Commit(); - } - - if (raiseEvents) - Saved.RaiseEvent(new SaveEventArgs(asArray, false, evtMsgs), this); - - Audit(AuditType.Save, "Bulk Save content performed by user", userId == -1 ? 0 : userId, Constants.System.Root); - - return OperationStatus.Success(evtMsgs); - } - } - - /// - /// Permanently deletes an object. - /// - /// - /// This method will also delete associated media files, child content and possibly associated domains. - /// - /// Please note that this method will completely remove the Content from the database - /// The to delete - /// Optional Id of the User deleting the Content - Attempt IContentServiceOperations.Delete(IContent content, int userId) - { - var evtMsgs = EventMessagesFactory.Get(); - - using (new WriteLock(Locker)) - { - if (Deleting.IsRaisedEventCancelled( - new DeleteEventArgs(content, evtMsgs), - this)) - { - return OperationStatus.Cancelled(evtMsgs); - } - - //Make sure that published content is unpublished before being deleted - if (HasPublishedVersion(content.Id)) - { - UnPublish(content, userId); - } - - //Delete children before deleting the 'possible parent' - var children = GetChildren(content.Id); - foreach (var child in children) - { - Delete(child, userId); - } - - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateContentRepository(uow)) - { - repository.Delete(content); - uow.Commit(); - - var args = new DeleteEventArgs(content, false, evtMsgs); - Deleted.RaiseEvent(args, this); - - //remove any flagged media files - repository.DeleteMediaFiles(args.MediaFilesToDelete); - } - - Audit(AuditType.Delete, "Delete Content performed by user", userId, content.Id); - - return OperationStatus.Success(evtMsgs); - } - } - - /// - /// Publishes a single object - /// - /// The to publish - /// Optional Id of the User issueing the publishing - /// The published status attempt - Attempt IContentServiceOperations.Publish(IContent content, int userId) - { - return SaveAndPublishDo(content, userId); - } - - /// - /// Saves a single object - /// - /// The to save - /// Optional Id of the User saving the Content - /// Optional boolean indicating whether or not to raise events. - Attempt IContentServiceOperations.Save(IContent content, int userId, bool raiseEvents) - { - return Save(content, true, userId, raiseEvents); - } - - /// - /// Saves a collection of objects. - /// - /// - /// If the collection of content contains new objects that references eachother by Id or ParentId, - /// then use the overload Save method with a collection of Lazy . - /// - /// Collection of to save - /// Optional Id of the User saving the Content - /// Optional boolean indicating whether or not to raise events. - public void Save(IEnumerable contents, int userId = 0, bool raiseEvents = true) - { - ((IContentServiceOperations)this).Save(contents, userId, raiseEvents); - } - - /// - /// Deletes all content of specified type. All children of deleted content is moved to Recycle Bin. - /// - /// This needs extra care and attention as its potentially a dangerous and extensive operation - /// Id of the - /// Optional Id of the user issueing the delete operation - public void DeleteContentOfType(int contentTypeId, int userId = 0) - { - //TODO: This currently this is called from the ContentTypeService but that needs to change, - // if we are deleting a content type, we should just delete the data and do this operation slightly differently. - // This method will recursively go lookup every content item, check if any of it's descendants are - // of a different type, move them to the recycle bin, then permanently delete the content items. - // The main problem with this is that for every content item being deleted, events are raised... - // which we need for many things like keeping caches in sync, but we can surely do this MUCH better. - - using (new WriteLock(Locker)) - { - using (var uow = UowProvider.GetUnitOfWork()) - { - var repository = RepositoryFactory.CreateContentRepository(uow); - //NOTE What about content that has the contenttype as part of its composition? - var query = Query.Builder.Where(x => x.ContentTypeId == contentTypeId); - var contents = repository.GetByQuery(query).ToArray(); - - if (Deleting.IsRaisedEventCancelled(new DeleteEventArgs(contents), this)) - return; - - foreach (var content in contents.OrderByDescending(x => x.ParentId)) - { - //Look for children of current content and move that to trash before the current content is deleted - var c = content; - var childQuery = Query.Builder.Where(x => x.Path.StartsWith(c.Path)); - var children = repository.GetByQuery(childQuery); - - foreach (var child in children) - { - if (child.ContentType.Id != contentTypeId) - MoveToRecycleBin(child, userId); - } - - //Permantly delete the content - Delete(content, userId); - } - } - - Audit(AuditType.Delete, - string.Format("Delete Content of Type {0} performed by user", contentTypeId), - userId, Constants.System.Root); - } - } - - /// - /// Permanently deletes an object as well as all of its Children. - /// - /// - /// This method will also delete associated media files, child content and possibly associated domains. - /// - /// Please note that this method will completely remove the Content from the database - /// The to delete - /// Optional Id of the User deleting the Content - public void Delete(IContent content, int userId = 0) - { - ((IContentServiceOperations)this).Delete(content, userId); - } - - /// - /// Permanently deletes versions from an object prior to a specific date. - /// This method will never delete the latest version of a content item. - /// - /// Id of the object to delete versions from - /// Latest version date - /// Optional Id of the User deleting versions of a Content object - public void DeleteVersions(int id, DateTime versionDate, int userId = 0) - { - if (DeletingVersions.IsRaisedEventCancelled(new DeleteRevisionsEventArgs(id, dateToRetain: versionDate), this)) - return; - - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateContentRepository(uow)) - { - repository.DeleteVersions(id, versionDate); - uow.Commit(); - } - - DeletedVersions.RaiseEvent(new DeleteRevisionsEventArgs(id, false, dateToRetain: versionDate), this); - - Audit(AuditType.Delete, "Delete Content by version date performed by user", userId, Constants.System.Root); - } - - /// - /// Permanently deletes specific version(s) from an object. - /// This method will never delete the latest version of a content item. - /// - /// Id of the object to delete a version from - /// Id of the version to delete - /// Boolean indicating whether to delete versions prior to the versionId - /// Optional Id of the User deleting versions of a Content object - public void DeleteVersion(int id, Guid versionId, bool deletePriorVersions, int userId = 0) - { - using (new WriteLock(Locker)) - { - if (DeletingVersions.IsRaisedEventCancelled(new DeleteRevisionsEventArgs(id, specificVersion: versionId), this)) - return; - - if (deletePriorVersions) - { - var content = GetByVersion(versionId); - DeleteVersions(id, content.UpdateDate, userId); - } - - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateContentRepository(uow)) - { - repository.DeleteVersion(versionId); - uow.Commit(); - } - - DeletedVersions.RaiseEvent(new DeleteRevisionsEventArgs(id, false, specificVersion: versionId), this); - - Audit(AuditType.Delete, "Delete Content by version performed by user", userId, Constants.System.Root); - } - } - - /// - /// Deletes an object by moving it to the Recycle Bin - /// - /// Move an item to the Recycle Bin will result in the item being unpublished - /// The to delete - /// Optional Id of the User deleting the Content - public void MoveToRecycleBin(IContent content, int userId = 0) - { - ((IContentServiceOperations) this).MoveToRecycleBin(content, userId); - } - - /// - /// Moves an object to a new location by changing its parent id. - /// - /// - /// If the object is already published it will be - /// published after being moved to its new location. Otherwise it'll just - /// be saved with a new parent id. - /// - /// The to move - /// Id of the Content's new Parent - /// Optional Id of the User moving the Content - public void Move(IContent content, int parentId, int userId = 0) - { - using (new WriteLock(Locker)) - { - //This ensures that the correct method is called if this method is used to Move to recycle bin. - if (parentId == Constants.System.RecycleBinContent) - { - MoveToRecycleBin(content, userId); - return; - } - - if (Moving.IsRaisedEventCancelled( - new MoveEventArgs( - new MoveEventInfo(content, content.Path, parentId)), this)) - { - return; - } - - //used to track all the moved entities to be given to the event - var moveInfo = new List>(); - - //call private method that does the recursive moving - PerformMove(content, parentId, userId, moveInfo); - - Moved.RaiseEvent(new MoveEventArgs(false, moveInfo.ToArray()), this); - - Audit(AuditType.Move, "Move Content performed by user", userId, content.Id); - } - } - - /// - /// Empties the Recycle Bin by deleting all that resides in the bin - /// - public void EmptyRecycleBin() - { - using (new WriteLock(Locker)) - { - Dictionary> entities; - List files; - bool success; - var nodeObjectType = new Guid(Constants.ObjectTypes.Document); - - using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) - { - //Create a dictionary of ids -> dictionary of property aliases + values - entities = repository.GetEntitiesInRecycleBin() - .ToDictionary( - key => key.Id, - val => (IEnumerable)val.Properties); - - files = ((ContentRepository)repository).GetFilesInRecycleBinForUploadField(); - - if (EmptyingRecycleBin.IsRaisedEventCancelled(new RecycleBinEventArgs(nodeObjectType, entities, files), this)) - return; - - success = repository.EmptyRecycleBin(); - - EmptiedRecycleBin.RaiseEvent(new RecycleBinEventArgs(nodeObjectType, entities, files, success), this); - - if (success) - repository.DeleteMediaFiles(files); - } - } - Audit(AuditType.Delete, "Empty Content Recycle Bin performed by user", 0, Constants.System.RecycleBinContent); - } - - /// - /// Copies an object by creating a new Content object of the same type and copies all data from the current - /// to the new copy which is returned. Recursively copies all children. - /// - /// The to copy - /// Id of the Content's new Parent - /// Boolean indicating whether the copy should be related to the original - /// Optional Id of the User copying the Content - /// The newly created object - public IContent Copy(IContent content, int parentId, bool relateToOriginal, int userId = 0) - { - return Copy(content, parentId, relateToOriginal, true, userId); - } - - /// - /// Copies an object by creating a new Content object of the same type and copies all data from the current - /// to the new copy which is returned. - /// - /// The to copy - /// Id of the Content's new Parent - /// Boolean indicating whether the copy should be related to the original - /// A value indicating whether to recursively copy children. - /// Optional Id of the User copying the Content - /// The newly created object - public IContent Copy(IContent content, int parentId, bool relateToOriginal, bool recursive, int userId = 0) - { - //TODO: This all needs to be managed correctly so that the logic is submitted in one - // transaction, the CRUD needs to be moved to the repo - - using (new WriteLock(Locker)) - { - var copy = content.DeepCloneWithResetIdentities(); - copy.ParentId = parentId; - - // A copy should never be set to published automatically even if the original was. - copy.ChangePublishedState(PublishedState.Unpublished); - - if (Copying.IsRaisedEventCancelled(new CopyEventArgs(content, copy, parentId), this)) - return null; - - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateContentRepository(uow)) - { - // Update the create author and last edit author - copy.CreatorId = userId; - copy.WriterId = userId; - - repository.AddOrUpdate(copy); - //add or update a preview - repository.AddOrUpdatePreviewXml(copy, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); - uow.Commit(); - - - //Special case for the associated tags - //TODO: Move this to the repository layer in a single transaction! - //don't copy tags data in tags table if the item is in the recycle bin - if (parentId != Constants.System.RecycleBinContent) - { - - var tags = uow.Database.Fetch("WHERE nodeId = @Id", new { Id = content.Id }); - foreach (var tag in tags) - { - uow.Database.Insert(new TagRelationshipDto { NodeId = copy.Id, TagId = tag.TagId, PropertyTypeId = tag.PropertyTypeId }); - } - } - } - - if (recursive) - { - //Look for children and copy those as well - var children = GetChildren(content.Id); - foreach (var child in children) - { - //TODO: This shouldn't recurse back to this method, it should be done in a private method - // that doesn't have a nested lock and so we can perform the entire operation in one commit. - Copy(child, copy.Id, relateToOriginal, true, userId); - } - } - - Copied.RaiseEvent(new CopyEventArgs(content, copy, false, parentId, relateToOriginal), this); - - Audit(AuditType.Copy, "Copy Content performed by user", content.WriterId, content.Id); - return copy; - } - } - - - /// - /// Sends an to Publication, which executes handlers and events for the 'Send to Publication' action. - /// - /// The to send to publication - /// Optional Id of the User issueing the send to publication - /// True if sending publication was succesfull otherwise false - public bool SendToPublication(IContent content, int userId = 0) - { - if (SendingToPublish.IsRaisedEventCancelled(new SendToPublishEventArgs(content), this)) - return false; - - //Save before raising event - Save(content, userId); - - SentToPublish.RaiseEvent(new SendToPublishEventArgs(content, false), this); - - Audit(AuditType.SendToPublish, "Send to Publish performed by user", content.WriterId, content.Id); - - return true; - } - - /// - /// Rollback an object to a previous version. - /// This will create a new version, which is a copy of all the old data. - /// - /// - /// The way data is stored actually only allows us to rollback on properties - /// and not data like Name and Alias of the Content. - /// - /// Id of the being rolled back - /// Id of the version to rollback to - /// Optional Id of the User issueing the rollback of the Content - /// The newly created object - public IContent Rollback(int id, Guid versionId, int userId = 0) - { - var content = GetByVersion(versionId); - - if (RollingBack.IsRaisedEventCancelled(new RollbackEventArgs(content), this)) - return content; - - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateContentRepository(uow)) - { - content.WriterId = userId; - content.CreatorId = userId; - content.ChangePublishedState(PublishedState.Unpublished); - - repository.AddOrUpdate(content); - //add or update a preview - repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); - uow.Commit(); - } - - RolledBack.RaiseEvent(new RollbackEventArgs(content, false), this); - - Audit(AuditType.RollBack, "Content rollback performed by user", content.WriterId, content.Id); - - return content; - } - - /// - /// Sorts a collection of objects by updating the SortOrder according - /// to the ordering of items in the passed in . - /// - /// - /// Using this method will ensure that the Published-state is maintained upon sorting - /// so the cache is updated accordingly - as needed. - /// - /// - /// - /// - /// True if sorting succeeded, otherwise False - public bool Sort(IEnumerable items, int userId = 0, bool raiseEvents = true) - { - if (raiseEvents) - { - if (Saving.IsRaisedEventCancelled(new SaveEventArgs(items), this)) - return false; - } - - var shouldBePublished = new List(); - var shouldBeSaved = new List(); - - var asArray = items.ToArray(); - using (new WriteLock(Locker)) - { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateContentRepository(uow)) - { - int i = 0; - foreach (var content in asArray) - { - //If the current sort order equals that of the content - //we don't need to update it, so just increment the sort order - //and continue. - if (content.SortOrder == i) - { - i++; - continue; - } - - content.SortOrder = i; - content.WriterId = userId; - i++; - - if (content.Published) - { - //TODO: This should not be an inner operation, but if we do this, it cannot raise events and cannot be cancellable! - var published = _publishingStrategy.Publish(content, userId); - shouldBePublished.Add(content); - } - else - shouldBeSaved.Add(content); - - repository.AddOrUpdate(content); - //add or update a preview - repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); - } - - foreach (var content in shouldBePublished) - { - //Create and Save ContentXml DTO - repository.AddOrUpdateContentXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); - } - - uow.Commit(); - } - } - - if (raiseEvents) - Saved.RaiseEvent(new SaveEventArgs(asArray, false), this); - - if (shouldBePublished.Any()) - { - //TODO: This should not be an inner operation, but if we do this, it cannot raise events and cannot be cancellable! - _publishingStrategy.PublishingFinalized(shouldBePublished, false); - } - - - Audit(AuditType.Sort, "Sorting content performed by user", userId, 0); - - return true; - } - - /// - /// Rebuilds all xml content in the cmsContentXml table for all documents - /// - /// - /// Only rebuild the xml structures for the content type ids passed in, if none then rebuilds the structures - /// for all content - /// - public void RebuildXmlStructures(params int[] contentTypeIds) - { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateContentRepository(uow)) - { - repository.RebuildXmlStructures( - content => _entitySerializer.Serialize(this, _dataTypeService, _userService, content), - contentTypeIds: contentTypeIds.Length == 0 ? null : contentTypeIds); - - uow.Commit(); - } - - Audit(AuditType.Publish, "ContentService.RebuildXmlStructures completed, the xml has been regenerated in the database", 0, Constants.System.Root); - - } - - #region Internal Methods - - /// - /// Gets a collection of descendants by the first Parent. - /// - /// item to retrieve Descendants from - /// An Enumerable list of objects - internal IEnumerable GetPublishedDescendants(IContent content) - { - using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) - { - var query = Query.Builder.Where(x => x.Id != content.Id && x.Path.StartsWith(content.Path) && x.Trashed == false); - var contents = repository.GetByPublishedVersion(query); - - return contents; - } - } - - #endregion - - #region Private Methods - - private void Audit(AuditType type, string message, int userId, int objectId) - { - var uow = UowProvider.GetUnitOfWork(); - using (var auditRepo = RepositoryFactory.CreateAuditRepository(uow)) - { - auditRepo.AddOrUpdate(new AuditItem(objectId, message, type, userId)); - uow.Commit(); - } - } - - //TODO: All of this needs to be moved to the repository - private void PerformMove(IContent content, int parentId, int userId, ICollection> moveInfo) - { - //add a tracking item to use in the Moved event - moveInfo.Add(new MoveEventInfo(content, content.Path, parentId)); - - content.WriterId = userId; - if (parentId == Constants.System.Root) - { - content.Path = string.Concat(Constants.System.Root, ",", content.Id); - content.Level = 1; - } - else - { - var parent = GetById(parentId); - content.Path = string.Concat(parent.Path, ",", content.Id); - content.Level = parent.Level + 1; - } - - //If Content is being moved away from Recycle Bin, its state should be un-trashed - if (content.Trashed && parentId != Constants.System.RecycleBinContent) - { - content.ChangeTrashedState(false, parentId); - } - else - { - content.ParentId = parentId; - } - - //If Content is published, it should be (re)published from its new location - if (content.Published) - { - //If Content is Publishable its saved and published - //otherwise we save the content without changing the publish state, and generate new xml because the Path, Level and Parent has changed. - if (IsPublishable(content)) - { - //TODO: This is raising events, probably not desirable as this costs performance for event listeners like Examine - SaveAndPublish(content, userId); - } - else - { - //TODO: This is raising events, probably not desirable as this costs performance for event listeners like Examine - Save(content, false, userId); - - //TODO: This shouldn't be here! This needs to be part of the repository logic but in order to fix this we need to - // change how this method calls "Save" as it needs to save using an internal method - using (var uow = UowProvider.GetUnitOfWork()) - { - var xml = _entitySerializer.Serialize(this, _dataTypeService, _userService, content); - - var poco = new ContentXmlDto { NodeId = content.Id, Xml = xml.ToDataString() }; - var exists = - uow.Database.FirstOrDefault("WHERE nodeId = @Id", new { Id = content.Id }) != - null; - int result = exists - ? uow.Database.Update(poco) - : Convert.ToInt32(uow.Database.Insert(poco)); - } - } - } - else - { - //TODO: This is raising events, probably not desirable as this costs performance for event listeners like Examine - Save(content, userId); - } - - //Ensure that Path and Level is updated on children - var children = GetChildren(content.Id).ToArray(); - if (children.Any()) - { - foreach (var child in children) - { - PerformMove(child, content.Id, userId, moveInfo); - } - } - } - - /// - /// Publishes a object and all its children - /// - /// The to publish along with its children - /// Optional Id of the User issueing the publishing - /// If set to true, this will also publish descendants that are completely unpublished, normally this will only publish children that have previously been published - /// - /// A list of publish statues. If the parent document is not valid or cannot be published because it's parent(s) is not published - /// then the list will only contain one status item, otherwise it will contain status items for it and all of it's descendants that - /// are to be published. - /// - private IEnumerable> PublishWithChildrenDo( - IContent content, int userId = 0, bool includeUnpublished = false) - { - if (content == null) throw new ArgumentNullException("content"); - - var evtMsgs = EventMessagesFactory.Get(); - - using (new WriteLock(Locker)) - { - var result = new List>(); - - //Check if parent is published (although not if its a root node) - if parent isn't published this Content cannot be published - if (content.ParentId != Constants.System.Root && content.ParentId != Constants.System.RecycleBinContent && IsPublishable(content) == false) - { - Logger.Info( - string.Format( - "Content '{0}' with Id '{1}' could not be published because its parent or one of its ancestors is not published.", - content.Name, content.Id)); - result.Add(Attempt.Fail(new PublishStatus(content, PublishStatusType.FailedPathNotPublished, evtMsgs))); - return result; - } - - //Content contains invalid property values and can therefore not be published - fire event? - if (!content.IsValid()) - { - Logger.Info( - string.Format("Content '{0}' with Id '{1}' could not be published because of invalid properties.", - content.Name, content.Id)); - result.Add( - Attempt.Fail( - new PublishStatus(content, PublishStatusType.FailedContentInvalid, evtMsgs) - { - InvalidProperties = ((ContentBase)content).LastInvalidProperties - })); - return result; - } - - //Consider creating a Path query instead of recursive method: - //var query = Query.Builder.Where(x => x.Path.StartsWith(content.Path)); - - var updated = new List(); - var list = new List(); - list.Add(content); //include parent item - list.AddRange(GetDescendants(content)); - - var internalStrategy = (PublishingStrategy)_publishingStrategy; - - //Publish and then update the database with new status - var publishedOutcome = internalStrategy.PublishWithChildrenInternal(list, userId, includeUnpublished).ToArray(); - var published = publishedOutcome - .Where(x => x.Success || x.Result.StatusType == PublishStatusType.SuccessAlreadyPublished) - // ensure proper order (for events) - cannot publish a child before its parent! - .OrderBy(x => x.Result.ContentItem.Level) - .ThenBy(x => x.Result.ContentItem.SortOrder); - - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateContentRepository(uow)) - { - //NOTE The Publish with subpages-dialog was used more as a republish-type-thing, so we'll have to include PublishStatusType.SuccessAlreadyPublished - //in the updated-list, so the Published event is triggered with the expected set of pages and the xml is updated. - foreach (var item in published) - { - item.Result.ContentItem.WriterId = userId; - repository.AddOrUpdate(item.Result.ContentItem); - //add or update a preview - repository.AddOrUpdatePreviewXml(item.Result.ContentItem, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); - //add or update the published xml - repository.AddOrUpdateContentXml(item.Result.ContentItem, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); - updated.Add(item.Result.ContentItem); - } - - uow.Commit(); - - } - //Save xml to db and call following method to fire event: - _publishingStrategy.PublishingFinalized(updated, false); - - Audit(AuditType.Publish, "Publish with Children performed by user", userId, content.Id); - - - return publishedOutcome; - } - } - - /// - /// UnPublishes a single object - /// - /// The to publish - /// Optional boolean to avoid having the cache refreshed when calling this Unpublish method. By default this method will update the cache. - /// Optional Id of the User issueing the publishing - /// True if unpublishing succeeded, otherwise False - private Attempt UnPublishDo(IContent content, bool omitCacheRefresh = false, int userId = 0) - { - var newest = GetById(content.Id); // ensure we have the newest version - if (content.Version != newest.Version) // but use the original object if it's already the newest version - content = newest; - - var evtMsgs = EventMessagesFactory.Get(); - - var published = content.Published ? content : GetPublishedVersion(content.Id); // get the published version - if (published == null) - { - return Attempt.Succeed(new UnPublishStatus(content, UnPublishedStatusType.SuccessAlreadyUnPublished, evtMsgs)); // already unpublished - } - - var unpublished = _publishingStrategy.UnPublish(content, userId); - if (unpublished == false) return Attempt.Fail(new UnPublishStatus(content, UnPublishedStatusType.FailedCancelledByEvent, evtMsgs)); - - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateContentRepository(uow)) - { - content.WriterId = userId; - repository.AddOrUpdate(content); - // is published is not newest, reset the published flag on published version - if (published.Version != content.Version) - repository.ClearPublished(published); - repository.DeleteContentXml(content); - - uow.Commit(); - } - //Delete xml from db? and call following method to fire event through PublishingStrategy to update cache - if (omitCacheRefresh == false) - _publishingStrategy.UnPublishingFinalized(content); - - Audit(AuditType.UnPublish, "UnPublish performed by user", userId, content.Id); - - return Attempt.Succeed(new UnPublishStatus(content, UnPublishedStatusType.Success, evtMsgs)); - } - - /// - /// Saves and Publishes a single object - /// - /// The to save and publish - /// Optional Id of the User issueing the publishing - /// Optional boolean indicating whether or not to raise save events. - /// True if publishing succeeded, otherwise False - private Attempt SaveAndPublishDo(IContent content, int userId = 0, bool raiseEvents = true) - { - var evtMsgs = EventMessagesFactory.Get(); - - if (raiseEvents) - { - if (Saving.IsRaisedEventCancelled( - new SaveEventArgs(content, evtMsgs), this)) - { - return Attempt.Fail(new PublishStatus(content, PublishStatusType.FailedCancelledByEvent, evtMsgs)); - } - } - - using (new WriteLock(Locker)) - { - //Has this content item previously been published? If so, we don't need to refresh the children - var previouslyPublished = content.HasIdentity && HasPublishedVersion(content.Id); //content might not have an id - var publishStatus = new PublishStatus(content, PublishStatusType.Success, evtMsgs); //initially set to success - - //Check if parent is published (although not if its a root node) - if parent isn't published this Content cannot be published - publishStatus.StatusType = CheckAndLogIsPublishable(content); - //if it is not successful, then check if the props are valid - if ((int)publishStatus.StatusType < 10) - { - //Content contains invalid property values and can therefore not be published - fire event? - publishStatus.StatusType = CheckAndLogIsValid(content); - //set the invalid properties (if there are any) - publishStatus.InvalidProperties = ((ContentBase)content).LastInvalidProperties; - } - //if we're still successful, then publish using the strategy - if (publishStatus.StatusType == PublishStatusType.Success) - { - var internalStrategy = (PublishingStrategy)_publishingStrategy; - //Publish and then update the database with new status - var publishResult = internalStrategy.PublishInternal(content, userId); - //set the status type to the publish result - publishStatus.StatusType = publishResult.Result.StatusType; - } - - //we are successfully published if our publishStatus is still Successful - bool published = publishStatus.StatusType == PublishStatusType.Success; - - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateContentRepository(uow)) - { - if (published == false) - { - content.ChangePublishedState(PublishedState.Saved); - } - //Since this is the Save and Publish method, the content should be saved even though the publish fails or isn't allowed - if (content.HasIdentity == false) - { - content.CreatorId = userId; - } - content.WriterId = userId; - - repository.AddOrUpdate(content); - - //Generate a new preview - repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); - - if (published) - { - //Content Xml - repository.AddOrUpdateContentXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); - } - - uow.Commit(); - } - - if (raiseEvents) - Saved.RaiseEvent(new SaveEventArgs(content, false, evtMsgs), this); - - //Save xml to db and call following method to fire event through PublishingStrategy to update cache - if (published) - { - _publishingStrategy.PublishingFinalized(content); - } - - //We need to check if children and their publish state to ensure that we 'republish' content that was previously published - if (published && previouslyPublished == false && HasChildren(content.Id)) - { - var descendants = GetPublishedDescendants(content); - - _publishingStrategy.PublishingFinalized(descendants, false); - } - - Audit(AuditType.Publish, "Save and Publish performed by user", userId, content.Id); - - return Attempt.If(publishStatus.StatusType == PublishStatusType.Success, publishStatus); - } - } - - /// - /// Saves a single object - /// - /// The to save - /// Boolean indicating whether or not to change the Published state upon saving - /// Optional Id of the User saving the Content - /// Optional boolean indicating whether or not to raise events. - private Attempt Save(IContent content, bool changeState, int userId = 0, bool raiseEvents = true) - { - var evtMsgs = EventMessagesFactory.Get(); - - if (raiseEvents) - { - if (Saving.IsRaisedEventCancelled( - new SaveEventArgs(content, evtMsgs), - this)) - { - return OperationStatus.Cancelled(evtMsgs); - } - } - - using (new WriteLock(Locker)) - { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateContentRepository(uow)) - { - if (content.HasIdentity == false) - { - content.CreatorId = userId; - } - content.WriterId = userId; - - //Only change the publish state if the "previous" version was actually published or marked as unpublished - if (changeState && (content.Published || ((Content)content).PublishedState == PublishedState.Unpublished)) - content.ChangePublishedState(PublishedState.Saved); - - repository.AddOrUpdate(content); - - //Generate a new preview - repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); - - uow.Commit(); - } - - if (raiseEvents) - Saved.RaiseEvent(new SaveEventArgs(content, false, evtMsgs), this); - - Audit(AuditType.Save, "Save Content performed by user", userId, content.Id); - - return OperationStatus.Success(evtMsgs); - } - } - - /// - /// Checks if the passed in can be published based on the anscestors publish state. - /// - /// - /// Check current is only used when falling back to checking the Parent of non-saved content, as - /// non-saved content doesn't have a valid path yet. - /// - /// to check if anscestors are published - /// Boolean indicating whether the passed in content should also be checked for published versions - /// True if the Content can be published, otherwise False - private bool IsPublishable(IContent content, bool checkCurrent) - { - var ids = content.Path.Split(',').Select(int.Parse).ToList(); - foreach (var id in ids) - { - //If Id equals that of the recycle bin we return false because nothing in the bin can be published - if (id == Constants.System.RecycleBinContent) - return false; - - //We don't check the System Root, so just continue - if (id == Constants.System.Root) continue; - - //If the current id equals that of the passed in content and if current shouldn't be checked we skip it. - if (checkCurrent == false && id == content.Id) continue; - - //Check if the content for the current id is published - escape the loop if we encounter content that isn't published - var hasPublishedVersion = HasPublishedVersion(id); - if (hasPublishedVersion == false) - return false; - } - - return true; - } - - private PublishStatusType CheckAndLogIsPublishable(IContent content) - { - //Check if parent is published (although not if its a root node) - if parent isn't published this Content cannot be published - if (content.ParentId != Constants.System.Root && content.ParentId != Constants.System.RecycleBinContent && IsPublishable(content) == false) - { - Logger.Info( - string.Format( - "Content '{0}' with Id '{1}' could not be published because its parent is not published.", - content.Name, content.Id)); - return PublishStatusType.FailedPathNotPublished; - } - else if (content.ExpireDate.HasValue && content.ExpireDate.Value > DateTime.MinValue && DateTime.Now > content.ExpireDate.Value) - { - Logger.Info( - string.Format( - "Content '{0}' with Id '{1}' has expired and could not be published.", - content.Name, content.Id)); - return PublishStatusType.FailedHasExpired; - } - else if (content.ReleaseDate.HasValue && content.ReleaseDate.Value > DateTime.MinValue && content.ReleaseDate.Value > DateTime.Now) - { - Logger.Info( - string.Format( - "Content '{0}' with Id '{1}' is awaiting release and could not be published.", - content.Name, content.Id)); - return PublishStatusType.FailedAwaitingRelease; - } - - return PublishStatusType.Success; - } - - private PublishStatusType CheckAndLogIsValid(IContent content) - { - //Content contains invalid property values and can therefore not be published - fire event? - if (content.IsValid() == false) - { - Logger.Info( - string.Format( - "Content '{0}' with Id '{1}' could not be published because of invalid properties.", - content.Name, content.Id)); - return PublishStatusType.FailedContentInvalid; - } - - return PublishStatusType.Success; - } - - private IContentType FindContentTypeByAlias(string contentTypeAlias) - { - using (var repository = RepositoryFactory.CreateContentTypeRepository(UowProvider.GetUnitOfWork())) - { - var query = Query.Builder.Where(x => x.Alias == contentTypeAlias); - var types = repository.GetByQuery(query); - - if (types.Any() == false) - throw new Exception( - string.Format("No ContentType matching the passed in Alias: '{0}' was found", - contentTypeAlias)); - - var contentType = types.First(); - - if (contentType == null) - throw new Exception(string.Format("ContentType matching the passed in Alias: '{0}' was null", - contentTypeAlias)); - - return contentType; - } - } - - #endregion - - #region Proxy Event Handlers - /// - /// Occurs before publish. - /// - /// Proxy to the real event on the - public static event TypedEventHandler> Publishing - { - add { PublishingStrategy.Publishing += value; } - remove { PublishingStrategy.Publishing -= value; } - } - - /// - /// Occurs after publish. - /// - /// Proxy to the real event on the - public static event TypedEventHandler> Published - { - add { PublishingStrategy.Published += value; } - remove { PublishingStrategy.Published -= value; } - } - /// - /// Occurs before unpublish. - /// - /// Proxy to the real event on the - public static event TypedEventHandler> UnPublishing - { - add { PublishingStrategy.UnPublishing += value; } - remove { PublishingStrategy.UnPublishing -= value; } - } - - /// - /// Occurs after unpublish. - /// - /// Proxy to the real event on the - public static event TypedEventHandler> UnPublished - { - add { PublishingStrategy.UnPublished += value; } - remove { PublishingStrategy.UnPublished -= value; } - } - #endregion - - #region Event Handlers - /// - /// Occurs before Delete - /// - public static event TypedEventHandler> Deleting; - - /// - /// Occurs after Delete - /// - public static event TypedEventHandler> Deleted; - - /// - /// Occurs before Delete Versions - /// - public static event TypedEventHandler DeletingVersions; - - /// - /// Occurs after Delete Versions - /// - public static event TypedEventHandler DeletedVersions; - - /// - /// Occurs before Save - /// - public static event TypedEventHandler> Saving; - - /// - /// Occurs after Save - /// - public static event TypedEventHandler> Saved; - - /// - /// Occurs before Create - /// - [Obsolete("Use the Created event instead, the Creating and Created events both offer the same functionality, Creating event has been deprecated.")] - public static event TypedEventHandler> Creating; - - /// - /// Occurs after Create - /// - /// - /// Please note that the Content object has been created, but might not have been saved - /// so it does not have an identity yet (meaning no Id has been set). - /// - public static event TypedEventHandler> Created; - - /// - /// Occurs before Copy - /// - public static event TypedEventHandler> Copying; - - /// - /// Occurs after Copy - /// - public static event TypedEventHandler> Copied; - - /// - /// Occurs before Content is moved to Recycle Bin - /// - public static event TypedEventHandler> Trashing; - - /// - /// Occurs after Content is moved to Recycle Bin - /// - public static event TypedEventHandler> Trashed; - - /// - /// Occurs before Move - /// - public static event TypedEventHandler> Moving; - - /// - /// Occurs after Move - /// - public static event TypedEventHandler> Moved; - - /// - /// Occurs before Rollback - /// - public static event TypedEventHandler> RollingBack; - - /// - /// Occurs after Rollback - /// - public static event TypedEventHandler> RolledBack; - - /// - /// Occurs before Send to Publish - /// - public static event TypedEventHandler> SendingToPublish; - - /// - /// Occurs after Send to Publish - /// - public static event TypedEventHandler> SentToPublish; - - /// - /// Occurs before the Recycle Bin is emptied - /// - public static event TypedEventHandler EmptyingRecycleBin; - - /// - /// Occurs after the Recycle Bin has been Emptied - /// - public static event TypedEventHandler EmptiedRecycleBin; - #endregion - } + /// + /// Represents the Content Service, which is an easy access to operations involving + /// + public class ContentService : RepositoryService, IContentService, IContentServiceOperations + { + private readonly IPublishingStrategy _publishingStrategy; + private readonly EntityXmlSerializer _entitySerializer = new EntityXmlSerializer(); + private readonly IDataTypeService _dataTypeService; + private readonly IUserService _userService; + + //Support recursive locks because some of the methods that require locking call other methods that require locking. + //for example, the Move method needs to be locked but this calls the Save method which also needs to be locked. + private static readonly ReaderWriterLockSlim Locker = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion); + + public ContentService( + IDatabaseUnitOfWorkProvider provider, + RepositoryFactory repositoryFactory, + ILogger logger, + IEventMessagesFactory eventMessagesFactory, + IPublishingStrategy publishingStrategy, + IDataTypeService dataTypeService, + IUserService userService) + : base(provider, repositoryFactory, logger, eventMessagesFactory) + { + if (publishingStrategy == null) throw new ArgumentNullException("publishingStrategy"); + if (dataTypeService == null) throw new ArgumentNullException("dataTypeService"); + if (userService == null) throw new ArgumentNullException("userService"); + _publishingStrategy = publishingStrategy; + _dataTypeService = dataTypeService; + _userService = userService; + } + + public int CountPublished(string contentTypeAlias = null) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateContentRepository(uow)) + { + return repository.CountPublished(); + } + } + + public int Count(string contentTypeAlias = null) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateContentRepository(uow)) + { + return repository.Count(contentTypeAlias); + } + } + + public int CountChildren(int parentId, string contentTypeAlias = null) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateContentRepository(uow)) + { + return repository.CountChildren(parentId, contentTypeAlias); + } + } + + public int CountDescendants(int parentId, string contentTypeAlias = null) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateContentRepository(uow)) + { + return repository.CountDescendants(parentId, contentTypeAlias); + } + } + + /// + /// Used to bulk update the permissions set for a content item. This will replace all permissions + /// assigned to an entity with a list of user id & permission pairs. + /// + /// + public void ReplaceContentPermissions(EntityPermissionSet permissionSet) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateContentRepository(uow)) + { + repository.ReplaceContentPermissions(permissionSet); + } + } + + /// + /// Assigns a single permission to the current content item for the specified user ids + /// + /// + /// + /// + public void AssignContentPermission(IContent entity, char permission, IEnumerable userIds) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateContentRepository(uow)) + { + repository.AssignEntityPermission(entity, permission, userIds); + } + } + + /// + /// Gets the list of permissions for the content item + /// + /// + /// + public IEnumerable GetPermissionsForEntity(IContent content) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateContentRepository(uow)) + { + return repository.GetPermissionsForEntity(content.Id); + } + } + + /// + /// 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, int parentId, string contentTypeAlias, int userId = 0) + { + var contentType = FindContentTypeByAlias(contentTypeAlias); + var content = new Content(name, parentId, contentType); + var parent = GetById(content.ParentId); + content.Path = string.Concat(parent.IfNotNull(x => x.Path, content.ParentId.ToString()), ",", content.Id); + + + if (Creating.IsRaisedEventCancelled(new NewEventArgs(content, contentTypeAlias, parentId), this)) + { + content.WasCancelled = true; + return content; + } + + content.CreatorId = userId; + content.WriterId = userId; + + Created.RaiseEvent(new NewEventArgs(content, false, contentTypeAlias, parentId), this); + + var uow = UowProvider.GetUnitOfWork(); + using (var auditRepo = RepositoryFactory.CreateAuditRepository(uow)) + { + auditRepo.AddOrUpdate(new AuditItem(content.Id, string.Format("Content '{0}' was created", name), AuditType.New, content.CreatorId)); + uow.Commit(); + } + + return content; + } + + /// + /// 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 + /// Parent object for the new Content + /// Alias of the + /// Optional id of the user creating the content + /// + public IContent CreateContent(string name, IContent parent, string contentTypeAlias, int userId = 0) + { + if (parent == null) throw new ArgumentNullException("parent"); + + var contentType = FindContentTypeByAlias(contentTypeAlias); + var content = new Content(name, parent, contentType); + content.Path = string.Concat(parent.Path, ",", content.Id); + + if (Creating.IsRaisedEventCancelled(new NewEventArgs(content, contentTypeAlias, parent), this)) + { + content.WasCancelled = true; + return content; + } + + content.CreatorId = userId; + content.WriterId = userId; + + Created.RaiseEvent(new NewEventArgs(content, false, contentTypeAlias, parent), this); + + Audit(AuditType.New, string.Format("Content '{0}' was created", name), content.CreatorId, content.Id); + + return content; + } + + /// + /// Creates and saves an object using the alias of the + /// that this Content should based on. + /// + /// + /// This method returns an object that has been persisted to the database + /// and therefor has an identity. + /// + /// 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 CreateContentWithIdentity(string name, int parentId, string contentTypeAlias, int userId = 0) + { + var contentType = FindContentTypeByAlias(contentTypeAlias); + var content = new Content(name, parentId, contentType); + + //NOTE: I really hate the notion of these Creating/Created events - they are so inconsistent, I've only just found + // out that in these 'WithIdentity' methods, the Saving/Saved events were not fired, wtf. Anyways, they're added now. + if (Creating.IsRaisedEventCancelled(new NewEventArgs(content, contentTypeAlias, parentId), this)) + { + content.WasCancelled = true; + return content; + } + + if (Saving.IsRaisedEventCancelled(new SaveEventArgs(content), this)) + { + content.WasCancelled = true; + return content; + } + + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateContentRepository(uow)) + { + content.CreatorId = userId; + content.WriterId = userId; + repository.AddOrUpdate(content); + //Generate a new preview + repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); + uow.Commit(); + } + + Saved.RaiseEvent(new SaveEventArgs(content, false), this); + + Created.RaiseEvent(new NewEventArgs(content, false, contentTypeAlias, parentId), this); + + Audit(AuditType.New, string.Format("Content '{0}' was created with Id {1}", name, content.Id), content.CreatorId, content.Id); + + return content; + } + + /// + /// Creates and saves an object using the alias of the + /// that this Content should based on. + /// + /// + /// This method returns an object that has been persisted to the database + /// and therefor has an identity. + /// + /// Name of the Content object + /// Parent object for the new Content + /// Alias of the + /// Optional id of the user creating the content + /// + public IContent CreateContentWithIdentity(string name, IContent parent, string contentTypeAlias, int userId = 0) + { + if (parent == null) throw new ArgumentNullException("parent"); + + var contentType = FindContentTypeByAlias(contentTypeAlias); + var content = new Content(name, parent, contentType); + + //NOTE: I really hate the notion of these Creating/Created events - they are so inconsistent, I've only just found + // out that in these 'WithIdentity' methods, the Saving/Saved events were not fired, wtf. Anyways, they're added now. + if (Creating.IsRaisedEventCancelled(new NewEventArgs(content, contentTypeAlias, parent), this)) + { + content.WasCancelled = true; + return content; + } + + if (Saving.IsRaisedEventCancelled(new SaveEventArgs(content), this)) + { + content.WasCancelled = true; + return content; + } + + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateContentRepository(uow)) + { + content.CreatorId = userId; + content.WriterId = userId; + repository.AddOrUpdate(content); + //Generate a new preview + repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); + uow.Commit(); + } + + Saved.RaiseEvent(new SaveEventArgs(content, false), this); + + Created.RaiseEvent(new NewEventArgs(content, false, contentTypeAlias, parent), this); + + Audit(AuditType.New, string.Format("Content '{0}' was created with Id {1}", name, content.Id), content.CreatorId, content.Id); + + return content; + } + + /// + /// Gets an object by Id + /// + /// Id of the Content to retrieve + /// + public IContent GetById(int id) + { + using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) + { + return repository.Get(id); + } + } + + /// + /// Gets an object by Id + /// + /// Ids of the Content to retrieve + /// + public IEnumerable GetByIds(IEnumerable ids) + { + if (ids.Any() == false) return Enumerable.Empty(); + + using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) + { + return repository.GetAll(ids.ToArray()); + } + } + + /// + /// Gets an object by its 'UniqueId' + /// + /// Guid key of the Content to retrieve + /// + public IContent GetById(Guid key) + { + using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) + { + var query = Query.Builder.Where(x => x.Key == key); + var contents = repository.GetByQuery(query); + return contents.SingleOrDefault(); + } + } + + /// + /// Gets a collection of objects by the Id of the + /// + /// Id of the + /// An Enumerable list of objects + public IEnumerable GetContentOfContentType(int id) + { + using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) + { + var query = Query.Builder.Where(x => x.ContentTypeId == id); + var contents = repository.GetByQuery(query); + + return contents; + } + } + + internal IEnumerable GetPublishedContentOfContentType(int id) + { + using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) + { + var query = Query.Builder.Where(x => x.ContentTypeId == id); + var contents = repository.GetByPublishedVersion(query); + + return contents; + } + } + + /// + /// Gets a collection of objects by Level + /// + /// The level to retrieve Content from + /// An Enumerable list of objects + public IEnumerable GetByLevel(int level) + { + using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) + { + var query = Query.Builder.Where(x => x.Level == level && !x.Path.StartsWith(Constants.System.RecycleBinContent.ToInvariantString())); + var contents = repository.GetByQuery(query); + + return contents; + } + } + + /// + /// Gets a specific version of an item. + /// + /// Id of the version to retrieve + /// An item + public IContent GetByVersion(Guid versionId) + { + using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) + { + return repository.GetByVersion(versionId); + } + } + + + /// + /// Gets a collection of an objects versions by Id + /// + /// + /// An Enumerable list of objects + public IEnumerable GetVersions(int id) + { + using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) + { + var versions = repository.GetAllVersions(id); + return versions; + } + } + + /// + /// Gets a collection of objects, which are ancestors of the current content. + /// + /// Id of the to retrieve ancestors for + /// An Enumerable list of objects + public IEnumerable GetAncestors(int id) + { + var content = GetById(id); + return GetAncestors(content); + } + + /// + /// Gets a collection of objects, which are ancestors of the current content. + /// + /// to retrieve ancestors for + /// An Enumerable list of objects + public IEnumerable GetAncestors(IContent content) + { + //null check otherwise we get exceptions + if (content.Path.IsNullOrWhiteSpace()) return Enumerable.Empty(); + + var ids = content.Path.Split(',').Where(x => x != Constants.System.Root.ToInvariantString() && x != content.Id.ToString(CultureInfo.InvariantCulture)).Select(int.Parse).ToArray(); + if (ids.Any() == false) + return new List(); + + using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) + { + return repository.GetAll(ids); + } + } + + /// + /// Gets a collection of objects by Parent Id + /// + /// Id of the Parent to retrieve Children from + /// An Enumerable list of objects + public IEnumerable GetChildren(int id) + { + using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) + { + var query = Query.Builder.Where(x => x.ParentId == id); + var contents = repository.GetByQuery(query).OrderBy(x => x.SortOrder); + + return contents; + } + } + + [Obsolete("Use the overload with 'long' parameter types instead")] + [EditorBrowsable(EditorBrowsableState.Never)] + public IEnumerable GetPagedChildren(int id, int pageIndex, int pageSize, out int totalChildren, + string orderBy, Direction orderDirection, bool orderBySystemField = true, string filter = "") + { + Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); + Mandate.ParameterCondition(pageSize > 0, "pageSize"); + using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) + { + + var query = Query.Builder; + //if the id is System Root, then just get all + if (id != Constants.System.Root) + { + query.Where(x => x.ParentId == id); + } + long total; + var contents = repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out total, orderBy, orderDirection, orderBySystemField, filter); + totalChildren = Convert.ToInt32(total); + return contents; + } + } + + /// + /// Gets a collection of objects by Parent Id + /// + /// Id of the Parent to retrieve Children from + /// Page index (zero based) + /// Page size + /// Total records query would return without paging + /// Field to order by + /// Direction to order by + /// Flag to indicate when ordering by system field + /// Search text filter + /// An Enumerable list of objects + public IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize, out long totalChildren, + string orderBy, Direction orderDirection, bool orderBySystemField = true, string filter = "") + { + Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); + Mandate.ParameterCondition(pageSize > 0, "pageSize"); + using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) + { + + var query = Query.Builder; + //if the id is System Root, then just get all + if (id != Constants.System.Root) + { + query.Where(x => x.ParentId == id); + } + var contents = repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, orderBySystemField, filter); + + return contents; + } + } + + [Obsolete("Use the overload with 'long' parameter types instead")] + [EditorBrowsable(EditorBrowsableState.Never)] + public IEnumerable GetPagedDescendants(int id, int pageIndex, int pageSize, out int totalChildren, string orderBy = "Path", Direction orderDirection = Direction.Ascending, bool orderBySystemField = true, string filter = "") + { + Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); + Mandate.ParameterCondition(pageSize > 0, "pageSize"); + using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) + { + + var query = Query.Builder; + //if the id is System Root, then just get all + if (id != Constants.System.Root) + { + query.Where(x => x.Path.SqlContains(string.Format(",{0},", id), TextColumnType.NVarchar)); + } + long total; + var contents = repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out total, orderBy, orderDirection, orderBySystemField, filter); + totalChildren = Convert.ToInt32(total); + return contents; + } + } + + /// + /// Gets a collection of objects by Parent Id + /// + /// Id of the Parent to retrieve Descendants from + /// Page number + /// Page size + /// Total records query would return without paging + /// Field to order by + /// Direction to order by + /// Flag to indicate when ordering by system field + /// Search text filter + /// An Enumerable list of objects + public IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalChildren, string orderBy = "Path", Direction orderDirection = Direction.Ascending, bool orderBySystemField = true, string filter = "") + { + Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); + Mandate.ParameterCondition(pageSize > 0, "pageSize"); + using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) + { + + var query = Query.Builder; + //if the id is System Root, then just get all + if (id != Constants.System.Root) + { + query.Where(x => x.Path.SqlContains(string.Format(",{0},", id), TextColumnType.NVarchar)); + } + var contents = repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, orderBySystemField, filter); + + return contents; + } + } + + /// + /// Gets a collection of objects by its name or partial name + /// + /// Id of the Parent to retrieve Children from + /// Full or partial name of the children + /// An Enumerable list of objects + public IEnumerable GetChildrenByName(int parentId, string name) + { + using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) + { + var query = Query.Builder.Where(x => x.ParentId == parentId && x.Name.Contains(name)); + var contents = repository.GetByQuery(query); + + return contents; + } + } + + /// + /// Gets a collection of objects by Parent Id + /// + /// Id of the Parent to retrieve Descendants from + /// An Enumerable list of objects + public IEnumerable GetDescendants(int id) + { + var content = GetById(id); + if (content == null) + { + return Enumerable.Empty(); + } + return GetDescendants(content); + } + + /// + /// Gets a collection of objects by Parent Id + /// + /// item to retrieve Descendants from + /// An Enumerable list of objects + public IEnumerable GetDescendants(IContent content) + { + using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) + { + var pathMatch = content.Path + ","; + var query = Query.Builder.Where(x => x.Path.StartsWith(pathMatch) && x.Id != content.Id); + var contents = repository.GetByQuery(query); + + return contents; + } + } + + /// + /// Gets the parent of the current content as an item. + /// + /// Id of the to retrieve the parent from + /// Parent object + public IContent GetParent(int id) + { + var content = GetById(id); + return GetParent(content); + } + + /// + /// Gets the parent of the current content as an item. + /// + /// to retrieve the parent from + /// Parent object + public IContent GetParent(IContent content) + { + if (content.ParentId == Constants.System.Root || content.ParentId == Constants.System.RecycleBinContent) + return null; + + return GetById(content.ParentId); + } + + /// + /// Gets the published version of an item + /// + /// Id of the to retrieve version from + /// An item + public IContent GetPublishedVersion(int id) + { + var version = GetVersions(id); + return version.FirstOrDefault(x => x.Published == true); + } + + /// + /// Gets the published version of a item. + /// + /// The content item. + /// The published version, if any; otherwise, null. + public IContent GetPublishedVersion(IContent content) + { + if (content.Published) return content; + return content.HasPublishedVersion + ? GetByVersion(content.PublishedVersionGuid) + : null; + } + + /// + /// Gets a collection of objects, which reside at the first level / root + /// + /// An Enumerable list of objects + public IEnumerable GetRootContent() + { + using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) + { + var query = Query.Builder.Where(x => x.ParentId == Constants.System.Root); + var contents = repository.GetByQuery(query); + + return contents; + } + } + + /// + /// Gets all published content items + /// + /// + internal IEnumerable GetAllPublished() + { + using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) + { + var query = Query.Builder.Where(x => x.Trashed == false); + return repository.GetByPublishedVersion(query); + } + } + + /// + /// Gets a collection of objects, which has an expiration date less than or equal to today. + /// + /// An Enumerable list of objects + public IEnumerable GetContentForExpiration() + { + using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) + { + var query = Query.Builder.Where(x => x.Published == true && x.ExpireDate <= DateTime.Now); + var contents = repository.GetByQuery(query); + + return contents; + } + } + + /// + /// Gets a collection of objects, which has a release date less than or equal to today. + /// + /// An Enumerable list of objects + public IEnumerable GetContentForRelease() + { + using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) + { + var query = Query.Builder.Where(x => x.Published == false && x.ReleaseDate <= DateTime.Now); + var contents = repository.GetByQuery(query); + + return contents; + } + } + + /// + /// Gets a collection of an objects, which resides in the Recycle Bin + /// + /// An Enumerable list of objects + public IEnumerable GetContentInRecycleBin() + { + using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) + { + var query = Query.Builder.Where(x => x.Path.Contains(Constants.System.RecycleBinContent.ToInvariantString())); + var contents = repository.GetByQuery(query); + + return contents; + } + } + + + + /// + /// Checks whether an item has any children + /// + /// Id of the + /// True if the content has any children otherwise False + public bool HasChildren(int id) + { + return CountChildren(id) > 0; + } + + internal int CountChildren(int id) + { + using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) + { + var query = Query.Builder.Where(x => x.ParentId == id); + var count = repository.Count(query); + return count; + } + } + + /// + /// Checks whether an item has any published versions + /// + /// Id of the + /// True if the content has any published version otherwise False + public bool HasPublishedVersion(int id) + { + using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) + { + var query = Query.Builder.Where(x => x.Published == true && x.Id == id && x.Trashed == false); + int count = repository.Count(query); + return count > 0; + } + } + + /// + /// Checks if the passed in can be published based on the anscestors publish state. + /// + /// to check if anscestors are published + /// True if the Content can be published, otherwise False + public bool IsPublishable(IContent content) + { + //If the passed in content has yet to be saved we "fallback" to checking the Parent + //because if the Parent is publishable then the current content can be Saved and Published + if (content.HasIdentity == false) + { + IContent parent = GetById(content.ParentId); + return IsPublishable(parent, true); + } + + return IsPublishable(content, false); + } + + /// + /// This will rebuild the xml structures for content in the database. + /// + /// This is not used for anything + /// True if publishing succeeded, otherwise False + /// + /// This is used for when a document type alias or a document type property is changed, the xml will need to + /// be regenerated. + /// + public bool RePublishAll(int userId = 0) + { + try + { + RebuildXmlStructures(); + return true; + } + catch (Exception ex) + { + Logger.Error("An error occurred executing RePublishAll", ex); + return false; + } + } + + /// + /// This will rebuild the xml structures for content in the database. + /// + /// + /// If specified will only rebuild the xml for the content type's specified, otherwise will update the structure + /// for all published content. + /// + internal void RePublishAll(params int[] contentTypeIds) + { + try + { + RebuildXmlStructures(contentTypeIds); + } + catch (Exception ex) + { + Logger.Error("An error occurred executing RePublishAll", ex); + } + } + + /// + /// Publishes a single object + /// + /// The to publish + /// Optional Id of the User issueing the publishing + /// True if publishing succeeded, otherwise False + public bool Publish(IContent content, int userId = 0) + { + var result = SaveAndPublishDo(content, userId); + Logger.Info("Call was made to ContentService.Publish, use PublishWithStatus instead since that method will provide more detailed information on the outcome"); + return result.Success; + } + + /// + /// Publishes a object and all its children + /// + /// The to publish along with its children + /// Optional Id of the User issueing the publishing + /// + /// The list of statuses for all published items + IEnumerable> IContentServiceOperations.PublishWithChildren(IContent content, int userId, bool includeUnpublished) + { + return PublishWithChildrenDo(content, userId, includeUnpublished); + } + + /// + /// Saves and Publishes a single object + /// + /// The to save and publish + /// Optional Id of the User issueing the publishing + /// Optional boolean indicating whether or not to raise save events. + /// True if publishing succeeded, otherwise False + Attempt IContentServiceOperations.SaveAndPublish(IContent content, int userId, bool raiseEvents) + { + return SaveAndPublishDo(content, userId, raiseEvents); + } + + /// + /// Deletes an object by moving it to the Recycle Bin + /// + /// Move an item to the Recycle Bin will result in the item being unpublished + /// The to delete + /// Optional Id of the User deleting the Content + Attempt IContentServiceOperations.MoveToRecycleBin(IContent content, int userId) + { + var evtMsgs = EventMessagesFactory.Get(); + + using (new WriteLock(Locker)) + { + var originalPath = content.Path; + + if (Trashing.IsRaisedEventCancelled( + new MoveEventArgs(evtMsgs, new MoveEventInfo(content, originalPath, Constants.System.RecycleBinContent)), + this)) + { + return OperationStatus.Cancelled(evtMsgs); + } + + var moveInfo = new List> + { + new MoveEventInfo(content, originalPath, Constants.System.RecycleBinContent) + }; + + //Make sure that published content is unpublished before being moved to the Recycle Bin + if (HasPublishedVersion(content.Id)) + { + //TODO: this shouldn't be a 'sub operation', and if it needs to be it cannot raise events and cannot be cancelled! + UnPublish(content, userId); + } + + //Unpublish descendents of the content item that is being moved to trash + var descendants = GetDescendants(content).OrderBy(x => x.Level).ToList(); + foreach (var descendant in descendants) + { + //TODO: this shouldn't be a 'sub operation', and if it needs to be it cannot raise events and cannot be cancelled! + UnPublish(descendant, userId); + } + + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateContentRepository(uow)) + { + content.WriterId = userId; + content.ChangeTrashedState(true); + repository.AddOrUpdate(content); + + //Loop through descendants to update their trash state, but ensuring structure by keeping the ParentId + foreach (var descendant in descendants) + { + moveInfo.Add(new MoveEventInfo(descendant, descendant.Path, descendant.ParentId)); + + descendant.WriterId = userId; + descendant.ChangeTrashedState(true, descendant.ParentId); + repository.AddOrUpdate(descendant); + } + + uow.Commit(); + } + + Trashed.RaiseEvent(new MoveEventArgs(false, evtMsgs, moveInfo.ToArray()), this); + + Audit(AuditType.Move, "Move Content to Recycle Bin performed by user", userId, content.Id); + + return OperationStatus.Success(evtMsgs); + } + } + + /// + /// UnPublishes a single object + /// + /// The to publish + /// Optional Id of the User issueing the publishing + /// True if unpublishing succeeded, otherwise False + Attempt IContentServiceOperations.UnPublish(IContent content, int userId) + { + return UnPublishDo(content, false, userId); + } + + /// + /// Publishes a single object + /// + /// The to publish + /// Optional Id of the User issueing the publishing + /// True if publishing succeeded, otherwise False + public Attempt PublishWithStatus(IContent content, int userId = 0) + { + return ((IContentServiceOperations)this).Publish(content, userId); + } + + /// + /// Publishes a object and all its children + /// + /// The to publish along with its children + /// Optional Id of the User issueing the publishing + /// True if publishing succeeded, otherwise False + [Obsolete("Use PublishWithChildrenWithStatus instead, that method will provide more detailed information on the outcome and also allows the includeUnpublished flag")] + public bool PublishWithChildren(IContent content, int userId = 0) + { + var result = PublishWithChildrenDo(content, userId, true); + + //This used to just return false only when the parent content failed, otherwise would always return true so we'll + // do the same thing for the moment + if (result.All(x => x.Result.ContentItem.Id != content.Id)) + return false; + + return result.Single(x => x.Result.ContentItem.Id == content.Id).Success; + } + + /// + /// Publishes a object and all its children + /// + /// The to publish along with its children + /// Optional Id of the User issueing the publishing + /// set to true if you want to also publish children that are currently unpublished + /// True if publishing succeeded, otherwise False + public IEnumerable> PublishWithChildrenWithStatus(IContent content, int userId = 0, bool includeUnpublished = false) + { + return ((IContentServiceOperations)this).PublishWithChildren(content, userId, includeUnpublished); + } + + /// + /// UnPublishes a single object + /// + /// The to publish + /// Optional Id of the User issueing the publishing + /// True if unpublishing succeeded, otherwise False + public bool UnPublish(IContent content, int userId = 0) + { + return ((IContentServiceOperations)this).UnPublish(content, userId).Success; + } + + /// + /// Saves and Publishes a single object + /// + /// The to save and publish + /// Optional Id of the User issueing the publishing + /// Optional boolean indicating whether or not to raise save events. + /// True if publishing succeeded, otherwise False + [Obsolete("Use SaveAndPublishWithStatus instead, that method will provide more detailed information on the outcome")] + public bool SaveAndPublish(IContent content, int userId = 0, bool raiseEvents = true) + { + var result = SaveAndPublishDo(content, userId, raiseEvents); + return result.Success; + } + + /// + /// Saves and Publishes a single object + /// + /// The to save and publish + /// Optional Id of the User issueing the publishing + /// Optional boolean indicating whether or not to raise save events. + /// True if publishing succeeded, otherwise False + public Attempt SaveAndPublishWithStatus(IContent content, int userId = 0, bool raiseEvents = true) + { + return ((IContentServiceOperations)this).SaveAndPublish(content, userId, raiseEvents); + } + + /// + /// Saves a single object + /// + /// The to save + /// Optional Id of the User saving the Content + /// Optional boolean indicating whether or not to raise events. + public void Save(IContent content, int userId = 0, bool raiseEvents = true) + { + ((IContentServiceOperations)this).Save(content, userId, raiseEvents); + } + + /// + /// Saves a collection of objects. + /// + /// Collection of to save + /// Optional Id of the User saving the Content + /// Optional boolean indicating whether or not to raise events. + Attempt IContentServiceOperations.Save(IEnumerable contents, int userId, bool raiseEvents) + { + var asArray = contents.ToArray(); + + var evtMsgs = EventMessagesFactory.Get(); + + if (raiseEvents) + { + if (Saving.IsRaisedEventCancelled( + new SaveEventArgs(asArray, evtMsgs), + this)) + { + return OperationStatus.Cancelled(evtMsgs); + } + } + using (new WriteLock(Locker)) + { + var containsNew = asArray.Any(x => x.HasIdentity == false); + + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateContentRepository(uow)) + { + if (containsNew) + { + foreach (var content in asArray) + { + content.WriterId = userId; + + //Only change the publish state if the "previous" version was actually published + if (content.Published) + content.ChangePublishedState(PublishedState.Saved); + + repository.AddOrUpdate(content); + //add or update preview + repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); + } + } + else + { + foreach (var content in asArray) + { + content.WriterId = userId; + repository.AddOrUpdate(content); + //add or update preview + repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); + } + } + + uow.Commit(); + } + + if (raiseEvents) + Saved.RaiseEvent(new SaveEventArgs(asArray, false, evtMsgs), this); + + Audit(AuditType.Save, "Bulk Save content performed by user", userId == -1 ? 0 : userId, Constants.System.Root); + + return OperationStatus.Success(evtMsgs); + } + } + + /// + /// Permanently deletes an object. + /// + /// + /// This method will also delete associated media files, child content and possibly associated domains. + /// + /// Please note that this method will completely remove the Content from the database + /// The to delete + /// Optional Id of the User deleting the Content + Attempt IContentServiceOperations.Delete(IContent content, int userId) + { + var evtMsgs = EventMessagesFactory.Get(); + + using (new WriteLock(Locker)) + { + if (Deleting.IsRaisedEventCancelled( + new DeleteEventArgs(content, evtMsgs), + this)) + { + return OperationStatus.Cancelled(evtMsgs); + } + + //Make sure that published content is unpublished before being deleted + if (HasPublishedVersion(content.Id)) + { + UnPublish(content, userId); + } + + //Delete children before deleting the 'possible parent' + var children = GetChildren(content.Id); + foreach (var child in children) + { + Delete(child, userId); + } + + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateContentRepository(uow)) + { + repository.Delete(content); + uow.Commit(); + + var args = new DeleteEventArgs(content, false, evtMsgs); + Deleted.RaiseEvent(args, this); + + //remove any flagged media files + repository.DeleteMediaFiles(args.MediaFilesToDelete); + } + + Audit(AuditType.Delete, "Delete Content performed by user", userId, content.Id); + + return OperationStatus.Success(evtMsgs); + } + } + + /// + /// Publishes a single object + /// + /// The to publish + /// Optional Id of the User issueing the publishing + /// The published status attempt + Attempt IContentServiceOperations.Publish(IContent content, int userId) + { + return SaveAndPublishDo(content, userId); + } + + /// + /// Saves a single object + /// + /// The to save + /// Optional Id of the User saving the Content + /// Optional boolean indicating whether or not to raise events. + Attempt IContentServiceOperations.Save(IContent content, int userId, bool raiseEvents) + { + return Save(content, true, userId, raiseEvents); + } + + /// + /// Saves a collection of objects. + /// + /// + /// If the collection of content contains new objects that references eachother by Id or ParentId, + /// then use the overload Save method with a collection of Lazy . + /// + /// Collection of to save + /// Optional Id of the User saving the Content + /// Optional boolean indicating whether or not to raise events. + public void Save(IEnumerable contents, int userId = 0, bool raiseEvents = true) + { + ((IContentServiceOperations)this).Save(contents, userId, raiseEvents); + } + + /// + /// Deletes all content of specified type. All children of deleted content is moved to Recycle Bin. + /// + /// This needs extra care and attention as its potentially a dangerous and extensive operation + /// Id of the + /// Optional Id of the user issueing the delete operation + public void DeleteContentOfType(int contentTypeId, int userId = 0) + { + //TODO: This currently this is called from the ContentTypeService but that needs to change, + // if we are deleting a content type, we should just delete the data and do this operation slightly differently. + // This method will recursively go lookup every content item, check if any of it's descendants are + // of a different type, move them to the recycle bin, then permanently delete the content items. + // The main problem with this is that for every content item being deleted, events are raised... + // which we need for many things like keeping caches in sync, but we can surely do this MUCH better. + + using (new WriteLock(Locker)) + { + using (var uow = UowProvider.GetUnitOfWork()) + { + var repository = RepositoryFactory.CreateContentRepository(uow); + //NOTE What about content that has the contenttype as part of its composition? + var query = Query.Builder.Where(x => x.ContentTypeId == contentTypeId); + var contents = repository.GetByQuery(query).ToArray(); + + if (Deleting.IsRaisedEventCancelled(new DeleteEventArgs(contents), this)) + return; + + foreach (var content in contents.OrderByDescending(x => x.ParentId)) + { + //Look for children of current content and move that to trash before the current content is deleted + var c = content; + var childQuery = Query.Builder.Where(x => x.Path.StartsWith(c.Path)); + var children = repository.GetByQuery(childQuery); + + foreach (var child in children) + { + if (child.ContentType.Id != contentTypeId) + MoveToRecycleBin(child, userId); + } + + //Permantly delete the content + Delete(content, userId); + } + } + + Audit(AuditType.Delete, + string.Format("Delete Content of Type {0} performed by user", contentTypeId), + userId, Constants.System.Root); + } + } + + /// + /// Permanently deletes an object as well as all of its Children. + /// + /// + /// This method will also delete associated media files, child content and possibly associated domains. + /// + /// Please note that this method will completely remove the Content from the database + /// The to delete + /// Optional Id of the User deleting the Content + public void Delete(IContent content, int userId = 0) + { + ((IContentServiceOperations)this).Delete(content, userId); + } + + /// + /// Permanently deletes versions from an object prior to a specific date. + /// This method will never delete the latest version of a content item. + /// + /// Id of the object to delete versions from + /// Latest version date + /// Optional Id of the User deleting versions of a Content object + public void DeleteVersions(int id, DateTime versionDate, int userId = 0) + { + if (DeletingVersions.IsRaisedEventCancelled(new DeleteRevisionsEventArgs(id, dateToRetain: versionDate), this)) + return; + + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateContentRepository(uow)) + { + repository.DeleteVersions(id, versionDate); + uow.Commit(); + } + + DeletedVersions.RaiseEvent(new DeleteRevisionsEventArgs(id, false, dateToRetain: versionDate), this); + + Audit(AuditType.Delete, "Delete Content by version date performed by user", userId, Constants.System.Root); + } + + /// + /// Permanently deletes specific version(s) from an object. + /// This method will never delete the latest version of a content item. + /// + /// Id of the object to delete a version from + /// Id of the version to delete + /// Boolean indicating whether to delete versions prior to the versionId + /// Optional Id of the User deleting versions of a Content object + public void DeleteVersion(int id, Guid versionId, bool deletePriorVersions, int userId = 0) + { + using (new WriteLock(Locker)) + { + if (DeletingVersions.IsRaisedEventCancelled(new DeleteRevisionsEventArgs(id, specificVersion: versionId), this)) + return; + + if (deletePriorVersions) + { + var content = GetByVersion(versionId); + DeleteVersions(id, content.UpdateDate, userId); + } + + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateContentRepository(uow)) + { + repository.DeleteVersion(versionId); + uow.Commit(); + } + + DeletedVersions.RaiseEvent(new DeleteRevisionsEventArgs(id, false, specificVersion: versionId), this); + + Audit(AuditType.Delete, "Delete Content by version performed by user", userId, Constants.System.Root); + } + } + + /// + /// Deletes an object by moving it to the Recycle Bin + /// + /// Move an item to the Recycle Bin will result in the item being unpublished + /// The to delete + /// Optional Id of the User deleting the Content + public void MoveToRecycleBin(IContent content, int userId = 0) + { + ((IContentServiceOperations)this).MoveToRecycleBin(content, userId); + } + + /// + /// Moves an object to a new location by changing its parent id. + /// + /// + /// If the object is already published it will be + /// published after being moved to its new location. Otherwise it'll just + /// be saved with a new parent id. + /// + /// The to move + /// Id of the Content's new Parent + /// Optional Id of the User moving the Content + public void Move(IContent content, int parentId, int userId = 0) + { + using (new WriteLock(Locker)) + { + //This ensures that the correct method is called if this method is used to Move to recycle bin. + if (parentId == Constants.System.RecycleBinContent) + { + MoveToRecycleBin(content, userId); + return; + } + + if (Moving.IsRaisedEventCancelled( + new MoveEventArgs( + new MoveEventInfo(content, content.Path, parentId)), this)) + { + return; + } + + //used to track all the moved entities to be given to the event + var moveInfo = new List>(); + + //call private method that does the recursive moving + PerformMove(content, parentId, userId, moveInfo); + + Moved.RaiseEvent(new MoveEventArgs(false, moveInfo.ToArray()), this); + + Audit(AuditType.Move, "Move Content performed by user", userId, content.Id); + } + } + + /// + /// Empties the Recycle Bin by deleting all that resides in the bin + /// + public void EmptyRecycleBin() + { + using (new WriteLock(Locker)) + { + Dictionary> entities; + List files; + bool success; + var nodeObjectType = new Guid(Constants.ObjectTypes.Document); + + using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) + { + //Create a dictionary of ids -> dictionary of property aliases + values + entities = repository.GetEntitiesInRecycleBin() + .ToDictionary( + key => key.Id, + val => (IEnumerable)val.Properties); + + files = ((ContentRepository)repository).GetFilesInRecycleBinForUploadField(); + + if (EmptyingRecycleBin.IsRaisedEventCancelled(new RecycleBinEventArgs(nodeObjectType, entities, files), this)) + return; + + success = repository.EmptyRecycleBin(); + + EmptiedRecycleBin.RaiseEvent(new RecycleBinEventArgs(nodeObjectType, entities, files, success), this); + + if (success) + repository.DeleteMediaFiles(files); + } + } + Audit(AuditType.Delete, "Empty Content Recycle Bin performed by user", 0, Constants.System.RecycleBinContent); + } + + /// + /// Copies an object by creating a new Content object of the same type and copies all data from the current + /// to the new copy which is returned. Recursively copies all children. + /// + /// The to copy + /// Id of the Content's new Parent + /// Boolean indicating whether the copy should be related to the original + /// Optional Id of the User copying the Content + /// The newly created object + public IContent Copy(IContent content, int parentId, bool relateToOriginal, int userId = 0) + { + return Copy(content, parentId, relateToOriginal, true, userId); + } + + /// + /// Copies an object by creating a new Content object of the same type and copies all data from the current + /// to the new copy which is returned. + /// + /// The to copy + /// Id of the Content's new Parent + /// Boolean indicating whether the copy should be related to the original + /// A value indicating whether to recursively copy children. + /// Optional Id of the User copying the Content + /// The newly created object + public IContent Copy(IContent content, int parentId, bool relateToOriginal, bool recursive, int userId = 0) + { + //TODO: This all needs to be managed correctly so that the logic is submitted in one + // transaction, the CRUD needs to be moved to the repo + + using (new WriteLock(Locker)) + { + var copy = content.DeepCloneWithResetIdentities(); + copy.ParentId = parentId; + + // A copy should never be set to published automatically even if the original was. + copy.ChangePublishedState(PublishedState.Unpublished); + + if (Copying.IsRaisedEventCancelled(new CopyEventArgs(content, copy, parentId), this)) + return null; + + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateContentRepository(uow)) + { + // Update the create author and last edit author + copy.CreatorId = userId; + copy.WriterId = userId; + + repository.AddOrUpdate(copy); + //add or update a preview + repository.AddOrUpdatePreviewXml(copy, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); + uow.Commit(); + + + //Special case for the associated tags + //TODO: Move this to the repository layer in a single transaction! + //don't copy tags data in tags table if the item is in the recycle bin + if (parentId != Constants.System.RecycleBinContent) + { + + var tags = uow.Database.Fetch("WHERE nodeId = @Id", new { Id = content.Id }); + foreach (var tag in tags) + { + uow.Database.Insert(new TagRelationshipDto { NodeId = copy.Id, TagId = tag.TagId, PropertyTypeId = tag.PropertyTypeId }); + } + } + } + + if (recursive) + { + //Look for children and copy those as well + var children = GetChildren(content.Id); + foreach (var child in children) + { + //TODO: This shouldn't recurse back to this method, it should be done in a private method + // that doesn't have a nested lock and so we can perform the entire operation in one commit. + Copy(child, copy.Id, relateToOriginal, true, userId); + } + } + + Copied.RaiseEvent(new CopyEventArgs(content, copy, false, parentId, relateToOriginal), this); + + Audit(AuditType.Copy, "Copy Content performed by user", content.WriterId, content.Id); + return copy; + } + } + + + /// + /// Sends an to Publication, which executes handlers and events for the 'Send to Publication' action. + /// + /// The to send to publication + /// Optional Id of the User issueing the send to publication + /// True if sending publication was succesfull otherwise false + public bool SendToPublication(IContent content, int userId = 0) + { + if (SendingToPublish.IsRaisedEventCancelled(new SendToPublishEventArgs(content), this)) + return false; + + //Save before raising event + Save(content, userId); + + SentToPublish.RaiseEvent(new SendToPublishEventArgs(content, false), this); + + Audit(AuditType.SendToPublish, "Send to Publish performed by user", content.WriterId, content.Id); + + return true; + } + + /// + /// Rollback an object to a previous version. + /// This will create a new version, which is a copy of all the old data. + /// + /// + /// The way data is stored actually only allows us to rollback on properties + /// and not data like Name and Alias of the Content. + /// + /// Id of the being rolled back + /// Id of the version to rollback to + /// Optional Id of the User issueing the rollback of the Content + /// The newly created object + public IContent Rollback(int id, Guid versionId, int userId = 0) + { + var content = GetByVersion(versionId); + + if (RollingBack.IsRaisedEventCancelled(new RollbackEventArgs(content), this)) + return content; + + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateContentRepository(uow)) + { + content.WriterId = userId; + content.CreatorId = userId; + content.ChangePublishedState(PublishedState.Unpublished); + + repository.AddOrUpdate(content); + //add or update a preview + repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); + uow.Commit(); + } + + RolledBack.RaiseEvent(new RollbackEventArgs(content, false), this); + + Audit(AuditType.RollBack, "Content rollback performed by user", content.WriterId, content.Id); + + return content; + } + + /// + /// Sorts a collection of objects by updating the SortOrder according + /// to the ordering of items in the passed in . + /// + /// + /// Using this method will ensure that the Published-state is maintained upon sorting + /// so the cache is updated accordingly - as needed. + /// + /// + /// + /// + /// True if sorting succeeded, otherwise False + public bool Sort(IEnumerable items, int userId = 0, bool raiseEvents = true) + { + if (raiseEvents) + { + if (Saving.IsRaisedEventCancelled(new SaveEventArgs(items), this)) + return false; + } + + var shouldBePublished = new List(); + var shouldBeSaved = new List(); + + var asArray = items.ToArray(); + using (new WriteLock(Locker)) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateContentRepository(uow)) + { + int i = 0; + foreach (var content in asArray) + { + //If the current sort order equals that of the content + //we don't need to update it, so just increment the sort order + //and continue. + if (content.SortOrder == i) + { + i++; + continue; + } + + content.SortOrder = i; + content.WriterId = userId; + i++; + + if (content.Published) + { + //TODO: This should not be an inner operation, but if we do this, it cannot raise events and cannot be cancellable! + var published = _publishingStrategy.Publish(content, userId); + shouldBePublished.Add(content); + } + else + shouldBeSaved.Add(content); + + repository.AddOrUpdate(content); + //add or update a preview + repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); + } + + foreach (var content in shouldBePublished) + { + //Create and Save ContentXml DTO + repository.AddOrUpdateContentXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); + } + + uow.Commit(); + } + } + + if (raiseEvents) + Saved.RaiseEvent(new SaveEventArgs(asArray, false), this); + + if (shouldBePublished.Any()) + { + //TODO: This should not be an inner operation, but if we do this, it cannot raise events and cannot be cancellable! + _publishingStrategy.PublishingFinalized(shouldBePublished, false); + } + + + Audit(AuditType.Sort, "Sorting content performed by user", userId, 0); + + return true; + } + + /// + /// Rebuilds all xml content in the cmsContentXml table for all documents + /// + /// + /// Only rebuild the xml structures for the content type ids passed in, if none then rebuilds the structures + /// for all content + /// + public void RebuildXmlStructures(params int[] contentTypeIds) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateContentRepository(uow)) + { + repository.RebuildXmlStructures( + content => _entitySerializer.Serialize(this, _dataTypeService, _userService, content), + contentTypeIds: contentTypeIds.Length == 0 ? null : contentTypeIds); + + uow.Commit(); + } + + Audit(AuditType.Publish, "ContentService.RebuildXmlStructures completed, the xml has been regenerated in the database", 0, Constants.System.Root); + + } + + #region Internal Methods + + /// + /// Gets a collection of descendants by the first Parent. + /// + /// item to retrieve Descendants from + /// An Enumerable list of objects + internal IEnumerable GetPublishedDescendants(IContent content) + { + using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) + { + var query = Query.Builder.Where(x => x.Id != content.Id && x.Path.StartsWith(content.Path) && x.Trashed == false); + var contents = repository.GetByPublishedVersion(query); + + return contents; + } + } + + #endregion + + #region Private Methods + + private void Audit(AuditType type, string message, int userId, int objectId) + { + var uow = UowProvider.GetUnitOfWork(); + using (var auditRepo = RepositoryFactory.CreateAuditRepository(uow)) + { + auditRepo.AddOrUpdate(new AuditItem(objectId, message, type, userId)); + uow.Commit(); + } + } + + //TODO: All of this needs to be moved to the repository + private void PerformMove(IContent content, int parentId, int userId, ICollection> moveInfo) + { + //add a tracking item to use in the Moved event + moveInfo.Add(new MoveEventInfo(content, content.Path, parentId)); + + content.WriterId = userId; + if (parentId == Constants.System.Root) + { + content.Path = string.Concat(Constants.System.Root, ",", content.Id); + content.Level = 1; + } + else + { + var parent = GetById(parentId); + content.Path = string.Concat(parent.Path, ",", content.Id); + content.Level = parent.Level + 1; + } + + //If Content is being moved away from Recycle Bin, its state should be un-trashed + if (content.Trashed && parentId != Constants.System.RecycleBinContent) + { + content.ChangeTrashedState(false, parentId); + } + else + { + content.ParentId = parentId; + } + + //If Content is published, it should be (re)published from its new location + if (content.Published) + { + //If Content is Publishable its saved and published + //otherwise we save the content without changing the publish state, and generate new xml because the Path, Level and Parent has changed. + if (IsPublishable(content)) + { + //TODO: This is raising events, probably not desirable as this costs performance for event listeners like Examine + SaveAndPublish(content, userId); + } + else + { + //TODO: This is raising events, probably not desirable as this costs performance for event listeners like Examine + Save(content, false, userId); + + //TODO: This shouldn't be here! This needs to be part of the repository logic but in order to fix this we need to + // change how this method calls "Save" as it needs to save using an internal method + using (var uow = UowProvider.GetUnitOfWork()) + { + var xml = _entitySerializer.Serialize(this, _dataTypeService, _userService, content); + + var poco = new ContentXmlDto { NodeId = content.Id, Xml = xml.ToDataString() }; + var exists = + uow.Database.FirstOrDefault("WHERE nodeId = @Id", new { Id = content.Id }) != + null; + int result = exists + ? uow.Database.Update(poco) + : Convert.ToInt32(uow.Database.Insert(poco)); + } + } + } + else + { + //TODO: This is raising events, probably not desirable as this costs performance for event listeners like Examine + Save(content, userId); + } + + //Ensure that Path and Level is updated on children + var children = GetChildren(content.Id).ToArray(); + if (children.Any()) + { + foreach (var child in children) + { + PerformMove(child, content.Id, userId, moveInfo); + } + } + } + + /// + /// Publishes a object and all its children + /// + /// The to publish along with its children + /// Optional Id of the User issueing the publishing + /// If set to true, this will also publish descendants that are completely unpublished, normally this will only publish children that have previously been published + /// + /// A list of publish statues. If the parent document is not valid or cannot be published because it's parent(s) is not published + /// then the list will only contain one status item, otherwise it will contain status items for it and all of it's descendants that + /// are to be published. + /// + private IEnumerable> PublishWithChildrenDo( + IContent content, int userId = 0, bool includeUnpublished = false) + { + if (content == null) throw new ArgumentNullException("content"); + + var evtMsgs = EventMessagesFactory.Get(); + + using (new WriteLock(Locker)) + { + var result = new List>(); + + //Check if parent is published (although not if its a root node) - if parent isn't published this Content cannot be published + if (content.ParentId != Constants.System.Root && content.ParentId != Constants.System.RecycleBinContent && IsPublishable(content) == false) + { + Logger.Info( + string.Format( + "Content '{0}' with Id '{1}' could not be published because its parent or one of its ancestors is not published.", + content.Name, content.Id)); + result.Add(Attempt.Fail(new PublishStatus(content, PublishStatusType.FailedPathNotPublished, evtMsgs))); + return result; + } + + //Content contains invalid property values and can therefore not be published - fire event? + if (!content.IsValid()) + { + Logger.Info( + string.Format("Content '{0}' with Id '{1}' could not be published because of invalid properties.", + content.Name, content.Id)); + result.Add( + Attempt.Fail( + new PublishStatus(content, PublishStatusType.FailedContentInvalid, evtMsgs) + { + InvalidProperties = ((ContentBase)content).LastInvalidProperties + })); + return result; + } + + //Consider creating a Path query instead of recursive method: + //var query = Query.Builder.Where(x => x.Path.StartsWith(content.Path)); + + var updated = new List(); + var list = new List(); + list.Add(content); //include parent item + list.AddRange(GetDescendants(content)); + + var internalStrategy = (PublishingStrategy)_publishingStrategy; + + //Publish and then update the database with new status + var publishedOutcome = internalStrategy.PublishWithChildrenInternal(list, userId, includeUnpublished).ToArray(); + var published = publishedOutcome + .Where(x => x.Success || x.Result.StatusType == PublishStatusType.SuccessAlreadyPublished) + // ensure proper order (for events) - cannot publish a child before its parent! + .OrderBy(x => x.Result.ContentItem.Level) + .ThenBy(x => x.Result.ContentItem.SortOrder); + + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateContentRepository(uow)) + { + //NOTE The Publish with subpages-dialog was used more as a republish-type-thing, so we'll have to include PublishStatusType.SuccessAlreadyPublished + //in the updated-list, so the Published event is triggered with the expected set of pages and the xml is updated. + foreach (var item in published) + { + item.Result.ContentItem.WriterId = userId; + repository.AddOrUpdate(item.Result.ContentItem); + //add or update a preview + repository.AddOrUpdatePreviewXml(item.Result.ContentItem, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); + //add or update the published xml + repository.AddOrUpdateContentXml(item.Result.ContentItem, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); + updated.Add(item.Result.ContentItem); + } + + uow.Commit(); + + } + //Save xml to db and call following method to fire event: + _publishingStrategy.PublishingFinalized(updated, false); + + Audit(AuditType.Publish, "Publish with Children performed by user", userId, content.Id); + + + return publishedOutcome; + } + } + + /// + /// UnPublishes a single object + /// + /// The to publish + /// Optional boolean to avoid having the cache refreshed when calling this Unpublish method. By default this method will update the cache. + /// Optional Id of the User issueing the publishing + /// True if unpublishing succeeded, otherwise False + private Attempt UnPublishDo(IContent content, bool omitCacheRefresh = false, int userId = 0) + { + var newest = GetById(content.Id); // ensure we have the newest version + if (content.Version != newest.Version) // but use the original object if it's already the newest version + content = newest; + + var evtMsgs = EventMessagesFactory.Get(); + + var published = content.Published ? content : GetPublishedVersion(content.Id); // get the published version + if (published == null) + { + return Attempt.Succeed(new UnPublishStatus(content, UnPublishedStatusType.SuccessAlreadyUnPublished, evtMsgs)); // already unpublished + } + + var unpublished = _publishingStrategy.UnPublish(content, userId); + if (unpublished == false) return Attempt.Fail(new UnPublishStatus(content, UnPublishedStatusType.FailedCancelledByEvent, evtMsgs)); + + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateContentRepository(uow)) + { + content.WriterId = userId; + repository.AddOrUpdate(content); + // is published is not newest, reset the published flag on published version + if (published.Version != content.Version) + repository.ClearPublished(published); + repository.DeleteContentXml(content); + + uow.Commit(); + } + //Delete xml from db? and call following method to fire event through PublishingStrategy to update cache + if (omitCacheRefresh == false) + _publishingStrategy.UnPublishingFinalized(content); + + Audit(AuditType.UnPublish, "UnPublish performed by user", userId, content.Id); + + return Attempt.Succeed(new UnPublishStatus(content, UnPublishedStatusType.Success, evtMsgs)); + } + + /// + /// Saves and Publishes a single object + /// + /// The to save and publish + /// Optional Id of the User issueing the publishing + /// Optional boolean indicating whether or not to raise save events. + /// True if publishing succeeded, otherwise False + private Attempt SaveAndPublishDo(IContent content, int userId = 0, bool raiseEvents = true) + { + var evtMsgs = EventMessagesFactory.Get(); + + if (raiseEvents) + { + if (Saving.IsRaisedEventCancelled( + new SaveEventArgs(content, evtMsgs), this)) + { + return Attempt.Fail(new PublishStatus(content, PublishStatusType.FailedCancelledByEvent, evtMsgs)); + } + } + + using (new WriteLock(Locker)) + { + //Has this content item previously been published? If so, we don't need to refresh the children + var previouslyPublished = content.HasIdentity && HasPublishedVersion(content.Id); //content might not have an id + var publishStatus = new PublishStatus(content, PublishStatusType.Success, evtMsgs); //initially set to success + + //Check if parent is published (although not if its a root node) - if parent isn't published this Content cannot be published + publishStatus.StatusType = CheckAndLogIsPublishable(content); + //if it is not successful, then check if the props are valid + if ((int)publishStatus.StatusType < 10) + { + //Content contains invalid property values and can therefore not be published - fire event? + publishStatus.StatusType = CheckAndLogIsValid(content); + //set the invalid properties (if there are any) + publishStatus.InvalidProperties = ((ContentBase)content).LastInvalidProperties; + } + //if we're still successful, then publish using the strategy + if (publishStatus.StatusType == PublishStatusType.Success) + { + var internalStrategy = (PublishingStrategy)_publishingStrategy; + //Publish and then update the database with new status + var publishResult = internalStrategy.PublishInternal(content, userId); + //set the status type to the publish result + publishStatus.StatusType = publishResult.Result.StatusType; + } + + //we are successfully published if our publishStatus is still Successful + bool published = publishStatus.StatusType == PublishStatusType.Success; + + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateContentRepository(uow)) + { + if (published == false) + { + content.ChangePublishedState(PublishedState.Saved); + } + //Since this is the Save and Publish method, the content should be saved even though the publish fails or isn't allowed + if (content.HasIdentity == false) + { + content.CreatorId = userId; + } + content.WriterId = userId; + + repository.AddOrUpdate(content); + + //Generate a new preview + repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); + + if (published) + { + //Content Xml + repository.AddOrUpdateContentXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); + } + + uow.Commit(); + } + + if (raiseEvents) + Saved.RaiseEvent(new SaveEventArgs(content, false, evtMsgs), this); + + //Save xml to db and call following method to fire event through PublishingStrategy to update cache + if (published) + { + _publishingStrategy.PublishingFinalized(content); + } + + //We need to check if children and their publish state to ensure that we 'republish' content that was previously published + if (published && previouslyPublished == false && HasChildren(content.Id)) + { + var descendants = GetPublishedDescendants(content); + + _publishingStrategy.PublishingFinalized(descendants, false); + } + + Audit(AuditType.Publish, "Save and Publish performed by user", userId, content.Id); + + return Attempt.If(publishStatus.StatusType == PublishStatusType.Success, publishStatus); + } + } + + /// + /// Saves a single object + /// + /// The to save + /// Boolean indicating whether or not to change the Published state upon saving + /// Optional Id of the User saving the Content + /// Optional boolean indicating whether or not to raise events. + private Attempt Save(IContent content, bool changeState, int userId = 0, bool raiseEvents = true) + { + var evtMsgs = EventMessagesFactory.Get(); + + if (raiseEvents) + { + if (Saving.IsRaisedEventCancelled( + new SaveEventArgs(content, evtMsgs), + this)) + { + return OperationStatus.Cancelled(evtMsgs); + } + } + + using (new WriteLock(Locker)) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateContentRepository(uow)) + { + if (content.HasIdentity == false) + { + content.CreatorId = userId; + } + content.WriterId = userId; + + //Only change the publish state if the "previous" version was actually published or marked as unpublished + if (changeState && (content.Published || ((Content)content).PublishedState == PublishedState.Unpublished)) + content.ChangePublishedState(PublishedState.Saved); + + repository.AddOrUpdate(content); + + //Generate a new preview + repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); + + uow.Commit(); + } + + if (raiseEvents) + Saved.RaiseEvent(new SaveEventArgs(content, false, evtMsgs), this); + + Audit(AuditType.Save, "Save Content performed by user", userId, content.Id); + + return OperationStatus.Success(evtMsgs); + } + } + + /// + /// Checks if the passed in can be published based on the anscestors publish state. + /// + /// + /// Check current is only used when falling back to checking the Parent of non-saved content, as + /// non-saved content doesn't have a valid path yet. + /// + /// to check if anscestors are published + /// Boolean indicating whether the passed in content should also be checked for published versions + /// True if the Content can be published, otherwise False + private bool IsPublishable(IContent content, bool checkCurrent) + { + var ids = content.Path.Split(',').Select(int.Parse).ToList(); + foreach (var id in ids) + { + //If Id equals that of the recycle bin we return false because nothing in the bin can be published + if (id == Constants.System.RecycleBinContent) + return false; + + //We don't check the System Root, so just continue + if (id == Constants.System.Root) continue; + + //If the current id equals that of the passed in content and if current shouldn't be checked we skip it. + if (checkCurrent == false && id == content.Id) continue; + + //Check if the content for the current id is published - escape the loop if we encounter content that isn't published + var hasPublishedVersion = HasPublishedVersion(id); + if (hasPublishedVersion == false) + return false; + } + + return true; + } + + private PublishStatusType CheckAndLogIsPublishable(IContent content) + { + //Check if parent is published (although not if its a root node) - if parent isn't published this Content cannot be published + if (content.ParentId != Constants.System.Root && content.ParentId != Constants.System.RecycleBinContent && IsPublishable(content) == false) + { + Logger.Info( + string.Format( + "Content '{0}' with Id '{1}' could not be published because its parent is not published.", + content.Name, content.Id)); + return PublishStatusType.FailedPathNotPublished; + } + else if (content.ExpireDate.HasValue && content.ExpireDate.Value > DateTime.MinValue && DateTime.Now > content.ExpireDate.Value) + { + Logger.Info( + string.Format( + "Content '{0}' with Id '{1}' has expired and could not be published.", + content.Name, content.Id)); + return PublishStatusType.FailedHasExpired; + } + else if (content.ReleaseDate.HasValue && content.ReleaseDate.Value > DateTime.MinValue && content.ReleaseDate.Value > DateTime.Now) + { + Logger.Info( + string.Format( + "Content '{0}' with Id '{1}' is awaiting release and could not be published.", + content.Name, content.Id)); + return PublishStatusType.FailedAwaitingRelease; + } + + return PublishStatusType.Success; + } + + private PublishStatusType CheckAndLogIsValid(IContent content) + { + //Content contains invalid property values and can therefore not be published - fire event? + if (content.IsValid() == false) + { + Logger.Info( + string.Format( + "Content '{0}' with Id '{1}' could not be published because of invalid properties.", + content.Name, content.Id)); + return PublishStatusType.FailedContentInvalid; + } + + return PublishStatusType.Success; + } + + private IContentType FindContentTypeByAlias(string contentTypeAlias) + { + using (var repository = RepositoryFactory.CreateContentTypeRepository(UowProvider.GetUnitOfWork())) + { + var query = Query.Builder.Where(x => x.Alias == contentTypeAlias); + var types = repository.GetByQuery(query); + + if (types.Any() == false) + throw new Exception( + string.Format("No ContentType matching the passed in Alias: '{0}' was found", + contentTypeAlias)); + + var contentType = types.First(); + + if (contentType == null) + throw new Exception(string.Format("ContentType matching the passed in Alias: '{0}' was null", + contentTypeAlias)); + + return contentType; + } + } + + #endregion + + #region Proxy Event Handlers + /// + /// Occurs before publish. + /// + /// Proxy to the real event on the + public static event TypedEventHandler> Publishing + { + add { PublishingStrategy.Publishing += value; } + remove { PublishingStrategy.Publishing -= value; } + } + + /// + /// Occurs after publish. + /// + /// Proxy to the real event on the + public static event TypedEventHandler> Published + { + add { PublishingStrategy.Published += value; } + remove { PublishingStrategy.Published -= value; } + } + /// + /// Occurs before unpublish. + /// + /// Proxy to the real event on the + public static event TypedEventHandler> UnPublishing + { + add { PublishingStrategy.UnPublishing += value; } + remove { PublishingStrategy.UnPublishing -= value; } + } + + /// + /// Occurs after unpublish. + /// + /// Proxy to the real event on the + public static event TypedEventHandler> UnPublished + { + add { PublishingStrategy.UnPublished += value; } + remove { PublishingStrategy.UnPublished -= value; } + } + #endregion + + #region Event Handlers + /// + /// Occurs before Delete + /// + public static event TypedEventHandler> Deleting; + + /// + /// Occurs after Delete + /// + public static event TypedEventHandler> Deleted; + + /// + /// Occurs before Delete Versions + /// + public static event TypedEventHandler DeletingVersions; + + /// + /// Occurs after Delete Versions + /// + public static event TypedEventHandler DeletedVersions; + + /// + /// Occurs before Save + /// + public static event TypedEventHandler> Saving; + + /// + /// Occurs after Save + /// + public static event TypedEventHandler> Saved; + + /// + /// Occurs before Create + /// + [Obsolete("Use the Created event instead, the Creating and Created events both offer the same functionality, Creating event has been deprecated.")] + public static event TypedEventHandler> Creating; + + /// + /// Occurs after Create + /// + /// + /// Please note that the Content object has been created, but might not have been saved + /// so it does not have an identity yet (meaning no Id has been set). + /// + public static event TypedEventHandler> Created; + + /// + /// Occurs before Copy + /// + public static event TypedEventHandler> Copying; + + /// + /// Occurs after Copy + /// + public static event TypedEventHandler> Copied; + + /// + /// Occurs before Content is moved to Recycle Bin + /// + public static event TypedEventHandler> Trashing; + + /// + /// Occurs after Content is moved to Recycle Bin + /// + public static event TypedEventHandler> Trashed; + + /// + /// Occurs before Move + /// + public static event TypedEventHandler> Moving; + + /// + /// Occurs after Move + /// + public static event TypedEventHandler> Moved; + + /// + /// Occurs before Rollback + /// + public static event TypedEventHandler> RollingBack; + + /// + /// Occurs after Rollback + /// + public static event TypedEventHandler> RolledBack; + + /// + /// Occurs before Send to Publish + /// + public static event TypedEventHandler> SendingToPublish; + + /// + /// Occurs after Send to Publish + /// + public static event TypedEventHandler> SentToPublish; + + /// + /// Occurs before the Recycle Bin is emptied + /// + public static event TypedEventHandler EmptyingRecycleBin; + + /// + /// Occurs after the Recycle Bin has been Emptied + /// + public static event TypedEventHandler EmptiedRecycleBin; + #endregion + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Services/IContentService.cs b/src/Umbraco.Core/Services/IContentService.cs index c686cf4891..fa688eae39 100644 --- a/src/Umbraco.Core/Services/IContentService.cs +++ b/src/Umbraco.Core/Services/IContentService.cs @@ -8,577 +8,579 @@ using Umbraco.Core.Publishing; namespace Umbraco.Core.Services { - /// - /// A temporary interface until we are in v8, this is used to return a different result for the same method and this interface gets implemented - /// explicitly. These methods will replace the normal ones in IContentService in v8 and this will be removed. - /// - public interface IContentServiceOperations - { - //TODO: Remove this class in v8 - - //TODO: There's probably more that needs to be added like the EmptyRecycleBin, etc... - - /// - /// Saves a single object - /// - /// The to save - /// Optional Id of the User saving the Content - /// Optional boolean indicating whether or not to raise events. - Attempt Save(IContent content, int userId = 0, bool raiseEvents = true); - - /// - /// Saves a collection of objects. - /// - /// Collection of to save - /// Optional Id of the User saving the Content - /// Optional boolean indicating whether or not to raise events. - Attempt Save(IEnumerable contents, int userId = 0, bool raiseEvents = true); - - /// - /// Permanently deletes an object. - /// - /// - /// This method will also delete associated media files, child content and possibly associated domains. - /// - /// Please note that this method will completely remove the Content from the database - /// The to delete - /// Optional Id of the User deleting the Content - Attempt Delete(IContent content, int userId = 0); - - /// - /// Publishes a single object - /// - /// The to publish - /// Optional Id of the User issueing the publishing - /// The published status attempt - Attempt Publish(IContent content, int userId = 0); - - /// - /// Publishes a object and all its children - /// - /// The to publish along with its children - /// Optional Id of the User issueing the publishing - /// - /// The list of statuses for all published items - IEnumerable> PublishWithChildren(IContent content, int userId = 0, bool includeUnpublished = false); - - /// - /// Saves and Publishes a single object - /// - /// The to save and publish - /// Optional Id of the User issueing the publishing - /// Optional boolean indicating whether or not to raise save events. - /// True if publishing succeeded, otherwise False - Attempt SaveAndPublish(IContent content, int userId = 0, bool raiseEvents = true); - - /// - /// Deletes an object by moving it to the Recycle Bin - /// - /// Move an item to the Recycle Bin will result in the item being unpublished - /// The to delete - /// Optional Id of the User deleting the Content - Attempt MoveToRecycleBin(IContent content, int userId = 0); - - /// - /// UnPublishes a single object - /// - /// The to publish - /// Optional Id of the User issueing the publishing - /// True if unpublishing succeeded, otherwise False - Attempt UnPublish(IContent content, int userId = 0); - } - - /// - /// Defines the ContentService, which is an easy access to operations involving - /// - public interface IContentService : IService - { - /// - /// Rebuilds all xml content in the cmsContentXml table for all documents - /// - /// - /// Only rebuild the xml structures for the content type ids passed in, if none then rebuilds the structures - /// for all content - /// - void RebuildXmlStructures(params int[] contentTypeIds); - - int CountPublished(string contentTypeAlias = null); - int Count(string contentTypeAlias = null); - int CountChildren(int parentId, string contentTypeAlias = null); - int CountDescendants(int parentId, string contentTypeAlias = null); - - /// - /// Used to bulk update the permissions set for a content item. This will replace all permissions - /// assigned to an entity with a list of user id & permission pairs. - /// - /// - void ReplaceContentPermissions(EntityPermissionSet permissionSet); - - /// - /// Assigns a single permission to the current content item for the specified user ids - /// - /// - /// - /// - void AssignContentPermission(IContent entity, char permission, IEnumerable userIds); - - /// - /// Gets the list of permissions for the content item - /// - /// - /// - IEnumerable GetPermissionsForEntity(IContent content); - - bool SendToPublication(IContent content, int userId = 0); - - IEnumerable GetByIds(IEnumerable ids); - - /// - /// 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 - /// - IContent CreateContent(string name, int parentId, string contentTypeAlias, int userId = 0); - - /// - /// 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 - /// Parent object for the new Content - /// Alias of the - /// Optional id of the user creating the content - /// - IContent CreateContent(string name, IContent parent, string contentTypeAlias, int userId = 0); - - /// - /// Gets an object by Id - /// - /// Id of the Content to retrieve - /// - IContent GetById(int id); - - /// - /// Gets an object by its 'UniqueId' - /// - /// Guid key of the Content to retrieve - /// - IContent GetById(Guid key); - - /// - /// Gets a collection of objects by the Id of the - /// - /// Id of the - /// An Enumerable list of objects - IEnumerable GetContentOfContentType(int id); - - /// - /// Gets a collection of objects by Level - /// - /// The level to retrieve Content from - /// An Enumerable list of objects - IEnumerable GetByLevel(int level); - - /// - /// Gets a collection of objects by Parent Id - /// - /// Id of the Parent to retrieve Children from - /// An Enumerable list of objects - IEnumerable GetChildren(int id); - - [Obsolete("Use the overload with 'long' parameter types instead")] - [EditorBrowsable(EditorBrowsableState.Never)] - IEnumerable GetPagedChildren(int id, int pageIndex, int pageSize, out int totalRecords, - string orderBy = "SortOrder", Direction orderDirection = Direction.Ascending, string filter = ""); - - /// - /// Gets a collection of objects by Parent Id - /// - /// Id of the Parent to retrieve Children from - /// Page number - /// Page size - /// Total records query would return without paging - /// Field to order by - /// Direction to order by - /// Search text filter - /// An Enumerable list of objects - IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize, out long totalRecords, - string orderBy = "SortOrder", Direction orderDirection = Direction.Ascending, string filter = ""); - - [Obsolete("Use the overload with 'long' parameter types instead")] - [EditorBrowsable(EditorBrowsableState.Never)] - IEnumerable GetPagedDescendants(int id, int pageIndex, int pageSize, out int totalRecords, - string orderBy = "Path", Direction orderDirection = Direction.Ascending, string filter = ""); - - /// - /// Gets a collection of objects by Parent Id - /// - /// Id of the Parent to retrieve Descendants from - /// Page number - /// Page size - /// Total records query would return without paging - /// Field to order by - /// Direction to order by - /// Search text filter - /// An Enumerable list of objects - IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalRecords, - string orderBy = "Path", Direction orderDirection = Direction.Ascending, string filter = ""); - - /// - /// Gets a collection of an objects versions by its Id - /// - /// - /// An Enumerable list of objects - IEnumerable GetVersions(int id); - - /// - /// Gets a collection of objects, which reside at the first level / root - /// - /// An Enumerable list of objects - IEnumerable GetRootContent(); - - /// - /// Gets a collection of objects, which has an expiration date greater then today - /// - /// An Enumerable list of objects - IEnumerable GetContentForExpiration(); - - /// - /// Gets a collection of objects, which has a release date greater then today - /// - /// An Enumerable list of objects - IEnumerable GetContentForRelease(); - - /// - /// Gets a collection of an objects, which resides in the Recycle Bin - /// - /// An Enumerable list of objects - IEnumerable GetContentInRecycleBin(); - - /// - /// Saves a single object - /// - /// The to save - /// Optional Id of the User saving the Content - /// Optional boolean indicating whether or not to raise events. - void Save(IContent content, int userId = 0, bool raiseEvents = true); - - /// - /// Saves a collection of objects. - /// - /// Collection of to save - /// Optional Id of the User saving the Content - /// Optional boolean indicating whether or not to raise events. - void Save(IEnumerable contents, int userId = 0, bool raiseEvents = true); - - /// - /// Deletes all content of specified type. All children of deleted content is moved to Recycle Bin. - /// - /// This needs extra care and attention as its potentially a dangerous and extensive operation - /// Id of the - /// Optional Id of the user issueing the delete operation - void DeleteContentOfType(int contentTypeId, int userId = 0); - - /// - /// Permanently deletes versions from an object prior to a specific date. - /// - /// Id of the object to delete versions from - /// Latest version date - /// Optional Id of the User deleting versions of a Content object - void DeleteVersions(int id, DateTime versionDate, int userId = 0); - - /// - /// Permanently deletes a specific version from an object. - /// - /// Id of the object to delete a version from - /// Id of the version to delete - /// Boolean indicating whether to delete versions prior to the versionId - /// Optional Id of the User deleting versions of a Content object - void DeleteVersion(int id, Guid versionId, bool deletePriorVersions, int userId = 0); - - /// - /// Deletes an object by moving it to the Recycle Bin - /// - /// Move an item to the Recycle Bin will result in the item being unpublished - /// The to delete - /// Optional Id of the User deleting the Content - void MoveToRecycleBin(IContent content, int userId = 0); - - /// - /// Moves an object to a new location - /// - /// The to move - /// Id of the Content's new Parent - /// Optional Id of the User moving the Content - void Move(IContent content, int parentId, int userId = 0); - - /// - /// Empties the Recycle Bin by deleting all that resides in the bin - /// - void EmptyRecycleBin(); - - /// - /// Rollback an object to a previous version. - /// This will create a new version, which is a copy of all the old data. - /// - /// Id of the being rolled back - /// Id of the version to rollback to - /// Optional Id of the User issueing the rollback of the Content - /// The newly created object - IContent Rollback(int id, Guid versionId, int userId = 0); - - /// - /// Gets a collection of objects by its name or partial name - /// - /// Id of the Parent to retrieve Children from - /// Full or partial name of the children - /// An Enumerable list of objects - IEnumerable GetChildrenByName(int parentId, string name); - - /// - /// Gets a collection of objects by Parent Id - /// - /// Id of the Parent to retrieve Descendants from - /// An Enumerable list of objects - IEnumerable GetDescendants(int id); - - /// - /// Gets a collection of objects by Parent Id - /// - /// item to retrieve Descendants from - /// An Enumerable list of objects - IEnumerable GetDescendants(IContent content); - - /// - /// Gets a specific version of an item. - /// - /// Id of the version to retrieve - /// An item - IContent GetByVersion(Guid versionId); - - /// - /// Gets the published version of an item - /// - /// Id of the to retrieve version from - /// An item - IContent GetPublishedVersion(int id); - - /// - /// Gets the published version of a item. - /// - /// The content item. - /// The published version, if any; otherwise, null. - IContent GetPublishedVersion(IContent content); - - /// - /// Checks whether an item has any children - /// - /// Id of the - /// True if the content has any children otherwise False - bool HasChildren(int id); - - /// - /// Checks whether an item has any published versions - /// - /// Id of the - /// True if the content has any published version otherwise False - bool HasPublishedVersion(int id); - - /// - /// Re-Publishes all Content - /// - /// Optional Id of the User issueing the publishing - /// True if publishing succeeded, otherwise False - bool RePublishAll(int userId = 0); - - /// - /// Publishes a single object - /// - /// The to publish - /// Optional Id of the User issueing the publishing - /// True if publishing succeeded, otherwise False - bool Publish(IContent content, int userId = 0); - - /// - /// Publishes a single object - /// - /// The to publish - /// Optional Id of the User issueing the publishing - /// The published status attempt - Attempt PublishWithStatus(IContent content, int userId = 0); - - /// - /// Publishes a object and all its children - /// - /// The to publish along with its children - /// Optional Id of the User issueing the publishing - /// True if publishing succeeded, otherwise False - [EditorBrowsable(EditorBrowsableState.Never)] - [Obsolete("Use PublishWithChildrenWithStatus instead, that method will provide more detailed information on the outcome and also allows the includeUnpublished flag")] - bool PublishWithChildren(IContent content, int userId = 0); - - /// - /// Publishes a object and all its children - /// - /// The to publish along with its children - /// Optional Id of the User issueing the publishing - /// - /// The list of statuses for all published items - IEnumerable> PublishWithChildrenWithStatus(IContent content, int userId = 0, bool includeUnpublished = false); - - /// - /// UnPublishes a single object - /// - /// The to publish - /// Optional Id of the User issueing the publishing - /// True if unpublishing succeeded, otherwise False - bool UnPublish(IContent content, int userId = 0); - - /// - /// Saves and Publishes a single object - /// - /// The to save and publish - /// Optional Id of the User issueing the publishing - /// Optional boolean indicating whether or not to raise save events. - /// True if publishing succeeded, otherwise False - [Obsolete("Use SaveAndPublishWithStatus instead, that method will provide more detailed information on the outcome")] - [EditorBrowsable(EditorBrowsableState.Never)] - bool SaveAndPublish(IContent content, int userId = 0, bool raiseEvents = true); - - /// - /// Saves and Publishes a single object - /// - /// The to save and publish - /// Optional Id of the User issueing the publishing - /// Optional boolean indicating whether or not to raise save events. - /// True if publishing succeeded, otherwise False - Attempt SaveAndPublishWithStatus(IContent content, int userId = 0, bool raiseEvents = true); - - /// - /// Permanently deletes an object. - /// - /// - /// This method will also delete associated media files, child content and possibly associated domains. - /// - /// Please note that this method will completely remove the Content from the database - /// The to delete - /// Optional Id of the User deleting the Content - void Delete(IContent content, int userId = 0); - - /// - /// Copies an object by creating a new Content object of the same type and copies all data from the current - /// to the new copy, which is returned. Recursively copies all children. - /// - /// The to copy - /// Id of the Content's new Parent - /// Boolean indicating whether the copy should be related to the original - /// Optional Id of the User copying the Content - /// The newly created object - IContent Copy(IContent content, int parentId, bool relateToOriginal, int userId = 0); - - /// - /// Copies an object by creating a new Content object of the same type and copies all data from the current - /// to the new copy which is returned. - /// - /// The to copy - /// Id of the Content's new Parent - /// Boolean indicating whether the copy should be related to the original - /// A value indicating whether to recursively copy children. - /// Optional Id of the User copying the Content - /// The newly created object - IContent Copy(IContent content, int parentId, bool relateToOriginal, bool recursive, int userId = 0); - - /// - /// Checks if the passed in can be published based on the anscestors publish state. - /// - /// to check if anscestors are published - /// True if the Content can be published, otherwise False - bool IsPublishable(IContent content); - - /// - /// Gets a collection of objects, which are ancestors of the current content. - /// - /// Id of the to retrieve ancestors for - /// An Enumerable list of objects - IEnumerable GetAncestors(int id); - - /// - /// Gets a collection of objects, which are ancestors of the current content. - /// - /// to retrieve ancestors for - /// An Enumerable list of objects - IEnumerable GetAncestors(IContent content); - - /// - /// Sorts a collection of objects by updating the SortOrder according - /// to the ordering of items in the passed in . - /// - /// - /// Using this method will ensure that the Published-state is maintained upon sorting - /// so the cache is updated accordingly - as needed. - /// - /// - /// - /// - /// True if sorting succeeded, otherwise False - bool Sort(IEnumerable items, int userId = 0, bool raiseEvents = true); - - /// - /// Gets the parent of the current content as an item. - /// - /// Id of the to retrieve the parent from - /// Parent object - IContent GetParent(int id); - - /// - /// Gets the parent of the current content as an item. - /// - /// to retrieve the parent from - /// Parent object - IContent GetParent(IContent content); - - /// - /// Creates and saves an object using the alias of the - /// that this Content should based on. - /// - /// - /// This method returns an object that has been persisted to the database - /// and therefor has an identity. - /// - /// Name of the Content object - /// Parent object for the new Content - /// Alias of the - /// Optional id of the user creating the content - /// - IContent CreateContentWithIdentity(string name, IContent parent, string contentTypeAlias, int userId = 0); - - /// - /// Creates and saves an object using the alias of the - /// that this Content should based on. - /// - /// - /// This method returns an object that has been persisted to the database - /// and therefor has an identity. - /// - /// Name of the Content object - /// Id of Parent for the new Content - /// Alias of the - /// Optional id of the user creating the content - /// - IContent CreateContentWithIdentity(string name, int parentId, string contentTypeAlias, int userId = 0); - } + /// + /// A temporary interface until we are in v8, this is used to return a different result for the same method and this interface gets implemented + /// explicitly. These methods will replace the normal ones in IContentService in v8 and this will be removed. + /// + public interface IContentServiceOperations + { + //TODO: Remove this class in v8 + + //TODO: There's probably more that needs to be added like the EmptyRecycleBin, etc... + + /// + /// Saves a single object + /// + /// The to save + /// Optional Id of the User saving the Content + /// Optional boolean indicating whether or not to raise events. + Attempt Save(IContent content, int userId = 0, bool raiseEvents = true); + + /// + /// Saves a collection of objects. + /// + /// Collection of to save + /// Optional Id of the User saving the Content + /// Optional boolean indicating whether or not to raise events. + Attempt Save(IEnumerable contents, int userId = 0, bool raiseEvents = true); + + /// + /// Permanently deletes an object. + /// + /// + /// This method will also delete associated media files, child content and possibly associated domains. + /// + /// Please note that this method will completely remove the Content from the database + /// The to delete + /// Optional Id of the User deleting the Content + Attempt Delete(IContent content, int userId = 0); + + /// + /// Publishes a single object + /// + /// The to publish + /// Optional Id of the User issueing the publishing + /// The published status attempt + Attempt Publish(IContent content, int userId = 0); + + /// + /// Publishes a object and all its children + /// + /// The to publish along with its children + /// Optional Id of the User issueing the publishing + /// + /// The list of statuses for all published items + IEnumerable> PublishWithChildren(IContent content, int userId = 0, bool includeUnpublished = false); + + /// + /// Saves and Publishes a single object + /// + /// The to save and publish + /// Optional Id of the User issueing the publishing + /// Optional boolean indicating whether or not to raise save events. + /// True if publishing succeeded, otherwise False + Attempt SaveAndPublish(IContent content, int userId = 0, bool raiseEvents = true); + + /// + /// Deletes an object by moving it to the Recycle Bin + /// + /// Move an item to the Recycle Bin will result in the item being unpublished + /// The to delete + /// Optional Id of the User deleting the Content + Attempt MoveToRecycleBin(IContent content, int userId = 0); + + /// + /// UnPublishes a single object + /// + /// The to publish + /// Optional Id of the User issueing the publishing + /// True if unpublishing succeeded, otherwise False + Attempt UnPublish(IContent content, int userId = 0); + } + + /// + /// Defines the ContentService, which is an easy access to operations involving + /// + public interface IContentService : IService + { + /// + /// Rebuilds all xml content in the cmsContentXml table for all documents + /// + /// + /// Only rebuild the xml structures for the content type ids passed in, if none then rebuilds the structures + /// for all content + /// + void RebuildXmlStructures(params int[] contentTypeIds); + + int CountPublished(string contentTypeAlias = null); + int Count(string contentTypeAlias = null); + int CountChildren(int parentId, string contentTypeAlias = null); + int CountDescendants(int parentId, string contentTypeAlias = null); + + /// + /// Used to bulk update the permissions set for a content item. This will replace all permissions + /// assigned to an entity with a list of user id & permission pairs. + /// + /// + void ReplaceContentPermissions(EntityPermissionSet permissionSet); + + /// + /// Assigns a single permission to the current content item for the specified user ids + /// + /// + /// + /// + void AssignContentPermission(IContent entity, char permission, IEnumerable userIds); + + /// + /// Gets the list of permissions for the content item + /// + /// + /// + IEnumerable GetPermissionsForEntity(IContent content); + + bool SendToPublication(IContent content, int userId = 0); + + IEnumerable GetByIds(IEnumerable ids); + + /// + /// 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 + /// + IContent CreateContent(string name, int parentId, string contentTypeAlias, int userId = 0); + + /// + /// 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 + /// Parent object for the new Content + /// Alias of the + /// Optional id of the user creating the content + /// + IContent CreateContent(string name, IContent parent, string contentTypeAlias, int userId = 0); + + /// + /// Gets an object by Id + /// + /// Id of the Content to retrieve + /// + IContent GetById(int id); + + /// + /// Gets an object by its 'UniqueId' + /// + /// Guid key of the Content to retrieve + /// + IContent GetById(Guid key); + + /// + /// Gets a collection of objects by the Id of the + /// + /// Id of the + /// An Enumerable list of objects + IEnumerable GetContentOfContentType(int id); + + /// + /// Gets a collection of objects by Level + /// + /// The level to retrieve Content from + /// An Enumerable list of objects + IEnumerable GetByLevel(int level); + + /// + /// Gets a collection of objects by Parent Id + /// + /// Id of the Parent to retrieve Children from + /// An Enumerable list of objects + IEnumerable GetChildren(int id); + + [Obsolete("Use the overload with 'long' parameter types instead")] + [EditorBrowsable(EditorBrowsableState.Never)] + IEnumerable GetPagedChildren(int id, int pageIndex, int pageSize, out int totalRecords, + string orderBy = "SortOrder", Direction orderDirection = Direction.Ascending, bool orderBySystemField = true, string filter = ""); + + /// + /// Gets a collection of objects by Parent Id + /// + /// Id of the Parent to retrieve Children from + /// Page number + /// Page size + /// Total records query would return without paging + /// Field to order by + /// Direction to order by + /// Flag to indicate when ordering by system field + /// Search text filter + /// An Enumerable list of objects + IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize, out long totalRecords, + string orderBy = "SortOrder", Direction orderDirection = Direction.Ascending, bool orderBySystemField = true, string filter = ""); + + [Obsolete("Use the overload with 'long' parameter types instead")] + [EditorBrowsable(EditorBrowsableState.Never)] + IEnumerable GetPagedDescendants(int id, int pageIndex, int pageSize, out int totalRecords, + string orderBy = "Path", Direction orderDirection = Direction.Ascending, bool orderBySystemField = true, string filter = ""); + + /// + /// Gets a collection of objects by Parent Id + /// + /// Id of the Parent to retrieve Descendants from + /// Page number + /// Page size + /// Total records query would return without paging + /// Field to order by + /// Direction to order by + /// Flag to indicate when ordering by system field + /// Search text filter + /// An Enumerable list of objects + IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalRecords, + string orderBy = "Path", Direction orderDirection = Direction.Ascending, bool orderBySystemField = true, string filter = ""); + + /// + /// Gets a collection of an objects versions by its Id + /// + /// + /// An Enumerable list of objects + IEnumerable GetVersions(int id); + + /// + /// Gets a collection of objects, which reside at the first level / root + /// + /// An Enumerable list of objects + IEnumerable GetRootContent(); + + /// + /// Gets a collection of objects, which has an expiration date greater then today + /// + /// An Enumerable list of objects + IEnumerable GetContentForExpiration(); + + /// + /// Gets a collection of objects, which has a release date greater then today + /// + /// An Enumerable list of objects + IEnumerable GetContentForRelease(); + + /// + /// Gets a collection of an objects, which resides in the Recycle Bin + /// + /// An Enumerable list of objects + IEnumerable GetContentInRecycleBin(); + + /// + /// Saves a single object + /// + /// The to save + /// Optional Id of the User saving the Content + /// Optional boolean indicating whether or not to raise events. + void Save(IContent content, int userId = 0, bool raiseEvents = true); + + /// + /// Saves a collection of objects. + /// + /// Collection of to save + /// Optional Id of the User saving the Content + /// Optional boolean indicating whether or not to raise events. + void Save(IEnumerable contents, int userId = 0, bool raiseEvents = true); + + /// + /// Deletes all content of specified type. All children of deleted content is moved to Recycle Bin. + /// + /// This needs extra care and attention as its potentially a dangerous and extensive operation + /// Id of the + /// Optional Id of the user issueing the delete operation + void DeleteContentOfType(int contentTypeId, int userId = 0); + + /// + /// Permanently deletes versions from an object prior to a specific date. + /// + /// Id of the object to delete versions from + /// Latest version date + /// Optional Id of the User deleting versions of a Content object + void DeleteVersions(int id, DateTime versionDate, int userId = 0); + + /// + /// Permanently deletes a specific version from an object. + /// + /// Id of the object to delete a version from + /// Id of the version to delete + /// Boolean indicating whether to delete versions prior to the versionId + /// Optional Id of the User deleting versions of a Content object + void DeleteVersion(int id, Guid versionId, bool deletePriorVersions, int userId = 0); + + /// + /// Deletes an object by moving it to the Recycle Bin + /// + /// Move an item to the Recycle Bin will result in the item being unpublished + /// The to delete + /// Optional Id of the User deleting the Content + void MoveToRecycleBin(IContent content, int userId = 0); + + /// + /// Moves an object to a new location + /// + /// The to move + /// Id of the Content's new Parent + /// Optional Id of the User moving the Content + void Move(IContent content, int parentId, int userId = 0); + + /// + /// Empties the Recycle Bin by deleting all that resides in the bin + /// + void EmptyRecycleBin(); + + /// + /// Rollback an object to a previous version. + /// This will create a new version, which is a copy of all the old data. + /// + /// Id of the being rolled back + /// Id of the version to rollback to + /// Optional Id of the User issueing the rollback of the Content + /// The newly created object + IContent Rollback(int id, Guid versionId, int userId = 0); + + /// + /// Gets a collection of objects by its name or partial name + /// + /// Id of the Parent to retrieve Children from + /// Full or partial name of the children + /// An Enumerable list of objects + IEnumerable GetChildrenByName(int parentId, string name); + + /// + /// Gets a collection of objects by Parent Id + /// + /// Id of the Parent to retrieve Descendants from + /// An Enumerable list of objects + IEnumerable GetDescendants(int id); + + /// + /// Gets a collection of objects by Parent Id + /// + /// item to retrieve Descendants from + /// An Enumerable list of objects + IEnumerable GetDescendants(IContent content); + + /// + /// Gets a specific version of an item. + /// + /// Id of the version to retrieve + /// An item + IContent GetByVersion(Guid versionId); + + /// + /// Gets the published version of an item + /// + /// Id of the to retrieve version from + /// An item + IContent GetPublishedVersion(int id); + + /// + /// Gets the published version of a item. + /// + /// The content item. + /// The published version, if any; otherwise, null. + IContent GetPublishedVersion(IContent content); + + /// + /// Checks whether an item has any children + /// + /// Id of the + /// True if the content has any children otherwise False + bool HasChildren(int id); + + /// + /// Checks whether an item has any published versions + /// + /// Id of the + /// True if the content has any published version otherwise False + bool HasPublishedVersion(int id); + + /// + /// Re-Publishes all Content + /// + /// Optional Id of the User issueing the publishing + /// True if publishing succeeded, otherwise False + bool RePublishAll(int userId = 0); + + /// + /// Publishes a single object + /// + /// The to publish + /// Optional Id of the User issueing the publishing + /// True if publishing succeeded, otherwise False + bool Publish(IContent content, int userId = 0); + + /// + /// Publishes a single object + /// + /// The to publish + /// Optional Id of the User issueing the publishing + /// The published status attempt + Attempt PublishWithStatus(IContent content, int userId = 0); + + /// + /// Publishes a object and all its children + /// + /// The to publish along with its children + /// Optional Id of the User issueing the publishing + /// True if publishing succeeded, otherwise False + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Use PublishWithChildrenWithStatus instead, that method will provide more detailed information on the outcome and also allows the includeUnpublished flag")] + bool PublishWithChildren(IContent content, int userId = 0); + + /// + /// Publishes a object and all its children + /// + /// The to publish along with its children + /// Optional Id of the User issueing the publishing + /// + /// The list of statuses for all published items + IEnumerable> PublishWithChildrenWithStatus(IContent content, int userId = 0, bool includeUnpublished = false); + + /// + /// UnPublishes a single object + /// + /// The to publish + /// Optional Id of the User issueing the publishing + /// True if unpublishing succeeded, otherwise False + bool UnPublish(IContent content, int userId = 0); + + /// + /// Saves and Publishes a single object + /// + /// The to save and publish + /// Optional Id of the User issueing the publishing + /// Optional boolean indicating whether or not to raise save events. + /// True if publishing succeeded, otherwise False + [Obsolete("Use SaveAndPublishWithStatus instead, that method will provide more detailed information on the outcome")] + [EditorBrowsable(EditorBrowsableState.Never)] + bool SaveAndPublish(IContent content, int userId = 0, bool raiseEvents = true); + + /// + /// Saves and Publishes a single object + /// + /// The to save and publish + /// Optional Id of the User issueing the publishing + /// Optional boolean indicating whether or not to raise save events. + /// True if publishing succeeded, otherwise False + Attempt SaveAndPublishWithStatus(IContent content, int userId = 0, bool raiseEvents = true); + + /// + /// Permanently deletes an object. + /// + /// + /// This method will also delete associated media files, child content and possibly associated domains. + /// + /// Please note that this method will completely remove the Content from the database + /// The to delete + /// Optional Id of the User deleting the Content + void Delete(IContent content, int userId = 0); + + /// + /// Copies an object by creating a new Content object of the same type and copies all data from the current + /// to the new copy, which is returned. Recursively copies all children. + /// + /// The to copy + /// Id of the Content's new Parent + /// Boolean indicating whether the copy should be related to the original + /// Optional Id of the User copying the Content + /// The newly created object + IContent Copy(IContent content, int parentId, bool relateToOriginal, int userId = 0); + + /// + /// Copies an object by creating a new Content object of the same type and copies all data from the current + /// to the new copy which is returned. + /// + /// The to copy + /// Id of the Content's new Parent + /// Boolean indicating whether the copy should be related to the original + /// A value indicating whether to recursively copy children. + /// Optional Id of the User copying the Content + /// The newly created object + IContent Copy(IContent content, int parentId, bool relateToOriginal, bool recursive, int userId = 0); + + /// + /// Checks if the passed in can be published based on the anscestors publish state. + /// + /// to check if anscestors are published + /// True if the Content can be published, otherwise False + bool IsPublishable(IContent content); + + /// + /// Gets a collection of objects, which are ancestors of the current content. + /// + /// Id of the to retrieve ancestors for + /// An Enumerable list of objects + IEnumerable GetAncestors(int id); + + /// + /// Gets a collection of objects, which are ancestors of the current content. + /// + /// to retrieve ancestors for + /// An Enumerable list of objects + IEnumerable GetAncestors(IContent content); + + /// + /// Sorts a collection of objects by updating the SortOrder according + /// to the ordering of items in the passed in . + /// + /// + /// Using this method will ensure that the Published-state is maintained upon sorting + /// so the cache is updated accordingly - as needed. + /// + /// + /// + /// + /// True if sorting succeeded, otherwise False + bool Sort(IEnumerable items, int userId = 0, bool raiseEvents = true); + + /// + /// Gets the parent of the current content as an item. + /// + /// Id of the to retrieve the parent from + /// Parent object + IContent GetParent(int id); + + /// + /// Gets the parent of the current content as an item. + /// + /// to retrieve the parent from + /// Parent object + IContent GetParent(IContent content); + + /// + /// Creates and saves an object using the alias of the + /// that this Content should based on. + /// + /// + /// This method returns an object that has been persisted to the database + /// and therefor has an identity. + /// + /// Name of the Content object + /// Parent object for the new Content + /// Alias of the + /// Optional id of the user creating the content + /// + IContent CreateContentWithIdentity(string name, IContent parent, string contentTypeAlias, int userId = 0); + + /// + /// Creates and saves an object using the alias of the + /// that this Content should based on. + /// + /// + /// This method returns an object that has been persisted to the database + /// and therefor has an identity. + /// + /// Name of the Content object + /// Id of Parent for the new Content + /// Alias of the + /// Optional id of the user creating the content + /// + IContent CreateContentWithIdentity(string name, int parentId, string contentTypeAlias, int userId = 0); + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Services/IMediaService.cs b/src/Umbraco.Core/Services/IMediaService.cs index d104b95ddc..0bb56bb44b 100644 --- a/src/Umbraco.Core/Services/IMediaService.cs +++ b/src/Umbraco.Core/Services/IMediaService.cs @@ -7,368 +7,370 @@ using Umbraco.Core.Persistence.DatabaseModelDefinitions; namespace Umbraco.Core.Services { - /// - /// A temporary interface until we are in v8, this is used to return a different result for the same method and this interface gets implemented - /// explicitly. These methods will replace the normal ones in IContentService in v8 and this will be removed. - /// - public interface IMediaServiceOperations - { - //TODO: Remove this class in v8 + /// + /// A temporary interface until we are in v8, this is used to return a different result for the same method and this interface gets implemented + /// explicitly. These methods will replace the normal ones in IContentService in v8 and this will be removed. + /// + public interface IMediaServiceOperations + { + //TODO: Remove this class in v8 - //TODO: There's probably more that needs to be added like the EmptyRecycleBin, etc... + //TODO: There's probably more that needs to be added like the EmptyRecycleBin, etc... - /// - /// Deletes an object by moving it to the Recycle Bin - /// - /// The to delete - /// Id of the User deleting the Media - Attempt MoveToRecycleBin(IMedia media, int userId = 0); + /// + /// Deletes an object by moving it to the Recycle Bin + /// + /// The to delete + /// Id of the User deleting the Media + Attempt MoveToRecycleBin(IMedia media, int userId = 0); - /// - /// Permanently deletes an object - /// - /// - /// Please note that this method will completely remove the Media from the database, - /// but current not from the file system. - /// - /// The to delete - /// Id of the User deleting the Media - Attempt Delete(IMedia media, int userId = 0); + /// + /// Permanently deletes an object + /// + /// + /// Please note that this method will completely remove the Media from the database, + /// but current not from the file system. + /// + /// The to delete + /// Id of the User deleting the Media + Attempt Delete(IMedia media, int userId = 0); - /// - /// Saves a single object - /// - /// The to save - /// Id of the User saving the Media - /// Optional boolean indicating whether or not to raise events. - Attempt Save(IMedia media, int userId = 0, bool raiseEvents = true); + /// + /// Saves a single object + /// + /// The to save + /// Id of the User saving the Media + /// Optional boolean indicating whether or not to raise events. + Attempt Save(IMedia media, int userId = 0, bool raiseEvents = true); - /// - /// Saves a collection of objects - /// - /// Collection of to save - /// Id of the User saving the Media - /// Optional boolean indicating whether or not to raise events. - Attempt Save(IEnumerable medias, int userId = 0, bool raiseEvents = true); - } + /// + /// Saves a collection of objects + /// + /// Collection of to save + /// Id of the User saving the Media + /// Optional boolean indicating whether or not to raise events. + Attempt Save(IEnumerable medias, int userId = 0, bool raiseEvents = true); + } - /// - /// Defines the Media Service, which is an easy access to operations involving - /// - public interface IMediaService : IService - { - /// - /// Rebuilds all xml content in the cmsContentXml table for all media - /// - /// - /// Only rebuild the xml structures for the content type ids passed in, if none then rebuilds the structures - /// for all media - /// - void RebuildXmlStructures(params int[] contentTypeIds); + /// + /// Defines the Media Service, which is an easy access to operations involving + /// + public interface IMediaService : IService + { + /// + /// Rebuilds all xml content in the cmsContentXml table for all media + /// + /// + /// Only rebuild the xml structures for the content type ids passed in, if none then rebuilds the structures + /// for all media + /// + void RebuildXmlStructures(params int[] contentTypeIds); - int Count(string contentTypeAlias = null); - int CountChildren(int parentId, string contentTypeAlias = null); - int CountDescendants(int parentId, string contentTypeAlias = null); + int Count(string contentTypeAlias = null); + int CountChildren(int parentId, string contentTypeAlias = null); + int CountDescendants(int parentId, string contentTypeAlias = null); - IEnumerable GetByIds(IEnumerable ids); + IEnumerable GetByIds(IEnumerable ids); - /// - /// Creates an object using the alias of the - /// that this Media should based on. - /// - /// - /// Note that using this method will simply return a new IMedia without any identity - /// as it has not yet been persisted. It is intended as a shortcut to creating new media objects - /// that does not invoke a save operation against the database. - /// - /// Name of the Media object - /// Id of Parent for the new Media item - /// Alias of the - /// Optional id of the user creating the media item - /// - IMedia CreateMedia(string name, int parentId, string mediaTypeAlias, int userId = 0); + /// + /// Creates an object using the alias of the + /// that this Media should based on. + /// + /// + /// Note that using this method will simply return a new IMedia without any identity + /// as it has not yet been persisted. It is intended as a shortcut to creating new media objects + /// that does not invoke a save operation against the database. + /// + /// Name of the Media object + /// Id of Parent for the new Media item + /// Alias of the + /// Optional id of the user creating the media item + /// + IMedia CreateMedia(string name, int parentId, string mediaTypeAlias, int userId = 0); - /// - /// Creates an object using the alias of the - /// that this Media should based on. - /// - /// - /// Note that using this method will simply return a new IMedia without any identity - /// as it has not yet been persisted. It is intended as a shortcut to creating new media objects - /// that does not invoke a save operation against the database. - /// - /// Name of the Media object - /// Parent for the new Media item - /// Alias of the - /// Optional id of the user creating the media item - /// - IMedia CreateMedia(string name, IMedia parent, string mediaTypeAlias, int userId = 0); + /// + /// Creates an object using the alias of the + /// that this Media should based on. + /// + /// + /// Note that using this method will simply return a new IMedia without any identity + /// as it has not yet been persisted. It is intended as a shortcut to creating new media objects + /// that does not invoke a save operation against the database. + /// + /// Name of the Media object + /// Parent for the new Media item + /// Alias of the + /// Optional id of the user creating the media item + /// + IMedia CreateMedia(string name, IMedia parent, string mediaTypeAlias, int userId = 0); - /// - /// Gets an object by Id - /// - /// Id of the Content to retrieve - /// - IMedia GetById(int id); + /// + /// Gets an object by Id + /// + /// Id of the Content to retrieve + /// + IMedia GetById(int id); - /// - /// Gets a collection of objects by Parent Id - /// - /// Id of the Parent to retrieve Children from - /// An Enumerable list of objects - IEnumerable GetChildren(int id); + /// + /// Gets a collection of objects by Parent Id + /// + /// Id of the Parent to retrieve Children from + /// An Enumerable list of objects + IEnumerable GetChildren(int id); - [Obsolete("Use the overload with 'long' parameter types instead")] - [EditorBrowsable(EditorBrowsableState.Never)] - IEnumerable GetPagedChildren(int id, int pageIndex, int pageSize, out int totalRecords, - string orderBy = "SortOrder", Direction orderDirection = Direction.Ascending, string filter = ""); + [Obsolete("Use the overload with 'long' parameter types instead")] + [EditorBrowsable(EditorBrowsableState.Never)] + IEnumerable GetPagedChildren(int id, int pageIndex, int pageSize, out int totalRecords, + string orderBy = "SortOrder", Direction orderDirection = Direction.Ascending, bool orderBySystemField = true, string filter = ""); - /// - /// Gets a collection of objects by Parent Id - /// - /// Id of the Parent to retrieve Children from - /// Page number - /// Page size - /// Total records query would return without paging - /// Field to order by - /// Direction to order by - /// Search text filter - /// An Enumerable list of objects - IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize, out long totalRecords, - string orderBy = "SortOrder", Direction orderDirection = Direction.Ascending, string filter = ""); + /// + /// Gets a collection of objects by Parent Id + /// + /// Id of the Parent to retrieve Children from + /// Page number + /// Page size + /// Total records query would return without paging + /// Field to order by + /// Direction to order by + /// Flag to indicate when ordering by system field + /// Search text filter + /// An Enumerable list of objects + IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize, out long totalRecords, + string orderBy = "SortOrder", Direction orderDirection = Direction.Ascending, bool orderBySystemField = true, string filter = ""); - [Obsolete("Use the overload with 'long' parameter types instead")] - [EditorBrowsable(EditorBrowsableState.Never)] - IEnumerable GetPagedDescendants(int id, int pageIndex, int pageSize, out int totalRecords, - string orderBy = "Path", Direction orderDirection = Direction.Ascending, string filter = ""); + [Obsolete("Use the overload with 'long' parameter types instead")] + [EditorBrowsable(EditorBrowsableState.Never)] + IEnumerable GetPagedDescendants(int id, int pageIndex, int pageSize, out int totalRecords, + string orderBy = "Path", Direction orderDirection = Direction.Ascending, bool orderBySystemField = true, string filter = ""); - /// - /// Gets a collection of objects by Parent Id - /// - /// Id of the Parent to retrieve Descendants from - /// Page number - /// Page size - /// Total records query would return without paging - /// Field to order by - /// Direction to order by - /// Search text filter - /// An Enumerable list of objects - IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalRecords, - string orderBy = "Path", Direction orderDirection = Direction.Ascending, string filter = ""); + /// + /// Gets a collection of objects by Parent Id + /// + /// Id of the Parent to retrieve Descendants from + /// Page number + /// Page size + /// Total records query would return without paging + /// Field to order by + /// Direction to order by + /// Flag to indicate when ordering by system field + /// Search text filter + /// An Enumerable list of objects + IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalRecords, + string orderBy = "Path", Direction orderDirection = Direction.Ascending, bool orderBySystemField = true, string filter = ""); - /// - /// Gets descendants of a object by its Id - /// - /// Id of the Parent to retrieve descendants from - /// An Enumerable flat list of objects - IEnumerable GetDescendants(int id); + /// + /// Gets descendants of a object by its Id + /// + /// Id of the Parent to retrieve descendants from + /// An Enumerable flat list of objects + IEnumerable GetDescendants(int id); - /// - /// Gets a collection of objects by the Id of the - /// - /// Id of the - /// An Enumerable list of objects - IEnumerable GetMediaOfMediaType(int id); + /// + /// Gets a collection of objects by the Id of the + /// + /// Id of the + /// An Enumerable list of objects + IEnumerable GetMediaOfMediaType(int id); - /// - /// Gets a collection of objects, which reside at the first level / root - /// - /// An Enumerable list of objects - IEnumerable GetRootMedia(); + /// + /// Gets a collection of objects, which reside at the first level / root + /// + /// An Enumerable list of objects + IEnumerable GetRootMedia(); - /// - /// Gets a collection of an objects, which resides in the Recycle Bin - /// - /// An Enumerable list of objects - IEnumerable GetMediaInRecycleBin(); + /// + /// Gets a collection of an objects, which resides in the Recycle Bin + /// + /// An Enumerable list of objects + IEnumerable GetMediaInRecycleBin(); - /// - /// Moves an object to a new location - /// - /// The to move - /// Id of the Media's new Parent - /// Id of the User moving the Media - void Move(IMedia media, int parentId, int userId = 0); + /// + /// Moves an object to a new location + /// + /// The to move + /// Id of the Media's new Parent + /// Id of the User moving the Media + void Move(IMedia media, int parentId, int userId = 0); - /// - /// Deletes an object by moving it to the Recycle Bin - /// - /// The to delete - /// Id of the User deleting the Media - void MoveToRecycleBin(IMedia media, int userId = 0); + /// + /// Deletes an object by moving it to the Recycle Bin + /// + /// The to delete + /// Id of the User deleting the Media + void MoveToRecycleBin(IMedia media, int userId = 0); - /// - /// Empties the Recycle Bin by deleting all that resides in the bin - /// - void EmptyRecycleBin(); + /// + /// Empties the Recycle Bin by deleting all that resides in the bin + /// + void EmptyRecycleBin(); - /// - /// Deletes all media of specified type. All children of deleted media is moved to Recycle Bin. - /// - /// This needs extra care and attention as its potentially a dangerous and extensive operation - /// Id of the - /// Optional Id of the user deleting Media - void DeleteMediaOfType(int mediaTypeId, int userId = 0); + /// + /// Deletes all media of specified type. All children of deleted media is moved to Recycle Bin. + /// + /// This needs extra care and attention as its potentially a dangerous and extensive operation + /// Id of the + /// Optional Id of the user deleting Media + void DeleteMediaOfType(int mediaTypeId, int userId = 0); - /// - /// Permanently deletes an object - /// - /// - /// Please note that this method will completely remove the Media from the database, - /// but current not from the file system. - /// - /// The to delete - /// Id of the User deleting the Media - void Delete(IMedia media, int userId = 0); - - /// - /// Saves a single object - /// - /// The to save - /// Id of the User saving the Media - /// Optional boolean indicating whether or not to raise events. - void Save(IMedia media, int userId = 0, bool raiseEvents = true); + /// + /// Permanently deletes an object + /// + /// + /// Please note that this method will completely remove the Media from the database, + /// but current not from the file system. + /// + /// The to delete + /// Id of the User deleting the Media + void Delete(IMedia media, int userId = 0); - /// - /// Saves a collection of objects - /// - /// Collection of to save - /// Id of the User saving the Media - /// Optional boolean indicating whether or not to raise events. - void Save(IEnumerable medias, int userId = 0, bool raiseEvents = true); + /// + /// Saves a single object + /// + /// The to save + /// Id of the User saving the Media + /// Optional boolean indicating whether or not to raise events. + void Save(IMedia media, int userId = 0, bool raiseEvents = true); - /// - /// Gets an object by its 'UniqueId' - /// - /// Guid key of the Media to retrieve - /// - IMedia GetById(Guid key); + /// + /// Saves a collection of objects + /// + /// Collection of to save + /// Id of the User saving the Media + /// Optional boolean indicating whether or not to raise events. + void Save(IEnumerable medias, int userId = 0, bool raiseEvents = true); - /// - /// Gets a collection of objects by Level - /// - /// The level to retrieve Media from - /// An Enumerable list of objects - IEnumerable GetByLevel(int level); + /// + /// Gets an object by its 'UniqueId' + /// + /// Guid key of the Media to retrieve + /// + IMedia GetById(Guid key); - /// - /// Gets a specific version of an item. - /// - /// Id of the version to retrieve - /// An item - IMedia GetByVersion(Guid versionId); + /// + /// Gets a collection of objects by Level + /// + /// The level to retrieve Media from + /// An Enumerable list of objects + IEnumerable GetByLevel(int level); - /// - /// Gets a collection of an objects versions by Id - /// - /// - /// An Enumerable list of objects - IEnumerable GetVersions(int id); + /// + /// Gets a specific version of an item. + /// + /// Id of the version to retrieve + /// An item + IMedia GetByVersion(Guid versionId); - /// - /// Checks whether an item has any children - /// - /// Id of the - /// True if the media has any children otherwise False - bool HasChildren(int id); + /// + /// Gets a collection of an objects versions by Id + /// + /// + /// An Enumerable list of objects + IEnumerable GetVersions(int id); - /// - /// Permanently deletes versions from an object prior to a specific date. - /// - /// Id of the object to delete versions from - /// Latest version date - /// Optional Id of the User deleting versions of a Content object - void DeleteVersions(int id, DateTime versionDate, int userId = 0); + /// + /// Checks whether an item has any children + /// + /// Id of the + /// True if the media has any children otherwise False + bool HasChildren(int id); - /// - /// Permanently deletes specific version(s) from an object. - /// - /// Id of the object to delete a version from - /// Id of the version to delete - /// Boolean indicating whether to delete versions prior to the versionId - /// Optional Id of the User deleting versions of a Content object - void DeleteVersion(int id, Guid versionId, bool deletePriorVersions, int userId = 0); + /// + /// Permanently deletes versions from an object prior to a specific date. + /// + /// Id of the object to delete versions from + /// Latest version date + /// Optional Id of the User deleting versions of a Content object + void DeleteVersions(int id, DateTime versionDate, int userId = 0); - /// - /// Gets an object from the path stored in the 'umbracoFile' property. - /// - /// Path of the media item to retrieve (for example: /media/1024/koala_403x328.jpg) - /// - IMedia GetMediaByPath(string mediaPath); + /// + /// Permanently deletes specific version(s) from an object. + /// + /// Id of the object to delete a version from + /// Id of the version to delete + /// Boolean indicating whether to delete versions prior to the versionId + /// Optional Id of the User deleting versions of a Content object + void DeleteVersion(int id, Guid versionId, bool deletePriorVersions, int userId = 0); - /// - /// Gets a collection of objects, which are ancestors of the current media. - /// - /// Id of the to retrieve ancestors for - /// An Enumerable list of objects - IEnumerable GetAncestors(int id); + /// + /// Gets an object from the path stored in the 'umbracoFile' property. + /// + /// Path of the media item to retrieve (for example: /media/1024/koala_403x328.jpg) + /// + IMedia GetMediaByPath(string mediaPath); - /// - /// Gets a collection of objects, which are ancestors of the current media. - /// - /// to retrieve ancestors for - /// An Enumerable list of objects - IEnumerable GetAncestors(IMedia media); + /// + /// Gets a collection of objects, which are ancestors of the current media. + /// + /// Id of the to retrieve ancestors for + /// An Enumerable list of objects + IEnumerable GetAncestors(int id); - /// - /// Gets descendants of a object by its Id - /// - /// The Parent object to retrieve descendants from - /// An Enumerable flat list of objects - IEnumerable GetDescendants(IMedia media); + /// + /// Gets a collection of objects, which are ancestors of the current media. + /// + /// to retrieve ancestors for + /// An Enumerable list of objects + IEnumerable GetAncestors(IMedia media); - /// - /// Gets the parent of the current media as an item. - /// - /// Id of the to retrieve the parent from - /// Parent object - IMedia GetParent(int id); + /// + /// Gets descendants of a object by its Id + /// + /// The Parent object to retrieve descendants from + /// An Enumerable flat list of objects + IEnumerable GetDescendants(IMedia media); - /// - /// Gets the parent of the current media as an item. - /// - /// to retrieve the parent from - /// Parent object - IMedia GetParent(IMedia media); + /// + /// Gets the parent of the current media as an item. + /// + /// Id of the to retrieve the parent from + /// Parent object + IMedia GetParent(int id); - /// - /// Sorts a collection of objects by updating the SortOrder according - /// to the ordering of items in the passed in . - /// - /// - /// - /// - /// True if sorting succeeded, otherwise False - bool Sort(IEnumerable items, int userId = 0, bool raiseEvents = true); + /// + /// Gets the parent of the current media as an item. + /// + /// to retrieve the parent from + /// Parent object + IMedia GetParent(IMedia media); - /// - /// Creates an object using the alias of the - /// that this Media should based on. - /// - /// - /// This method returns an object that has been persisted to the database - /// and therefor has an identity. - /// - /// Name of the Media object - /// Parent for the new Media item - /// Alias of the - /// Optional id of the user creating the media item - /// - IMedia CreateMediaWithIdentity(string name, IMedia parent, string mediaTypeAlias, int userId = 0); + /// + /// Sorts a collection of objects by updating the SortOrder according + /// to the ordering of items in the passed in . + /// + /// + /// + /// + /// True if sorting succeeded, otherwise False + bool Sort(IEnumerable items, int userId = 0, bool raiseEvents = true); - /// - /// Creates an object using the alias of the - /// that this Media should based on. - /// - /// - /// This method returns an object that has been persisted to the database - /// and therefor has an identity. - /// - /// Name of the Media object - /// Id of Parent for the new Media item - /// Alias of the - /// Optional id of the user creating the media item - /// - IMedia CreateMediaWithIdentity(string name, int parentId, string mediaTypeAlias, int userId = 0); - } + /// + /// Creates an object using the alias of the + /// that this Media should based on. + /// + /// + /// This method returns an object that has been persisted to the database + /// and therefor has an identity. + /// + /// Name of the Media object + /// Parent for the new Media item + /// Alias of the + /// Optional id of the user creating the media item + /// + IMedia CreateMediaWithIdentity(string name, IMedia parent, string mediaTypeAlias, int userId = 0); + + /// + /// Creates an object using the alias of the + /// that this Media should based on. + /// + /// + /// This method returns an object that has been persisted to the database + /// and therefor has an identity. + /// + /// Name of the Media object + /// Id of Parent for the new Media item + /// Alias of the + /// Optional id of the user creating the media item + /// + IMedia CreateMediaWithIdentity(string name, int parentId, string mediaTypeAlias, int userId = 0); + } } diff --git a/src/Umbraco.Core/Services/IMemberService.cs b/src/Umbraco.Core/Services/IMemberService.cs index 20eef54b3c..2177d3b14e 100644 --- a/src/Umbraco.Core/Services/IMemberService.cs +++ b/src/Umbraco.Core/Services/IMemberService.cs @@ -8,213 +8,214 @@ using Umbraco.Core.Persistence.Querying; namespace Umbraco.Core.Services { - /// - /// Defines the MemberService, which is an easy access to operations involving (umbraco) members. - /// - public interface IMemberService : IMembershipMemberService - { - /// - /// Rebuilds all xml content in the cmsContentXml table for all documents - /// - /// - /// Only rebuild the xml structures for the content type ids passed in, if none then rebuilds the structures - /// for all content - /// - void RebuildXmlStructures(params int[] contentTypeIds); + /// + /// Defines the MemberService, which is an easy access to operations involving (umbraco) members. + /// + public interface IMemberService : IMembershipMemberService + { + /// + /// Rebuilds all xml content in the cmsContentXml table for all documents + /// + /// + /// Only rebuild the xml structures for the content type ids passed in, if none then rebuilds the structures + /// for all content + /// + void RebuildXmlStructures(params int[] contentTypeIds); - [Obsolete("Use the overload with 'long' parameter types instead")] - [EditorBrowsable(EditorBrowsableState.Never)] - IEnumerable GetAll(int pageIndex, int pageSize, out int totalRecords, - string orderBy, Direction orderDirection, string memberTypeAlias = null, string filter = ""); + [Obsolete("Use the overload with 'long' parameter types instead")] + [EditorBrowsable(EditorBrowsableState.Never)] + IEnumerable GetAll(int pageIndex, int pageSize, out int totalRecords, + string orderBy, Direction orderDirection, bool orderBySystemField = true, string memberTypeAlias = null, string filter = ""); - /// - /// Gets a list of paged objects - /// - /// An can be of type - /// Current page index - /// Size of the page - /// Total number of records found (out) - /// - /// - /// - /// - /// - IEnumerable GetAll(long pageIndex, int pageSize, out long totalRecords, - string orderBy, Direction orderDirection, string memberTypeAlias = null, string filter = ""); - - /// - /// Creates an object without persisting it - /// - /// This method is convenient for when you need to add properties to a new Member - /// before persisting it in order to limit the amount of times its saved. - /// Also note that the returned will not have an Id until its saved. - /// Username of the Member to create - /// Email of the Member to create - /// Name of the Member to create - /// Alias of the MemberType the Member should be based on - /// - IMember CreateMember(string username, string email, string name, string memberTypeAlias); + /// + /// Gets a list of paged objects + /// + /// An can be of type + /// Current page index + /// Size of the page + /// Total number of records found (out) + /// Field to order by + /// Direction to order by + /// Flag to indicate when ordering by system field + /// + /// Search text filter + /// + IEnumerable GetAll(long pageIndex, int pageSize, out long totalRecords, + string orderBy, Direction orderDirection, bool orderBySystemField = true, string memberTypeAlias = null, string filter = ""); - /// - /// Creates an object without persisting it - /// - /// This method is convenient for when you need to add properties to a new Member - /// before persisting it in order to limit the amount of times its saved. - /// Also note that the returned will not have an Id until its saved. - /// Username of the Member to create - /// Email of the Member to create - /// Name of the Member to create - /// MemberType the Member should be based on - /// - IMember CreateMember(string username, string email, string name, IMemberType memberType); + /// + /// Creates an object without persisting it + /// + /// This method is convenient for when you need to add properties to a new Member + /// before persisting it in order to limit the amount of times its saved. + /// Also note that the returned will not have an Id until its saved. + /// Username of the Member to create + /// Email of the Member to create + /// Name of the Member to create + /// Alias of the MemberType the Member should be based on + /// + IMember CreateMember(string username, string email, string name, string memberTypeAlias); - /// - /// Creates and persists a Member - /// - /// Using this method will persist the Member object before its returned - /// meaning that it will have an Id available (unlike the CreateMember method) - /// Username of the Member to create - /// Email of the Member to create - /// Name of the Member to create - /// Alias of the MemberType the Member should be based on - /// - IMember CreateMemberWithIdentity(string username, string email, string name, string memberTypeAlias); + /// + /// Creates an object without persisting it + /// + /// This method is convenient for when you need to add properties to a new Member + /// before persisting it in order to limit the amount of times its saved. + /// Also note that the returned will not have an Id until its saved. + /// Username of the Member to create + /// Email of the Member to create + /// Name of the Member to create + /// MemberType the Member should be based on + /// + IMember CreateMember(string username, string email, string name, IMemberType memberType); - /// - /// Creates and persists a Member - /// - /// Using this method will persist the Member object before its returned - /// meaning that it will have an Id available (unlike the CreateMember method) - /// Username of the Member to create - /// Email of the Member to create - /// Name of the Member to create - /// MemberType the Member should be based on - /// - IMember CreateMemberWithIdentity(string username, string email, string name, IMemberType memberType); - - /// - /// This is simply a helper method which essentially just wraps the MembershipProvider's ChangePassword method - /// - /// This method exists so that Umbraco developers can use one entry point to create/update - /// Members if they choose to. - /// The Member to save the password for - /// The password to encrypt and save - void SavePassword(IMember member, string password); + /// + /// Creates and persists a Member + /// + /// Using this method will persist the Member object before its returned + /// meaning that it will have an Id available (unlike the CreateMember method) + /// Username of the Member to create + /// Email of the Member to create + /// Name of the Member to create + /// Alias of the MemberType the Member should be based on + /// + IMember CreateMemberWithIdentity(string username, string email, string name, string memberTypeAlias); - /// - /// Gets the count of Members by an optional MemberType alias - /// - /// If no alias is supplied then the count for all Member will be returned - /// Optional alias for the MemberType when counting number of Members - /// with number of Members - int Count(string memberTypeAlias = null); + /// + /// Creates and persists a Member + /// + /// Using this method will persist the Member object before its returned + /// meaning that it will have an Id available (unlike the CreateMember method) + /// Username of the Member to create + /// Email of the Member to create + /// Name of the Member to create + /// MemberType the Member should be based on + /// + IMember CreateMemberWithIdentity(string username, string email, string name, IMemberType memberType); - /// - /// Checks if a Member with the id exists - /// - /// Id of the Member - /// True if the Member exists otherwise False - bool Exists(int id); - - /// - /// Gets a Member by the unique key - /// - /// The guid key corresponds to the unique id in the database - /// and the user id in the membership provider. - /// Id - /// - IMember GetByKey(Guid id); + /// + /// This is simply a helper method which essentially just wraps the MembershipProvider's ChangePassword method + /// + /// This method exists so that Umbraco developers can use one entry point to create/update + /// Members if they choose to. + /// The Member to save the password for + /// The password to encrypt and save + void SavePassword(IMember member, string password); - /// - /// Gets a Member by its integer id - /// - /// Id - /// - IMember GetById(int id); + /// + /// Gets the count of Members by an optional MemberType alias + /// + /// If no alias is supplied then the count for all Member will be returned + /// Optional alias for the MemberType when counting number of Members + /// with number of Members + int Count(string memberTypeAlias = null); - /// - /// Gets all Members for the specified MemberType alias - /// - /// Alias of the MemberType - /// - IEnumerable GetMembersByMemberType(string memberTypeAlias); + /// + /// Checks if a Member with the id exists + /// + /// Id of the Member + /// True if the Member exists otherwise False + bool Exists(int id); - /// - /// Gets all Members for the MemberType id - /// - /// Id of the MemberType - /// - IEnumerable GetMembersByMemberType(int memberTypeId); + /// + /// Gets a Member by the unique key + /// + /// The guid key corresponds to the unique id in the database + /// and the user id in the membership provider. + /// Id + /// + IMember GetByKey(Guid id); - /// - /// Gets all Members within the specified MemberGroup name - /// - /// Name of the MemberGroup - /// - IEnumerable GetMembersByGroup(string memberGroupName); + /// + /// Gets a Member by its integer id + /// + /// Id + /// + IMember GetById(int id); - /// - /// Gets all Members with the ids specified - /// - /// If no Ids are specified all Members will be retrieved - /// Optional list of Member Ids - /// - IEnumerable GetAllMembers(params int[] ids); - - /// - /// Delete Members of the specified MemberType id - /// - /// Id of the MemberType - void DeleteMembersOfType(int memberTypeId); + /// + /// Gets all Members for the specified MemberType alias + /// + /// Alias of the MemberType + /// + IEnumerable GetMembersByMemberType(string memberTypeAlias); - [Obsolete("Use the overload with 'long' parameter types instead")] - [EditorBrowsable(EditorBrowsableState.Never)] - IEnumerable FindMembersByDisplayName(string displayNameToMatch, int pageIndex, int pageSize, out int totalRecords, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith); + /// + /// Gets all Members for the MemberType id + /// + /// Id of the MemberType + /// + IEnumerable GetMembersByMemberType(int memberTypeId); - /// - /// Finds Members based on their display name - /// - /// Display name to match - /// Current page index - /// Size of the page - /// Total number of records found (out) - /// The type of match to make as . Default is - /// - IEnumerable FindMembersByDisplayName(string displayNameToMatch, long pageIndex, int pageSize, out long totalRecords, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith); + /// + /// Gets all Members within the specified MemberGroup name + /// + /// Name of the MemberGroup + /// + IEnumerable GetMembersByGroup(string memberGroupName); - /// - /// Gets a list of Members based on a property search - /// - /// Alias of the PropertyType to search for - /// Value to match - /// The type of match to make as . Default is - /// - IEnumerable GetMembersByPropertyValue(string propertyTypeAlias, string value, StringPropertyMatchType matchType = StringPropertyMatchType.Exact); + /// + /// Gets all Members with the ids specified + /// + /// If no Ids are specified all Members will be retrieved + /// Optional list of Member Ids + /// + IEnumerable GetAllMembers(params int[] ids); - /// - /// Gets a list of Members based on a property search - /// - /// Alias of the PropertyType to search for - /// Value to match - /// The type of match to make as . Default is - /// - IEnumerable GetMembersByPropertyValue(string propertyTypeAlias, int value, ValuePropertyMatchType matchType = ValuePropertyMatchType.Exact); + /// + /// Delete Members of the specified MemberType id + /// + /// Id of the MemberType + void DeleteMembersOfType(int memberTypeId); - /// - /// Gets a list of Members based on a property search - /// - /// Alias of the PropertyType to search for - /// Value to match - /// - IEnumerable GetMembersByPropertyValue(string propertyTypeAlias, bool value); + [Obsolete("Use the overload with 'long' parameter types instead")] + [EditorBrowsable(EditorBrowsableState.Never)] + IEnumerable FindMembersByDisplayName(string displayNameToMatch, int pageIndex, int pageSize, out int totalRecords, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith); - /// - /// Gets a list of Members based on a property search - /// - /// Alias of the PropertyType to search for - /// Value to match - /// The type of match to make as . Default is - /// - IEnumerable GetMembersByPropertyValue(string propertyTypeAlias, DateTime value, ValuePropertyMatchType matchType = ValuePropertyMatchType.Exact); - } + /// + /// Finds Members based on their display name + /// + /// Display name to match + /// Current page index + /// Size of the page + /// Total number of records found (out) + /// The type of match to make as . Default is + /// + IEnumerable FindMembersByDisplayName(string displayNameToMatch, long pageIndex, int pageSize, out long totalRecords, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith); + + /// + /// Gets a list of Members based on a property search + /// + /// Alias of the PropertyType to search for + /// Value to match + /// The type of match to make as . Default is + /// + IEnumerable GetMembersByPropertyValue(string propertyTypeAlias, string value, StringPropertyMatchType matchType = StringPropertyMatchType.Exact); + + /// + /// Gets a list of Members based on a property search + /// + /// Alias of the PropertyType to search for + /// Value to match + /// The type of match to make as . Default is + /// + IEnumerable GetMembersByPropertyValue(string propertyTypeAlias, int value, ValuePropertyMatchType matchType = ValuePropertyMatchType.Exact); + + /// + /// Gets a list of Members based on a property search + /// + /// Alias of the PropertyType to search for + /// Value to match + /// + IEnumerable GetMembersByPropertyValue(string propertyTypeAlias, bool value); + + /// + /// Gets a list of Members based on a property search + /// + /// Alias of the PropertyType to search for + /// Value to match + /// The type of match to make as . Default is + /// + IEnumerable GetMembersByPropertyValue(string propertyTypeAlias, DateTime value, ValuePropertyMatchType matchType = ValuePropertyMatchType.Exact); + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Services/MediaService.cs b/src/Umbraco.Core/Services/MediaService.cs index accbebfe0b..5af8487b3f 100644 --- a/src/Umbraco.Core/Services/MediaService.cs +++ b/src/Umbraco.Core/Services/MediaService.cs @@ -22,1316 +22,1318 @@ using Umbraco.Core.Publishing; namespace Umbraco.Core.Services { - /// - /// Represents the Media Service, which is an easy access to operations involving - /// - public class MediaService : RepositoryService, IMediaService, IMediaServiceOperations - { - - //Support recursive locks because some of the methods that require locking call other methods that require locking. - //for example, the Move method needs to be locked but this calls the Save method which also needs to be locked. - private static readonly ReaderWriterLockSlim Locker = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion); - - private readonly EntityXmlSerializer _entitySerializer = new EntityXmlSerializer(); - private readonly IDataTypeService _dataTypeService; - private readonly IUserService _userService; - - public MediaService(IDatabaseUnitOfWorkProvider provider, RepositoryFactory repositoryFactory, ILogger logger, IEventMessagesFactory eventMessagesFactory, IDataTypeService dataTypeService, IUserService userService) - : base(provider, repositoryFactory, logger, eventMessagesFactory) - { - if (dataTypeService == null) throw new ArgumentNullException("dataTypeService"); - if (userService == null) throw new ArgumentNullException("userService"); - _dataTypeService = dataTypeService; - _userService = userService; - } - - /// - /// Creates an object using the alias of the - /// that this Media should based on. - /// - /// - /// Note that using this method will simply return a new IMedia without any identity - /// as it has not yet been persisted. It is intended as a shortcut to creating new media objects - /// that does not invoke a save operation against the database. - /// - /// Name of the Media object - /// Id of Parent for the new Media item - /// Alias of the - /// Optional id of the user creating the media item - /// - public IMedia CreateMedia(string name, int parentId, string mediaTypeAlias, int userId = 0) - { - var mediaType = FindMediaTypeByAlias(mediaTypeAlias); - var media = new Models.Media(name, parentId, mediaType); - var parent = GetById(media.ParentId); - media.Path = string.Concat(parent.IfNotNull(x => x.Path, media.ParentId.ToString()), ",", media.Id); - - if (Creating.IsRaisedEventCancelled(new NewEventArgs(media, mediaTypeAlias, parentId), this)) - { - media.WasCancelled = true; - return media; - } - - media.CreatorId = userId; - - Created.RaiseEvent(new NewEventArgs(media, false, mediaTypeAlias, parentId), this); - - Audit(AuditType.New, string.Format("Media '{0}' was created", name), media.CreatorId, media.Id); - - return media; - } - - /// - /// Creates an object using the alias of the - /// that this Media should based on. - /// - /// - /// Note that using this method will simply return a new IMedia without any identity - /// as it has not yet been persisted. It is intended as a shortcut to creating new media objects - /// that does not invoke a save operation against the database. - /// - /// Name of the Media object - /// Parent for the new Media item - /// Alias of the - /// Optional id of the user creating the media item - /// - public IMedia CreateMedia(string name, IMedia parent, string mediaTypeAlias, int userId = 0) - { - if (parent == null) throw new ArgumentNullException("parent"); - - var mediaType = FindMediaTypeByAlias(mediaTypeAlias); - var media = new Models.Media(name, parent, mediaType); - media.Path = string.Concat(parent.Path, ",", media.Id); - - if (Creating.IsRaisedEventCancelled(new NewEventArgs(media, mediaTypeAlias, parent), this)) - { - media.WasCancelled = true; - return media; - } - - media.CreatorId = userId; - - Created.RaiseEvent(new NewEventArgs(media, false, mediaTypeAlias, parent), this); - - Audit(AuditType.New, string.Format("Media '{0}' was created", name), media.CreatorId, media.Id); - - return media; - } - - /// - /// Creates an object using the alias of the - /// that this Media should based on. - /// - /// - /// This method returns an object that has been persisted to the database - /// and therefor has an identity. - /// - /// Name of the Media object - /// Id of Parent for the new Media item - /// Alias of the - /// Optional id of the user creating the media item - /// - public IMedia CreateMediaWithIdentity(string name, int parentId, string mediaTypeAlias, int userId = 0) - { - var mediaType = FindMediaTypeByAlias(mediaTypeAlias); - var media = new Models.Media(name, parentId, mediaType); - - //NOTE: I really hate the notion of these Creating/Created events - they are so inconsistent, I've only just found - // out that in these 'WithIdentity' methods, the Saving/Saved events were not fired, wtf. Anyways, they're added now. - if (Creating.IsRaisedEventCancelled(new NewEventArgs(media, mediaTypeAlias, parentId), this)) - { - media.WasCancelled = true; - return media; - } - - if (Saving.IsRaisedEventCancelled(new SaveEventArgs(media), this)) - { - media.WasCancelled = true; - return media; - } - - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMediaRepository(uow)) - { - media.CreatorId = userId; - repository.AddOrUpdate(media); - - repository.AddOrUpdateContentXml(media, m => _entitySerializer.Serialize(this, _dataTypeService, _userService, m)); - // generate preview for blame history? - if (UmbracoConfig.For.UmbracoSettings().Content.GlobalPreviewStorageEnabled) - { - repository.AddOrUpdatePreviewXml(media, m => _entitySerializer.Serialize(this, _dataTypeService, _userService, m)); - } - - uow.Commit(); - } - - Saved.RaiseEvent(new SaveEventArgs(media, false), this); - - Created.RaiseEvent(new NewEventArgs(media, false, mediaTypeAlias, parentId), this); - - Audit(AuditType.New, string.Format("Media '{0}' was created with Id {1}", name, media.Id), media.CreatorId, media.Id); - - return media; - } - - /// - /// Creates an object using the alias of the - /// that this Media should based on. - /// - /// - /// This method returns an object that has been persisted to the database - /// and therefor has an identity. - /// - /// Name of the Media object - /// Parent for the new Media item - /// Alias of the - /// Optional id of the user creating the media item - /// - public IMedia CreateMediaWithIdentity(string name, IMedia parent, string mediaTypeAlias, int userId = 0) - { - if (parent == null) throw new ArgumentNullException("parent"); - - var mediaType = FindMediaTypeByAlias(mediaTypeAlias); - var media = new Models.Media(name, parent, mediaType); - - //NOTE: I really hate the notion of these Creating/Created events - they are so inconsistent, I've only just found - // out that in these 'WithIdentity' methods, the Saving/Saved events were not fired, wtf. Anyways, they're added now. - if (Creating.IsRaisedEventCancelled(new NewEventArgs(media, mediaTypeAlias, parent), this)) - { - media.WasCancelled = true; - return media; - } - - if (Saving.IsRaisedEventCancelled(new SaveEventArgs(media), this)) - { - media.WasCancelled = true; - return media; - } - - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMediaRepository(uow)) - { - media.CreatorId = userId; - repository.AddOrUpdate(media); - repository.AddOrUpdateContentXml(media, m => _entitySerializer.Serialize(this, _dataTypeService, _userService, m)); - // generate preview for blame history? - if (UmbracoConfig.For.UmbracoSettings().Content.GlobalPreviewStorageEnabled) - { - repository.AddOrUpdatePreviewXml(media, m => _entitySerializer.Serialize(this, _dataTypeService, _userService, m)); - } - - uow.Commit(); - } - - Saved.RaiseEvent(new SaveEventArgs(media, false), this); - - Created.RaiseEvent(new NewEventArgs(media, false, mediaTypeAlias, parent), this); - - Audit(AuditType.New, string.Format("Media '{0}' was created with Id {1}", name, media.Id), media.CreatorId, media.Id); - - return media; - } - - /// - /// Gets an object by Id - /// - /// Id of the Content to retrieve - /// - public IMedia GetById(int id) - { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMediaRepository(uow)) - { - return repository.Get(id); - } - } - - public int Count(string contentTypeAlias = null) - { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMediaRepository(uow)) - { - return repository.Count(contentTypeAlias); - } - } - - public int CountChildren(int parentId, string contentTypeAlias = null) - { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMediaRepository(uow)) - { - return repository.CountChildren(parentId, contentTypeAlias); - } - } - - public int CountDescendants(int parentId, string contentTypeAlias = null) - { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMediaRepository(uow)) - { - return repository.CountDescendants(parentId, contentTypeAlias); - } - } - - /// - /// Gets an object by Id - /// - /// Ids of the Media to retrieve - /// - public IEnumerable GetByIds(IEnumerable ids) - { - if (ids.Any() == false) return Enumerable.Empty(); - - using (var repository = RepositoryFactory.CreateMediaRepository(UowProvider.GetUnitOfWork())) - { - return repository.GetAll(ids.ToArray()); - } - } - - /// - /// Gets an object by its 'UniqueId' - /// - /// Guid key of the Media to retrieve - /// - public IMedia GetById(Guid key) - { - using (var repository = RepositoryFactory.CreateMediaRepository(UowProvider.GetUnitOfWork())) - { - var query = Query.Builder.Where(x => x.Key == key); - var contents = repository.GetByQuery(query); - return contents.SingleOrDefault(); - } - } - - /// - /// Gets a collection of objects by Level - /// - /// The level to retrieve Media from - /// An Enumerable list of objects - public IEnumerable GetByLevel(int level) - { - using (var repository = RepositoryFactory.CreateMediaRepository(UowProvider.GetUnitOfWork())) - { - var query = Query.Builder.Where(x => x.Level == level && !x.Path.StartsWith("-21")); - var contents = repository.GetByQuery(query); - - return contents; - } - } - - /// - /// Gets a specific version of an item. - /// - /// Id of the version to retrieve - /// An item - public IMedia GetByVersion(Guid versionId) - { - using (var repository = RepositoryFactory.CreateMediaRepository(UowProvider.GetUnitOfWork())) - { - return repository.GetByVersion(versionId); - } - } - - /// - /// Gets a collection of an objects versions by Id - /// - /// - /// An Enumerable list of objects - public IEnumerable GetVersions(int id) - { - using (var repository = RepositoryFactory.CreateMediaRepository(UowProvider.GetUnitOfWork())) - { - var versions = repository.GetAllVersions(id); - return versions; - } - } - - /// - /// Gets a collection of objects, which are ancestors of the current media. - /// - /// Id of the to retrieve ancestors for - /// An Enumerable list of objects - public IEnumerable GetAncestors(int id) - { - var media = GetById(id); - return GetAncestors(media); - } - - /// - /// Gets a collection of objects, which are ancestors of the current media. - /// - /// to retrieve ancestors for - /// An Enumerable list of objects - public IEnumerable GetAncestors(IMedia media) - { - var ids = media.Path.Split(',').Where(x => x != "-1" && x != media.Id.ToString(CultureInfo.InvariantCulture)).Select(int.Parse).ToArray(); - if (ids.Any() == false) - return new List(); - - using (var repository = RepositoryFactory.CreateMediaRepository(UowProvider.GetUnitOfWork())) - { - return repository.GetAll(ids); - } - } - - /// - /// Gets a collection of objects by Parent Id - /// - /// Id of the Parent to retrieve Children from - /// An Enumerable list of objects - public IEnumerable GetChildren(int id) - { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMediaRepository(uow)) - { - var query = Query.Builder.Where(x => x.ParentId == id); - var medias = repository.GetByQuery(query); - - return medias; - } - } - - [Obsolete("Use the overload with 'long' parameter types instead")] - [EditorBrowsable(EditorBrowsableState.Never)] - public IEnumerable GetPagedChildren(int id, int pageIndex, int pageSize, out int totalChildren, - string orderBy, Direction orderDirection, string filter = "") - { - Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); - Mandate.ParameterCondition(pageSize > 0, "pageSize"); - using (var repository = RepositoryFactory.CreateMediaRepository(UowProvider.GetUnitOfWork())) - { - var query = Query.Builder; - query.Where(x => x.ParentId == id); - - long total; - var medias = repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out total, orderBy, orderDirection, filter); - - totalChildren = Convert.ToInt32(total); - return medias; - } - } - - /// - /// Gets a collection of objects by Parent Id - /// - /// Id of the Parent to retrieve Children from - /// Page index (zero based) - /// Page size - /// Total records query would return without paging - /// Field to order by - /// Direction to order by - /// Search text filter - /// An Enumerable list of objects - public IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize, out long totalChildren, - string orderBy, Direction orderDirection, string filter = "") - { - Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); - Mandate.ParameterCondition(pageSize > 0, "pageSize"); - using (var repository = RepositoryFactory.CreateMediaRepository(UowProvider.GetUnitOfWork())) - { - var query = Query.Builder; - query.Where(x => x.ParentId == id); - - var medias = repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, filter); - - return medias; - } - } - - [Obsolete("Use the overload with 'long' parameter types instead")] - [EditorBrowsable(EditorBrowsableState.Never)] - public IEnumerable GetPagedDescendants(int id, int pageIndex, int pageSize, out int totalChildren, string orderBy = "Path", Direction orderDirection = Direction.Ascending, string filter = "") - { - Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); - Mandate.ParameterCondition(pageSize > 0, "pageSize"); - using (var repository = RepositoryFactory.CreateMediaRepository(UowProvider.GetUnitOfWork())) - { - - var query = Query.Builder; - //if the id is -1, then just get all - if (id != -1) - { - query.Where(x => x.Path.SqlContains(string.Format(",{0},", id), TextColumnType.NVarchar)); - } - long total; - var contents = repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out total, orderBy, orderDirection, filter); - totalChildren = Convert.ToInt32(total); - return contents; - } - } - - /// - /// Gets a collection of objects by Parent Id - /// - /// Id of the Parent to retrieve Descendants from - /// Page number - /// Page size - /// Total records query would return without paging - /// Field to order by - /// Direction to order by - /// Search text filter - /// An Enumerable list of objects - public IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalChildren, string orderBy = "Path", Direction orderDirection = Direction.Ascending, string filter = "") - { - Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); - Mandate.ParameterCondition(pageSize > 0, "pageSize"); - using (var repository = RepositoryFactory.CreateMediaRepository(UowProvider.GetUnitOfWork())) - { - - var query = Query.Builder; - //if the id is -1, then just get all - if (id != -1) - { - query.Where(x => x.Path.SqlContains(string.Format(",{0},", id), TextColumnType.NVarchar)); - } - var contents = repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, filter); - - return contents; - } - } - - /// - /// Gets descendants of a object by its Id - /// - /// Id of the Parent to retrieve descendants from - /// An Enumerable flat list of objects - public IEnumerable GetDescendants(int id) - { - var media = GetById(id); - if (media == null) - { - return Enumerable.Empty(); - } - return GetDescendants(media); - } - - /// - /// Gets descendants of a object by its Id - /// - /// The Parent object to retrieve descendants from - /// An Enumerable flat list of objects - public IEnumerable GetDescendants(IMedia media) - { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMediaRepository(uow)) - { - var pathMatch = media.Path + ","; - var query = Query.Builder.Where(x => x.Path.StartsWith(pathMatch) && x.Id != media.Id); - var medias = repository.GetByQuery(query); - - return medias; - } - } - - /// - /// Gets the parent of the current media as an item. - /// - /// Id of the to retrieve the parent from - /// Parent object - public IMedia GetParent(int id) - { - var media = GetById(id); - return GetParent(media); - } - - /// - /// Gets the parent of the current media as an item. - /// - /// to retrieve the parent from - /// Parent object - public IMedia GetParent(IMedia media) - { - if (media.ParentId == -1 || media.ParentId == -21) - return null; - - return GetById(media.ParentId); - } - - /// - /// Gets a collection of objects by the Id of the - /// - /// Id of the - /// An Enumerable list of objects - public IEnumerable GetMediaOfMediaType(int id) - { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMediaRepository(uow)) - { - var query = Query.Builder.Where(x => x.ContentTypeId == id); - var medias = repository.GetByQuery(query); - - return medias; - } - } - - /// - /// Gets a collection of objects, which reside at the first level / root - /// - /// An Enumerable list of objects - public IEnumerable GetRootMedia() - { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMediaRepository(uow)) - { - var query = Query.Builder.Where(x => x.ParentId == -1); - var medias = repository.GetByQuery(query); - - return medias; - } - } - - /// - /// Gets a collection of an objects, which resides in the Recycle Bin - /// - /// An Enumerable list of objects - public IEnumerable GetMediaInRecycleBin() - { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMediaRepository(uow)) - { - var query = Query.Builder.Where(x => x.Path.Contains("-21")); - var medias = repository.GetByQuery(query); - - return medias; - } - } - - /// - /// Gets an object from the path stored in the 'umbracoFile' property. - /// - /// Path of the media item to retrieve (for example: /media/1024/koala_403x328.jpg) - /// - public IMedia GetMediaByPath(string mediaPath) - { - var umbracoFileValue = mediaPath; - const string Pattern = ".*[_][0-9]+[x][0-9]+[.].*"; - var isResized = Regex.IsMatch(mediaPath, Pattern); - - // If the image has been resized we strip the "_403x328" of the original "/media/1024/koala_403x328.jpg" url. - if (isResized) - { - var underscoreIndex = mediaPath.LastIndexOf('_'); - var dotIndex = mediaPath.LastIndexOf('.'); - umbracoFileValue = string.Concat(mediaPath.Substring(0, underscoreIndex), mediaPath.Substring(dotIndex)); - } - - Func createSql = url => new Sql().Select("*") - .From() - .InnerJoin() - .On(left => left.PropertyTypeId, right => right.Id) - .Where(x => x.Alias == "umbracoFile") - .Where(x => x.VarChar == url); - - var sql = createSql(umbracoFileValue); - - using (var uow = UowProvider.GetUnitOfWork()) - { - var propertyDataDto = uow.Database.Fetch(sql).FirstOrDefault(); - - // If the stripped-down url returns null, we try again with the original url. - // Previously, the function would fail on e.g. "my_x_image.jpg" - if (propertyDataDto == null) - { - sql = createSql(mediaPath); - propertyDataDto = uow.Database.Fetch(sql).FirstOrDefault(); - } - - return propertyDataDto == null ? null : GetById(propertyDataDto.NodeId); - } - } - - /// - /// Checks whether an item has any children - /// - /// Id of the - /// True if the media has any children otherwise False - public bool HasChildren(int id) - { - using (var repository = RepositoryFactory.CreateMediaRepository(UowProvider.GetUnitOfWork())) - { - var query = Query.Builder.Where(x => x.ParentId == id); - int count = repository.Count(query); - return count > 0; - } - } - - /// - /// Moves an object to a new location - /// - /// The to move - /// Id of the Media's new Parent - /// Id of the User moving the Media - public void Move(IMedia media, int parentId, int userId = 0) - { - //TODO: This all needs to be on the repo layer in one transaction! - - if (media == null) throw new ArgumentNullException("media"); - - using (new WriteLock(Locker)) - { - //This ensures that the correct method is called if this method is used to Move to recycle bin. - if (parentId == -21) - { - MoveToRecycleBin(media, userId); - return; - } - - var originalPath = media.Path; - - if (Moving.IsRaisedEventCancelled( - new MoveEventArgs( - new MoveEventInfo(media, originalPath, parentId)), this)) - { - return; - } - - media.ParentId = parentId; - if (media.Trashed) - { - media.ChangeTrashedState(false, parentId); - } - Save(media, userId, - //no events! - false); - - //used to track all the moved entities to be given to the event - var moveInfo = new List> - { - new MoveEventInfo(media, originalPath, parentId) - }; - - //Ensure that relevant properties are updated on children - var children = GetChildren(media.Id).ToArray(); - if (children.Any()) - { - var parentPath = media.Path; - var parentLevel = media.Level; - var parentTrashed = media.Trashed; - var updatedDescendants = UpdatePropertiesOnChildren(children, parentPath, parentLevel, parentTrashed, moveInfo); - Save(updatedDescendants, userId, - //no events! - false); - } - - Moved.RaiseEvent(new MoveEventArgs(false, moveInfo.ToArray()), this); - - Audit(AuditType.Move, "Move Media performed by user", userId, media.Id); - } - } - - /// - /// Deletes an object by moving it to the Recycle Bin - /// - /// The to delete - /// Id of the User deleting the Media - public void MoveToRecycleBin(IMedia media, int userId = 0) - { - ((IMediaServiceOperations) this).MoveToRecycleBin(media, userId); - } - - /// - /// Permanently deletes an object - /// - /// - /// Please note that this method will completely remove the Media from the database, - /// but current not from the file system. - /// - /// The to delete - /// Id of the User deleting the Media - Attempt IMediaServiceOperations.Delete(IMedia media, int userId) - { - //TODO: IT would be much nicer to mass delete all in one trans in the repo level! - var evtMsgs = EventMessagesFactory.Get(); - - if (Deleting.IsRaisedEventCancelled( - new DeleteEventArgs(media, evtMsgs), this)) - { - return OperationStatus.Cancelled(evtMsgs); - } - - //Delete children before deleting the 'possible parent' - var children = GetChildren(media.Id); - foreach (var child in children) - { - Delete(child, userId); - } - - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMediaRepository(uow)) - { - repository.Delete(media); - uow.Commit(); - - var args = new DeleteEventArgs(media, false, evtMsgs); - Deleted.RaiseEvent(args, this); - - //remove any flagged media files - repository.DeleteMediaFiles(args.MediaFilesToDelete); - } - - Audit(AuditType.Delete, "Delete Media performed by user", userId, media.Id); - - return OperationStatus.Success(evtMsgs); - } - - /// - /// Saves a single object - /// - /// The to save - /// Id of the User saving the Media - /// Optional boolean indicating whether or not to raise events. - Attempt IMediaServiceOperations.Save(IMedia media, int userId, bool raiseEvents) - { - var evtMsgs = EventMessagesFactory.Get(); - - if (raiseEvents) - { - if (Saving.IsRaisedEventCancelled( - new SaveEventArgs(media, evtMsgs), - this)) - { - return OperationStatus.Cancelled(evtMsgs); - } - - } - - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMediaRepository(uow)) - { - media.CreatorId = userId; - repository.AddOrUpdate(media); - repository.AddOrUpdateContentXml(media, m => _entitySerializer.Serialize(this, _dataTypeService, _userService, m)); - // generate preview for blame history? - if (UmbracoConfig.For.UmbracoSettings().Content.GlobalPreviewStorageEnabled) - { - repository.AddOrUpdatePreviewXml(media, m => _entitySerializer.Serialize(this, _dataTypeService, _userService, m)); - } - - uow.Commit(); - } - - if (raiseEvents) - Saved.RaiseEvent(new SaveEventArgs(media, false, evtMsgs), this); - - Audit(AuditType.Save, "Save Media performed by user", userId, media.Id); - - return OperationStatus.Success(evtMsgs); - } - - /// - /// Saves a collection of objects - /// - /// Collection of to save - /// Id of the User saving the Media - /// Optional boolean indicating whether or not to raise events. - Attempt IMediaServiceOperations.Save(IEnumerable medias, int userId, bool raiseEvents) - { - var asArray = medias.ToArray(); - var evtMsgs = EventMessagesFactory.Get(); - - if (raiseEvents) - { - if (Saving.IsRaisedEventCancelled( - new SaveEventArgs(asArray, evtMsgs), - this)) - { - return OperationStatus.Cancelled(evtMsgs); - } - } - - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMediaRepository(uow)) - { - foreach (var media in asArray) - { - media.CreatorId = userId; - repository.AddOrUpdate(media); - repository.AddOrUpdateContentXml(media, m => _entitySerializer.Serialize(this, _dataTypeService, _userService, m)); - // generate preview for blame history? - if (UmbracoConfig.For.UmbracoSettings().Content.GlobalPreviewStorageEnabled) - { - repository.AddOrUpdatePreviewXml(media, m => _entitySerializer.Serialize(this, _dataTypeService, _userService, m)); - } - } - - //commit the whole lot in one go - uow.Commit(); - } - - if (raiseEvents) - Saved.RaiseEvent(new SaveEventArgs(asArray, false, evtMsgs), this); - - Audit(AuditType.Save, "Save Media items performed by user", userId, -1); - - return OperationStatus.Success(evtMsgs); - } - - /// - /// Empties the Recycle Bin by deleting all that resides in the bin - /// - public void EmptyRecycleBin() - { - using (new WriteLock(Locker)) - { - Dictionary> entities; - List files; - bool success; - var nodeObjectType = new Guid(Constants.ObjectTypes.Media); - - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMediaRepository(uow)) - { - //Create a dictionary of ids -> dictionary of property aliases + values - entities = repository.GetEntitiesInRecycleBin() - .ToDictionary( - key => key.Id, - val => (IEnumerable)val.Properties); - - files = ((MediaRepository)repository).GetFilesInRecycleBinForUploadField(); - - if (EmptyingRecycleBin.IsRaisedEventCancelled(new RecycleBinEventArgs(nodeObjectType, entities, files), this)) - return; - - success = repository.EmptyRecycleBin(); - - EmptiedRecycleBin.RaiseEvent(new RecycleBinEventArgs(nodeObjectType, entities, files, success), this); - - if (success) - repository.DeleteMediaFiles(files); - } - } - Audit(AuditType.Delete, "Empty Media Recycle Bin performed by user", 0, -21); - } - - /// - /// Deletes all media of specified type. All children of deleted media is moved to Recycle Bin. - /// - /// This needs extra care and attention as its potentially a dangerous and extensive operation - /// Id of the - /// Optional id of the user deleting the media - public void DeleteMediaOfType(int mediaTypeId, int userId = 0) - { - //TODO: This all needs to be done on the repo level in one trans - - using (new WriteLock(Locker)) - { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMediaRepository(uow)) - { - //NOTE What about media that has the contenttype as part of its composition? - //The ContentType has to be removed from the composition somehow as it would otherwise break - //Dbl.check+test that the ContentType's Id is removed from the ContentType2ContentType table - var query = Query.Builder.Where(x => x.ContentTypeId == mediaTypeId); - var contents = repository.GetByQuery(query).ToArray(); - - if (Deleting.IsRaisedEventCancelled(new DeleteEventArgs(contents), this)) - return; - - foreach (var content in contents.OrderByDescending(x => x.ParentId)) - { - //Look for children of current content and move that to trash before the current content is deleted - var c = content; - var childQuery = Query.Builder.Where(x => x.Path.StartsWith(c.Path)); - var children = repository.GetByQuery(childQuery); - - foreach (var child in children) - { - if (child.ContentType.Id != mediaTypeId) - MoveToRecycleBin(child, userId); - } - - //Permanently delete the content - Delete(content, userId); - } - } - - Audit(AuditType.Delete, "Delete Media items by Type performed by user", userId, -1); - } - } - - /// - /// Deletes an object by moving it to the Recycle Bin - /// - /// The to delete - /// Id of the User deleting the Media - Attempt IMediaServiceOperations.MoveToRecycleBin(IMedia media, int userId) - { - if (media == null) throw new ArgumentNullException("media"); - - var originalPath = media.Path; - - var evtMsgs = EventMessagesFactory.Get(); - - if (Trashing.IsRaisedEventCancelled( - new MoveEventArgs(new MoveEventInfo(media, originalPath, Constants.System.RecycleBinMedia)), this)) - { - return OperationStatus.Cancelled(evtMsgs); - } - - var moveInfo = new List> - { - new MoveEventInfo(media, originalPath, Constants.System.RecycleBinMedia) - }; - - //Find Descendants, which will be moved to the recycle bin along with the parent/grandparent. - var descendants = GetDescendants(media).OrderBy(x => x.Level).ToList(); - - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMediaRepository(uow)) - { - //TODO: This should be part of the repo! - - //Remove 'published' xml from the cmsContentXml table for the unpublished media - uow.Database.Delete("WHERE nodeId = @Id", new { Id = media.Id }); - - media.ChangeTrashedState(true, Constants.System.RecycleBinMedia); - repository.AddOrUpdate(media); - - //Loop through descendants to update their trash state, but ensuring structure by keeping the ParentId - foreach (var descendant in descendants) - { - //Remove 'published' xml from the cmsContentXml table for the unpublished media - uow.Database.Delete("WHERE nodeId = @Id", new { Id = descendant.Id }); - - descendant.ChangeTrashedState(true, descendant.ParentId); - repository.AddOrUpdate(descendant); - - moveInfo.Add(new MoveEventInfo(descendant, descendant.Path, descendant.ParentId)); - } - - uow.Commit(); - } - - Trashed.RaiseEvent( - new MoveEventArgs(false, evtMsgs, moveInfo.ToArray()), this); - - Audit(AuditType.Move, "Move Media to Recycle Bin performed by user", userId, media.Id); - - return OperationStatus.Success(evtMsgs); - } - - /// - /// Permanently deletes an object as well as all of its Children. - /// - /// - /// Please note that this method will completely remove the Media from the database, - /// as well as associated media files from the file system. - /// - /// The to delete - /// Id of the User deleting the Media - public void Delete(IMedia media, int userId = 0) - { - ((IMediaServiceOperations)this).Delete(media, userId); - } - - - - /// - /// Permanently deletes versions from an object prior to a specific date. - /// This method will never delete the latest version of a content item. - /// - /// Id of the object to delete versions from - /// Latest version date - /// Optional Id of the User deleting versions of a Content object - public void DeleteVersions(int id, DateTime versionDate, int userId = 0) - { - if (DeletingVersions.IsRaisedEventCancelled(new DeleteRevisionsEventArgs(id, dateToRetain: versionDate), this)) - return; - - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMediaRepository(uow)) - { - repository.DeleteVersions(id, versionDate); - uow.Commit(); - } - - DeletedVersions.RaiseEvent(new DeleteRevisionsEventArgs(id, false, dateToRetain: versionDate), this); - - Audit(AuditType.Delete, "Delete Media by version date performed by user", userId, -1); - } - - /// - /// Permanently deletes specific version(s) from an object. - /// This method will never delete the latest version of a content item. - /// - /// Id of the object to delete a version from - /// Id of the version to delete - /// Boolean indicating whether to delete versions prior to the versionId - /// Optional Id of the User deleting versions of a Content object - public void DeleteVersion(int id, Guid versionId, bool deletePriorVersions, int userId = 0) - { - if (DeletingVersions.IsRaisedEventCancelled(new DeleteRevisionsEventArgs(id, specificVersion: versionId), this)) - return; - - if (deletePriorVersions) - { - var content = GetByVersion(versionId); - DeleteVersions(id, content.UpdateDate, userId); - } - - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMediaRepository(uow)) - { - repository.DeleteVersion(versionId); - uow.Commit(); - } - - DeletedVersions.RaiseEvent(new DeleteRevisionsEventArgs(id, false, specificVersion: versionId), this); - - Audit(AuditType.Delete, "Delete Media by version performed by user", userId, -1); - } - - /// - /// Saves a single object - /// - /// The to save - /// Id of the User saving the Content - /// Optional boolean indicating whether or not to raise events. - public void Save(IMedia media, int userId = 0, bool raiseEvents = true) - { - ((IMediaServiceOperations)this).Save (media, userId, raiseEvents); - } - - /// - /// Saves a collection of objects - /// - /// Collection of to save - /// Id of the User saving the Content - /// Optional boolean indicating whether or not to raise events. - public void Save(IEnumerable medias, int userId = 0, bool raiseEvents = true) - { - ((IMediaServiceOperations)this).Save(medias, userId, raiseEvents); - } - - /// - /// Sorts a collection of objects by updating the SortOrder according - /// to the ordering of items in the passed in . - /// - /// - /// - /// - /// True if sorting succeeded, otherwise False - public bool Sort(IEnumerable items, int userId = 0, bool raiseEvents = true) - { - var asArray = items.ToArray(); - - if (raiseEvents) - { - if (Saving.IsRaisedEventCancelled(new SaveEventArgs(asArray), this)) - return false; - } - - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMediaRepository(uow)) - { - int i = 0; - foreach (var media in asArray) - { - //If the current sort order equals that of the media - //we don't need to update it, so just increment the sort order - //and continue. - if (media.SortOrder == i) - { - i++; - continue; - } - - media.SortOrder = i; - i++; - - repository.AddOrUpdate(media); - repository.AddOrUpdateContentXml(media, m => _entitySerializer.Serialize(this, _dataTypeService, _userService, m)); - // generate preview for blame history? - if (UmbracoConfig.For.UmbracoSettings().Content.GlobalPreviewStorageEnabled) - { - repository.AddOrUpdatePreviewXml(media, m => _entitySerializer.Serialize(this, _dataTypeService, _userService, m)); - } - } - - uow.Commit(); - } - - if (raiseEvents) - Saved.RaiseEvent(new SaveEventArgs(asArray, false), this); - - Audit(AuditType.Sort, "Sorting Media performed by user", userId, 0); - - return true; - } - - /// - /// Rebuilds all xml content in the cmsContentXml table for all media - /// - /// - /// Only rebuild the xml structures for the content type ids passed in, if none then rebuilds the structures - /// for all media - /// - public void RebuildXmlStructures(params int[] contentTypeIds) - { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMediaRepository(uow)) - { - repository.RebuildXmlStructures( - media => _entitySerializer.Serialize(this, _dataTypeService, _userService, media), - contentTypeIds: contentTypeIds.Length == 0 ? null : contentTypeIds); - } - - Audit(AuditType.Publish, "MediaService.RebuildXmlStructures completed, the xml has been regenerated in the database", 0, -1); - } - - /// - /// Updates the Path and Level on a collection of objects - /// based on the Parent's Path and Level. Also change the trashed state if relevant. - /// - /// Collection of objects to update - /// Path of the Parent media - /// Level of the Parent media - /// Indicates whether the Parent is trashed or not - /// Used to track the objects to be used in the move event - /// Collection of updated objects - private IEnumerable UpdatePropertiesOnChildren(IEnumerable children, string parentPath, int parentLevel, bool parentTrashed, ICollection> eventInfo) - { - var list = new List(); - foreach (var child in children) - { - var originalPath = child.Path; - child.Path = string.Concat(parentPath, ",", child.Id); - child.Level = parentLevel + 1; - if (parentTrashed != child.Trashed) - { - child.ChangeTrashedState(parentTrashed, child.ParentId); - } - - eventInfo.Add(new MoveEventInfo(child, originalPath, child.ParentId)); - list.Add(child); - - var grandkids = GetChildren(child.Id).ToArray(); - if (grandkids.Any()) - { - list.AddRange(UpdatePropertiesOnChildren(grandkids, child.Path, child.Level, child.Trashed, eventInfo)); - } - } - return list; - } - - //private void CreateAndSaveMediaXml(XElement xml, int id, UmbracoDatabase db) - //{ - // var poco = new ContentXmlDto { NodeId = id, Xml = xml.ToDataString() }; - // var exists = db.FirstOrDefault("WHERE nodeId = @Id", new { Id = id }) != null; - // int result = exists ? db.Update(poco) : Convert.ToInt32(db.Insert(poco)); - //} - - private IMediaType FindMediaTypeByAlias(string mediaTypeAlias) - { - Mandate.ParameterNotNullOrEmpty(mediaTypeAlias, "mediaTypeAlias"); - - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMediaTypeRepository(uow)) - { - var query = Query.Builder.Where(x => x.Alias == mediaTypeAlias); - var mediaTypes = repository.GetByQuery(query); - - if (mediaTypes.Any() == false) - throw new Exception(string.Format("No MediaType matching the passed in Alias: '{0}' was found", - mediaTypeAlias)); - - var mediaType = mediaTypes.First(); - - if (mediaType == null) - throw new Exception(string.Format("MediaType matching the passed in Alias: '{0}' was null", - mediaTypeAlias)); - - return mediaType; - } - } - - private void Audit(AuditType type, string message, int userId, int objectId) - { - var uow = UowProvider.GetUnitOfWork(); - using (var auditRepo = RepositoryFactory.CreateAuditRepository(uow)) - { - auditRepo.AddOrUpdate(new AuditItem(objectId, message, type, userId)); - uow.Commit(); - } - } - - #region Event Handlers - - /// - /// Occurs before Delete - /// - public static event TypedEventHandler DeletingVersions; - - /// - /// Occurs after Delete - /// - public static event TypedEventHandler DeletedVersions; - - /// - /// Occurs before Delete - /// - public static event TypedEventHandler> Deleting; - - /// - /// Occurs after Delete - /// - public static event TypedEventHandler> Deleted; - - /// - /// Occurs before Save - /// - public static event TypedEventHandler> Saving; - - /// - /// Occurs after Save - /// - public static event TypedEventHandler> Saved; - - /// - /// Occurs before Create - /// - [Obsolete("Use the Created event instead, the Creating and Created events both offer the same functionality, Creating event has been deprecated.")] - public static event TypedEventHandler> Creating; - - /// - /// Occurs after Create - /// - /// - /// Please note that the Media object has been created, but not saved - /// so it does not have an identity yet (meaning no Id has been set). - /// - public static event TypedEventHandler> Created; - - /// - /// Occurs before Content is moved to Recycle Bin - /// - public static event TypedEventHandler> Trashing; - - /// - /// Occurs after Content is moved to Recycle Bin - /// - public static event TypedEventHandler> Trashed; - - /// - /// Occurs before Move - /// - public static event TypedEventHandler> Moving; - - /// - /// Occurs after Move - /// - public static event TypedEventHandler> Moved; - - /// - /// Occurs before the Recycle Bin is emptied - /// - public static event TypedEventHandler EmptyingRecycleBin; - - /// - /// Occurs after the Recycle Bin has been Emptied - /// - public static event TypedEventHandler EmptiedRecycleBin; - #endregion - } + /// + /// Represents the Media Service, which is an easy access to operations involving + /// + public class MediaService : RepositoryService, IMediaService, IMediaServiceOperations + { + + //Support recursive locks because some of the methods that require locking call other methods that require locking. + //for example, the Move method needs to be locked but this calls the Save method which also needs to be locked. + private static readonly ReaderWriterLockSlim Locker = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion); + + private readonly EntityXmlSerializer _entitySerializer = new EntityXmlSerializer(); + private readonly IDataTypeService _dataTypeService; + private readonly IUserService _userService; + + public MediaService(IDatabaseUnitOfWorkProvider provider, RepositoryFactory repositoryFactory, ILogger logger, IEventMessagesFactory eventMessagesFactory, IDataTypeService dataTypeService, IUserService userService) + : base(provider, repositoryFactory, logger, eventMessagesFactory) + { + if (dataTypeService == null) throw new ArgumentNullException("dataTypeService"); + if (userService == null) throw new ArgumentNullException("userService"); + _dataTypeService = dataTypeService; + _userService = userService; + } + + /// + /// Creates an object using the alias of the + /// that this Media should based on. + /// + /// + /// Note that using this method will simply return a new IMedia without any identity + /// as it has not yet been persisted. It is intended as a shortcut to creating new media objects + /// that does not invoke a save operation against the database. + /// + /// Name of the Media object + /// Id of Parent for the new Media item + /// Alias of the + /// Optional id of the user creating the media item + /// + public IMedia CreateMedia(string name, int parentId, string mediaTypeAlias, int userId = 0) + { + var mediaType = FindMediaTypeByAlias(mediaTypeAlias); + var media = new Models.Media(name, parentId, mediaType); + var parent = GetById(media.ParentId); + media.Path = string.Concat(parent.IfNotNull(x => x.Path, media.ParentId.ToString()), ",", media.Id); + + if (Creating.IsRaisedEventCancelled(new NewEventArgs(media, mediaTypeAlias, parentId), this)) + { + media.WasCancelled = true; + return media; + } + + media.CreatorId = userId; + + Created.RaiseEvent(new NewEventArgs(media, false, mediaTypeAlias, parentId), this); + + Audit(AuditType.New, string.Format("Media '{0}' was created", name), media.CreatorId, media.Id); + + return media; + } + + /// + /// Creates an object using the alias of the + /// that this Media should based on. + /// + /// + /// Note that using this method will simply return a new IMedia without any identity + /// as it has not yet been persisted. It is intended as a shortcut to creating new media objects + /// that does not invoke a save operation against the database. + /// + /// Name of the Media object + /// Parent for the new Media item + /// Alias of the + /// Optional id of the user creating the media item + /// + public IMedia CreateMedia(string name, IMedia parent, string mediaTypeAlias, int userId = 0) + { + if (parent == null) throw new ArgumentNullException("parent"); + + var mediaType = FindMediaTypeByAlias(mediaTypeAlias); + var media = new Models.Media(name, parent, mediaType); + media.Path = string.Concat(parent.Path, ",", media.Id); + + if (Creating.IsRaisedEventCancelled(new NewEventArgs(media, mediaTypeAlias, parent), this)) + { + media.WasCancelled = true; + return media; + } + + media.CreatorId = userId; + + Created.RaiseEvent(new NewEventArgs(media, false, mediaTypeAlias, parent), this); + + Audit(AuditType.New, string.Format("Media '{0}' was created", name), media.CreatorId, media.Id); + + return media; + } + + /// + /// Creates an object using the alias of the + /// that this Media should based on. + /// + /// + /// This method returns an object that has been persisted to the database + /// and therefor has an identity. + /// + /// Name of the Media object + /// Id of Parent for the new Media item + /// Alias of the + /// Optional id of the user creating the media item + /// + public IMedia CreateMediaWithIdentity(string name, int parentId, string mediaTypeAlias, int userId = 0) + { + var mediaType = FindMediaTypeByAlias(mediaTypeAlias); + var media = new Models.Media(name, parentId, mediaType); + + //NOTE: I really hate the notion of these Creating/Created events - they are so inconsistent, I've only just found + // out that in these 'WithIdentity' methods, the Saving/Saved events were not fired, wtf. Anyways, they're added now. + if (Creating.IsRaisedEventCancelled(new NewEventArgs(media, mediaTypeAlias, parentId), this)) + { + media.WasCancelled = true; + return media; + } + + if (Saving.IsRaisedEventCancelled(new SaveEventArgs(media), this)) + { + media.WasCancelled = true; + return media; + } + + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMediaRepository(uow)) + { + media.CreatorId = userId; + repository.AddOrUpdate(media); + + repository.AddOrUpdateContentXml(media, m => _entitySerializer.Serialize(this, _dataTypeService, _userService, m)); + // generate preview for blame history? + if (UmbracoConfig.For.UmbracoSettings().Content.GlobalPreviewStorageEnabled) + { + repository.AddOrUpdatePreviewXml(media, m => _entitySerializer.Serialize(this, _dataTypeService, _userService, m)); + } + + uow.Commit(); + } + + Saved.RaiseEvent(new SaveEventArgs(media, false), this); + + Created.RaiseEvent(new NewEventArgs(media, false, mediaTypeAlias, parentId), this); + + Audit(AuditType.New, string.Format("Media '{0}' was created with Id {1}", name, media.Id), media.CreatorId, media.Id); + + return media; + } + + /// + /// Creates an object using the alias of the + /// that this Media should based on. + /// + /// + /// This method returns an object that has been persisted to the database + /// and therefor has an identity. + /// + /// Name of the Media object + /// Parent for the new Media item + /// Alias of the + /// Optional id of the user creating the media item + /// + public IMedia CreateMediaWithIdentity(string name, IMedia parent, string mediaTypeAlias, int userId = 0) + { + if (parent == null) throw new ArgumentNullException("parent"); + + var mediaType = FindMediaTypeByAlias(mediaTypeAlias); + var media = new Models.Media(name, parent, mediaType); + + //NOTE: I really hate the notion of these Creating/Created events - they are so inconsistent, I've only just found + // out that in these 'WithIdentity' methods, the Saving/Saved events were not fired, wtf. Anyways, they're added now. + if (Creating.IsRaisedEventCancelled(new NewEventArgs(media, mediaTypeAlias, parent), this)) + { + media.WasCancelled = true; + return media; + } + + if (Saving.IsRaisedEventCancelled(new SaveEventArgs(media), this)) + { + media.WasCancelled = true; + return media; + } + + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMediaRepository(uow)) + { + media.CreatorId = userId; + repository.AddOrUpdate(media); + repository.AddOrUpdateContentXml(media, m => _entitySerializer.Serialize(this, _dataTypeService, _userService, m)); + // generate preview for blame history? + if (UmbracoConfig.For.UmbracoSettings().Content.GlobalPreviewStorageEnabled) + { + repository.AddOrUpdatePreviewXml(media, m => _entitySerializer.Serialize(this, _dataTypeService, _userService, m)); + } + + uow.Commit(); + } + + Saved.RaiseEvent(new SaveEventArgs(media, false), this); + + Created.RaiseEvent(new NewEventArgs(media, false, mediaTypeAlias, parent), this); + + Audit(AuditType.New, string.Format("Media '{0}' was created with Id {1}", name, media.Id), media.CreatorId, media.Id); + + return media; + } + + /// + /// Gets an object by Id + /// + /// Id of the Content to retrieve + /// + public IMedia GetById(int id) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMediaRepository(uow)) + { + return repository.Get(id); + } + } + + public int Count(string contentTypeAlias = null) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMediaRepository(uow)) + { + return repository.Count(contentTypeAlias); + } + } + + public int CountChildren(int parentId, string contentTypeAlias = null) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMediaRepository(uow)) + { + return repository.CountChildren(parentId, contentTypeAlias); + } + } + + public int CountDescendants(int parentId, string contentTypeAlias = null) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMediaRepository(uow)) + { + return repository.CountDescendants(parentId, contentTypeAlias); + } + } + + /// + /// Gets an object by Id + /// + /// Ids of the Media to retrieve + /// + public IEnumerable GetByIds(IEnumerable ids) + { + if (ids.Any() == false) return Enumerable.Empty(); + + using (var repository = RepositoryFactory.CreateMediaRepository(UowProvider.GetUnitOfWork())) + { + return repository.GetAll(ids.ToArray()); + } + } + + /// + /// Gets an object by its 'UniqueId' + /// + /// Guid key of the Media to retrieve + /// + public IMedia GetById(Guid key) + { + using (var repository = RepositoryFactory.CreateMediaRepository(UowProvider.GetUnitOfWork())) + { + var query = Query.Builder.Where(x => x.Key == key); + var contents = repository.GetByQuery(query); + return contents.SingleOrDefault(); + } + } + + /// + /// Gets a collection of objects by Level + /// + /// The level to retrieve Media from + /// An Enumerable list of objects + public IEnumerable GetByLevel(int level) + { + using (var repository = RepositoryFactory.CreateMediaRepository(UowProvider.GetUnitOfWork())) + { + var query = Query.Builder.Where(x => x.Level == level && !x.Path.StartsWith("-21")); + var contents = repository.GetByQuery(query); + + return contents; + } + } + + /// + /// Gets a specific version of an item. + /// + /// Id of the version to retrieve + /// An item + public IMedia GetByVersion(Guid versionId) + { + using (var repository = RepositoryFactory.CreateMediaRepository(UowProvider.GetUnitOfWork())) + { + return repository.GetByVersion(versionId); + } + } + + /// + /// Gets a collection of an objects versions by Id + /// + /// + /// An Enumerable list of objects + public IEnumerable GetVersions(int id) + { + using (var repository = RepositoryFactory.CreateMediaRepository(UowProvider.GetUnitOfWork())) + { + var versions = repository.GetAllVersions(id); + return versions; + } + } + + /// + /// Gets a collection of objects, which are ancestors of the current media. + /// + /// Id of the to retrieve ancestors for + /// An Enumerable list of objects + public IEnumerable GetAncestors(int id) + { + var media = GetById(id); + return GetAncestors(media); + } + + /// + /// Gets a collection of objects, which are ancestors of the current media. + /// + /// to retrieve ancestors for + /// An Enumerable list of objects + public IEnumerable GetAncestors(IMedia media) + { + var ids = media.Path.Split(',').Where(x => x != "-1" && x != media.Id.ToString(CultureInfo.InvariantCulture)).Select(int.Parse).ToArray(); + if (ids.Any() == false) + return new List(); + + using (var repository = RepositoryFactory.CreateMediaRepository(UowProvider.GetUnitOfWork())) + { + return repository.GetAll(ids); + } + } + + /// + /// Gets a collection of objects by Parent Id + /// + /// Id of the Parent to retrieve Children from + /// An Enumerable list of objects + public IEnumerable GetChildren(int id) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMediaRepository(uow)) + { + var query = Query.Builder.Where(x => x.ParentId == id); + var medias = repository.GetByQuery(query); + + return medias; + } + } + + [Obsolete("Use the overload with 'long' parameter types instead")] + [EditorBrowsable(EditorBrowsableState.Never)] + public IEnumerable GetPagedChildren(int id, int pageIndex, int pageSize, out int totalChildren, + string orderBy, Direction orderDirection, bool orderBySystemField = true, string filter = "") + { + Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); + Mandate.ParameterCondition(pageSize > 0, "pageSize"); + using (var repository = RepositoryFactory.CreateMediaRepository(UowProvider.GetUnitOfWork())) + { + var query = Query.Builder; + query.Where(x => x.ParentId == id); + + long total; + var medias = repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out total, orderBy, orderDirection, orderBySystemField, filter); + + totalChildren = Convert.ToInt32(total); + return medias; + } + } + + /// + /// Gets a collection of objects by Parent Id + /// + /// Id of the Parent to retrieve Children from + /// Page index (zero based) + /// Page size + /// Total records query would return without paging + /// Field to order by + /// Direction to order by + /// Flag to indicate when ordering by system field + /// Search text filter + /// An Enumerable list of objects + public IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize, out long totalChildren, + string orderBy, Direction orderDirection, bool orderBySystemField = true, string filter = "") + { + Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); + Mandate.ParameterCondition(pageSize > 0, "pageSize"); + using (var repository = RepositoryFactory.CreateMediaRepository(UowProvider.GetUnitOfWork())) + { + var query = Query.Builder; + query.Where(x => x.ParentId == id); + + var medias = repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, orderBySystemField, filter); + + return medias; + } + } + + [Obsolete("Use the overload with 'long' parameter types instead")] + [EditorBrowsable(EditorBrowsableState.Never)] + public IEnumerable GetPagedDescendants(int id, int pageIndex, int pageSize, out int totalChildren, string orderBy = "Path", Direction orderDirection = Direction.Ascending, bool orderBySystemField = true, string filter = "") + { + Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); + Mandate.ParameterCondition(pageSize > 0, "pageSize"); + using (var repository = RepositoryFactory.CreateMediaRepository(UowProvider.GetUnitOfWork())) + { + + var query = Query.Builder; + //if the id is -1, then just get all + if (id != -1) + { + query.Where(x => x.Path.SqlContains(string.Format(",{0},", id), TextColumnType.NVarchar)); + } + long total; + var contents = repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out total, orderBy, orderDirection, orderBySystemField, filter); + totalChildren = Convert.ToInt32(total); + return contents; + } + } + + /// + /// Gets a collection of objects by Parent Id + /// + /// Id of the Parent to retrieve Descendants from + /// Page number + /// Page size + /// Total records query would return without paging + /// Field to order by + /// Direction to order by + /// Flag to indicate when ordering by system field + /// Search text filter + /// An Enumerable list of objects + public IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalChildren, string orderBy = "Path", Direction orderDirection = Direction.Ascending, bool orderBySystemField = true, string filter = "") + { + Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); + Mandate.ParameterCondition(pageSize > 0, "pageSize"); + using (var repository = RepositoryFactory.CreateMediaRepository(UowProvider.GetUnitOfWork())) + { + + var query = Query.Builder; + //if the id is -1, then just get all + if (id != -1) + { + query.Where(x => x.Path.SqlContains(string.Format(",{0},", id), TextColumnType.NVarchar)); + } + var contents = repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, orderBySystemField, filter); + + return contents; + } + } + + /// + /// Gets descendants of a object by its Id + /// + /// Id of the Parent to retrieve descendants from + /// An Enumerable flat list of objects + public IEnumerable GetDescendants(int id) + { + var media = GetById(id); + if (media == null) + { + return Enumerable.Empty(); + } + return GetDescendants(media); + } + + /// + /// Gets descendants of a object by its Id + /// + /// The Parent object to retrieve descendants from + /// An Enumerable flat list of objects + public IEnumerable GetDescendants(IMedia media) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMediaRepository(uow)) + { + var pathMatch = media.Path + ","; + var query = Query.Builder.Where(x => x.Path.StartsWith(pathMatch) && x.Id != media.Id); + var medias = repository.GetByQuery(query); + + return medias; + } + } + + /// + /// Gets the parent of the current media as an item. + /// + /// Id of the to retrieve the parent from + /// Parent object + public IMedia GetParent(int id) + { + var media = GetById(id); + return GetParent(media); + } + + /// + /// Gets the parent of the current media as an item. + /// + /// to retrieve the parent from + /// Parent object + public IMedia GetParent(IMedia media) + { + if (media.ParentId == -1 || media.ParentId == -21) + return null; + + return GetById(media.ParentId); + } + + /// + /// Gets a collection of objects by the Id of the + /// + /// Id of the + /// An Enumerable list of objects + public IEnumerable GetMediaOfMediaType(int id) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMediaRepository(uow)) + { + var query = Query.Builder.Where(x => x.ContentTypeId == id); + var medias = repository.GetByQuery(query); + + return medias; + } + } + + /// + /// Gets a collection of objects, which reside at the first level / root + /// + /// An Enumerable list of objects + public IEnumerable GetRootMedia() + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMediaRepository(uow)) + { + var query = Query.Builder.Where(x => x.ParentId == -1); + var medias = repository.GetByQuery(query); + + return medias; + } + } + + /// + /// Gets a collection of an objects, which resides in the Recycle Bin + /// + /// An Enumerable list of objects + public IEnumerable GetMediaInRecycleBin() + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMediaRepository(uow)) + { + var query = Query.Builder.Where(x => x.Path.Contains("-21")); + var medias = repository.GetByQuery(query); + + return medias; + } + } + + /// + /// Gets an object from the path stored in the 'umbracoFile' property. + /// + /// Path of the media item to retrieve (for example: /media/1024/koala_403x328.jpg) + /// + public IMedia GetMediaByPath(string mediaPath) + { + var umbracoFileValue = mediaPath; + const string Pattern = ".*[_][0-9]+[x][0-9]+[.].*"; + var isResized = Regex.IsMatch(mediaPath, Pattern); + + // If the image has been resized we strip the "_403x328" of the original "/media/1024/koala_403x328.jpg" url. + if (isResized) + { + var underscoreIndex = mediaPath.LastIndexOf('_'); + var dotIndex = mediaPath.LastIndexOf('.'); + umbracoFileValue = string.Concat(mediaPath.Substring(0, underscoreIndex), mediaPath.Substring(dotIndex)); + } + + Func createSql = url => new Sql().Select("*") + .From() + .InnerJoin() + .On(left => left.PropertyTypeId, right => right.Id) + .Where(x => x.Alias == "umbracoFile") + .Where(x => x.VarChar == url); + + var sql = createSql(umbracoFileValue); + + using (var uow = UowProvider.GetUnitOfWork()) + { + var propertyDataDto = uow.Database.Fetch(sql).FirstOrDefault(); + + // If the stripped-down url returns null, we try again with the original url. + // Previously, the function would fail on e.g. "my_x_image.jpg" + if (propertyDataDto == null) + { + sql = createSql(mediaPath); + propertyDataDto = uow.Database.Fetch(sql).FirstOrDefault(); + } + + return propertyDataDto == null ? null : GetById(propertyDataDto.NodeId); + } + } + + /// + /// Checks whether an item has any children + /// + /// Id of the + /// True if the media has any children otherwise False + public bool HasChildren(int id) + { + using (var repository = RepositoryFactory.CreateMediaRepository(UowProvider.GetUnitOfWork())) + { + var query = Query.Builder.Where(x => x.ParentId == id); + int count = repository.Count(query); + return count > 0; + } + } + + /// + /// Moves an object to a new location + /// + /// The to move + /// Id of the Media's new Parent + /// Id of the User moving the Media + public void Move(IMedia media, int parentId, int userId = 0) + { + //TODO: This all needs to be on the repo layer in one transaction! + + if (media == null) throw new ArgumentNullException("media"); + + using (new WriteLock(Locker)) + { + //This ensures that the correct method is called if this method is used to Move to recycle bin. + if (parentId == -21) + { + MoveToRecycleBin(media, userId); + return; + } + + var originalPath = media.Path; + + if (Moving.IsRaisedEventCancelled( + new MoveEventArgs( + new MoveEventInfo(media, originalPath, parentId)), this)) + { + return; + } + + media.ParentId = parentId; + if (media.Trashed) + { + media.ChangeTrashedState(false, parentId); + } + Save(media, userId, + //no events! + false); + + //used to track all the moved entities to be given to the event + var moveInfo = new List> + { + new MoveEventInfo(media, originalPath, parentId) + }; + + //Ensure that relevant properties are updated on children + var children = GetChildren(media.Id).ToArray(); + if (children.Any()) + { + var parentPath = media.Path; + var parentLevel = media.Level; + var parentTrashed = media.Trashed; + var updatedDescendants = UpdatePropertiesOnChildren(children, parentPath, parentLevel, parentTrashed, moveInfo); + Save(updatedDescendants, userId, + //no events! + false); + } + + Moved.RaiseEvent(new MoveEventArgs(false, moveInfo.ToArray()), this); + + Audit(AuditType.Move, "Move Media performed by user", userId, media.Id); + } + } + + /// + /// Deletes an object by moving it to the Recycle Bin + /// + /// The to delete + /// Id of the User deleting the Media + public void MoveToRecycleBin(IMedia media, int userId = 0) + { + ((IMediaServiceOperations)this).MoveToRecycleBin(media, userId); + } + + /// + /// Permanently deletes an object + /// + /// + /// Please note that this method will completely remove the Media from the database, + /// but current not from the file system. + /// + /// The to delete + /// Id of the User deleting the Media + Attempt IMediaServiceOperations.Delete(IMedia media, int userId) + { + //TODO: IT would be much nicer to mass delete all in one trans in the repo level! + var evtMsgs = EventMessagesFactory.Get(); + + if (Deleting.IsRaisedEventCancelled( + new DeleteEventArgs(media, evtMsgs), this)) + { + return OperationStatus.Cancelled(evtMsgs); + } + + //Delete children before deleting the 'possible parent' + var children = GetChildren(media.Id); + foreach (var child in children) + { + Delete(child, userId); + } + + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMediaRepository(uow)) + { + repository.Delete(media); + uow.Commit(); + + var args = new DeleteEventArgs(media, false, evtMsgs); + Deleted.RaiseEvent(args, this); + + //remove any flagged media files + repository.DeleteMediaFiles(args.MediaFilesToDelete); + } + + Audit(AuditType.Delete, "Delete Media performed by user", userId, media.Id); + + return OperationStatus.Success(evtMsgs); + } + + /// + /// Saves a single object + /// + /// The to save + /// Id of the User saving the Media + /// Optional boolean indicating whether or not to raise events. + Attempt IMediaServiceOperations.Save(IMedia media, int userId, bool raiseEvents) + { + var evtMsgs = EventMessagesFactory.Get(); + + if (raiseEvents) + { + if (Saving.IsRaisedEventCancelled( + new SaveEventArgs(media, evtMsgs), + this)) + { + return OperationStatus.Cancelled(evtMsgs); + } + + } + + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMediaRepository(uow)) + { + media.CreatorId = userId; + repository.AddOrUpdate(media); + repository.AddOrUpdateContentXml(media, m => _entitySerializer.Serialize(this, _dataTypeService, _userService, m)); + // generate preview for blame history? + if (UmbracoConfig.For.UmbracoSettings().Content.GlobalPreviewStorageEnabled) + { + repository.AddOrUpdatePreviewXml(media, m => _entitySerializer.Serialize(this, _dataTypeService, _userService, m)); + } + + uow.Commit(); + } + + if (raiseEvents) + Saved.RaiseEvent(new SaveEventArgs(media, false, evtMsgs), this); + + Audit(AuditType.Save, "Save Media performed by user", userId, media.Id); + + return OperationStatus.Success(evtMsgs); + } + + /// + /// Saves a collection of objects + /// + /// Collection of to save + /// Id of the User saving the Media + /// Optional boolean indicating whether or not to raise events. + Attempt IMediaServiceOperations.Save(IEnumerable medias, int userId, bool raiseEvents) + { + var asArray = medias.ToArray(); + var evtMsgs = EventMessagesFactory.Get(); + + if (raiseEvents) + { + if (Saving.IsRaisedEventCancelled( + new SaveEventArgs(asArray, evtMsgs), + this)) + { + return OperationStatus.Cancelled(evtMsgs); + } + } + + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMediaRepository(uow)) + { + foreach (var media in asArray) + { + media.CreatorId = userId; + repository.AddOrUpdate(media); + repository.AddOrUpdateContentXml(media, m => _entitySerializer.Serialize(this, _dataTypeService, _userService, m)); + // generate preview for blame history? + if (UmbracoConfig.For.UmbracoSettings().Content.GlobalPreviewStorageEnabled) + { + repository.AddOrUpdatePreviewXml(media, m => _entitySerializer.Serialize(this, _dataTypeService, _userService, m)); + } + } + + //commit the whole lot in one go + uow.Commit(); + } + + if (raiseEvents) + Saved.RaiseEvent(new SaveEventArgs(asArray, false, evtMsgs), this); + + Audit(AuditType.Save, "Save Media items performed by user", userId, -1); + + return OperationStatus.Success(evtMsgs); + } + + /// + /// Empties the Recycle Bin by deleting all that resides in the bin + /// + public void EmptyRecycleBin() + { + using (new WriteLock(Locker)) + { + Dictionary> entities; + List files; + bool success; + var nodeObjectType = new Guid(Constants.ObjectTypes.Media); + + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMediaRepository(uow)) + { + //Create a dictionary of ids -> dictionary of property aliases + values + entities = repository.GetEntitiesInRecycleBin() + .ToDictionary( + key => key.Id, + val => (IEnumerable)val.Properties); + + files = ((MediaRepository)repository).GetFilesInRecycleBinForUploadField(); + + if (EmptyingRecycleBin.IsRaisedEventCancelled(new RecycleBinEventArgs(nodeObjectType, entities, files), this)) + return; + + success = repository.EmptyRecycleBin(); + + EmptiedRecycleBin.RaiseEvent(new RecycleBinEventArgs(nodeObjectType, entities, files, success), this); + + if (success) + repository.DeleteMediaFiles(files); + } + } + Audit(AuditType.Delete, "Empty Media Recycle Bin performed by user", 0, -21); + } + + /// + /// Deletes all media of specified type. All children of deleted media is moved to Recycle Bin. + /// + /// This needs extra care and attention as its potentially a dangerous and extensive operation + /// Id of the + /// Optional id of the user deleting the media + public void DeleteMediaOfType(int mediaTypeId, int userId = 0) + { + //TODO: This all needs to be done on the repo level in one trans + + using (new WriteLock(Locker)) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMediaRepository(uow)) + { + //NOTE What about media that has the contenttype as part of its composition? + //The ContentType has to be removed from the composition somehow as it would otherwise break + //Dbl.check+test that the ContentType's Id is removed from the ContentType2ContentType table + var query = Query.Builder.Where(x => x.ContentTypeId == mediaTypeId); + var contents = repository.GetByQuery(query).ToArray(); + + if (Deleting.IsRaisedEventCancelled(new DeleteEventArgs(contents), this)) + return; + + foreach (var content in contents.OrderByDescending(x => x.ParentId)) + { + //Look for children of current content and move that to trash before the current content is deleted + var c = content; + var childQuery = Query.Builder.Where(x => x.Path.StartsWith(c.Path)); + var children = repository.GetByQuery(childQuery); + + foreach (var child in children) + { + if (child.ContentType.Id != mediaTypeId) + MoveToRecycleBin(child, userId); + } + + //Permanently delete the content + Delete(content, userId); + } + } + + Audit(AuditType.Delete, "Delete Media items by Type performed by user", userId, -1); + } + } + + /// + /// Deletes an object by moving it to the Recycle Bin + /// + /// The to delete + /// Id of the User deleting the Media + Attempt IMediaServiceOperations.MoveToRecycleBin(IMedia media, int userId) + { + if (media == null) throw new ArgumentNullException("media"); + + var originalPath = media.Path; + + var evtMsgs = EventMessagesFactory.Get(); + + if (Trashing.IsRaisedEventCancelled( + new MoveEventArgs(new MoveEventInfo(media, originalPath, Constants.System.RecycleBinMedia)), this)) + { + return OperationStatus.Cancelled(evtMsgs); + } + + var moveInfo = new List> + { + new MoveEventInfo(media, originalPath, Constants.System.RecycleBinMedia) + }; + + //Find Descendants, which will be moved to the recycle bin along with the parent/grandparent. + var descendants = GetDescendants(media).OrderBy(x => x.Level).ToList(); + + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMediaRepository(uow)) + { + //TODO: This should be part of the repo! + + //Remove 'published' xml from the cmsContentXml table for the unpublished media + uow.Database.Delete("WHERE nodeId = @Id", new { Id = media.Id }); + + media.ChangeTrashedState(true, Constants.System.RecycleBinMedia); + repository.AddOrUpdate(media); + + //Loop through descendants to update their trash state, but ensuring structure by keeping the ParentId + foreach (var descendant in descendants) + { + //Remove 'published' xml from the cmsContentXml table for the unpublished media + uow.Database.Delete("WHERE nodeId = @Id", new { Id = descendant.Id }); + + descendant.ChangeTrashedState(true, descendant.ParentId); + repository.AddOrUpdate(descendant); + + moveInfo.Add(new MoveEventInfo(descendant, descendant.Path, descendant.ParentId)); + } + + uow.Commit(); + } + + Trashed.RaiseEvent( + new MoveEventArgs(false, evtMsgs, moveInfo.ToArray()), this); + + Audit(AuditType.Move, "Move Media to Recycle Bin performed by user", userId, media.Id); + + return OperationStatus.Success(evtMsgs); + } + + /// + /// Permanently deletes an object as well as all of its Children. + /// + /// + /// Please note that this method will completely remove the Media from the database, + /// as well as associated media files from the file system. + /// + /// The to delete + /// Id of the User deleting the Media + public void Delete(IMedia media, int userId = 0) + { + ((IMediaServiceOperations)this).Delete(media, userId); + } + + + + /// + /// Permanently deletes versions from an object prior to a specific date. + /// This method will never delete the latest version of a content item. + /// + /// Id of the object to delete versions from + /// Latest version date + /// Optional Id of the User deleting versions of a Content object + public void DeleteVersions(int id, DateTime versionDate, int userId = 0) + { + if (DeletingVersions.IsRaisedEventCancelled(new DeleteRevisionsEventArgs(id, dateToRetain: versionDate), this)) + return; + + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMediaRepository(uow)) + { + repository.DeleteVersions(id, versionDate); + uow.Commit(); + } + + DeletedVersions.RaiseEvent(new DeleteRevisionsEventArgs(id, false, dateToRetain: versionDate), this); + + Audit(AuditType.Delete, "Delete Media by version date performed by user", userId, -1); + } + + /// + /// Permanently deletes specific version(s) from an object. + /// This method will never delete the latest version of a content item. + /// + /// Id of the object to delete a version from + /// Id of the version to delete + /// Boolean indicating whether to delete versions prior to the versionId + /// Optional Id of the User deleting versions of a Content object + public void DeleteVersion(int id, Guid versionId, bool deletePriorVersions, int userId = 0) + { + if (DeletingVersions.IsRaisedEventCancelled(new DeleteRevisionsEventArgs(id, specificVersion: versionId), this)) + return; + + if (deletePriorVersions) + { + var content = GetByVersion(versionId); + DeleteVersions(id, content.UpdateDate, userId); + } + + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMediaRepository(uow)) + { + repository.DeleteVersion(versionId); + uow.Commit(); + } + + DeletedVersions.RaiseEvent(new DeleteRevisionsEventArgs(id, false, specificVersion: versionId), this); + + Audit(AuditType.Delete, "Delete Media by version performed by user", userId, -1); + } + + /// + /// Saves a single object + /// + /// The to save + /// Id of the User saving the Content + /// Optional boolean indicating whether or not to raise events. + public void Save(IMedia media, int userId = 0, bool raiseEvents = true) + { + ((IMediaServiceOperations)this).Save(media, userId, raiseEvents); + } + + /// + /// Saves a collection of objects + /// + /// Collection of to save + /// Id of the User saving the Content + /// Optional boolean indicating whether or not to raise events. + public void Save(IEnumerable medias, int userId = 0, bool raiseEvents = true) + { + ((IMediaServiceOperations)this).Save(medias, userId, raiseEvents); + } + + /// + /// Sorts a collection of objects by updating the SortOrder according + /// to the ordering of items in the passed in . + /// + /// + /// + /// + /// True if sorting succeeded, otherwise False + public bool Sort(IEnumerable items, int userId = 0, bool raiseEvents = true) + { + var asArray = items.ToArray(); + + if (raiseEvents) + { + if (Saving.IsRaisedEventCancelled(new SaveEventArgs(asArray), this)) + return false; + } + + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMediaRepository(uow)) + { + int i = 0; + foreach (var media in asArray) + { + //If the current sort order equals that of the media + //we don't need to update it, so just increment the sort order + //and continue. + if (media.SortOrder == i) + { + i++; + continue; + } + + media.SortOrder = i; + i++; + + repository.AddOrUpdate(media); + repository.AddOrUpdateContentXml(media, m => _entitySerializer.Serialize(this, _dataTypeService, _userService, m)); + // generate preview for blame history? + if (UmbracoConfig.For.UmbracoSettings().Content.GlobalPreviewStorageEnabled) + { + repository.AddOrUpdatePreviewXml(media, m => _entitySerializer.Serialize(this, _dataTypeService, _userService, m)); + } + } + + uow.Commit(); + } + + if (raiseEvents) + Saved.RaiseEvent(new SaveEventArgs(asArray, false), this); + + Audit(AuditType.Sort, "Sorting Media performed by user", userId, 0); + + return true; + } + + /// + /// Rebuilds all xml content in the cmsContentXml table for all media + /// + /// + /// Only rebuild the xml structures for the content type ids passed in, if none then rebuilds the structures + /// for all media + /// + public void RebuildXmlStructures(params int[] contentTypeIds) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMediaRepository(uow)) + { + repository.RebuildXmlStructures( + media => _entitySerializer.Serialize(this, _dataTypeService, _userService, media), + contentTypeIds: contentTypeIds.Length == 0 ? null : contentTypeIds); + } + + Audit(AuditType.Publish, "MediaService.RebuildXmlStructures completed, the xml has been regenerated in the database", 0, -1); + } + + /// + /// Updates the Path and Level on a collection of objects + /// based on the Parent's Path and Level. Also change the trashed state if relevant. + /// + /// Collection of objects to update + /// Path of the Parent media + /// Level of the Parent media + /// Indicates whether the Parent is trashed or not + /// Used to track the objects to be used in the move event + /// Collection of updated objects + private IEnumerable UpdatePropertiesOnChildren(IEnumerable children, string parentPath, int parentLevel, bool parentTrashed, ICollection> eventInfo) + { + var list = new List(); + foreach (var child in children) + { + var originalPath = child.Path; + child.Path = string.Concat(parentPath, ",", child.Id); + child.Level = parentLevel + 1; + if (parentTrashed != child.Trashed) + { + child.ChangeTrashedState(parentTrashed, child.ParentId); + } + + eventInfo.Add(new MoveEventInfo(child, originalPath, child.ParentId)); + list.Add(child); + + var grandkids = GetChildren(child.Id).ToArray(); + if (grandkids.Any()) + { + list.AddRange(UpdatePropertiesOnChildren(grandkids, child.Path, child.Level, child.Trashed, eventInfo)); + } + } + return list; + } + + //private void CreateAndSaveMediaXml(XElement xml, int id, UmbracoDatabase db) + //{ + // var poco = new ContentXmlDto { NodeId = id, Xml = xml.ToDataString() }; + // var exists = db.FirstOrDefault("WHERE nodeId = @Id", new { Id = id }) != null; + // int result = exists ? db.Update(poco) : Convert.ToInt32(db.Insert(poco)); + //} + + private IMediaType FindMediaTypeByAlias(string mediaTypeAlias) + { + Mandate.ParameterNotNullOrEmpty(mediaTypeAlias, "mediaTypeAlias"); + + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMediaTypeRepository(uow)) + { + var query = Query.Builder.Where(x => x.Alias == mediaTypeAlias); + var mediaTypes = repository.GetByQuery(query); + + if (mediaTypes.Any() == false) + throw new Exception(string.Format("No MediaType matching the passed in Alias: '{0}' was found", + mediaTypeAlias)); + + var mediaType = mediaTypes.First(); + + if (mediaType == null) + throw new Exception(string.Format("MediaType matching the passed in Alias: '{0}' was null", + mediaTypeAlias)); + + return mediaType; + } + } + + private void Audit(AuditType type, string message, int userId, int objectId) + { + var uow = UowProvider.GetUnitOfWork(); + using (var auditRepo = RepositoryFactory.CreateAuditRepository(uow)) + { + auditRepo.AddOrUpdate(new AuditItem(objectId, message, type, userId)); + uow.Commit(); + } + } + + #region Event Handlers + + /// + /// Occurs before Delete + /// + public static event TypedEventHandler DeletingVersions; + + /// + /// Occurs after Delete + /// + public static event TypedEventHandler DeletedVersions; + + /// + /// Occurs before Delete + /// + public static event TypedEventHandler> Deleting; + + /// + /// Occurs after Delete + /// + public static event TypedEventHandler> Deleted; + + /// + /// Occurs before Save + /// + public static event TypedEventHandler> Saving; + + /// + /// Occurs after Save + /// + public static event TypedEventHandler> Saved; + + /// + /// Occurs before Create + /// + [Obsolete("Use the Created event instead, the Creating and Created events both offer the same functionality, Creating event has been deprecated.")] + public static event TypedEventHandler> Creating; + + /// + /// Occurs after Create + /// + /// + /// Please note that the Media object has been created, but not saved + /// so it does not have an identity yet (meaning no Id has been set). + /// + public static event TypedEventHandler> Created; + + /// + /// Occurs before Content is moved to Recycle Bin + /// + public static event TypedEventHandler> Trashing; + + /// + /// Occurs after Content is moved to Recycle Bin + /// + public static event TypedEventHandler> Trashed; + + /// + /// Occurs before Move + /// + public static event TypedEventHandler> Moving; + + /// + /// Occurs after Move + /// + public static event TypedEventHandler> Moved; + + /// + /// Occurs before the Recycle Bin is emptied + /// + public static event TypedEventHandler EmptyingRecycleBin; + + /// + /// Occurs after the Recycle Bin has been Emptied + /// + public static event TypedEventHandler EmptiedRecycleBin; + #endregion + } } diff --git a/src/Umbraco.Core/Services/MemberService.cs b/src/Umbraco.Core/Services/MemberService.cs index 4f0647dbf7..6216bdfbe9 100644 --- a/src/Umbraco.Core/Services/MemberService.cs +++ b/src/Umbraco.Core/Services/MemberService.cs @@ -21,1296 +21,1296 @@ using Umbraco.Core.Security; namespace Umbraco.Core.Services { - /// - /// Represents the MemberService. - /// - public class MemberService : RepositoryService, IMemberService - { - private readonly IMemberGroupService _memberGroupService; - private readonly EntityXmlSerializer _entitySerializer = new EntityXmlSerializer(); - private readonly IDataTypeService _dataTypeService; - private static readonly ReaderWriterLockSlim Locker = new ReaderWriterLockSlim(); - - public MemberService(IDatabaseUnitOfWorkProvider provider, RepositoryFactory repositoryFactory, ILogger logger, IEventMessagesFactory eventMessagesFactory, IMemberGroupService memberGroupService, IDataTypeService dataTypeService) - : base(provider, repositoryFactory, logger, eventMessagesFactory) - { - if (memberGroupService == null) throw new ArgumentNullException("memberGroupService"); - if (dataTypeService == null) throw new ArgumentNullException("dataTypeService"); - _memberGroupService = memberGroupService; - _dataTypeService = dataTypeService; - } - - #region IMemberService Implementation - - /// - /// Gets the default MemberType alias - /// - /// By default we'll return the 'writer', but we need to check it exists. If it doesn't we'll - /// return the first type that is not an admin, otherwise if there's only one we will return that one. - /// Alias of the default MemberType - public string GetDefaultMemberType() - { - using (var repository = RepositoryFactory.CreateMemberTypeRepository(UowProvider.GetUnitOfWork())) - { - var types = repository.GetAll(new int[]{}).Select(x => x.Alias).ToArray(); - - if (types.Any() == false) - { - throw new InvalidOperationException("No member types could be resolved"); - } - - if (types.InvariantContains("Member")) - { - return types.First(x => x.InvariantEquals("Member")); - } - - return types.First(); - } - } - - /// - /// Checks if a Member with the username exists - /// - /// Username to check - /// True if the Member exists otherwise False - public bool Exists(string username) - { - using (var repository = RepositoryFactory.CreateMemberRepository(UowProvider.GetUnitOfWork())) - { - return repository.Exists(username); - } - } - - /// - /// This is simply a helper method which essentially just wraps the MembershipProvider's ChangePassword method - /// - /// This method exists so that Umbraco developers can use one entry point to create/update - /// Members if they choose to. - /// The Member to save the password for - /// The password to encrypt and save - public void SavePassword(IMember member, string password) - { - if (member == null) throw new ArgumentNullException("member"); - - var provider = MembershipProviderExtensions.GetMembersMembershipProvider(); - if (provider.IsUmbracoMembershipProvider()) - { - provider.ChangePassword(member.Username, "", password); - } - else - { - throw new NotSupportedException("When using a non-Umbraco membership provider you must change the member password by using the MembershipProvider.ChangePassword method"); - } - - //go re-fetch the member and update the properties that may have changed - var result = GetByUsername(member.Username); - - //should never be null but it could have been deleted by another thread. - if (result == null) - return; - - member.RawPasswordValue = result.RawPasswordValue; - member.LastPasswordChangeDate = result.LastPasswordChangeDate; - member.UpdateDate = result.UpdateDate; - } - - /// - /// Checks if a Member with the id exists - /// - /// Id of the Member - /// True if the Member exists otherwise False - public bool Exists(int id) - { - using (var repository = RepositoryFactory.CreateMemberRepository(UowProvider.GetUnitOfWork())) - { - return repository.Exists(id); - } - } - - /// - /// Gets a Member by its integer id - /// - /// Id - /// - public IMember GetById(int id) - { - using (var repository = RepositoryFactory.CreateMemberRepository(UowProvider.GetUnitOfWork())) - { - return repository.Get(id); - } - } - - /// - /// Gets a Member by the unique key - /// - /// The guid key corresponds to the unique id in the database - /// and the user id in the membership provider. - /// Id - /// - public IMember GetByKey(Guid id) - { - using (var repository = RepositoryFactory.CreateMemberRepository(UowProvider.GetUnitOfWork())) - { - var query = Query.Builder.Where(x => x.Key == id); - var member = repository.GetByQuery(query).FirstOrDefault(); - return member; - } - } - - /// - /// Gets all Members for the specified MemberType alias - /// - /// Alias of the MemberType - /// - public IEnumerable GetMembersByMemberType(string memberTypeAlias) - { - using (var repository = RepositoryFactory.CreateMemberRepository(UowProvider.GetUnitOfWork())) - { - var query = Query.Builder.Where(x => x.ContentTypeAlias == memberTypeAlias); - var members = repository.GetByQuery(query); - return members; - } - } - - /// - /// Gets all Members for the MemberType id - /// - /// Id of the MemberType - /// - public IEnumerable GetMembersByMemberType(int memberTypeId) - { - using (var repository = RepositoryFactory.CreateMemberRepository(UowProvider.GetUnitOfWork())) - { - repository.Get(memberTypeId); - var query = Query.Builder.Where(x => x.ContentTypeId == memberTypeId); - var members = repository.GetByQuery(query); - return members; - } - } - - /// - /// Gets all Members within the specified MemberGroup name - /// - /// Name of the MemberGroup - /// - public IEnumerable GetMembersByGroup(string memberGroupName) - { - using (var repository = RepositoryFactory.CreateMemberRepository(UowProvider.GetUnitOfWork())) - { - return repository.GetByMemberGroup(memberGroupName); - } - } - - /// - /// Gets all Members with the ids specified - /// - /// If no Ids are specified all Members will be retrieved - /// Optional list of Member Ids - /// - public IEnumerable GetAllMembers(params int[] ids) - { - using (var repository = RepositoryFactory.CreateMemberRepository(UowProvider.GetUnitOfWork())) - { - return repository.GetAll(ids); - } - } - - /// - /// Delete Members of the specified MemberType id - /// - /// Id of the MemberType - public void DeleteMembersOfType(int memberTypeId) - { - using (new WriteLock(Locker)) - { - using (var uow = UowProvider.GetUnitOfWork()) - { - var repository = RepositoryFactory.CreateMemberRepository(uow); - //TODO: What about content that has the contenttype as part of its composition? - var query = Query.Builder.Where(x => x.ContentTypeId == memberTypeId); - var members = repository.GetByQuery(query).ToArray(); - - if (Deleting.IsRaisedEventCancelled(new DeleteEventArgs(members), this)) - return; - - foreach (var member in members) - { - //Permantly delete the member - Delete(member); - } - } - } - } - - [Obsolete("Use the overload with 'long' parameter types instead")] - [EditorBrowsable(EditorBrowsableState.Never)] - public IEnumerable FindMembersByDisplayName(string displayNameToMatch, int pageIndex, int pageSize, out int totalRecords, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith) - { - long total; - var result = FindMembersByDisplayName(displayNameToMatch, Convert.ToInt64(pageIndex), pageSize, out total, matchType); - totalRecords = Convert.ToInt32(total); - return result; - } - - /// - /// Finds Members based on their display name - /// - /// Display name to match - /// Current page index - /// Size of the page - /// Total number of records found (out) - /// The type of match to make as . Default is - /// - public IEnumerable FindMembersByDisplayName(string displayNameToMatch, long pageIndex, int pageSize, out long totalRecords, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith) - { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMemberRepository(uow)) - { - var query = new Query(); - - switch (matchType) - { - case StringPropertyMatchType.Exact: - query.Where(member => member.Name.Equals(displayNameToMatch)); - break; - case StringPropertyMatchType.Contains: - query.Where(member => member.Name.Contains(displayNameToMatch)); - break; - case StringPropertyMatchType.StartsWith: - query.Where(member => member.Name.StartsWith(displayNameToMatch)); - break; - case StringPropertyMatchType.EndsWith: - query.Where(member => member.Name.EndsWith(displayNameToMatch)); - break; - case StringPropertyMatchType.Wildcard: - query.Where(member => member.Name.SqlWildcard(displayNameToMatch, TextColumnType.NVarchar)); - break; - default: - throw new ArgumentOutOfRangeException("matchType"); - } - - return repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, "Name", Direction.Ascending); - } - } - - [Obsolete("Use the overload with 'long' parameter types instead")] - [EditorBrowsable(EditorBrowsableState.Never)] - public IEnumerable FindByEmail(string emailStringToMatch, int pageIndex, int pageSize, out int totalRecords, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith) - { - long total; - var result = FindByEmail(emailStringToMatch, Convert.ToInt64(pageIndex), pageSize, out total, matchType); - totalRecords = Convert.ToInt32(total); - return result; - } - - /// - /// Finds a list of objects by a partial email string - /// - /// Partial email string to match - /// Current page index - /// Size of the page - /// Total number of records found (out) - /// The type of match to make as . Default is - /// - public IEnumerable FindByEmail(string emailStringToMatch, long pageIndex, int pageSize, out long totalRecords, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith) - { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMemberRepository(uow)) - { - var query = new Query(); - - switch (matchType) - { - case StringPropertyMatchType.Exact: - query.Where(member => member.Email.Equals(emailStringToMatch)); - break; - case StringPropertyMatchType.Contains: - query.Where(member => member.Email.Contains(emailStringToMatch)); - break; - case StringPropertyMatchType.StartsWith: - query.Where(member => member.Email.StartsWith(emailStringToMatch)); - break; - case StringPropertyMatchType.EndsWith: - query.Where(member => member.Email.EndsWith(emailStringToMatch)); - break; - case StringPropertyMatchType.Wildcard: - query.Where(member => member.Email.SqlWildcard(emailStringToMatch, TextColumnType.NVarchar)); - break; - default: - throw new ArgumentOutOfRangeException("matchType"); - } - - return repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, "Email", Direction.Ascending); - } - } - - [Obsolete("Use the overload with 'long' parameter types instead")] - [EditorBrowsable(EditorBrowsableState.Never)] - public IEnumerable FindByUsername(string login, int pageIndex, int pageSize, out int totalRecords, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith) - { - long total; - var result = FindByUsername(login, Convert.ToInt64(pageIndex), pageSize, out total, matchType); - totalRecords = Convert.ToInt32(total); - return result; - } - - /// - /// Finds a list of objects by a partial username - /// - /// Partial username to match - /// Current page index - /// Size of the page - /// Total number of records found (out) - /// The type of match to make as . Default is - /// - public IEnumerable FindByUsername(string login, long pageIndex, int pageSize, out long totalRecords, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith) - { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMemberRepository(uow)) - { - var query = new Query(); - - switch (matchType) - { - case StringPropertyMatchType.Exact: - query.Where(member => member.Username.Equals(login)); - break; - case StringPropertyMatchType.Contains: - query.Where(member => member.Username.Contains(login)); - break; - case StringPropertyMatchType.StartsWith: - query.Where(member => member.Username.StartsWith(login)); - break; - case StringPropertyMatchType.EndsWith: - query.Where(member => member.Username.EndsWith(login)); - break; - case StringPropertyMatchType.Wildcard: - query.Where(member => member.Email.SqlWildcard(login, TextColumnType.NVarchar)); - break; - default: - throw new ArgumentOutOfRangeException("matchType"); - } - - return repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, "LoginName", Direction.Ascending); - } - } - - /// - /// Gets a list of Members based on a property search - /// - /// Alias of the PropertyType to search for - /// Value to match - /// The type of match to make as . Default is - /// - public IEnumerable GetMembersByPropertyValue(string propertyTypeAlias, string value, StringPropertyMatchType matchType = StringPropertyMatchType.Exact) - { - using (var repository = RepositoryFactory.CreateMemberRepository(UowProvider.GetUnitOfWork())) - { - IQuery query; - - switch (matchType) - { - case StringPropertyMatchType.Exact: - query = - Query.Builder.Where( - x => - ((Member)x).PropertyTypeAlias == propertyTypeAlias && - (((Member)x).LongStringPropertyValue.SqlEquals(value, TextColumnType.NText) || - ((Member)x).ShortStringPropertyValue.SqlEquals(value, TextColumnType.NVarchar))); - break; - case StringPropertyMatchType.Contains: - query = - Query.Builder.Where( - x => - ((Member)x).PropertyTypeAlias == propertyTypeAlias && - (((Member)x).LongStringPropertyValue.SqlContains(value, TextColumnType.NText) || - ((Member)x).ShortStringPropertyValue.SqlContains(value, TextColumnType.NVarchar))); - break; - case StringPropertyMatchType.StartsWith: - query = - Query.Builder.Where( - x => - ((Member)x).PropertyTypeAlias == propertyTypeAlias && - (((Member)x).LongStringPropertyValue.SqlStartsWith(value, TextColumnType.NText) || - ((Member)x).ShortStringPropertyValue.SqlStartsWith(value, TextColumnType.NVarchar))); - break; - case StringPropertyMatchType.EndsWith: - query = - Query.Builder.Where( - x => - ((Member)x).PropertyTypeAlias == propertyTypeAlias && - (((Member)x).LongStringPropertyValue.SqlEndsWith(value, TextColumnType.NText) || - ((Member)x).ShortStringPropertyValue.SqlEndsWith(value, TextColumnType.NVarchar))); - break; - default: - throw new ArgumentOutOfRangeException("matchType"); - } - - var members = repository.GetByQuery(query); - return members; - } - } - - /// - /// Gets a list of Members based on a property search - /// - /// Alias of the PropertyType to search for - /// Value to match - /// The type of match to make as . Default is - /// - public IEnumerable GetMembersByPropertyValue(string propertyTypeAlias, int value, ValuePropertyMatchType matchType = ValuePropertyMatchType.Exact) - { - using (var repository = RepositoryFactory.CreateMemberRepository(UowProvider.GetUnitOfWork())) - { - IQuery query; - - switch (matchType) - { - case ValuePropertyMatchType.Exact: - query = - Query.Builder.Where( - x => - ((Member)x).PropertyTypeAlias == propertyTypeAlias && - ((Member)x).IntegerPropertyValue == value); - break; - case ValuePropertyMatchType.GreaterThan: - query = - Query.Builder.Where( - x => - ((Member)x).PropertyTypeAlias == propertyTypeAlias && - ((Member)x).IntegerPropertyValue > value); - break; - case ValuePropertyMatchType.LessThan: - query = - Query.Builder.Where( - x => - ((Member)x).PropertyTypeAlias == propertyTypeAlias && - ((Member)x).IntegerPropertyValue < value); - break; - case ValuePropertyMatchType.GreaterThanOrEqualTo: - query = - Query.Builder.Where( - x => - ((Member)x).PropertyTypeAlias == propertyTypeAlias && - ((Member)x).IntegerPropertyValue >= value); - break; - case ValuePropertyMatchType.LessThanOrEqualTo: - query = - Query.Builder.Where( - x => - ((Member)x).PropertyTypeAlias == propertyTypeAlias && - ((Member)x).IntegerPropertyValue <= value); - break; - default: - throw new ArgumentOutOfRangeException("matchType"); - } - - var members = repository.GetByQuery(query); - return members; - } - } - - /// - /// Gets a list of Members based on a property search - /// - /// Alias of the PropertyType to search for - /// Value to match - /// - public IEnumerable GetMembersByPropertyValue(string propertyTypeAlias, bool value) - { - using (var repository = RepositoryFactory.CreateMemberRepository(UowProvider.GetUnitOfWork())) - { - var query = - Query.Builder.Where( - x => - ((Member)x).PropertyTypeAlias == propertyTypeAlias && - ((Member)x).BoolPropertyValue == value); - - var members = repository.GetByQuery(query); - return members; - } - } - - /// - /// Gets a list of Members based on a property search - /// - /// Alias of the PropertyType to search for - /// Value to match - /// The type of match to make as . Default is - /// - public IEnumerable GetMembersByPropertyValue(string propertyTypeAlias, DateTime value, ValuePropertyMatchType matchType = ValuePropertyMatchType.Exact) - { - using (var repository = RepositoryFactory.CreateMemberRepository(UowProvider.GetUnitOfWork())) - { - IQuery query; - - switch (matchType) - { - case ValuePropertyMatchType.Exact: - query = - Query.Builder.Where( - x => - ((Member)x).PropertyTypeAlias == propertyTypeAlias && - ((Member)x).DateTimePropertyValue == value); - break; - case ValuePropertyMatchType.GreaterThan: - query = - Query.Builder.Where( - x => - ((Member)x).PropertyTypeAlias == propertyTypeAlias && - ((Member)x).DateTimePropertyValue > value); - break; - case ValuePropertyMatchType.LessThan: - query = - Query.Builder.Where( - x => - ((Member)x).PropertyTypeAlias == propertyTypeAlias && - ((Member)x).DateTimePropertyValue < value); - break; - case ValuePropertyMatchType.GreaterThanOrEqualTo: - query = - Query.Builder.Where( - x => - ((Member)x).PropertyTypeAlias == propertyTypeAlias && - ((Member)x).DateTimePropertyValue >= value); - break; - case ValuePropertyMatchType.LessThanOrEqualTo: - query = - Query.Builder.Where( - x => - ((Member)x).PropertyTypeAlias == propertyTypeAlias && - ((Member)x).DateTimePropertyValue <= value); - break; - default: - throw new ArgumentOutOfRangeException("matchType"); - } - - //TODO: Since this is by property value, we need a GetByPropertyQuery on the repo! - var members = repository.GetByQuery(query); - return members; - } - } - - /// - /// Rebuilds all xml content in the cmsContentXml table for all members - /// - /// - /// Only rebuild the xml structures for the content type ids passed in, if none then rebuilds the structures - /// for all members = USE WITH CARE! - /// - /// True if publishing succeeded, otherwise False - public void RebuildXmlStructures(params int[] memberTypeIds) - { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMemberRepository(uow)) - { - repository.RebuildXmlStructures( - member => _entitySerializer.Serialize(_dataTypeService, member), - contentTypeIds: memberTypeIds.Length == 0 ? null : memberTypeIds); - } - - Audit(AuditType.Publish, "MemberService.RebuildXmlStructures completed, the xml has been regenerated in the database", 0, -1); - } - - #endregion - - #region IMembershipMemberService Implementation - - /// - /// Gets the total number of Members based on the count type - /// - /// - /// The way the Online count is done is the same way that it is done in the MS SqlMembershipProvider - We query for any members - /// that have their last active date within the Membership.UserIsOnlineTimeWindow (which is in minutes). It isn't exact science - /// but that is how MS have made theirs so we'll follow that principal. - /// - /// to count by - /// with number of Members for passed in type - public int GetCount(MemberCountType countType) - { - using (var repository = RepositoryFactory.CreateMemberRepository(UowProvider.GetUnitOfWork())) - { - IQuery query; - - switch (countType) - { - case MemberCountType.All: - query = new Query(); - return repository.Count(query); - case MemberCountType.Online: - var fromDate = DateTime.Now.AddMinutes(-Membership.UserIsOnlineTimeWindow); - query = - Query.Builder.Where( - x => - ((Member)x).PropertyTypeAlias == Constants.Conventions.Member.LastLoginDate && - ((Member)x).DateTimePropertyValue > fromDate); - return repository.GetCountByQuery(query); - case MemberCountType.LockedOut: - query = - Query.Builder.Where( - x => - ((Member)x).PropertyTypeAlias == Constants.Conventions.Member.IsLockedOut && - ((Member)x).BoolPropertyValue == true); - return repository.GetCountByQuery(query); - case MemberCountType.Approved: - query = - Query.Builder.Where( - x => - ((Member)x).PropertyTypeAlias == Constants.Conventions.Member.IsApproved && - ((Member)x).BoolPropertyValue == true); - return repository.GetCountByQuery(query); - default: - throw new ArgumentOutOfRangeException("countType"); - } - } - - } - - [Obsolete("Use the overload with 'long' parameter types instead")] - [EditorBrowsable(EditorBrowsableState.Never)] - public IEnumerable GetAll(int pageIndex, int pageSize, out int totalRecords) - { - long total; - var result = GetAll(Convert.ToInt64(pageIndex), pageSize, out total); - totalRecords = Convert.ToInt32(total); - return result; - } - - /// - /// Gets a list of paged objects - /// - /// Current page index - /// Size of the page - /// Total number of records found (out) - /// - public IEnumerable GetAll(long pageIndex, int pageSize, out long totalRecords) - { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMemberRepository(uow)) - { - return repository.GetPagedResultsByQuery(null, pageIndex, pageSize, out totalRecords, "LoginName", Direction.Ascending); - } - } - - [Obsolete("Use the overload with 'long' parameter types instead")] - [EditorBrowsable(EditorBrowsableState.Never)] - public IEnumerable GetAll(int pageIndex, int pageSize, out int totalRecords, - string orderBy, Direction orderDirection, string memberTypeAlias = null, string filter = "") - { - long total; - var result = GetAll(Convert.ToInt64(pageIndex), pageSize, out total, orderBy, orderDirection, memberTypeAlias, filter); - totalRecords = Convert.ToInt32(total); - return result; - } - - public IEnumerable GetAll(long pageIndex, int pageSize, out long totalRecords, - string orderBy, Direction orderDirection, string memberTypeAlias = null, string filter = "") - { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMemberRepository(uow)) - { - if (memberTypeAlias == null) - { - return repository.GetPagedResultsByQuery(null, pageIndex, pageSize, out totalRecords, orderBy, orderDirection, filter); - } - var query = new Query().Where(x => x.ContentTypeAlias == memberTypeAlias); - return repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, orderBy, orderDirection, filter); - } - } - - /// - /// Gets the count of Members by an optional MemberType alias - /// - /// If no alias is supplied then the count for all Member will be returned - /// Optional alias for the MemberType when counting number of Members - /// with number of Members - public int Count(string memberTypeAlias = null) - { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMemberRepository(uow)) - { - return repository.Count(memberTypeAlias); - } - } - - /// - /// Creates an object without persisting it - /// - /// This method is convenient for when you need to add properties to a new Member - /// before persisting it in order to limit the amount of times its saved. - /// Also note that the returned will not have an Id until its saved. - /// Username of the Member to create - /// Email of the Member to create - /// Name of the Member to create - /// Alias of the MemberType the Member should be based on - /// - public IMember CreateMember(string username, string email, string name, string memberTypeAlias) - { - var memberType = FindMemberTypeByAlias(memberTypeAlias); - return CreateMember(username, email, name, memberType); - } - - /// - /// Creates an object without persisting it - /// - /// This method is convenient for when you need to add properties to a new Member - /// before persisting it in order to limit the amount of times its saved. - /// Also note that the returned will not have an Id until its saved. - /// Username of the Member to create - /// Email of the Member to create - /// Name of the Member to create - /// MemberType the Member should be based on - /// - public IMember CreateMember(string username, string email, string name, IMemberType memberType) - { - var member = new Member(name, email.ToLower().Trim(), username, memberType); - - Created.RaiseEvent(new NewEventArgs(member, false, memberType.Alias, -1), this); - - return member; - } - - /// - /// Creates and persists a Member - /// - /// Using this method will persist the Member object before its returned - /// meaning that it will have an Id available (unlike the CreateMember method) - /// Username of the Member to create - /// Email of the Member to create - /// Name of the Member to create - /// Alias of the MemberType the Member should be based on - /// - public IMember CreateMemberWithIdentity(string username, string email, string name, string memberTypeAlias) - { - var memberType = FindMemberTypeByAlias(memberTypeAlias); - return CreateMemberWithIdentity(username, email, name, memberType); - } - - /// - /// Creates and persists a Member - /// - /// Using this method will persist the Member object before its returned - /// meaning that it will have an Id available (unlike the CreateMember method) - /// Username of the Member to create - /// Email of the Member to create - /// MemberType the Member should be based on - /// - public IMember CreateMemberWithIdentity(string username, string email, IMemberType memberType) - { - return CreateMemberWithIdentity(username, email, username, memberType); - } - - /// - /// Creates and persists a Member - /// - /// Using this method will persist the Member object before its returned - /// meaning that it will have an Id available (unlike the CreateMember method) - /// Username of the Member to create - /// Email of the Member to create - /// Name of the Member to create - /// MemberType the Member should be based on - /// - public IMember CreateMemberWithIdentity(string username, string email, string name, IMemberType memberType) - { - return CreateMemberWithIdentity(username, email, name, "", memberType); - } - - /// - /// Creates and persists a new - /// - /// An can be of type or - /// Username of the to create - /// Email of the to create - /// This value should be the encoded/encrypted/hashed value for the password that will be stored in the database - /// Alias of the Type - /// - IMember IMembershipMemberService.CreateWithIdentity(string username, string email, string passwordValue, string memberTypeAlias) - { - var memberType = FindMemberTypeByAlias(memberTypeAlias); - return CreateMemberWithIdentity(username, email, username, passwordValue, memberType); - } - - /// - /// Creates and persists a Member - /// - /// Using this method will persist the Member object before its returned - /// meaning that it will have an Id available (unlike the CreateMember method) - /// Username of the Member to create - /// Email of the Member to create - /// Name of the Member to create - /// This value should be the encoded/encrypted/hashed value for the password that will be stored in the database - /// MemberType the Member should be based on - /// - private IMember CreateMemberWithIdentity(string username, string email, string name, string passwordValue, IMemberType memberType) - { - if (memberType == null) throw new ArgumentNullException("memberType"); - - var member = new Member(name, email.ToLower().Trim(), username, passwordValue, memberType); - - if (Saving.IsRaisedEventCancelled(new SaveEventArgs(member), this)) - { - member.WasCancelled = true; - return member; - } - - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMemberRepository(uow)) - { - repository.AddOrUpdate(member); - //insert the xml - repository.AddOrUpdateContentXml(member, m => _entitySerializer.Serialize(_dataTypeService, m)); - // generate preview for blame history? - if (UmbracoConfig.For.UmbracoSettings().Content.GlobalPreviewStorageEnabled) - { - repository.AddOrUpdatePreviewXml(member, m => _entitySerializer.Serialize(_dataTypeService, m)); - } - - uow.Commit(); - } - - Saved.RaiseEvent(new SaveEventArgs(member, false), this); - Created.RaiseEvent(new NewEventArgs(member, false, memberType.Alias, -1), this); - - return member; - } - - /// - /// Gets an by its provider key - /// - /// Id to use for retrieval - /// - public IMember GetByProviderKey(object id) - { - var asGuid = id.TryConvertTo(); - if (asGuid.Success) - { - return GetByKey((Guid)id); - } - var asInt = id.TryConvertTo(); - if (asInt.Success) - { - return GetById((int)id); - } - - return null; - } - - /// - /// Get an by email - /// - /// Email to use for retrieval - /// - public IMember GetByEmail(string email) - { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMemberRepository(uow)) - { - var query = Query.Builder.Where(x => x.Email.Equals(email)); - var member = repository.GetByQuery(query).FirstOrDefault(); - - return member; - } - } - - /// - /// Get an by username - /// - /// Username to use for retrieval - /// - public IMember GetByUsername(string username) - { - //TODO: Somewhere in here, whether at this level or the repository level, we need to add - // a caching mechanism since this method is used by all the membership providers and could be - // called quite a bit when dealing with members. - - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMemberRepository(uow)) - { - var query = Query.Builder.Where(x => x.Username.Equals(username)); - var member = repository.GetByQuery(query).FirstOrDefault(); - - return member; - } - } - - /// - /// Deletes an - /// - /// to Delete - public void Delete(IMember member) - { - if (Deleting.IsRaisedEventCancelled(new DeleteEventArgs(member), this)) - return; - - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMemberRepository(uow)) - { - repository.Delete(member); - uow.Commit(); - - var args = new DeleteEventArgs(member, false); - Deleted.RaiseEvent(args, this); - - //remove any flagged media files - repository.DeleteMediaFiles(args.MediaFilesToDelete); - } - } - - /// - /// Saves an - /// - /// to Save - /// Optional parameter to raise events. - /// Default is True otherwise set to False to not raise events - public void Save(IMember entity, bool raiseEvents = true) - { - if (raiseEvents) - { - if (Saving.IsRaisedEventCancelled(new SaveEventArgs(entity), this)) - { - return; - } - } - - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMemberRepository(uow)) - { - repository.AddOrUpdate(entity); - repository.AddOrUpdateContentXml(entity, m => _entitySerializer.Serialize(_dataTypeService, m)); - // generate preview for blame history? - if (UmbracoConfig.For.UmbracoSettings().Content.GlobalPreviewStorageEnabled) - { - repository.AddOrUpdatePreviewXml(entity, m => _entitySerializer.Serialize(_dataTypeService, m)); - } - - uow.Commit(); - } - - if (raiseEvents) - Saved.RaiseEvent(new SaveEventArgs(entity, false), this); - } - - /// - /// Saves a list of objects - /// - /// to save - /// Optional parameter to raise events. - /// Default is True otherwise set to False to not raise events - public void Save(IEnumerable entities, bool raiseEvents = true) - { - var asArray = entities.ToArray(); - - if (raiseEvents) - { - if (Saving.IsRaisedEventCancelled(new SaveEventArgs(asArray), this)) - return; - } - using (new WriteLock(Locker)) - { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMemberRepository(uow)) - { - foreach (var member in asArray) - { - repository.AddOrUpdate(member); - repository.AddOrUpdateContentXml(member, m => _entitySerializer.Serialize(_dataTypeService, m)); - // generate preview for blame history? - if (UmbracoConfig.For.UmbracoSettings().Content.GlobalPreviewStorageEnabled) - { - repository.AddOrUpdatePreviewXml(member, m => _entitySerializer.Serialize(_dataTypeService, m)); - } - } - - //commit the whole lot in one go - uow.Commit(); - } - - if (raiseEvents) - Saved.RaiseEvent(new SaveEventArgs(asArray, false), this); - } - } - - #endregion - - #region IMembershipRoleService Implementation - - public void AddRole(string roleName) - { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMemberGroupRepository(uow)) - { - repository.CreateIfNotExists(roleName); - } - } - - public IEnumerable GetAllRoles() - { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMemberGroupRepository(uow)) - { - var result = repository.GetAll(); - return result.Select(x => x.Name).Distinct(); - } - } - - public IEnumerable GetAllRoles(int memberId) - { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMemberGroupRepository(uow)) - { - var result = repository.GetMemberGroupsForMember(memberId); - return result.Select(x => x.Name).Distinct(); - } - } - - public IEnumerable GetAllRoles(string username) - { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMemberGroupRepository(uow)) - { - var result = repository.GetMemberGroupsForMember(username); - return result.Select(x => x.Name).Distinct(); - } - } - - public IEnumerable GetMembersInRole(string roleName) - { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMemberRepository(uow)) - { - return repository.GetByMemberGroup(roleName); - } - } - - public IEnumerable FindMembersInRole(string roleName, string usernameToMatch, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith) - { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMemberRepository(uow)) - { - return repository.FindMembersInRole(roleName, usernameToMatch, matchType); - } - } - - public bool DeleteRole(string roleName, bool throwIfBeingUsed) - { - using (new WriteLock(Locker)) - { - if (throwIfBeingUsed) - { - var inRole = GetMembersInRole(roleName); - if (inRole.Any()) - { - throw new InvalidOperationException("The role " + roleName + " is currently assigned to members"); - } - } - - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMemberGroupRepository(uow)) - { - var qry = new Query().Where(g => g.Name == roleName); - var found = repository.GetByQuery(qry).ToArray(); - - foreach (var memberGroup in found) - { - _memberGroupService.Delete(memberGroup); - } - return found.Any(); - } - } - } - public void AssignRole(string username, string roleName) - { - AssignRoles(new[] { username }, new[] { roleName }); - } - - public void AssignRoles(string[] usernames, string[] roleNames) - { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMemberGroupRepository(uow)) - { - repository.AssignRoles(usernames, roleNames); - } - } - - public void DissociateRole(string username, string roleName) - { - DissociateRoles(new[] { username }, new[] { roleName }); - } - - public void DissociateRoles(string[] usernames, string[] roleNames) - { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMemberGroupRepository(uow)) - { - repository.DissociateRoles(usernames, roleNames); - } - } - - public void AssignRole(int memberId, string roleName) - { - AssignRoles(new[] { memberId }, new[] { roleName }); - } - - public void AssignRoles(int[] memberIds, string[] roleNames) - { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMemberGroupRepository(uow)) - { - repository.AssignRoles(memberIds, roleNames); - } - } - - public void DissociateRole(int memberId, string roleName) - { - DissociateRoles(new[] { memberId }, new[] { roleName }); - } - - public void DissociateRoles(int[] memberIds, string[] roleNames) - { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMemberGroupRepository(uow)) - { - repository.DissociateRoles(memberIds, roleNames); - } - } - - - - #endregion - - private IMemberType FindMemberTypeByAlias(string memberTypeAlias) - { - using (var repository = RepositoryFactory.CreateMemberTypeRepository(UowProvider.GetUnitOfWork())) - { - var query = Query.Builder.Where(x => x.Alias == memberTypeAlias); - var types = repository.GetByQuery(query); - - if (types.Any() == false) - throw new Exception( - string.Format("No MemberType matching the passed in Alias: '{0}' was found", - memberTypeAlias)); - - var contentType = types.First(); - - if (contentType == null) - throw new Exception(string.Format("MemberType matching the passed in Alias: '{0}' was null", - memberTypeAlias)); - - return contentType; - } - } - - private void Audit(AuditType type, string message, int userId, int objectId) - { - var uow = UowProvider.GetUnitOfWork(); - using (var auditRepo = RepositoryFactory.CreateAuditRepository(uow)) - { - auditRepo.AddOrUpdate(new AuditItem(objectId, message, type, userId)); - uow.Commit(); - } - } - - #region Event Handlers - - /// - /// Occurs before Delete - /// - public static event TypedEventHandler> Deleting; - - /// - /// Occurs after Delete - /// - public static event TypedEventHandler> Deleted; - - /// - /// Occurs before Save - /// - public static event TypedEventHandler> Saving; - - /// - /// Occurs after Create - /// - /// - /// Please note that the Member object has been created, but might not have been saved - /// so it does not have an identity yet (meaning no Id has been set). - /// - public static event TypedEventHandler> Created; - - /// - /// Occurs after Save - /// - public static event TypedEventHandler> Saved; - - #endregion - - /// - /// A helper method that will create a basic/generic member for use with a generic membership provider - /// - /// - internal static IMember CreateGenericMembershipProviderMember(string name, string email, string username, string password) - { - var identity = int.MaxValue; - - var memType = new MemberType(-1); - var propGroup = new PropertyGroup - { - Name = "Membership", - Id = --identity - }; - propGroup.PropertyTypes.Add(new PropertyType(Constants.PropertyEditors.TextboxAlias, DataTypeDatabaseType.Ntext, Constants.Conventions.Member.Comments) - { - Name = Constants.Conventions.Member.CommentsLabel, - SortOrder = 0, - Id = --identity, - Key = identity.ToGuid() - }); - propGroup.PropertyTypes.Add(new PropertyType(Constants.PropertyEditors.TrueFalseAlias, DataTypeDatabaseType.Integer, Constants.Conventions.Member.IsApproved) - { - Name = Constants.Conventions.Member.IsApprovedLabel, - SortOrder = 3, - Id = --identity, - Key = identity.ToGuid() - }); - propGroup.PropertyTypes.Add(new PropertyType(Constants.PropertyEditors.TrueFalseAlias, DataTypeDatabaseType.Integer, Constants.Conventions.Member.IsLockedOut) - { - Name = Constants.Conventions.Member.IsLockedOutLabel, - SortOrder = 4, - Id = --identity, - Key = identity.ToGuid() - }); - propGroup.PropertyTypes.Add(new PropertyType(Constants.PropertyEditors.NoEditAlias, DataTypeDatabaseType.Date, Constants.Conventions.Member.LastLockoutDate) - { - Name = Constants.Conventions.Member.LastLockoutDateLabel, - SortOrder = 5, - Id = --identity, - Key = identity.ToGuid() - }); - propGroup.PropertyTypes.Add(new PropertyType(Constants.PropertyEditors.NoEditAlias, DataTypeDatabaseType.Date, Constants.Conventions.Member.LastLoginDate) - { - Name = Constants.Conventions.Member.LastLoginDateLabel, - SortOrder = 6, - Id = --identity, - Key = identity.ToGuid() - }); - propGroup.PropertyTypes.Add(new PropertyType(Constants.PropertyEditors.NoEditAlias, DataTypeDatabaseType.Date, Constants.Conventions.Member.LastPasswordChangeDate) - { - Name = Constants.Conventions.Member.LastPasswordChangeDateLabel, - SortOrder = 7, - Id = --identity, - Key = identity.ToGuid() - }); - - memType.PropertyGroups.Add(propGroup); - - var member = new Member(name, email, username, password, memType); - - //we've assigned ids to the property types and groups but we also need to assign fake ids to the properties themselves. - foreach (var property in member.Properties) - { - property.Id = --identity; - } - - return member; - } - } + /// + /// Represents the MemberService. + /// + public class MemberService : RepositoryService, IMemberService + { + private readonly IMemberGroupService _memberGroupService; + private readonly EntityXmlSerializer _entitySerializer = new EntityXmlSerializer(); + private readonly IDataTypeService _dataTypeService; + private static readonly ReaderWriterLockSlim Locker = new ReaderWriterLockSlim(); + + public MemberService(IDatabaseUnitOfWorkProvider provider, RepositoryFactory repositoryFactory, ILogger logger, IEventMessagesFactory eventMessagesFactory, IMemberGroupService memberGroupService, IDataTypeService dataTypeService) + : base(provider, repositoryFactory, logger, eventMessagesFactory) + { + if (memberGroupService == null) throw new ArgumentNullException("memberGroupService"); + if (dataTypeService == null) throw new ArgumentNullException("dataTypeService"); + _memberGroupService = memberGroupService; + _dataTypeService = dataTypeService; + } + + #region IMemberService Implementation + + /// + /// Gets the default MemberType alias + /// + /// By default we'll return the 'writer', but we need to check it exists. If it doesn't we'll + /// return the first type that is not an admin, otherwise if there's only one we will return that one. + /// Alias of the default MemberType + public string GetDefaultMemberType() + { + using (var repository = RepositoryFactory.CreateMemberTypeRepository(UowProvider.GetUnitOfWork())) + { + var types = repository.GetAll(new int[] { }).Select(x => x.Alias).ToArray(); + + if (types.Any() == false) + { + throw new InvalidOperationException("No member types could be resolved"); + } + + if (types.InvariantContains("Member")) + { + return types.First(x => x.InvariantEquals("Member")); + } + + return types.First(); + } + } + + /// + /// Checks if a Member with the username exists + /// + /// Username to check + /// True if the Member exists otherwise False + public bool Exists(string username) + { + using (var repository = RepositoryFactory.CreateMemberRepository(UowProvider.GetUnitOfWork())) + { + return repository.Exists(username); + } + } + + /// + /// This is simply a helper method which essentially just wraps the MembershipProvider's ChangePassword method + /// + /// This method exists so that Umbraco developers can use one entry point to create/update + /// Members if they choose to. + /// The Member to save the password for + /// The password to encrypt and save + public void SavePassword(IMember member, string password) + { + if (member == null) throw new ArgumentNullException("member"); + + var provider = MembershipProviderExtensions.GetMembersMembershipProvider(); + if (provider.IsUmbracoMembershipProvider()) + { + provider.ChangePassword(member.Username, "", password); + } + else + { + throw new NotSupportedException("When using a non-Umbraco membership provider you must change the member password by using the MembershipProvider.ChangePassword method"); + } + + //go re-fetch the member and update the properties that may have changed + var result = GetByUsername(member.Username); + + //should never be null but it could have been deleted by another thread. + if (result == null) + return; + + member.RawPasswordValue = result.RawPasswordValue; + member.LastPasswordChangeDate = result.LastPasswordChangeDate; + member.UpdateDate = result.UpdateDate; + } + + /// + /// Checks if a Member with the id exists + /// + /// Id of the Member + /// True if the Member exists otherwise False + public bool Exists(int id) + { + using (var repository = RepositoryFactory.CreateMemberRepository(UowProvider.GetUnitOfWork())) + { + return repository.Exists(id); + } + } + + /// + /// Gets a Member by its integer id + /// + /// Id + /// + public IMember GetById(int id) + { + using (var repository = RepositoryFactory.CreateMemberRepository(UowProvider.GetUnitOfWork())) + { + return repository.Get(id); + } + } + + /// + /// Gets a Member by the unique key + /// + /// The guid key corresponds to the unique id in the database + /// and the user id in the membership provider. + /// Id + /// + public IMember GetByKey(Guid id) + { + using (var repository = RepositoryFactory.CreateMemberRepository(UowProvider.GetUnitOfWork())) + { + var query = Query.Builder.Where(x => x.Key == id); + var member = repository.GetByQuery(query).FirstOrDefault(); + return member; + } + } + + /// + /// Gets all Members for the specified MemberType alias + /// + /// Alias of the MemberType + /// + public IEnumerable GetMembersByMemberType(string memberTypeAlias) + { + using (var repository = RepositoryFactory.CreateMemberRepository(UowProvider.GetUnitOfWork())) + { + var query = Query.Builder.Where(x => x.ContentTypeAlias == memberTypeAlias); + var members = repository.GetByQuery(query); + return members; + } + } + + /// + /// Gets all Members for the MemberType id + /// + /// Id of the MemberType + /// + public IEnumerable GetMembersByMemberType(int memberTypeId) + { + using (var repository = RepositoryFactory.CreateMemberRepository(UowProvider.GetUnitOfWork())) + { + repository.Get(memberTypeId); + var query = Query.Builder.Where(x => x.ContentTypeId == memberTypeId); + var members = repository.GetByQuery(query); + return members; + } + } + + /// + /// Gets all Members within the specified MemberGroup name + /// + /// Name of the MemberGroup + /// + public IEnumerable GetMembersByGroup(string memberGroupName) + { + using (var repository = RepositoryFactory.CreateMemberRepository(UowProvider.GetUnitOfWork())) + { + return repository.GetByMemberGroup(memberGroupName); + } + } + + /// + /// Gets all Members with the ids specified + /// + /// If no Ids are specified all Members will be retrieved + /// Optional list of Member Ids + /// + public IEnumerable GetAllMembers(params int[] ids) + { + using (var repository = RepositoryFactory.CreateMemberRepository(UowProvider.GetUnitOfWork())) + { + return repository.GetAll(ids); + } + } + + /// + /// Delete Members of the specified MemberType id + /// + /// Id of the MemberType + public void DeleteMembersOfType(int memberTypeId) + { + using (new WriteLock(Locker)) + { + using (var uow = UowProvider.GetUnitOfWork()) + { + var repository = RepositoryFactory.CreateMemberRepository(uow); + //TODO: What about content that has the contenttype as part of its composition? + var query = Query.Builder.Where(x => x.ContentTypeId == memberTypeId); + var members = repository.GetByQuery(query).ToArray(); + + if (Deleting.IsRaisedEventCancelled(new DeleteEventArgs(members), this)) + return; + + foreach (var member in members) + { + //Permantly delete the member + Delete(member); + } + } + } + } + + [Obsolete("Use the overload with 'long' parameter types instead")] + [EditorBrowsable(EditorBrowsableState.Never)] + public IEnumerable FindMembersByDisplayName(string displayNameToMatch, int pageIndex, int pageSize, out int totalRecords, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith) + { + long total; + var result = FindMembersByDisplayName(displayNameToMatch, Convert.ToInt64(pageIndex), pageSize, out total, matchType); + totalRecords = Convert.ToInt32(total); + return result; + } + + /// + /// Finds Members based on their display name + /// + /// Display name to match + /// Current page index + /// Size of the page + /// Total number of records found (out) + /// The type of match to make as . Default is + /// + public IEnumerable FindMembersByDisplayName(string displayNameToMatch, long pageIndex, int pageSize, out long totalRecords, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMemberRepository(uow)) + { + var query = new Query(); + + switch (matchType) + { + case StringPropertyMatchType.Exact: + query.Where(member => member.Name.Equals(displayNameToMatch)); + break; + case StringPropertyMatchType.Contains: + query.Where(member => member.Name.Contains(displayNameToMatch)); + break; + case StringPropertyMatchType.StartsWith: + query.Where(member => member.Name.StartsWith(displayNameToMatch)); + break; + case StringPropertyMatchType.EndsWith: + query.Where(member => member.Name.EndsWith(displayNameToMatch)); + break; + case StringPropertyMatchType.Wildcard: + query.Where(member => member.Name.SqlWildcard(displayNameToMatch, TextColumnType.NVarchar)); + break; + default: + throw new ArgumentOutOfRangeException("matchType"); + } + + return repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, "Name", Direction.Ascending, orderBySystemField: true); + } + } + + [Obsolete("Use the overload with 'long' parameter types instead")] + [EditorBrowsable(EditorBrowsableState.Never)] + public IEnumerable FindByEmail(string emailStringToMatch, int pageIndex, int pageSize, out int totalRecords, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith) + { + long total; + var result = FindByEmail(emailStringToMatch, Convert.ToInt64(pageIndex), pageSize, out total, matchType); + totalRecords = Convert.ToInt32(total); + return result; + } + + /// + /// Finds a list of objects by a partial email string + /// + /// Partial email string to match + /// Current page index + /// Size of the page + /// Total number of records found (out) + /// The type of match to make as . Default is + /// + public IEnumerable FindByEmail(string emailStringToMatch, long pageIndex, int pageSize, out long totalRecords, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMemberRepository(uow)) + { + var query = new Query(); + + switch (matchType) + { + case StringPropertyMatchType.Exact: + query.Where(member => member.Email.Equals(emailStringToMatch)); + break; + case StringPropertyMatchType.Contains: + query.Where(member => member.Email.Contains(emailStringToMatch)); + break; + case StringPropertyMatchType.StartsWith: + query.Where(member => member.Email.StartsWith(emailStringToMatch)); + break; + case StringPropertyMatchType.EndsWith: + query.Where(member => member.Email.EndsWith(emailStringToMatch)); + break; + case StringPropertyMatchType.Wildcard: + query.Where(member => member.Email.SqlWildcard(emailStringToMatch, TextColumnType.NVarchar)); + break; + default: + throw new ArgumentOutOfRangeException("matchType"); + } + + return repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, "Email", Direction.Ascending, orderBySystemField: true); + } + } + + [Obsolete("Use the overload with 'long' parameter types instead")] + [EditorBrowsable(EditorBrowsableState.Never)] + public IEnumerable FindByUsername(string login, int pageIndex, int pageSize, out int totalRecords, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith) + { + long total; + var result = FindByUsername(login, Convert.ToInt64(pageIndex), pageSize, out total, matchType); + totalRecords = Convert.ToInt32(total); + return result; + } + + /// + /// Finds a list of objects by a partial username + /// + /// Partial username to match + /// Current page index + /// Size of the page + /// Total number of records found (out) + /// The type of match to make as . Default is + /// + public IEnumerable FindByUsername(string login, long pageIndex, int pageSize, out long totalRecords, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMemberRepository(uow)) + { + var query = new Query(); + + switch (matchType) + { + case StringPropertyMatchType.Exact: + query.Where(member => member.Username.Equals(login)); + break; + case StringPropertyMatchType.Contains: + query.Where(member => member.Username.Contains(login)); + break; + case StringPropertyMatchType.StartsWith: + query.Where(member => member.Username.StartsWith(login)); + break; + case StringPropertyMatchType.EndsWith: + query.Where(member => member.Username.EndsWith(login)); + break; + case StringPropertyMatchType.Wildcard: + query.Where(member => member.Email.SqlWildcard(login, TextColumnType.NVarchar)); + break; + default: + throw new ArgumentOutOfRangeException("matchType"); + } + + return repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, "LoginName", Direction.Ascending, orderBySystemField: true); + } + } + + /// + /// Gets a list of Members based on a property search + /// + /// Alias of the PropertyType to search for + /// Value to match + /// The type of match to make as . Default is + /// + public IEnumerable GetMembersByPropertyValue(string propertyTypeAlias, string value, StringPropertyMatchType matchType = StringPropertyMatchType.Exact) + { + using (var repository = RepositoryFactory.CreateMemberRepository(UowProvider.GetUnitOfWork())) + { + IQuery query; + + switch (matchType) + { + case StringPropertyMatchType.Exact: + query = + Query.Builder.Where( + x => + ((Member)x).PropertyTypeAlias == propertyTypeAlias && + (((Member)x).LongStringPropertyValue.SqlEquals(value, TextColumnType.NText) || + ((Member)x).ShortStringPropertyValue.SqlEquals(value, TextColumnType.NVarchar))); + break; + case StringPropertyMatchType.Contains: + query = + Query.Builder.Where( + x => + ((Member)x).PropertyTypeAlias == propertyTypeAlias && + (((Member)x).LongStringPropertyValue.SqlContains(value, TextColumnType.NText) || + ((Member)x).ShortStringPropertyValue.SqlContains(value, TextColumnType.NVarchar))); + break; + case StringPropertyMatchType.StartsWith: + query = + Query.Builder.Where( + x => + ((Member)x).PropertyTypeAlias == propertyTypeAlias && + (((Member)x).LongStringPropertyValue.SqlStartsWith(value, TextColumnType.NText) || + ((Member)x).ShortStringPropertyValue.SqlStartsWith(value, TextColumnType.NVarchar))); + break; + case StringPropertyMatchType.EndsWith: + query = + Query.Builder.Where( + x => + ((Member)x).PropertyTypeAlias == propertyTypeAlias && + (((Member)x).LongStringPropertyValue.SqlEndsWith(value, TextColumnType.NText) || + ((Member)x).ShortStringPropertyValue.SqlEndsWith(value, TextColumnType.NVarchar))); + break; + default: + throw new ArgumentOutOfRangeException("matchType"); + } + + var members = repository.GetByQuery(query); + return members; + } + } + + /// + /// Gets a list of Members based on a property search + /// + /// Alias of the PropertyType to search for + /// Value to match + /// The type of match to make as . Default is + /// + public IEnumerable GetMembersByPropertyValue(string propertyTypeAlias, int value, ValuePropertyMatchType matchType = ValuePropertyMatchType.Exact) + { + using (var repository = RepositoryFactory.CreateMemberRepository(UowProvider.GetUnitOfWork())) + { + IQuery query; + + switch (matchType) + { + case ValuePropertyMatchType.Exact: + query = + Query.Builder.Where( + x => + ((Member)x).PropertyTypeAlias == propertyTypeAlias && + ((Member)x).IntegerPropertyValue == value); + break; + case ValuePropertyMatchType.GreaterThan: + query = + Query.Builder.Where( + x => + ((Member)x).PropertyTypeAlias == propertyTypeAlias && + ((Member)x).IntegerPropertyValue > value); + break; + case ValuePropertyMatchType.LessThan: + query = + Query.Builder.Where( + x => + ((Member)x).PropertyTypeAlias == propertyTypeAlias && + ((Member)x).IntegerPropertyValue < value); + break; + case ValuePropertyMatchType.GreaterThanOrEqualTo: + query = + Query.Builder.Where( + x => + ((Member)x).PropertyTypeAlias == propertyTypeAlias && + ((Member)x).IntegerPropertyValue >= value); + break; + case ValuePropertyMatchType.LessThanOrEqualTo: + query = + Query.Builder.Where( + x => + ((Member)x).PropertyTypeAlias == propertyTypeAlias && + ((Member)x).IntegerPropertyValue <= value); + break; + default: + throw new ArgumentOutOfRangeException("matchType"); + } + + var members = repository.GetByQuery(query); + return members; + } + } + + /// + /// Gets a list of Members based on a property search + /// + /// Alias of the PropertyType to search for + /// Value to match + /// + public IEnumerable GetMembersByPropertyValue(string propertyTypeAlias, bool value) + { + using (var repository = RepositoryFactory.CreateMemberRepository(UowProvider.GetUnitOfWork())) + { + var query = + Query.Builder.Where( + x => + ((Member)x).PropertyTypeAlias == propertyTypeAlias && + ((Member)x).BoolPropertyValue == value); + + var members = repository.GetByQuery(query); + return members; + } + } + + /// + /// Gets a list of Members based on a property search + /// + /// Alias of the PropertyType to search for + /// Value to match + /// The type of match to make as . Default is + /// + public IEnumerable GetMembersByPropertyValue(string propertyTypeAlias, DateTime value, ValuePropertyMatchType matchType = ValuePropertyMatchType.Exact) + { + using (var repository = RepositoryFactory.CreateMemberRepository(UowProvider.GetUnitOfWork())) + { + IQuery query; + + switch (matchType) + { + case ValuePropertyMatchType.Exact: + query = + Query.Builder.Where( + x => + ((Member)x).PropertyTypeAlias == propertyTypeAlias && + ((Member)x).DateTimePropertyValue == value); + break; + case ValuePropertyMatchType.GreaterThan: + query = + Query.Builder.Where( + x => + ((Member)x).PropertyTypeAlias == propertyTypeAlias && + ((Member)x).DateTimePropertyValue > value); + break; + case ValuePropertyMatchType.LessThan: + query = + Query.Builder.Where( + x => + ((Member)x).PropertyTypeAlias == propertyTypeAlias && + ((Member)x).DateTimePropertyValue < value); + break; + case ValuePropertyMatchType.GreaterThanOrEqualTo: + query = + Query.Builder.Where( + x => + ((Member)x).PropertyTypeAlias == propertyTypeAlias && + ((Member)x).DateTimePropertyValue >= value); + break; + case ValuePropertyMatchType.LessThanOrEqualTo: + query = + Query.Builder.Where( + x => + ((Member)x).PropertyTypeAlias == propertyTypeAlias && + ((Member)x).DateTimePropertyValue <= value); + break; + default: + throw new ArgumentOutOfRangeException("matchType"); + } + + //TODO: Since this is by property value, we need a GetByPropertyQuery on the repo! + var members = repository.GetByQuery(query); + return members; + } + } + + /// + /// Rebuilds all xml content in the cmsContentXml table for all members + /// + /// + /// Only rebuild the xml structures for the content type ids passed in, if none then rebuilds the structures + /// for all members = USE WITH CARE! + /// + /// True if publishing succeeded, otherwise False + public void RebuildXmlStructures(params int[] memberTypeIds) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMemberRepository(uow)) + { + repository.RebuildXmlStructures( + member => _entitySerializer.Serialize(_dataTypeService, member), + contentTypeIds: memberTypeIds.Length == 0 ? null : memberTypeIds); + } + + Audit(AuditType.Publish, "MemberService.RebuildXmlStructures completed, the xml has been regenerated in the database", 0, -1); + } + + #endregion + + #region IMembershipMemberService Implementation + + /// + /// Gets the total number of Members based on the count type + /// + /// + /// The way the Online count is done is the same way that it is done in the MS SqlMembershipProvider - We query for any members + /// that have their last active date within the Membership.UserIsOnlineTimeWindow (which is in minutes). It isn't exact science + /// but that is how MS have made theirs so we'll follow that principal. + /// + /// to count by + /// with number of Members for passed in type + public int GetCount(MemberCountType countType) + { + using (var repository = RepositoryFactory.CreateMemberRepository(UowProvider.GetUnitOfWork())) + { + IQuery query; + + switch (countType) + { + case MemberCountType.All: + query = new Query(); + return repository.Count(query); + case MemberCountType.Online: + var fromDate = DateTime.Now.AddMinutes(-Membership.UserIsOnlineTimeWindow); + query = + Query.Builder.Where( + x => + ((Member)x).PropertyTypeAlias == Constants.Conventions.Member.LastLoginDate && + ((Member)x).DateTimePropertyValue > fromDate); + return repository.GetCountByQuery(query); + case MemberCountType.LockedOut: + query = + Query.Builder.Where( + x => + ((Member)x).PropertyTypeAlias == Constants.Conventions.Member.IsLockedOut && + ((Member)x).BoolPropertyValue == true); + return repository.GetCountByQuery(query); + case MemberCountType.Approved: + query = + Query.Builder.Where( + x => + ((Member)x).PropertyTypeAlias == Constants.Conventions.Member.IsApproved && + ((Member)x).BoolPropertyValue == true); + return repository.GetCountByQuery(query); + default: + throw new ArgumentOutOfRangeException("countType"); + } + } + + } + + [Obsolete("Use the overload with 'long' parameter types instead")] + [EditorBrowsable(EditorBrowsableState.Never)] + public IEnumerable GetAll(int pageIndex, int pageSize, out int totalRecords) + { + long total; + var result = GetAll(Convert.ToInt64(pageIndex), pageSize, out total); + totalRecords = Convert.ToInt32(total); + return result; + } + + /// + /// Gets a list of paged objects + /// + /// Current page index + /// Size of the page + /// Total number of records found (out) + /// + public IEnumerable GetAll(long pageIndex, int pageSize, out long totalRecords) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMemberRepository(uow)) + { + return repository.GetPagedResultsByQuery(null, pageIndex, pageSize, out totalRecords, "LoginName", Direction.Ascending, orderBySystemField: true); + } + } + + [Obsolete("Use the overload with 'long' parameter types instead")] + [EditorBrowsable(EditorBrowsableState.Never)] + public IEnumerable GetAll(int pageIndex, int pageSize, out int totalRecords, + string orderBy, Direction orderDirection, bool orderBySystemField = true, string memberTypeAlias = null, string filter = "") + { + long total; + var result = GetAll(Convert.ToInt64(pageIndex), pageSize, out total, orderBy, orderDirection, orderBySystemField, memberTypeAlias, filter); + totalRecords = Convert.ToInt32(total); + return result; + } + + public IEnumerable GetAll(long pageIndex, int pageSize, out long totalRecords, + string orderBy, Direction orderDirection, bool orderBySystemField = true, string memberTypeAlias = null, string filter = "") + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMemberRepository(uow)) + { + if (memberTypeAlias == null) + { + return repository.GetPagedResultsByQuery(null, pageIndex, pageSize, out totalRecords, orderBy, orderDirection, orderBySystemField, filter); + } + var query = new Query().Where(x => x.ContentTypeAlias == memberTypeAlias); + return repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, orderBy, orderDirection, orderBySystemField, filter); + } + } + + /// + /// Gets the count of Members by an optional MemberType alias + /// + /// If no alias is supplied then the count for all Member will be returned + /// Optional alias for the MemberType when counting number of Members + /// with number of Members + public int Count(string memberTypeAlias = null) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMemberRepository(uow)) + { + return repository.Count(memberTypeAlias); + } + } + + /// + /// Creates an object without persisting it + /// + /// This method is convenient for when you need to add properties to a new Member + /// before persisting it in order to limit the amount of times its saved. + /// Also note that the returned will not have an Id until its saved. + /// Username of the Member to create + /// Email of the Member to create + /// Name of the Member to create + /// Alias of the MemberType the Member should be based on + /// + public IMember CreateMember(string username, string email, string name, string memberTypeAlias) + { + var memberType = FindMemberTypeByAlias(memberTypeAlias); + return CreateMember(username, email, name, memberType); + } + + /// + /// Creates an object without persisting it + /// + /// This method is convenient for when you need to add properties to a new Member + /// before persisting it in order to limit the amount of times its saved. + /// Also note that the returned will not have an Id until its saved. + /// Username of the Member to create + /// Email of the Member to create + /// Name of the Member to create + /// MemberType the Member should be based on + /// + public IMember CreateMember(string username, string email, string name, IMemberType memberType) + { + var member = new Member(name, email.ToLower().Trim(), username, memberType); + + Created.RaiseEvent(new NewEventArgs(member, false, memberType.Alias, -1), this); + + return member; + } + + /// + /// Creates and persists a Member + /// + /// Using this method will persist the Member object before its returned + /// meaning that it will have an Id available (unlike the CreateMember method) + /// Username of the Member to create + /// Email of the Member to create + /// Name of the Member to create + /// Alias of the MemberType the Member should be based on + /// + public IMember CreateMemberWithIdentity(string username, string email, string name, string memberTypeAlias) + { + var memberType = FindMemberTypeByAlias(memberTypeAlias); + return CreateMemberWithIdentity(username, email, name, memberType); + } + + /// + /// Creates and persists a Member + /// + /// Using this method will persist the Member object before its returned + /// meaning that it will have an Id available (unlike the CreateMember method) + /// Username of the Member to create + /// Email of the Member to create + /// MemberType the Member should be based on + /// + public IMember CreateMemberWithIdentity(string username, string email, IMemberType memberType) + { + return CreateMemberWithIdentity(username, email, username, memberType); + } + + /// + /// Creates and persists a Member + /// + /// Using this method will persist the Member object before its returned + /// meaning that it will have an Id available (unlike the CreateMember method) + /// Username of the Member to create + /// Email of the Member to create + /// Name of the Member to create + /// MemberType the Member should be based on + /// + public IMember CreateMemberWithIdentity(string username, string email, string name, IMemberType memberType) + { + return CreateMemberWithIdentity(username, email, name, "", memberType); + } + + /// + /// Creates and persists a new + /// + /// An can be of type or + /// Username of the to create + /// Email of the to create + /// This value should be the encoded/encrypted/hashed value for the password that will be stored in the database + /// Alias of the Type + /// + IMember IMembershipMemberService.CreateWithIdentity(string username, string email, string passwordValue, string memberTypeAlias) + { + var memberType = FindMemberTypeByAlias(memberTypeAlias); + return CreateMemberWithIdentity(username, email, username, passwordValue, memberType); + } + + /// + /// Creates and persists a Member + /// + /// Using this method will persist the Member object before its returned + /// meaning that it will have an Id available (unlike the CreateMember method) + /// Username of the Member to create + /// Email of the Member to create + /// Name of the Member to create + /// This value should be the encoded/encrypted/hashed value for the password that will be stored in the database + /// MemberType the Member should be based on + /// + private IMember CreateMemberWithIdentity(string username, string email, string name, string passwordValue, IMemberType memberType) + { + if (memberType == null) throw new ArgumentNullException("memberType"); + + var member = new Member(name, email.ToLower().Trim(), username, passwordValue, memberType); + + if (Saving.IsRaisedEventCancelled(new SaveEventArgs(member), this)) + { + member.WasCancelled = true; + return member; + } + + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMemberRepository(uow)) + { + repository.AddOrUpdate(member); + //insert the xml + repository.AddOrUpdateContentXml(member, m => _entitySerializer.Serialize(_dataTypeService, m)); + // generate preview for blame history? + if (UmbracoConfig.For.UmbracoSettings().Content.GlobalPreviewStorageEnabled) + { + repository.AddOrUpdatePreviewXml(member, m => _entitySerializer.Serialize(_dataTypeService, m)); + } + + uow.Commit(); + } + + Saved.RaiseEvent(new SaveEventArgs(member, false), this); + Created.RaiseEvent(new NewEventArgs(member, false, memberType.Alias, -1), this); + + return member; + } + + /// + /// Gets an by its provider key + /// + /// Id to use for retrieval + /// + public IMember GetByProviderKey(object id) + { + var asGuid = id.TryConvertTo(); + if (asGuid.Success) + { + return GetByKey((Guid)id); + } + var asInt = id.TryConvertTo(); + if (asInt.Success) + { + return GetById((int)id); + } + + return null; + } + + /// + /// Get an by email + /// + /// Email to use for retrieval + /// + public IMember GetByEmail(string email) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMemberRepository(uow)) + { + var query = Query.Builder.Where(x => x.Email.Equals(email)); + var member = repository.GetByQuery(query).FirstOrDefault(); + + return member; + } + } + + /// + /// Get an by username + /// + /// Username to use for retrieval + /// + public IMember GetByUsername(string username) + { + //TODO: Somewhere in here, whether at this level or the repository level, we need to add + // a caching mechanism since this method is used by all the membership providers and could be + // called quite a bit when dealing with members. + + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMemberRepository(uow)) + { + var query = Query.Builder.Where(x => x.Username.Equals(username)); + var member = repository.GetByQuery(query).FirstOrDefault(); + + return member; + } + } + + /// + /// Deletes an + /// + /// to Delete + public void Delete(IMember member) + { + if (Deleting.IsRaisedEventCancelled(new DeleteEventArgs(member), this)) + return; + + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMemberRepository(uow)) + { + repository.Delete(member); + uow.Commit(); + + var args = new DeleteEventArgs(member, false); + Deleted.RaiseEvent(args, this); + + //remove any flagged media files + repository.DeleteMediaFiles(args.MediaFilesToDelete); + } + } + + /// + /// Saves an + /// + /// to Save + /// Optional parameter to raise events. + /// Default is True otherwise set to False to not raise events + public void Save(IMember entity, bool raiseEvents = true) + { + if (raiseEvents) + { + if (Saving.IsRaisedEventCancelled(new SaveEventArgs(entity), this)) + { + return; + } + } + + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMemberRepository(uow)) + { + repository.AddOrUpdate(entity); + repository.AddOrUpdateContentXml(entity, m => _entitySerializer.Serialize(_dataTypeService, m)); + // generate preview for blame history? + if (UmbracoConfig.For.UmbracoSettings().Content.GlobalPreviewStorageEnabled) + { + repository.AddOrUpdatePreviewXml(entity, m => _entitySerializer.Serialize(_dataTypeService, m)); + } + + uow.Commit(); + } + + if (raiseEvents) + Saved.RaiseEvent(new SaveEventArgs(entity, false), this); + } + + /// + /// Saves a list of objects + /// + /// to save + /// Optional parameter to raise events. + /// Default is True otherwise set to False to not raise events + public void Save(IEnumerable entities, bool raiseEvents = true) + { + var asArray = entities.ToArray(); + + if (raiseEvents) + { + if (Saving.IsRaisedEventCancelled(new SaveEventArgs(asArray), this)) + return; + } + using (new WriteLock(Locker)) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMemberRepository(uow)) + { + foreach (var member in asArray) + { + repository.AddOrUpdate(member); + repository.AddOrUpdateContentXml(member, m => _entitySerializer.Serialize(_dataTypeService, m)); + // generate preview for blame history? + if (UmbracoConfig.For.UmbracoSettings().Content.GlobalPreviewStorageEnabled) + { + repository.AddOrUpdatePreviewXml(member, m => _entitySerializer.Serialize(_dataTypeService, m)); + } + } + + //commit the whole lot in one go + uow.Commit(); + } + + if (raiseEvents) + Saved.RaiseEvent(new SaveEventArgs(asArray, false), this); + } + } + + #endregion + + #region IMembershipRoleService Implementation + + public void AddRole(string roleName) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMemberGroupRepository(uow)) + { + repository.CreateIfNotExists(roleName); + } + } + + public IEnumerable GetAllRoles() + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMemberGroupRepository(uow)) + { + var result = repository.GetAll(); + return result.Select(x => x.Name).Distinct(); + } + } + + public IEnumerable GetAllRoles(int memberId) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMemberGroupRepository(uow)) + { + var result = repository.GetMemberGroupsForMember(memberId); + return result.Select(x => x.Name).Distinct(); + } + } + + public IEnumerable GetAllRoles(string username) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMemberGroupRepository(uow)) + { + var result = repository.GetMemberGroupsForMember(username); + return result.Select(x => x.Name).Distinct(); + } + } + + public IEnumerable GetMembersInRole(string roleName) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMemberRepository(uow)) + { + return repository.GetByMemberGroup(roleName); + } + } + + public IEnumerable FindMembersInRole(string roleName, string usernameToMatch, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMemberRepository(uow)) + { + return repository.FindMembersInRole(roleName, usernameToMatch, matchType); + } + } + + public bool DeleteRole(string roleName, bool throwIfBeingUsed) + { + using (new WriteLock(Locker)) + { + if (throwIfBeingUsed) + { + var inRole = GetMembersInRole(roleName); + if (inRole.Any()) + { + throw new InvalidOperationException("The role " + roleName + " is currently assigned to members"); + } + } + + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMemberGroupRepository(uow)) + { + var qry = new Query().Where(g => g.Name == roleName); + var found = repository.GetByQuery(qry).ToArray(); + + foreach (var memberGroup in found) + { + _memberGroupService.Delete(memberGroup); + } + return found.Any(); + } + } + } + public void AssignRole(string username, string roleName) + { + AssignRoles(new[] { username }, new[] { roleName }); + } + + public void AssignRoles(string[] usernames, string[] roleNames) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMemberGroupRepository(uow)) + { + repository.AssignRoles(usernames, roleNames); + } + } + + public void DissociateRole(string username, string roleName) + { + DissociateRoles(new[] { username }, new[] { roleName }); + } + + public void DissociateRoles(string[] usernames, string[] roleNames) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMemberGroupRepository(uow)) + { + repository.DissociateRoles(usernames, roleNames); + } + } + + public void AssignRole(int memberId, string roleName) + { + AssignRoles(new[] { memberId }, new[] { roleName }); + } + + public void AssignRoles(int[] memberIds, string[] roleNames) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMemberGroupRepository(uow)) + { + repository.AssignRoles(memberIds, roleNames); + } + } + + public void DissociateRole(int memberId, string roleName) + { + DissociateRoles(new[] { memberId }, new[] { roleName }); + } + + public void DissociateRoles(int[] memberIds, string[] roleNames) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMemberGroupRepository(uow)) + { + repository.DissociateRoles(memberIds, roleNames); + } + } + + + + #endregion + + private IMemberType FindMemberTypeByAlias(string memberTypeAlias) + { + using (var repository = RepositoryFactory.CreateMemberTypeRepository(UowProvider.GetUnitOfWork())) + { + var query = Query.Builder.Where(x => x.Alias == memberTypeAlias); + var types = repository.GetByQuery(query); + + if (types.Any() == false) + throw new Exception( + string.Format("No MemberType matching the passed in Alias: '{0}' was found", + memberTypeAlias)); + + var contentType = types.First(); + + if (contentType == null) + throw new Exception(string.Format("MemberType matching the passed in Alias: '{0}' was null", + memberTypeAlias)); + + return contentType; + } + } + + private void Audit(AuditType type, string message, int userId, int objectId) + { + var uow = UowProvider.GetUnitOfWork(); + using (var auditRepo = RepositoryFactory.CreateAuditRepository(uow)) + { + auditRepo.AddOrUpdate(new AuditItem(objectId, message, type, userId)); + uow.Commit(); + } + } + + #region Event Handlers + + /// + /// Occurs before Delete + /// + public static event TypedEventHandler> Deleting; + + /// + /// Occurs after Delete + /// + public static event TypedEventHandler> Deleted; + + /// + /// Occurs before Save + /// + public static event TypedEventHandler> Saving; + + /// + /// Occurs after Create + /// + /// + /// Please note that the Member object has been created, but might not have been saved + /// so it does not have an identity yet (meaning no Id has been set). + /// + public static event TypedEventHandler> Created; + + /// + /// Occurs after Save + /// + public static event TypedEventHandler> Saved; + + #endregion + + /// + /// A helper method that will create a basic/generic member for use with a generic membership provider + /// + /// + internal static IMember CreateGenericMembershipProviderMember(string name, string email, string username, string password) + { + var identity = int.MaxValue; + + var memType = new MemberType(-1); + var propGroup = new PropertyGroup + { + Name = "Membership", + Id = --identity + }; + propGroup.PropertyTypes.Add(new PropertyType(Constants.PropertyEditors.TextboxAlias, DataTypeDatabaseType.Ntext, Constants.Conventions.Member.Comments) + { + Name = Constants.Conventions.Member.CommentsLabel, + SortOrder = 0, + Id = --identity, + Key = identity.ToGuid() + }); + propGroup.PropertyTypes.Add(new PropertyType(Constants.PropertyEditors.TrueFalseAlias, DataTypeDatabaseType.Integer, Constants.Conventions.Member.IsApproved) + { + Name = Constants.Conventions.Member.IsApprovedLabel, + SortOrder = 3, + Id = --identity, + Key = identity.ToGuid() + }); + propGroup.PropertyTypes.Add(new PropertyType(Constants.PropertyEditors.TrueFalseAlias, DataTypeDatabaseType.Integer, Constants.Conventions.Member.IsLockedOut) + { + Name = Constants.Conventions.Member.IsLockedOutLabel, + SortOrder = 4, + Id = --identity, + Key = identity.ToGuid() + }); + propGroup.PropertyTypes.Add(new PropertyType(Constants.PropertyEditors.NoEditAlias, DataTypeDatabaseType.Date, Constants.Conventions.Member.LastLockoutDate) + { + Name = Constants.Conventions.Member.LastLockoutDateLabel, + SortOrder = 5, + Id = --identity, + Key = identity.ToGuid() + }); + propGroup.PropertyTypes.Add(new PropertyType(Constants.PropertyEditors.NoEditAlias, DataTypeDatabaseType.Date, Constants.Conventions.Member.LastLoginDate) + { + Name = Constants.Conventions.Member.LastLoginDateLabel, + SortOrder = 6, + Id = --identity, + Key = identity.ToGuid() + }); + propGroup.PropertyTypes.Add(new PropertyType(Constants.PropertyEditors.NoEditAlias, DataTypeDatabaseType.Date, Constants.Conventions.Member.LastPasswordChangeDate) + { + Name = Constants.Conventions.Member.LastPasswordChangeDateLabel, + SortOrder = 7, + Id = --identity, + Key = identity.ToGuid() + }); + + memType.PropertyGroups.Add(propGroup); + + var member = new Member(name, email, username, password, memType); + + //we've assigned ids to the property types and groups but we also need to assign fake ids to the properties themselves. + foreach (var property in member.Properties) + { + property.Id = --identity; + } + + return member; + } + } } \ No newline at end of file diff --git a/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs index c8ccecdb95..f21371ef2b 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs @@ -24,795 +24,795 @@ using Umbraco.Core.Persistence.DatabaseModelDefinitions; namespace Umbraco.Tests.Persistence.Repositories { - [DatabaseTestBehavior(DatabaseBehavior.NewDbFileAndSchemaPerTest)] - [TestFixture] - public class ContentRepositoryTest : BaseDatabaseFactoryTest - { - [SetUp] - public override void Initialize() - { - base.Initialize(); - - CreateTestData(); - } - - [TearDown] - public override void TearDown() - { - base.TearDown(); - } - - private ContentRepository CreateRepository(IDatabaseUnitOfWork unitOfWork, out ContentTypeRepository contentTypeRepository) - { - TemplateRepository tr; - return CreateRepository(unitOfWork, out contentTypeRepository, out tr); - } - - private ContentRepository CreateRepository(IDatabaseUnitOfWork unitOfWork, out ContentTypeRepository contentTypeRepository, out TemplateRepository templateRepository) - { - templateRepository = new TemplateRepository(unitOfWork, CacheHelper, Logger, SqlSyntax, Mock.Of(), Mock.Of(), Mock.Of()); - var tagRepository = new TagRepository(unitOfWork, CacheHelper, Logger, SqlSyntax); - contentTypeRepository = new ContentTypeRepository(unitOfWork, CacheHelper, Logger, SqlSyntax, templateRepository); - var repository = new ContentRepository(unitOfWork, CacheHelper, Logger, SqlSyntax, contentTypeRepository, templateRepository, tagRepository, Mock.Of()); - return repository; - } - - [Test] - public void Rebuild_Xml_Structures_With_Non_Latest_Version() - { - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - ContentTypeRepository contentTypeRepository; - using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) - { - var contentType1 = MockedContentTypes.CreateSimpleContentType("Textpage1", "Textpage1"); - contentTypeRepository.AddOrUpdate(contentType1); - - var allCreated = new List(); - - //create 100 non published - for (var i = 0; i < 100; i++) - { - var c1 = MockedContent.CreateSimpleContent(contentType1); - repository.AddOrUpdate(c1); - allCreated.Add(c1); - } - //create 100 published - for (var i = 0; i < 100; i++) - { - var c1 = MockedContent.CreateSimpleContent(contentType1); - c1.ChangePublishedState(PublishedState.Published); - repository.AddOrUpdate(c1); - allCreated.Add(c1); - } - unitOfWork.Commit(); - - //now create some versions of this content - this shouldn't affect the xml structures saved - for (int i = 0; i < allCreated.Count; i++) - { - allCreated[i].Name = "blah" + i; - //IMPORTANT testing note here: We need to changed the published state here so that - // it doesn't automatically think this is simply publishing again - this forces the latest - // version to be Saved and not published - allCreated[i].ChangePublishedState(PublishedState.Saved); - repository.AddOrUpdate(allCreated[i]); - } - unitOfWork.Commit(); - - //delete all xml - unitOfWork.Database.Execute("DELETE FROM cmsContentXml"); - Assert.AreEqual(0, unitOfWork.Database.ExecuteScalar("SELECT COUNT(*) FROM cmsContentXml")); - - repository.RebuildXmlStructures(media => new XElement("test"), 10); - - Assert.AreEqual(100, unitOfWork.Database.ExecuteScalar("SELECT COUNT(*) FROM cmsContentXml")); - } - } - - [Test] - public void Rebuild_All_Xml_Structures() - { - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - ContentTypeRepository contentTypeRepository; - using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) - { - - var contentType1 = MockedContentTypes.CreateSimpleContentType("Textpage1", "Textpage1"); - contentTypeRepository.AddOrUpdate(contentType1); - var allCreated = new List(); - - for (var i = 0; i < 100; i++) - { - //These will be non-published so shouldn't show up - var c1 = MockedContent.CreateSimpleContent(contentType1); - repository.AddOrUpdate(c1); - allCreated.Add(c1); - } - for (var i = 0; i < 100; i++) - { - var c1 = MockedContent.CreateSimpleContent(contentType1); - c1.ChangePublishedState(PublishedState.Published); - repository.AddOrUpdate(c1); - allCreated.Add(c1); - } - unitOfWork.Commit(); - - //now create some versions of this content - this shouldn't affect the xml structures saved - for (int i = 0; i < allCreated.Count; i++) - { - allCreated[i].Name = "blah" + i; - repository.AddOrUpdate(allCreated[i]); - } - unitOfWork.Commit(); - - //delete all xml - unitOfWork.Database.Execute("DELETE FROM cmsContentXml"); - Assert.AreEqual(0, unitOfWork.Database.ExecuteScalar("SELECT COUNT(*) FROM cmsContentXml")); - - repository.RebuildXmlStructures(media => new XElement("test"), 10); - - Assert.AreEqual(100, unitOfWork.Database.ExecuteScalar("SELECT COUNT(*) FROM cmsContentXml")); - } - } - - [Test] - public void Rebuild_All_Xml_Structures_For_Content_Type() - { - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - ContentTypeRepository contentTypeRepository; - using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) - { - var contentType1 = MockedContentTypes.CreateSimpleContentType("Textpage1", "Textpage1"); - var contentType2 = MockedContentTypes.CreateSimpleContentType("Textpage2", "Textpage2"); - var contentType3 = MockedContentTypes.CreateSimpleContentType("Textpage3", "Textpage3"); - contentTypeRepository.AddOrUpdate(contentType1); - contentTypeRepository.AddOrUpdate(contentType2); - contentTypeRepository.AddOrUpdate(contentType3); - - var allCreated = new List(); - - for (var i = 0; i < 30; i++) - { - //These will be non-published so shouldn't show up - var c1 = MockedContent.CreateSimpleContent(contentType1); - repository.AddOrUpdate(c1); - allCreated.Add(c1); - } - for (var i = 0; i < 30; i++) - { - var c1 = MockedContent.CreateSimpleContent(contentType1); - c1.ChangePublishedState(PublishedState.Published); - repository.AddOrUpdate(c1); - allCreated.Add(c1); - } - for (var i = 0; i < 30; i++) - { - var c1 = MockedContent.CreateSimpleContent(contentType2); - c1.ChangePublishedState(PublishedState.Published); - repository.AddOrUpdate(c1); - allCreated.Add(c1); - } - for (var i = 0; i < 30; i++) - { - var c1 = MockedContent.CreateSimpleContent(contentType3); - c1.ChangePublishedState(PublishedState.Published); - repository.AddOrUpdate(c1); - allCreated.Add(c1); - } - unitOfWork.Commit(); - - //now create some versions of this content - this shouldn't affect the xml structures saved - for (int i = 0; i < allCreated.Count; i++) - { - allCreated[i].Name = "blah" + i; - repository.AddOrUpdate(allCreated[i]); - } - unitOfWork.Commit(); - - //delete all xml - unitOfWork.Database.Execute("DELETE FROM cmsContentXml"); - Assert.AreEqual(0, unitOfWork.Database.ExecuteScalar("SELECT COUNT(*) FROM cmsContentXml")); - - repository.RebuildXmlStructures(media => new XElement("test"), 10, contentTypeIds: new[] { contentType1.Id, contentType2.Id }); - - Assert.AreEqual(60, unitOfWork.Database.ExecuteScalar("SELECT COUNT(*) FROM cmsContentXml")); - } - } - - [Test] - public void Ensures_Permissions_Are_Set_If_Parent_Entity_Permissions_Exist() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - - ContentTypeRepository contentTypeRepository; - using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) - { - var contentType = MockedContentTypes.CreateSimpleContentType("umbTextpage1", "Textpage"); - contentType.AllowedContentTypes = new List - { - new ContentTypeSort(new Lazy(() => contentType.Id), 0, contentType.Alias) - }; - var parentPage = MockedContent.CreateSimpleContent(contentType); - contentTypeRepository.AddOrUpdate(contentType); - repository.AddOrUpdate(parentPage); - unitOfWork.Commit(); - - // Act - repository.AssignEntityPermission(parentPage, 'A', new int[] { 0 }); - var childPage = MockedContent.CreateSimpleContent(contentType, "child", parentPage); - repository.AddOrUpdate(childPage); - unitOfWork.Commit(); - - // Assert - var permissions = repository.GetPermissionsForEntity(childPage.Id); - Assert.AreEqual(1, permissions.Count()); - Assert.AreEqual("A", permissions.Single().AssignedPermissions.First()); - } - - } - - [Test] - public void Can_Perform_Add_On_ContentRepository() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - ContentTypeRepository contentTypeRepository; - using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) - { - ContentType contentType = MockedContentTypes.CreateSimpleContentType("umbTextpage2", "Textpage"); - Content textpage = MockedContent.CreateSimpleContent(contentType); - - // Act - contentTypeRepository.AddOrUpdate(contentType); - repository.AddOrUpdate(textpage); - unitOfWork.Commit(); - - // Assert - Assert.That(contentType.HasIdentity, Is.True); - Assert.That(textpage.HasIdentity, Is.True); - - } - } - - [Test] - public void Can_Perform_Add_With_Default_Template() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - ContentTypeRepository contentTypeRepository; - TemplateRepository templateRepository; - using (var repository = CreateRepository(unitOfWork, out contentTypeRepository, out templateRepository)) - { - var template = new Template("hello", "hello"); - templateRepository.AddOrUpdate(template); - unitOfWork.Commit(); - - ContentType contentType = MockedContentTypes.CreateSimpleContentType("umbTextpage2", "Textpage"); - contentType.AllowedTemplates = Enumerable.Empty(); // because CreateSimple... assigns one - contentType.SetDefaultTemplate(template); - Content textpage = MockedContent.CreateSimpleContent(contentType); - - // Act - - contentTypeRepository.AddOrUpdate(contentType); - repository.AddOrUpdate(textpage); - unitOfWork.Commit(); - - // Assert - Assert.That(textpage.Template, Is.Not.Null); - Assert.That(textpage.Template, Is.EqualTo(contentType.DefaultTemplate)); - } - } - - //Covers issue U4-2791 and U4-2607 - [Test] - public void Can_Save_Content_With_AtSign_In_Name_On_ContentRepository() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - ContentTypeRepository contentTypeRepository; - using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) - { - var contentType = MockedContentTypes.CreateSimpleContentType("umbTextpage1", "Textpage"); - contentTypeRepository.AddOrUpdate(contentType); - unitOfWork.Commit(); - - var textpage = MockedContent.CreateSimpleContent(contentType, "test@umbraco.org", -1); - var anotherTextpage = MockedContent.CreateSimpleContent(contentType, "@lightgiants", -1); - - // Act - - repository.AddOrUpdate(textpage); - repository.AddOrUpdate(anotherTextpage); - unitOfWork.Commit(); - - // Assert - Assert.That(contentType.HasIdentity, Is.True); - Assert.That(textpage.HasIdentity, Is.True); - - var content = repository.Get(textpage.Id); - Assert.That(content.Name, Is.EqualTo(textpage.Name)); - - var content2 = repository.Get(anotherTextpage.Id); - Assert.That(content2.Name, Is.EqualTo(anotherTextpage.Name)); - } - } - - [Test] - public void Can_Perform_Multiple_Adds_On_ContentRepository() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - ContentTypeRepository contentTypeRepository; - using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) - { - ContentType contentType = MockedContentTypes.CreateSimpleContentType("umbTextpage1", "Textpage"); - Content textpage = MockedContent.CreateSimpleContent(contentType); - - // Act - contentTypeRepository.AddOrUpdate(contentType); - repository.AddOrUpdate(textpage); - unitOfWork.Commit(); - - Content subpage = MockedContent.CreateSimpleContent(contentType, "Text Page 1", textpage.Id); - repository.AddOrUpdate(subpage); - unitOfWork.Commit(); - - // Assert - Assert.That(contentType.HasIdentity, Is.True); - Assert.That(textpage.HasIdentity, Is.True); - Assert.That(subpage.HasIdentity, Is.True); - Assert.That(textpage.Id, Is.EqualTo(subpage.ParentId)); - } - - } - - - [Test] - public void Can_Verify_Fresh_Entity_Is_Not_Dirty() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - ContentTypeRepository contentTypeRepository; - using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) - { - // Act - var content = repository.Get(NodeDto.NodeIdSeed + 3); - bool dirty = ((Content)content).IsDirty(); - - // Assert - Assert.That(dirty, Is.False); - } - } - - [Test] - public void Can_Perform_Update_On_ContentRepository() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - ContentTypeRepository contentTypeRepository; - using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) - { - // Act - var content = repository.Get(NodeDto.NodeIdSeed + 2); - content.Name = "About 2"; - repository.AddOrUpdate(content); - unitOfWork.Commit(); - var updatedContent = repository.Get(NodeDto.NodeIdSeed + 2); - - // Assert - Assert.That(updatedContent.Id, Is.EqualTo(content.Id)); - Assert.That(updatedContent.Name, Is.EqualTo(content.Name)); - } - - } - - [Test] - public void Can_Update_With_Null_Template() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - ContentTypeRepository contentTypeRepository; - using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) - { - // Act - var content = repository.Get(NodeDto.NodeIdSeed + 2); - content.Template = null; - repository.AddOrUpdate(content); - unitOfWork.Commit(); - var updatedContent = repository.Get(NodeDto.NodeIdSeed + 2); - - // Assert - Assert.That(updatedContent.Template, Is.Null); - } - - } - - [Test] - public void Can_Perform_Delete_On_ContentRepository() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - ContentTypeRepository contentTypeRepository; - using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) - { - var contentType = contentTypeRepository.Get(NodeDto.NodeIdSeed); - var content = new Content("Textpage 2 Child Node", NodeDto.NodeIdSeed + 3, contentType); - content.CreatorId = 0; - content.WriterId = 0; - - // Act - repository.AddOrUpdate(content); - unitOfWork.Commit(); - var id = content.Id; - - repository.Delete(content); - unitOfWork.Commit(); - - var content1 = repository.Get(id); - - // Assert - Assert.That(content1, Is.Null); - } - } - - [Test] - public void Can_Perform_Get_On_ContentRepository() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - ContentTypeRepository contentTypeRepository; - using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) - { - // Act - var content = repository.Get(NodeDto.NodeIdSeed + 3); - - // Assert - Assert.That(content.Id, Is.EqualTo(NodeDto.NodeIdSeed + 3)); - Assert.That(content.CreateDate, Is.GreaterThan(DateTime.MinValue)); - Assert.That(content.UpdateDate, Is.GreaterThan(DateTime.MinValue)); - Assert.That(content.ParentId, Is.Not.EqualTo(0)); - Assert.That(content.Name, Is.EqualTo("Text Page 2")); - //Assert.That(content.SortOrder, Is.EqualTo(1)); - Assert.That(content.Version, Is.Not.EqualTo(Guid.Empty)); - Assert.That(content.ContentTypeId, Is.EqualTo(NodeDto.NodeIdSeed)); - Assert.That(content.Path, Is.Not.Empty); - Assert.That(content.Properties.Any(), Is.True); - } - } - - [Test] - public void Can_Perform_GetByQuery_On_ContentRepository() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - ContentTypeRepository contentTypeRepository; - using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) - { - // Act - var query = Query.Builder.Where(x => x.Level == 2); - var result = repository.GetByQuery(query); - - // Assert - Assert.That(result.Count(), Is.GreaterThanOrEqualTo(2)); - } - } - - [Test] - public void Can_Perform_Get_All_With_Many_Version() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - ContentTypeRepository contentTypeRepository; - using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) - { - var result = repository.GetAll().ToArray(); - foreach (var content in result) - { - content.ChangePublishedState(PublishedState.Saved); - repository.AddOrUpdate(content); - } - unitOfWork.Commit(); - foreach (var content in result) - { - content.ChangePublishedState(PublishedState.Published); - repository.AddOrUpdate(content); - } - unitOfWork.Commit(); - - //re-get - - var result2 = repository.GetAll().ToArray(); - - Assert.AreEqual(result.Count(), result2.Count()); - } - } - - [Test] - public void Can_Perform_GetPagedResultsByQuery_ForFirstPage_On_ContentRepository() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - ContentTypeRepository contentTypeRepository; - using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) - { - // Act - var query = Query.Builder.Where(x => x.Level == 2); - long totalRecords; - var result = repository.GetPagedResultsByQuery(query, 0, 1, out totalRecords, "Name", Direction.Ascending); - - // Assert - Assert.That(totalRecords, Is.GreaterThanOrEqualTo(2)); - Assert.That(result.Count(), Is.EqualTo(1)); - Assert.That(result.First().Name, Is.EqualTo("Text Page 1")); - } - } - - [Test] - public void Can_Perform_GetPagedResultsByQuery_ForSecondPage_On_ContentRepository() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - ContentTypeRepository contentTypeRepository; - using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) - { - // Act - var query = Query.Builder.Where(x => x.Level == 2); - long totalRecords; - var result = repository.GetPagedResultsByQuery(query, 1, 1, out totalRecords, "Name", Direction.Ascending); - - // Assert - Assert.That(totalRecords, Is.GreaterThanOrEqualTo(2)); - Assert.That(result.Count(), Is.EqualTo(1)); - Assert.That(result.First().Name, Is.EqualTo("Text Page 2")); - } - } - - [Test] - public void Can_Perform_GetPagedResultsByQuery_WithSinglePage_On_ContentRepository() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - ContentTypeRepository contentTypeRepository; - using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) - { - // Act - var query = Query.Builder.Where(x => x.Level == 2); - long totalRecords; - var result = repository.GetPagedResultsByQuery(query, 0, 2, out totalRecords, "Name", Direction.Ascending); - - // Assert - Assert.That(totalRecords, Is.GreaterThanOrEqualTo(2)); - Assert.That(result.Count(), Is.EqualTo(2)); - Assert.That(result.First().Name, Is.EqualTo("Text Page 1")); - } - } - - [Test] - public void Can_Perform_GetPagedResultsByQuery_WithDescendingOrder_On_ContentRepository() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - ContentTypeRepository contentTypeRepository; - using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) - { - // Act - var query = Query.Builder.Where(x => x.Level == 2); - long totalRecords; - var result = repository.GetPagedResultsByQuery(query, 0, 1, out totalRecords, "Name", Direction.Descending); - - // Assert - Assert.That(totalRecords, Is.GreaterThanOrEqualTo(2)); - Assert.That(result.Count(), Is.EqualTo(1)); - Assert.That(result.First().Name, Is.EqualTo("Text Page 2")); - } - } - - [Test] - public void Can_Perform_GetPagedResultsByQuery_WithFilterMatchingSome_On_ContentRepository() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - ContentTypeRepository contentTypeRepository; - using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) - { - // Act - var query = Query.Builder.Where(x => x.Level == 2); - long totalRecords; - var result = repository.GetPagedResultsByQuery(query, 0, 1, out totalRecords, "Name", Direction.Ascending, "Page 2"); - - // Assert - Assert.That(totalRecords, Is.EqualTo(1)); - Assert.That(result.Count(), Is.EqualTo(1)); - Assert.That(result.First().Name, Is.EqualTo("Text Page 2")); - } - } - - [Test] - public void Can_Perform_GetPagedResultsByQuery_WithFilterMatchingAll_On_ContentRepository() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - ContentTypeRepository contentTypeRepository; - using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) - { - // Act - var query = Query.Builder.Where(x => x.Level == 2); - long totalRecords; - var result = repository.GetPagedResultsByQuery(query, 0, 1, out totalRecords, "Name", Direction.Ascending, "Page"); - - // Assert - Assert.That(totalRecords, Is.EqualTo(2)); - Assert.That(result.Count(), Is.EqualTo(1)); - Assert.That(result.First().Name, Is.EqualTo("Text Page 1")); - } - } - - [Test] - public void Can_Perform_GetAll_By_Param_Ids_On_ContentRepository() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - ContentTypeRepository contentTypeRepository; - using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) - { - // Act - var contents = repository.GetAll(NodeDto.NodeIdSeed + 2, NodeDto.NodeIdSeed + 3); - - // Assert - Assert.That(contents, Is.Not.Null); - Assert.That(contents.Any(), Is.True); - Assert.That(contents.Count(), Is.EqualTo(2)); - } - } - - [Test] - public void Can_Perform_GetAll_On_ContentRepository() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - ContentTypeRepository contentTypeRepository; - using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) - { - // Act - var contents = repository.GetAll(); - - // Assert - Assert.That(contents, Is.Not.Null); - Assert.That(contents.Any(), Is.True); - Assert.That(contents.Count(), Is.GreaterThanOrEqualTo(4)); - } - - - } - - [Test] - public void Can_Perform_Exists_On_ContentRepository() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - ContentTypeRepository contentTypeRepository; - using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) - { - // Act - var exists = repository.Exists(NodeDto.NodeIdSeed + 1); - - // Assert - Assert.That(exists, Is.True); - } - - - } - - [Test] - public void Can_Perform_Count_On_ContentRepository() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - ContentTypeRepository contentTypeRepository; - using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) - { - // Act - int level = 2; - var query = Query.Builder.Where(x => x.Level == level); - var result = repository.Count(query); - - // Assert - Assert.That(result, Is.GreaterThanOrEqualTo(2)); - } - } - - [Test] - public void Can_Verify_Keys_Set() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - ContentTypeRepository contentTypeRepository; - using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) - { - // Act - var textpage = repository.Get(NodeDto.NodeIdSeed + 1); - var subpage = repository.Get(NodeDto.NodeIdSeed + 2); - var trashed = repository.Get(NodeDto.NodeIdSeed + 4); - - // Assert - Assert.That(textpage.Key.ToString().ToUpper(), Is.EqualTo("B58B3AD4-62C2-4E27-B1BE-837BD7C533E0")); - Assert.That(subpage.Key.ToString().ToUpper(), Is.EqualTo("FF11402B-7E53-4654-81A7-462AC2108059")); - Assert.That(trashed.Key, Is.Not.EqualTo(Guid.Empty)); - } - } - - [Test] - public void Can_Get_Content_By_Guid_Key() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - ContentTypeRepository contentTypeRepository; - using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) - { - // Act - var query = Query.Builder.Where(x => x.Key == new Guid("B58B3AD4-62C2-4E27-B1BE-837BD7C533E0")); - var content = repository.GetByQuery(query).SingleOrDefault(); - - // Assert - Assert.That(content, Is.Not.Null); - Assert.That(content.Id, Is.EqualTo(NodeDto.NodeIdSeed + 1)); - } - - } - - public void CreateTestData() - { - //Create and Save ContentType "umbTextpage" -> (NodeDto.NodeIdSeed) - ContentType contentType = MockedContentTypes.CreateSimpleContentType("umbTextpage", "Textpage"); - contentType.Key = new Guid("1D3A8E6E-2EA9-4CC1-B229-1AEE19821522"); - ServiceContext.ContentTypeService.Save(contentType); - - //Create and Save Content "Homepage" based on "umbTextpage" -> (NodeDto.NodeIdSeed + 1) - Content textpage = MockedContent.CreateSimpleContent(contentType); - textpage.Key = new Guid("B58B3AD4-62C2-4E27-B1BE-837BD7C533E0"); - ServiceContext.ContentService.Save(textpage, 0); - - //Create and Save Content "Text Page 1" based on "umbTextpage" -> (NodeDto.NodeIdSeed + 2) - Content subpage = MockedContent.CreateSimpleContent(contentType, "Text Page 1", textpage.Id); - subpage.Key = new Guid("FF11402B-7E53-4654-81A7-462AC2108059"); - ServiceContext.ContentService.Save(subpage, 0); - - //Create and Save Content "Text Page 1" based on "umbTextpage" -> (NodeDto.NodeIdSeed + 3) - Content subpage2 = MockedContent.CreateSimpleContent(contentType, "Text Page 2", textpage.Id); - ServiceContext.ContentService.Save(subpage2, 0); - - //Create and Save Content "Text Page Deleted" based on "umbTextpage" -> (NodeDto.NodeIdSeed + 4) - Content trashed = MockedContent.CreateSimpleContent(contentType, "Text Page Deleted", -20); - trashed.Trashed = true; - ServiceContext.ContentService.Save(trashed, 0); - } - } + [DatabaseTestBehavior(DatabaseBehavior.NewDbFileAndSchemaPerTest)] + [TestFixture] + public class ContentRepositoryTest : BaseDatabaseFactoryTest + { + [SetUp] + public override void Initialize() + { + base.Initialize(); + + CreateTestData(); + } + + [TearDown] + public override void TearDown() + { + base.TearDown(); + } + + private ContentRepository CreateRepository(IDatabaseUnitOfWork unitOfWork, out ContentTypeRepository contentTypeRepository) + { + TemplateRepository tr; + return CreateRepository(unitOfWork, out contentTypeRepository, out tr); + } + + private ContentRepository CreateRepository(IDatabaseUnitOfWork unitOfWork, out ContentTypeRepository contentTypeRepository, out TemplateRepository templateRepository) + { + templateRepository = new TemplateRepository(unitOfWork, CacheHelper, Logger, SqlSyntax, Mock.Of(), Mock.Of(), Mock.Of()); + var tagRepository = new TagRepository(unitOfWork, CacheHelper, Logger, SqlSyntax); + contentTypeRepository = new ContentTypeRepository(unitOfWork, CacheHelper, Logger, SqlSyntax, templateRepository); + var repository = new ContentRepository(unitOfWork, CacheHelper, Logger, SqlSyntax, contentTypeRepository, templateRepository, tagRepository, Mock.Of()); + return repository; + } + + [Test] + public void Rebuild_Xml_Structures_With_Non_Latest_Version() + { + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + ContentTypeRepository contentTypeRepository; + using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) + { + var contentType1 = MockedContentTypes.CreateSimpleContentType("Textpage1", "Textpage1"); + contentTypeRepository.AddOrUpdate(contentType1); + + var allCreated = new List(); + + //create 100 non published + for (var i = 0; i < 100; i++) + { + var c1 = MockedContent.CreateSimpleContent(contentType1); + repository.AddOrUpdate(c1); + allCreated.Add(c1); + } + //create 100 published + for (var i = 0; i < 100; i++) + { + var c1 = MockedContent.CreateSimpleContent(contentType1); + c1.ChangePublishedState(PublishedState.Published); + repository.AddOrUpdate(c1); + allCreated.Add(c1); + } + unitOfWork.Commit(); + + //now create some versions of this content - this shouldn't affect the xml structures saved + for (int i = 0; i < allCreated.Count; i++) + { + allCreated[i].Name = "blah" + i; + //IMPORTANT testing note here: We need to changed the published state here so that + // it doesn't automatically think this is simply publishing again - this forces the latest + // version to be Saved and not published + allCreated[i].ChangePublishedState(PublishedState.Saved); + repository.AddOrUpdate(allCreated[i]); + } + unitOfWork.Commit(); + + //delete all xml + unitOfWork.Database.Execute("DELETE FROM cmsContentXml"); + Assert.AreEqual(0, unitOfWork.Database.ExecuteScalar("SELECT COUNT(*) FROM cmsContentXml")); + + repository.RebuildXmlStructures(media => new XElement("test"), 10); + + Assert.AreEqual(100, unitOfWork.Database.ExecuteScalar("SELECT COUNT(*) FROM cmsContentXml")); + } + } + + [Test] + public void Rebuild_All_Xml_Structures() + { + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + ContentTypeRepository contentTypeRepository; + using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) + { + + var contentType1 = MockedContentTypes.CreateSimpleContentType("Textpage1", "Textpage1"); + contentTypeRepository.AddOrUpdate(contentType1); + var allCreated = new List(); + + for (var i = 0; i < 100; i++) + { + //These will be non-published so shouldn't show up + var c1 = MockedContent.CreateSimpleContent(contentType1); + repository.AddOrUpdate(c1); + allCreated.Add(c1); + } + for (var i = 0; i < 100; i++) + { + var c1 = MockedContent.CreateSimpleContent(contentType1); + c1.ChangePublishedState(PublishedState.Published); + repository.AddOrUpdate(c1); + allCreated.Add(c1); + } + unitOfWork.Commit(); + + //now create some versions of this content - this shouldn't affect the xml structures saved + for (int i = 0; i < allCreated.Count; i++) + { + allCreated[i].Name = "blah" + i; + repository.AddOrUpdate(allCreated[i]); + } + unitOfWork.Commit(); + + //delete all xml + unitOfWork.Database.Execute("DELETE FROM cmsContentXml"); + Assert.AreEqual(0, unitOfWork.Database.ExecuteScalar("SELECT COUNT(*) FROM cmsContentXml")); + + repository.RebuildXmlStructures(media => new XElement("test"), 10); + + Assert.AreEqual(100, unitOfWork.Database.ExecuteScalar("SELECT COUNT(*) FROM cmsContentXml")); + } + } + + [Test] + public void Rebuild_All_Xml_Structures_For_Content_Type() + { + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + ContentTypeRepository contentTypeRepository; + using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) + { + var contentType1 = MockedContentTypes.CreateSimpleContentType("Textpage1", "Textpage1"); + var contentType2 = MockedContentTypes.CreateSimpleContentType("Textpage2", "Textpage2"); + var contentType3 = MockedContentTypes.CreateSimpleContentType("Textpage3", "Textpage3"); + contentTypeRepository.AddOrUpdate(contentType1); + contentTypeRepository.AddOrUpdate(contentType2); + contentTypeRepository.AddOrUpdate(contentType3); + + var allCreated = new List(); + + for (var i = 0; i < 30; i++) + { + //These will be non-published so shouldn't show up + var c1 = MockedContent.CreateSimpleContent(contentType1); + repository.AddOrUpdate(c1); + allCreated.Add(c1); + } + for (var i = 0; i < 30; i++) + { + var c1 = MockedContent.CreateSimpleContent(contentType1); + c1.ChangePublishedState(PublishedState.Published); + repository.AddOrUpdate(c1); + allCreated.Add(c1); + } + for (var i = 0; i < 30; i++) + { + var c1 = MockedContent.CreateSimpleContent(contentType2); + c1.ChangePublishedState(PublishedState.Published); + repository.AddOrUpdate(c1); + allCreated.Add(c1); + } + for (var i = 0; i < 30; i++) + { + var c1 = MockedContent.CreateSimpleContent(contentType3); + c1.ChangePublishedState(PublishedState.Published); + repository.AddOrUpdate(c1); + allCreated.Add(c1); + } + unitOfWork.Commit(); + + //now create some versions of this content - this shouldn't affect the xml structures saved + for (int i = 0; i < allCreated.Count; i++) + { + allCreated[i].Name = "blah" + i; + repository.AddOrUpdate(allCreated[i]); + } + unitOfWork.Commit(); + + //delete all xml + unitOfWork.Database.Execute("DELETE FROM cmsContentXml"); + Assert.AreEqual(0, unitOfWork.Database.ExecuteScalar("SELECT COUNT(*) FROM cmsContentXml")); + + repository.RebuildXmlStructures(media => new XElement("test"), 10, contentTypeIds: new[] { contentType1.Id, contentType2.Id }); + + Assert.AreEqual(60, unitOfWork.Database.ExecuteScalar("SELECT COUNT(*) FROM cmsContentXml")); + } + } + + [Test] + public void Ensures_Permissions_Are_Set_If_Parent_Entity_Permissions_Exist() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + + ContentTypeRepository contentTypeRepository; + using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) + { + var contentType = MockedContentTypes.CreateSimpleContentType("umbTextpage1", "Textpage"); + contentType.AllowedContentTypes = new List + { + new ContentTypeSort(new Lazy(() => contentType.Id), 0, contentType.Alias) + }; + var parentPage = MockedContent.CreateSimpleContent(contentType); + contentTypeRepository.AddOrUpdate(contentType); + repository.AddOrUpdate(parentPage); + unitOfWork.Commit(); + + // Act + repository.AssignEntityPermission(parentPage, 'A', new int[] { 0 }); + var childPage = MockedContent.CreateSimpleContent(contentType, "child", parentPage); + repository.AddOrUpdate(childPage); + unitOfWork.Commit(); + + // Assert + var permissions = repository.GetPermissionsForEntity(childPage.Id); + Assert.AreEqual(1, permissions.Count()); + Assert.AreEqual("A", permissions.Single().AssignedPermissions.First()); + } + + } + + [Test] + public void Can_Perform_Add_On_ContentRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + ContentTypeRepository contentTypeRepository; + using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) + { + ContentType contentType = MockedContentTypes.CreateSimpleContentType("umbTextpage2", "Textpage"); + Content textpage = MockedContent.CreateSimpleContent(contentType); + + // Act + contentTypeRepository.AddOrUpdate(contentType); + repository.AddOrUpdate(textpage); + unitOfWork.Commit(); + + // Assert + Assert.That(contentType.HasIdentity, Is.True); + Assert.That(textpage.HasIdentity, Is.True); + + } + } + + [Test] + public void Can_Perform_Add_With_Default_Template() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + ContentTypeRepository contentTypeRepository; + TemplateRepository templateRepository; + using (var repository = CreateRepository(unitOfWork, out contentTypeRepository, out templateRepository)) + { + var template = new Template("hello", "hello"); + templateRepository.AddOrUpdate(template); + unitOfWork.Commit(); + + ContentType contentType = MockedContentTypes.CreateSimpleContentType("umbTextpage2", "Textpage"); + contentType.AllowedTemplates = Enumerable.Empty(); // because CreateSimple... assigns one + contentType.SetDefaultTemplate(template); + Content textpage = MockedContent.CreateSimpleContent(contentType); + + // Act + + contentTypeRepository.AddOrUpdate(contentType); + repository.AddOrUpdate(textpage); + unitOfWork.Commit(); + + // Assert + Assert.That(textpage.Template, Is.Not.Null); + Assert.That(textpage.Template, Is.EqualTo(contentType.DefaultTemplate)); + } + } + + //Covers issue U4-2791 and U4-2607 + [Test] + public void Can_Save_Content_With_AtSign_In_Name_On_ContentRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + ContentTypeRepository contentTypeRepository; + using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) + { + var contentType = MockedContentTypes.CreateSimpleContentType("umbTextpage1", "Textpage"); + contentTypeRepository.AddOrUpdate(contentType); + unitOfWork.Commit(); + + var textpage = MockedContent.CreateSimpleContent(contentType, "test@umbraco.org", -1); + var anotherTextpage = MockedContent.CreateSimpleContent(contentType, "@lightgiants", -1); + + // Act + + repository.AddOrUpdate(textpage); + repository.AddOrUpdate(anotherTextpage); + unitOfWork.Commit(); + + // Assert + Assert.That(contentType.HasIdentity, Is.True); + Assert.That(textpage.HasIdentity, Is.True); + + var content = repository.Get(textpage.Id); + Assert.That(content.Name, Is.EqualTo(textpage.Name)); + + var content2 = repository.Get(anotherTextpage.Id); + Assert.That(content2.Name, Is.EqualTo(anotherTextpage.Name)); + } + } + + [Test] + public void Can_Perform_Multiple_Adds_On_ContentRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + ContentTypeRepository contentTypeRepository; + using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) + { + ContentType contentType = MockedContentTypes.CreateSimpleContentType("umbTextpage1", "Textpage"); + Content textpage = MockedContent.CreateSimpleContent(contentType); + + // Act + contentTypeRepository.AddOrUpdate(contentType); + repository.AddOrUpdate(textpage); + unitOfWork.Commit(); + + Content subpage = MockedContent.CreateSimpleContent(contentType, "Text Page 1", textpage.Id); + repository.AddOrUpdate(subpage); + unitOfWork.Commit(); + + // Assert + Assert.That(contentType.HasIdentity, Is.True); + Assert.That(textpage.HasIdentity, Is.True); + Assert.That(subpage.HasIdentity, Is.True); + Assert.That(textpage.Id, Is.EqualTo(subpage.ParentId)); + } + + } + + + [Test] + public void Can_Verify_Fresh_Entity_Is_Not_Dirty() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + ContentTypeRepository contentTypeRepository; + using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) + { + // Act + var content = repository.Get(NodeDto.NodeIdSeed + 3); + bool dirty = ((Content)content).IsDirty(); + + // Assert + Assert.That(dirty, Is.False); + } + } + + [Test] + public void Can_Perform_Update_On_ContentRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + ContentTypeRepository contentTypeRepository; + using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) + { + // Act + var content = repository.Get(NodeDto.NodeIdSeed + 2); + content.Name = "About 2"; + repository.AddOrUpdate(content); + unitOfWork.Commit(); + var updatedContent = repository.Get(NodeDto.NodeIdSeed + 2); + + // Assert + Assert.That(updatedContent.Id, Is.EqualTo(content.Id)); + Assert.That(updatedContent.Name, Is.EqualTo(content.Name)); + } + + } + + [Test] + public void Can_Update_With_Null_Template() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + ContentTypeRepository contentTypeRepository; + using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) + { + // Act + var content = repository.Get(NodeDto.NodeIdSeed + 2); + content.Template = null; + repository.AddOrUpdate(content); + unitOfWork.Commit(); + var updatedContent = repository.Get(NodeDto.NodeIdSeed + 2); + + // Assert + Assert.That(updatedContent.Template, Is.Null); + } + + } + + [Test] + public void Can_Perform_Delete_On_ContentRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + ContentTypeRepository contentTypeRepository; + using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) + { + var contentType = contentTypeRepository.Get(NodeDto.NodeIdSeed); + var content = new Content("Textpage 2 Child Node", NodeDto.NodeIdSeed + 3, contentType); + content.CreatorId = 0; + content.WriterId = 0; + + // Act + repository.AddOrUpdate(content); + unitOfWork.Commit(); + var id = content.Id; + + repository.Delete(content); + unitOfWork.Commit(); + + var content1 = repository.Get(id); + + // Assert + Assert.That(content1, Is.Null); + } + } + + [Test] + public void Can_Perform_Get_On_ContentRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + ContentTypeRepository contentTypeRepository; + using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) + { + // Act + var content = repository.Get(NodeDto.NodeIdSeed + 3); + + // Assert + Assert.That(content.Id, Is.EqualTo(NodeDto.NodeIdSeed + 3)); + Assert.That(content.CreateDate, Is.GreaterThan(DateTime.MinValue)); + Assert.That(content.UpdateDate, Is.GreaterThan(DateTime.MinValue)); + Assert.That(content.ParentId, Is.Not.EqualTo(0)); + Assert.That(content.Name, Is.EqualTo("Text Page 2")); + //Assert.That(content.SortOrder, Is.EqualTo(1)); + Assert.That(content.Version, Is.Not.EqualTo(Guid.Empty)); + Assert.That(content.ContentTypeId, Is.EqualTo(NodeDto.NodeIdSeed)); + Assert.That(content.Path, Is.Not.Empty); + Assert.That(content.Properties.Any(), Is.True); + } + } + + [Test] + public void Can_Perform_GetByQuery_On_ContentRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + ContentTypeRepository contentTypeRepository; + using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) + { + // Act + var query = Query.Builder.Where(x => x.Level == 2); + var result = repository.GetByQuery(query); + + // Assert + Assert.That(result.Count(), Is.GreaterThanOrEqualTo(2)); + } + } + + [Test] + public void Can_Perform_Get_All_With_Many_Version() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + ContentTypeRepository contentTypeRepository; + using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) + { + var result = repository.GetAll().ToArray(); + foreach (var content in result) + { + content.ChangePublishedState(PublishedState.Saved); + repository.AddOrUpdate(content); + } + unitOfWork.Commit(); + foreach (var content in result) + { + content.ChangePublishedState(PublishedState.Published); + repository.AddOrUpdate(content); + } + unitOfWork.Commit(); + + //re-get + + var result2 = repository.GetAll().ToArray(); + + Assert.AreEqual(result.Count(), result2.Count()); + } + } + + [Test] + public void Can_Perform_GetPagedResultsByQuery_ForFirstPage_On_ContentRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + ContentTypeRepository contentTypeRepository; + using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) + { + // Act + var query = Query.Builder.Where(x => x.Level == 2); + long totalRecords; + var result = repository.GetPagedResultsByQuery(query, 0, 1, out totalRecords, "Name", Direction.Ascending, orderBySystemField: true); + + // Assert + Assert.That(totalRecords, Is.GreaterThanOrEqualTo(2)); + Assert.That(result.Count(), Is.EqualTo(1)); + Assert.That(result.First().Name, Is.EqualTo("Text Page 1")); + } + } + + [Test] + public void Can_Perform_GetPagedResultsByQuery_ForSecondPage_On_ContentRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + ContentTypeRepository contentTypeRepository; + using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) + { + // Act + var query = Query.Builder.Where(x => x.Level == 2); + long totalRecords; + var result = repository.GetPagedResultsByQuery(query, 1, 1, out totalRecords, "Name", Direction.Ascending, orderBySystemField: true); + + // Assert + Assert.That(totalRecords, Is.GreaterThanOrEqualTo(2)); + Assert.That(result.Count(), Is.EqualTo(1)); + Assert.That(result.First().Name, Is.EqualTo("Text Page 2")); + } + } + + [Test] + public void Can_Perform_GetPagedResultsByQuery_WithSinglePage_On_ContentRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + ContentTypeRepository contentTypeRepository; + using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) + { + // Act + var query = Query.Builder.Where(x => x.Level == 2); + long totalRecords; + var result = repository.GetPagedResultsByQuery(query, 0, 2, out totalRecords, "Name", Direction.Ascending, orderBySystemField: true); + + // Assert + Assert.That(totalRecords, Is.GreaterThanOrEqualTo(2)); + Assert.That(result.Count(), Is.EqualTo(2)); + Assert.That(result.First().Name, Is.EqualTo("Text Page 1")); + } + } + + [Test] + public void Can_Perform_GetPagedResultsByQuery_WithDescendingOrder_On_ContentRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + ContentTypeRepository contentTypeRepository; + using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) + { + // Act + var query = Query.Builder.Where(x => x.Level == 2); + long totalRecords; + var result = repository.GetPagedResultsByQuery(query, 0, 1, out totalRecords, "Name", Direction.Descending, orderBySystemField: true); + + // Assert + Assert.That(totalRecords, Is.GreaterThanOrEqualTo(2)); + Assert.That(result.Count(), Is.EqualTo(1)); + Assert.That(result.First().Name, Is.EqualTo("Text Page 2")); + } + } + + [Test] + public void Can_Perform_GetPagedResultsByQuery_WithFilterMatchingSome_On_ContentRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + ContentTypeRepository contentTypeRepository; + using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) + { + // Act + var query = Query.Builder.Where(x => x.Level == 2); + long totalRecords; + var result = repository.GetPagedResultsByQuery(query, 0, 1, out totalRecords, "Name", Direction.Ascending, true, "Page 2"); + + // Assert + Assert.That(totalRecords, Is.EqualTo(1)); + Assert.That(result.Count(), Is.EqualTo(1)); + Assert.That(result.First().Name, Is.EqualTo("Text Page 2")); + } + } + + [Test] + public void Can_Perform_GetPagedResultsByQuery_WithFilterMatchingAll_On_ContentRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + ContentTypeRepository contentTypeRepository; + using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) + { + // Act + var query = Query.Builder.Where(x => x.Level == 2); + long totalRecords; + var result = repository.GetPagedResultsByQuery(query, 0, 1, out totalRecords, "Name", Direction.Ascending, true, "Page"); + + // Assert + Assert.That(totalRecords, Is.EqualTo(2)); + Assert.That(result.Count(), Is.EqualTo(1)); + Assert.That(result.First().Name, Is.EqualTo("Text Page 1")); + } + } + + [Test] + public void Can_Perform_GetAll_By_Param_Ids_On_ContentRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + ContentTypeRepository contentTypeRepository; + using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) + { + // Act + var contents = repository.GetAll(NodeDto.NodeIdSeed + 2, NodeDto.NodeIdSeed + 3); + + // Assert + Assert.That(contents, Is.Not.Null); + Assert.That(contents.Any(), Is.True); + Assert.That(contents.Count(), Is.EqualTo(2)); + } + } + + [Test] + public void Can_Perform_GetAll_On_ContentRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + ContentTypeRepository contentTypeRepository; + using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) + { + // Act + var contents = repository.GetAll(); + + // Assert + Assert.That(contents, Is.Not.Null); + Assert.That(contents.Any(), Is.True); + Assert.That(contents.Count(), Is.GreaterThanOrEqualTo(4)); + } + + + } + + [Test] + public void Can_Perform_Exists_On_ContentRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + ContentTypeRepository contentTypeRepository; + using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) + { + // Act + var exists = repository.Exists(NodeDto.NodeIdSeed + 1); + + // Assert + Assert.That(exists, Is.True); + } + + + } + + [Test] + public void Can_Perform_Count_On_ContentRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + ContentTypeRepository contentTypeRepository; + using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) + { + // Act + int level = 2; + var query = Query.Builder.Where(x => x.Level == level); + var result = repository.Count(query); + + // Assert + Assert.That(result, Is.GreaterThanOrEqualTo(2)); + } + } + + [Test] + public void Can_Verify_Keys_Set() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + ContentTypeRepository contentTypeRepository; + using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) + { + // Act + var textpage = repository.Get(NodeDto.NodeIdSeed + 1); + var subpage = repository.Get(NodeDto.NodeIdSeed + 2); + var trashed = repository.Get(NodeDto.NodeIdSeed + 4); + + // Assert + Assert.That(textpage.Key.ToString().ToUpper(), Is.EqualTo("B58B3AD4-62C2-4E27-B1BE-837BD7C533E0")); + Assert.That(subpage.Key.ToString().ToUpper(), Is.EqualTo("FF11402B-7E53-4654-81A7-462AC2108059")); + Assert.That(trashed.Key, Is.Not.EqualTo(Guid.Empty)); + } + } + + [Test] + public void Can_Get_Content_By_Guid_Key() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + ContentTypeRepository contentTypeRepository; + using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) + { + // Act + var query = Query.Builder.Where(x => x.Key == new Guid("B58B3AD4-62C2-4E27-B1BE-837BD7C533E0")); + var content = repository.GetByQuery(query).SingleOrDefault(); + + // Assert + Assert.That(content, Is.Not.Null); + Assert.That(content.Id, Is.EqualTo(NodeDto.NodeIdSeed + 1)); + } + + } + + public void CreateTestData() + { + //Create and Save ContentType "umbTextpage" -> (NodeDto.NodeIdSeed) + ContentType contentType = MockedContentTypes.CreateSimpleContentType("umbTextpage", "Textpage"); + contentType.Key = new Guid("1D3A8E6E-2EA9-4CC1-B229-1AEE19821522"); + ServiceContext.ContentTypeService.Save(contentType); + + //Create and Save Content "Homepage" based on "umbTextpage" -> (NodeDto.NodeIdSeed + 1) + Content textpage = MockedContent.CreateSimpleContent(contentType); + textpage.Key = new Guid("B58B3AD4-62C2-4E27-B1BE-837BD7C533E0"); + ServiceContext.ContentService.Save(textpage, 0); + + //Create and Save Content "Text Page 1" based on "umbTextpage" -> (NodeDto.NodeIdSeed + 2) + Content subpage = MockedContent.CreateSimpleContent(contentType, "Text Page 1", textpage.Id); + subpage.Key = new Guid("FF11402B-7E53-4654-81A7-462AC2108059"); + ServiceContext.ContentService.Save(subpage, 0); + + //Create and Save Content "Text Page 1" based on "umbTextpage" -> (NodeDto.NodeIdSeed + 3) + Content subpage2 = MockedContent.CreateSimpleContent(contentType, "Text Page 2", textpage.Id); + ServiceContext.ContentService.Save(subpage2, 0); + + //Create and Save Content "Text Page Deleted" based on "umbTextpage" -> (NodeDto.NodeIdSeed + 4) + Content trashed = MockedContent.CreateSimpleContent(contentType, "Text Page Deleted", -20); + trashed.Trashed = true; + ServiceContext.ContentService.Save(trashed, 0); + } + } } \ No newline at end of file diff --git a/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs index 60c49f89cd..4ca987fe26 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs @@ -17,547 +17,547 @@ using Umbraco.Core.Persistence.DatabaseModelDefinitions; namespace Umbraco.Tests.Persistence.Repositories { - [DatabaseTestBehavior(DatabaseBehavior.NewDbFileAndSchemaPerTest)] - [TestFixture] - public class MediaRepositoryTest : BaseDatabaseFactoryTest - { - public MediaRepositoryTest() - { - } - - [SetUp] - public override void Initialize() - { - base.Initialize(); - - CreateTestData(); - } - - private MediaRepository CreateRepository(IDatabaseUnitOfWork unitOfWork, out MediaTypeRepository mediaTypeRepository) - { - mediaTypeRepository = new MediaTypeRepository(unitOfWork, CacheHelper, Mock.Of(), SqlSyntax); - var tagRepository = new TagRepository(unitOfWork, CacheHelper, Mock.Of(), SqlSyntax); - var repository = new MediaRepository(unitOfWork, CacheHelper, Mock.Of(), SqlSyntax, mediaTypeRepository, tagRepository, Mock.Of()); - return repository; - } - - [Test] - public void Rebuild_All_Xml_Structures() - { - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - MediaTypeRepository mediaTypeRepository; - using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository)) - { - - var mediaType = mediaTypeRepository.Get(1032); - - for (var i = 0; i < 100; i++) - { - var image = MockedMedia.CreateMediaImage(mediaType, -1); - repository.AddOrUpdate(image); - } - unitOfWork.Commit(); - - //delete all xml - unitOfWork.Database.Execute("DELETE FROM cmsContentXml"); - Assert.AreEqual(0, unitOfWork.Database.ExecuteScalar("SELECT COUNT(*) FROM cmsContentXml")); - - repository.RebuildXmlStructures(media => new XElement("test"), 10); - - Assert.AreEqual(103, unitOfWork.Database.ExecuteScalar("SELECT COUNT(*) FROM cmsContentXml")); - } - } - - [Test] - public void Rebuild_All_Xml_Structures_For_Content_Type() - { - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - MediaTypeRepository mediaTypeRepository; - using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository)) - { - - var imageMediaType = mediaTypeRepository.Get(1032); - var fileMediaType = mediaTypeRepository.Get(1033); - var folderMediaType = mediaTypeRepository.Get(1031); - - for (var i = 0; i < 30; i++) - { - var image = MockedMedia.CreateMediaImage(imageMediaType, -1); - repository.AddOrUpdate(image); - } - for (var i = 0; i < 30; i++) - { - var file = MockedMedia.CreateMediaFile(fileMediaType, -1); - repository.AddOrUpdate(file); - } - for (var i = 0; i < 30; i++) - { - var folder = MockedMedia.CreateMediaFolder(folderMediaType, -1); - repository.AddOrUpdate(folder); - } - unitOfWork.Commit(); - - //delete all xml - unitOfWork.Database.Execute("DELETE FROM cmsContentXml"); - Assert.AreEqual(0, unitOfWork.Database.ExecuteScalar("SELECT COUNT(*) FROM cmsContentXml")); - - repository.RebuildXmlStructures(media => new XElement("test"), 10, contentTypeIds: new[] {1032, 1033}); - - Assert.AreEqual(62, unitOfWork.Database.ExecuteScalar("SELECT COUNT(*) FROM cmsContentXml")); - } - } - - [Test] - public void Can_Perform_Add_On_MediaRepository() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - MediaTypeRepository mediaTypeRepository; - using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository)) - { - - var mediaType = mediaTypeRepository.Get(1032); - var image = MockedMedia.CreateMediaImage(mediaType, -1); - - // Act - mediaTypeRepository.AddOrUpdate(mediaType); - repository.AddOrUpdate(image); - unitOfWork.Commit(); - - // Assert - Assert.That(mediaType.HasIdentity, Is.True); - Assert.That(image.HasIdentity, Is.True); - } - } - - [Test] - public void Can_Perform_Multiple_Adds_On_MediaRepository() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - MediaTypeRepository mediaTypeRepository; - using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository)) - { - - var mediaType = mediaTypeRepository.Get(1032); - var file = MockedMedia.CreateMediaFile(mediaType, -1); - - // Act - repository.AddOrUpdate(file); - unitOfWork.Commit(); - - var image = MockedMedia.CreateMediaImage(mediaType, -1); - repository.AddOrUpdate(image); - unitOfWork.Commit(); - - // Assert - Assert.That(file.HasIdentity, Is.True); - Assert.That(image.HasIdentity, Is.True); - Assert.That(file.Name, Is.EqualTo("Test File")); - Assert.That(image.Name, Is.EqualTo("Test Image")); - Assert.That(file.ContentTypeId, Is.EqualTo(mediaType.Id)); - Assert.That(image.ContentTypeId, Is.EqualTo(mediaType.Id)); - } - } - - [Test] - public void Can_Perform_Multiple_Adds_On_MediaRepository_With_RepositoryResolver() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - MediaTypeRepository mediaTypeRepository; - using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository)) - { - - var mediaType = mediaTypeRepository.Get(1032); - var file = MockedMedia.CreateMediaFile(mediaType, -1); - - // Act - repository.AddOrUpdate(file); - unitOfWork.Commit(); - - var image = MockedMedia.CreateMediaImage(mediaType, -1); - repository.AddOrUpdate(image); - unitOfWork.Commit(); - - // Assert - Assert.That(file.HasIdentity, Is.True); - Assert.That(image.HasIdentity, Is.True); - Assert.That(file.Name, Is.EqualTo("Test File")); - Assert.That(image.Name, Is.EqualTo("Test Image")); - Assert.That(file.ContentTypeId, Is.EqualTo(mediaType.Id)); - Assert.That(image.ContentTypeId, Is.EqualTo(mediaType.Id)); - } - } - - [Test] - public void Can_Verify_Fresh_Entity_Is_Not_Dirty() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - MediaTypeRepository mediaTypeRepository; - using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository)) - { - - // Act - var media = repository.Get(NodeDto.NodeIdSeed + 1); - bool dirty = ((ICanBeDirty) media).IsDirty(); - - // Assert - Assert.That(dirty, Is.False); - } - } - - [Test] - public void Can_Perform_Update_On_MediaRepository() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - MediaTypeRepository mediaTypeRepository; - using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository)) - { - - // Act - var content = repository.Get(NodeDto.NodeIdSeed + 2); - content.Name = "Test File Updated"; - repository.AddOrUpdate(content); - unitOfWork.Commit(); - - var updatedContent = repository.Get(NodeDto.NodeIdSeed + 2); - - // Assert - Assert.That(updatedContent.Id, Is.EqualTo(content.Id)); - Assert.That(updatedContent.Name, Is.EqualTo(content.Name)); - } - } - - [Test] - public void Can_Perform_Delete_On_MediaRepository() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - MediaTypeRepository mediaTypeRepository; - using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository)) - { - - // Act - var media = repository.Get(NodeDto.NodeIdSeed + 2); - repository.Delete(media); - unitOfWork.Commit(); - - var deleted = repository.Get(NodeDto.NodeIdSeed + 2); - var exists = repository.Exists(NodeDto.NodeIdSeed + 2); - - // Assert - Assert.That(deleted, Is.Null); - Assert.That(exists, Is.False); - } - } - - [Test] - public void Can_Perform_Get_On_MediaRepository() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - MediaTypeRepository mediaTypeRepository; - using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository)) - { - - // Act - var media = repository.Get(NodeDto.NodeIdSeed + 1); - - // Assert - Assert.That(media.Id, Is.EqualTo(NodeDto.NodeIdSeed + 1)); - Assert.That(media.CreateDate, Is.GreaterThan(DateTime.MinValue)); - Assert.That(media.UpdateDate, Is.GreaterThan(DateTime.MinValue)); - Assert.That(media.ParentId, Is.Not.EqualTo(0)); - Assert.That(media.Name, Is.EqualTo("Test Image")); - Assert.That(media.SortOrder, Is.EqualTo(0)); - Assert.That(media.Version, Is.Not.EqualTo(Guid.Empty)); - Assert.That(media.ContentTypeId, Is.EqualTo(1032)); - Assert.That(media.Path, Is.Not.Empty); - Assert.That(media.Properties.Any(), Is.True); - } - } - - [Test] - public void Can_Perform_GetByQuery_On_MediaRepository() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - MediaTypeRepository mediaTypeRepository; - using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository)) - { - // Act - var query = Query.Builder.Where(x => x.Level == 2); - var result = repository.GetByQuery(query); - - // Assert - Assert.That(result.Count(), Is.GreaterThanOrEqualTo(2)); //There should be two entities on level 2: File and Media - } - } - - [Test] - public void Can_Perform_GetPagedResultsByQuery_ForFirstPage_On_MediaRepository() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - MediaTypeRepository mediaTypeRepository; - using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository)) - { - // Act - var query = Query.Builder.Where(x => x.Level == 2); - long totalRecords; - var result = repository.GetPagedResultsByQuery(query, 0, 1, out totalRecords, "SortOrder", Direction.Ascending); - - // Assert - Assert.That(totalRecords, Is.GreaterThanOrEqualTo(2)); - Assert.That(result.Count(), Is.EqualTo(1)); - Assert.That(result.First().Name, Is.EqualTo("Test Image")); - } - } - - [Test] - public void Can_Perform_GetPagedResultsByQuery_ForSecondPage_On_MediaRepository() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - MediaTypeRepository mediaTypeRepository; - using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository)) - { - // Act - var query = Query.Builder.Where(x => x.Level == 2); - long totalRecords; - var result = repository.GetPagedResultsByQuery(query, 1, 1, out totalRecords, "SortOrder", Direction.Ascending); - - // Assert - Assert.That(totalRecords, Is.GreaterThanOrEqualTo(2)); - Assert.That(result.Count(), Is.EqualTo(1)); - Assert.That(result.First().Name, Is.EqualTo("Test File")); - } - } - - [Test] - public void Can_Perform_GetPagedResultsByQuery_WithSinglePage_On_MediaRepository() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - MediaTypeRepository mediaTypeRepository; - using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository)) - { - // Act - var query = Query.Builder.Where(x => x.Level == 2); - long totalRecords; - var result = repository.GetPagedResultsByQuery(query, 0, 2, out totalRecords, "SortOrder", Direction.Ascending); - - // Assert - Assert.That(totalRecords, Is.GreaterThanOrEqualTo(2)); - Assert.That(result.Count(), Is.EqualTo(2)); - Assert.That(result.First().Name, Is.EqualTo("Test Image")); - } - } - - [Test] - public void Can_Perform_GetPagedResultsByQuery_WithDescendingOrder_On_MediaRepository() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - MediaTypeRepository mediaTypeRepository; - using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository)) - { - // Act - var query = Query.Builder.Where(x => x.Level == 2); - long totalRecords; - var result = repository.GetPagedResultsByQuery(query, 0, 1, out totalRecords, "SortOrder", Direction.Descending); - - // Assert - Assert.That(totalRecords, Is.GreaterThanOrEqualTo(2)); - Assert.That(result.Count(), Is.EqualTo(1)); - Assert.That(result.First().Name, Is.EqualTo("Test File")); - } - } - - [Test] - public void Can_Perform_GetPagedResultsByQuery_WitAlternateOrder_On_MediaRepository() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - MediaTypeRepository mediaTypeRepository; - using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository)) - { - // Act - var query = Query.Builder.Where(x => x.Level == 2); - long totalRecords; - var result = repository.GetPagedResultsByQuery(query, 0, 1, out totalRecords, "Name", Direction.Ascending); - - // Assert - Assert.That(totalRecords, Is.GreaterThanOrEqualTo(2)); - Assert.That(result.Count(), Is.EqualTo(1)); - Assert.That(result.First().Name, Is.EqualTo("Test File")); - } - } - - [Test] - public void Can_Perform_GetPagedResultsByQuery_WithFilterMatchingSome_On_MediaRepository() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - MediaTypeRepository mediaTypeRepository; - using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository)) - { - // Act - var query = Query.Builder.Where(x => x.Level == 2); - long totalRecords; - var result = repository.GetPagedResultsByQuery(query, 0, 1, out totalRecords, "SortOrder", Direction.Ascending, "File"); - - // Assert - Assert.That(totalRecords, Is.EqualTo(1)); - Assert.That(result.Count(), Is.EqualTo(1)); - Assert.That(result.First().Name, Is.EqualTo("Test File")); - } - } - - [Test] - public void Can_Perform_GetPagedResultsByQuery_WithFilterMatchingAll_On_MediaRepository() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - MediaTypeRepository mediaTypeRepository; - using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository)) - { - // Act - var query = Query.Builder.Where(x => x.Level == 2); - long totalRecords; - var result = repository.GetPagedResultsByQuery(query, 0, 1, out totalRecords, "SortOrder", Direction.Ascending, "Test"); - - // Assert - Assert.That(totalRecords, Is.EqualTo(2)); - Assert.That(result.Count(), Is.EqualTo(1)); - Assert.That(result.First().Name, Is.EqualTo("Test Image")); - } - } - - [Test] - public void Can_Perform_GetAll_By_Param_Ids_On_MediaRepository() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - MediaTypeRepository mediaTypeRepository; - using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository)) - { - - // Act - var medias = repository.GetAll(NodeDto.NodeIdSeed + 1, NodeDto.NodeIdSeed + 2); - - // Assert - Assert.That(medias, Is.Not.Null); - Assert.That(medias.Any(), Is.True); - Assert.That(medias.Count(), Is.EqualTo(2)); - } - } - - [Test] - public void Can_Perform_GetAll_On_MediaRepository() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - MediaTypeRepository mediaTypeRepository; - using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository)) - { - - // Act - var medias = repository.GetAll(); - - // Assert - Assert.That(medias, Is.Not.Null); - Assert.That(medias.Any(), Is.True); - Assert.That(medias.Count(), Is.GreaterThanOrEqualTo(3)); - } - } - - [Test] - public void Can_Perform_Exists_On_MediaRepository() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - MediaTypeRepository mediaTypeRepository; - using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository)) - { - - // Act - var exists = repository.Exists(NodeDto.NodeIdSeed + 1); - var existsToo = repository.Exists(NodeDto.NodeIdSeed + 1); - var doesntExists = repository.Exists(NodeDto.NodeIdSeed + 5); - - // Assert - Assert.That(exists, Is.True); - Assert.That(existsToo, Is.True); - Assert.That(doesntExists, Is.False); - } - } - - [Test] - public void Can_Perform_Count_On_MediaRepository() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - MediaTypeRepository mediaTypeRepository; - using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository)) - { - - // Act - int level = 2; - var query = Query.Builder.Where(x => x.Level == level); - var result = repository.Count(query); - - // Assert - Assert.That(result, Is.GreaterThanOrEqualTo(2)); - } - } - - [TearDown] - public override void TearDown() - { - base.TearDown(); - } - - public void CreateTestData() - { - //Create and Save folder-Media -> (NodeDto.NodeIdSeed) - var folderMediaType = ServiceContext.ContentTypeService.GetMediaType(1031); - var folder = MockedMedia.CreateMediaFolder(folderMediaType, -1); - ServiceContext.MediaService.Save(folder, 0); - - //Create and Save image-Media -> (NodeDto.NodeIdSeed + 1) - var imageMediaType = ServiceContext.ContentTypeService.GetMediaType(1032); - var image = MockedMedia.CreateMediaImage(imageMediaType, folder.Id); - ServiceContext.MediaService.Save(image, 0); - - //Create and Save file-Media -> (NodeDto.NodeIdSeed + 2) - var fileMediaType = ServiceContext.ContentTypeService.GetMediaType(1033); - var file = MockedMedia.CreateMediaFile(fileMediaType, folder.Id); - ServiceContext.MediaService.Save(file, 0); - } - } + [DatabaseTestBehavior(DatabaseBehavior.NewDbFileAndSchemaPerTest)] + [TestFixture] + public class MediaRepositoryTest : BaseDatabaseFactoryTest + { + public MediaRepositoryTest() + { + } + + [SetUp] + public override void Initialize() + { + base.Initialize(); + + CreateTestData(); + } + + private MediaRepository CreateRepository(IDatabaseUnitOfWork unitOfWork, out MediaTypeRepository mediaTypeRepository) + { + mediaTypeRepository = new MediaTypeRepository(unitOfWork, CacheHelper, Mock.Of(), SqlSyntax); + var tagRepository = new TagRepository(unitOfWork, CacheHelper, Mock.Of(), SqlSyntax); + var repository = new MediaRepository(unitOfWork, CacheHelper, Mock.Of(), SqlSyntax, mediaTypeRepository, tagRepository, Mock.Of()); + return repository; + } + + [Test] + public void Rebuild_All_Xml_Structures() + { + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + MediaTypeRepository mediaTypeRepository; + using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository)) + { + + var mediaType = mediaTypeRepository.Get(1032); + + for (var i = 0; i < 100; i++) + { + var image = MockedMedia.CreateMediaImage(mediaType, -1); + repository.AddOrUpdate(image); + } + unitOfWork.Commit(); + + //delete all xml + unitOfWork.Database.Execute("DELETE FROM cmsContentXml"); + Assert.AreEqual(0, unitOfWork.Database.ExecuteScalar("SELECT COUNT(*) FROM cmsContentXml")); + + repository.RebuildXmlStructures(media => new XElement("test"), 10); + + Assert.AreEqual(103, unitOfWork.Database.ExecuteScalar("SELECT COUNT(*) FROM cmsContentXml")); + } + } + + [Test] + public void Rebuild_All_Xml_Structures_For_Content_Type() + { + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + MediaTypeRepository mediaTypeRepository; + using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository)) + { + + var imageMediaType = mediaTypeRepository.Get(1032); + var fileMediaType = mediaTypeRepository.Get(1033); + var folderMediaType = mediaTypeRepository.Get(1031); + + for (var i = 0; i < 30; i++) + { + var image = MockedMedia.CreateMediaImage(imageMediaType, -1); + repository.AddOrUpdate(image); + } + for (var i = 0; i < 30; i++) + { + var file = MockedMedia.CreateMediaFile(fileMediaType, -1); + repository.AddOrUpdate(file); + } + for (var i = 0; i < 30; i++) + { + var folder = MockedMedia.CreateMediaFolder(folderMediaType, -1); + repository.AddOrUpdate(folder); + } + unitOfWork.Commit(); + + //delete all xml + unitOfWork.Database.Execute("DELETE FROM cmsContentXml"); + Assert.AreEqual(0, unitOfWork.Database.ExecuteScalar("SELECT COUNT(*) FROM cmsContentXml")); + + repository.RebuildXmlStructures(media => new XElement("test"), 10, contentTypeIds: new[] { 1032, 1033 }); + + Assert.AreEqual(62, unitOfWork.Database.ExecuteScalar("SELECT COUNT(*) FROM cmsContentXml")); + } + } + + [Test] + public void Can_Perform_Add_On_MediaRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + MediaTypeRepository mediaTypeRepository; + using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository)) + { + + var mediaType = mediaTypeRepository.Get(1032); + var image = MockedMedia.CreateMediaImage(mediaType, -1); + + // Act + mediaTypeRepository.AddOrUpdate(mediaType); + repository.AddOrUpdate(image); + unitOfWork.Commit(); + + // Assert + Assert.That(mediaType.HasIdentity, Is.True); + Assert.That(image.HasIdentity, Is.True); + } + } + + [Test] + public void Can_Perform_Multiple_Adds_On_MediaRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + MediaTypeRepository mediaTypeRepository; + using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository)) + { + + var mediaType = mediaTypeRepository.Get(1032); + var file = MockedMedia.CreateMediaFile(mediaType, -1); + + // Act + repository.AddOrUpdate(file); + unitOfWork.Commit(); + + var image = MockedMedia.CreateMediaImage(mediaType, -1); + repository.AddOrUpdate(image); + unitOfWork.Commit(); + + // Assert + Assert.That(file.HasIdentity, Is.True); + Assert.That(image.HasIdentity, Is.True); + Assert.That(file.Name, Is.EqualTo("Test File")); + Assert.That(image.Name, Is.EqualTo("Test Image")); + Assert.That(file.ContentTypeId, Is.EqualTo(mediaType.Id)); + Assert.That(image.ContentTypeId, Is.EqualTo(mediaType.Id)); + } + } + + [Test] + public void Can_Perform_Multiple_Adds_On_MediaRepository_With_RepositoryResolver() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + MediaTypeRepository mediaTypeRepository; + using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository)) + { + + var mediaType = mediaTypeRepository.Get(1032); + var file = MockedMedia.CreateMediaFile(mediaType, -1); + + // Act + repository.AddOrUpdate(file); + unitOfWork.Commit(); + + var image = MockedMedia.CreateMediaImage(mediaType, -1); + repository.AddOrUpdate(image); + unitOfWork.Commit(); + + // Assert + Assert.That(file.HasIdentity, Is.True); + Assert.That(image.HasIdentity, Is.True); + Assert.That(file.Name, Is.EqualTo("Test File")); + Assert.That(image.Name, Is.EqualTo("Test Image")); + Assert.That(file.ContentTypeId, Is.EqualTo(mediaType.Id)); + Assert.That(image.ContentTypeId, Is.EqualTo(mediaType.Id)); + } + } + + [Test] + public void Can_Verify_Fresh_Entity_Is_Not_Dirty() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + MediaTypeRepository mediaTypeRepository; + using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository)) + { + + // Act + var media = repository.Get(NodeDto.NodeIdSeed + 1); + bool dirty = ((ICanBeDirty)media).IsDirty(); + + // Assert + Assert.That(dirty, Is.False); + } + } + + [Test] + public void Can_Perform_Update_On_MediaRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + MediaTypeRepository mediaTypeRepository; + using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository)) + { + + // Act + var content = repository.Get(NodeDto.NodeIdSeed + 2); + content.Name = "Test File Updated"; + repository.AddOrUpdate(content); + unitOfWork.Commit(); + + var updatedContent = repository.Get(NodeDto.NodeIdSeed + 2); + + // Assert + Assert.That(updatedContent.Id, Is.EqualTo(content.Id)); + Assert.That(updatedContent.Name, Is.EqualTo(content.Name)); + } + } + + [Test] + public void Can_Perform_Delete_On_MediaRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + MediaTypeRepository mediaTypeRepository; + using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository)) + { + + // Act + var media = repository.Get(NodeDto.NodeIdSeed + 2); + repository.Delete(media); + unitOfWork.Commit(); + + var deleted = repository.Get(NodeDto.NodeIdSeed + 2); + var exists = repository.Exists(NodeDto.NodeIdSeed + 2); + + // Assert + Assert.That(deleted, Is.Null); + Assert.That(exists, Is.False); + } + } + + [Test] + public void Can_Perform_Get_On_MediaRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + MediaTypeRepository mediaTypeRepository; + using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository)) + { + + // Act + var media = repository.Get(NodeDto.NodeIdSeed + 1); + + // Assert + Assert.That(media.Id, Is.EqualTo(NodeDto.NodeIdSeed + 1)); + Assert.That(media.CreateDate, Is.GreaterThan(DateTime.MinValue)); + Assert.That(media.UpdateDate, Is.GreaterThan(DateTime.MinValue)); + Assert.That(media.ParentId, Is.Not.EqualTo(0)); + Assert.That(media.Name, Is.EqualTo("Test Image")); + Assert.That(media.SortOrder, Is.EqualTo(0)); + Assert.That(media.Version, Is.Not.EqualTo(Guid.Empty)); + Assert.That(media.ContentTypeId, Is.EqualTo(1032)); + Assert.That(media.Path, Is.Not.Empty); + Assert.That(media.Properties.Any(), Is.True); + } + } + + [Test] + public void Can_Perform_GetByQuery_On_MediaRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + MediaTypeRepository mediaTypeRepository; + using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository)) + { + // Act + var query = Query.Builder.Where(x => x.Level == 2); + var result = repository.GetByQuery(query); + + // Assert + Assert.That(result.Count(), Is.GreaterThanOrEqualTo(2)); //There should be two entities on level 2: File and Media + } + } + + [Test] + public void Can_Perform_GetPagedResultsByQuery_ForFirstPage_On_MediaRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + MediaTypeRepository mediaTypeRepository; + using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository)) + { + // Act + var query = Query.Builder.Where(x => x.Level == 2); + long totalRecords; + var result = repository.GetPagedResultsByQuery(query, 0, 1, out totalRecords, "SortOrder", Direction.Ascending, orderBySystemField: true); + + // Assert + Assert.That(totalRecords, Is.GreaterThanOrEqualTo(2)); + Assert.That(result.Count(), Is.EqualTo(1)); + Assert.That(result.First().Name, Is.EqualTo("Test Image")); + } + } + + [Test] + public void Can_Perform_GetPagedResultsByQuery_ForSecondPage_On_MediaRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + MediaTypeRepository mediaTypeRepository; + using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository)) + { + // Act + var query = Query.Builder.Where(x => x.Level == 2); + long totalRecords; + var result = repository.GetPagedResultsByQuery(query, 1, 1, out totalRecords, "SortOrder", Direction.Ascending, orderBySystemField: true); + + // Assert + Assert.That(totalRecords, Is.GreaterThanOrEqualTo(2)); + Assert.That(result.Count(), Is.EqualTo(1)); + Assert.That(result.First().Name, Is.EqualTo("Test File")); + } + } + + [Test] + public void Can_Perform_GetPagedResultsByQuery_WithSinglePage_On_MediaRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + MediaTypeRepository mediaTypeRepository; + using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository)) + { + // Act + var query = Query.Builder.Where(x => x.Level == 2); + long totalRecords; + var result = repository.GetPagedResultsByQuery(query, 0, 2, out totalRecords, "SortOrder", Direction.Ascending, orderBySystemField: true); + + // Assert + Assert.That(totalRecords, Is.GreaterThanOrEqualTo(2)); + Assert.That(result.Count(), Is.EqualTo(2)); + Assert.That(result.First().Name, Is.EqualTo("Test Image")); + } + } + + [Test] + public void Can_Perform_GetPagedResultsByQuery_WithDescendingOrder_On_MediaRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + MediaTypeRepository mediaTypeRepository; + using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository)) + { + // Act + var query = Query.Builder.Where(x => x.Level == 2); + long totalRecords; + var result = repository.GetPagedResultsByQuery(query, 0, 1, out totalRecords, "SortOrder", Direction.Descending, orderBySystemField: true); + + // Assert + Assert.That(totalRecords, Is.GreaterThanOrEqualTo(2)); + Assert.That(result.Count(), Is.EqualTo(1)); + Assert.That(result.First().Name, Is.EqualTo("Test File")); + } + } + + [Test] + public void Can_Perform_GetPagedResultsByQuery_WitAlternateOrder_On_MediaRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + MediaTypeRepository mediaTypeRepository; + using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository)) + { + // Act + var query = Query.Builder.Where(x => x.Level == 2); + long totalRecords; + var result = repository.GetPagedResultsByQuery(query, 0, 1, out totalRecords, "Name", Direction.Ascending, orderBySystemField: true); + + // Assert + Assert.That(totalRecords, Is.GreaterThanOrEqualTo(2)); + Assert.That(result.Count(), Is.EqualTo(1)); + Assert.That(result.First().Name, Is.EqualTo("Test File")); + } + } + + [Test] + public void Can_Perform_GetPagedResultsByQuery_WithFilterMatchingSome_On_MediaRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + MediaTypeRepository mediaTypeRepository; + using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository)) + { + // Act + var query = Query.Builder.Where(x => x.Level == 2); + long totalRecords; + var result = repository.GetPagedResultsByQuery(query, 0, 1, out totalRecords, "SortOrder", Direction.Ascending, true, "File"); + + // Assert + Assert.That(totalRecords, Is.EqualTo(1)); + Assert.That(result.Count(), Is.EqualTo(1)); + Assert.That(result.First().Name, Is.EqualTo("Test File")); + } + } + + [Test] + public void Can_Perform_GetPagedResultsByQuery_WithFilterMatchingAll_On_MediaRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + MediaTypeRepository mediaTypeRepository; + using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository)) + { + // Act + var query = Query.Builder.Where(x => x.Level == 2); + long totalRecords; + var result = repository.GetPagedResultsByQuery(query, 0, 1, out totalRecords, "SortOrder", Direction.Ascending, true, "Test"); + + // Assert + Assert.That(totalRecords, Is.EqualTo(2)); + Assert.That(result.Count(), Is.EqualTo(1)); + Assert.That(result.First().Name, Is.EqualTo("Test Image")); + } + } + + [Test] + public void Can_Perform_GetAll_By_Param_Ids_On_MediaRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + MediaTypeRepository mediaTypeRepository; + using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository)) + { + + // Act + var medias = repository.GetAll(NodeDto.NodeIdSeed + 1, NodeDto.NodeIdSeed + 2); + + // Assert + Assert.That(medias, Is.Not.Null); + Assert.That(medias.Any(), Is.True); + Assert.That(medias.Count(), Is.EqualTo(2)); + } + } + + [Test] + public void Can_Perform_GetAll_On_MediaRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + MediaTypeRepository mediaTypeRepository; + using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository)) + { + + // Act + var medias = repository.GetAll(); + + // Assert + Assert.That(medias, Is.Not.Null); + Assert.That(medias.Any(), Is.True); + Assert.That(medias.Count(), Is.GreaterThanOrEqualTo(3)); + } + } + + [Test] + public void Can_Perform_Exists_On_MediaRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + MediaTypeRepository mediaTypeRepository; + using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository)) + { + + // Act + var exists = repository.Exists(NodeDto.NodeIdSeed + 1); + var existsToo = repository.Exists(NodeDto.NodeIdSeed + 1); + var doesntExists = repository.Exists(NodeDto.NodeIdSeed + 5); + + // Assert + Assert.That(exists, Is.True); + Assert.That(existsToo, Is.True); + Assert.That(doesntExists, Is.False); + } + } + + [Test] + public void Can_Perform_Count_On_MediaRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + MediaTypeRepository mediaTypeRepository; + using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository)) + { + + // Act + int level = 2; + var query = Query.Builder.Where(x => x.Level == level); + var result = repository.Count(query); + + // Assert + Assert.That(result, Is.GreaterThanOrEqualTo(2)); + } + } + + [TearDown] + public override void TearDown() + { + base.TearDown(); + } + + public void CreateTestData() + { + //Create and Save folder-Media -> (NodeDto.NodeIdSeed) + var folderMediaType = ServiceContext.ContentTypeService.GetMediaType(1031); + var folder = MockedMedia.CreateMediaFolder(folderMediaType, -1); + ServiceContext.MediaService.Save(folder, 0); + + //Create and Save image-Media -> (NodeDto.NodeIdSeed + 1) + var imageMediaType = ServiceContext.ContentTypeService.GetMediaType(1032); + var image = MockedMedia.CreateMediaImage(imageMediaType, folder.Id); + ServiceContext.MediaService.Save(image, 0); + + //Create and Save file-Media -> (NodeDto.NodeIdSeed + 2) + var fileMediaType = ServiceContext.ContentTypeService.GetMediaType(1033); + var file = MockedMedia.CreateMediaFile(fileMediaType, folder.Id); + ServiceContext.MediaService.Save(file, 0); + } + } } \ No newline at end of file diff --git a/src/Umbraco.Tests/UmbracoExamine/IndexInitializer.cs b/src/Umbraco.Tests/UmbracoExamine/IndexInitializer.cs index 6c56c76ef7..2f9ecce9b2 100644 --- a/src/Umbraco.Tests/UmbracoExamine/IndexInitializer.cs +++ b/src/Umbraco.Tests/UmbracoExamine/IndexInitializer.cs @@ -20,124 +20,124 @@ using Version = Lucene.Net.Util.Version; namespace Umbraco.Tests.UmbracoExamine { - /// - /// Used internally by test classes to initialize a new index from the template - /// - internal static class IndexInitializer + /// + /// Used internally by test classes to initialize a new index from the template + /// + internal static class IndexInitializer + { + public static UmbracoContentIndexer GetUmbracoIndexer( + Directory luceneDir, + Analyzer analyzer = null, + IDataService dataService = null, + IContentService contentService = null, + IMediaService mediaService = null, + IDataTypeService dataTypeService = null, + IMemberService memberService = null, + IUserService userService = null) { - public static UmbracoContentIndexer GetUmbracoIndexer( - Directory luceneDir, - Analyzer analyzer = null, - IDataService dataService = null, - IContentService contentService = null, - IMediaService mediaService = null, - IDataTypeService dataTypeService = null, - IMemberService memberService = null, - IUserService userService = null) - { - if (dataService == null) - { - dataService = new TestDataService(); - } - if (contentService == null) - { - contentService = Mock.Of(); - } - if (userService == null) - { - userService = Mock.Of(x => x.GetProfileById(It.IsAny()) == Mock.Of(p => p.Id == (object)0 && p.Name == "admin")); - } - if (mediaService == null) - { - long totalRecs; + if (dataService == null) + { + dataService = new TestDataService(); + } + if (contentService == null) + { + contentService = Mock.Of(); + } + if (userService == null) + { + userService = Mock.Of(x => x.GetProfileById(It.IsAny()) == Mock.Of(p => p.Id == (object)0 && p.Name == "admin")); + } + if (mediaService == null) + { + long totalRecs; - var allRecs = dataService.MediaService.GetLatestMediaByXpath("//node") - .Root - .Elements() - .Select(x => Mock.Of( - m => - m.Id == (int) x.Attribute("id") && - m.ParentId == (int) x.Attribute("parentID") && - m.Level == (int) x.Attribute("level") && - m.CreatorId == 0 && - m.SortOrder == (int) x.Attribute("sortOrder") && - m.CreateDate == (DateTime) x.Attribute("createDate") && - m.UpdateDate == (DateTime) x.Attribute("updateDate") && - m.Name == (string) x.Attribute("nodeName") && - m.Path == (string) x.Attribute("path") && - m.Properties == new PropertyCollection() && - m.ContentType == Mock.Of(mt => - mt.Alias == (string) x.Attribute("nodeTypeAlias") && - mt.Id == (int) x.Attribute("nodeType")))) - .ToArray(); - - - mediaService = Mock.Of( - x => x.GetPagedDescendants( - It.IsAny(), It.IsAny(), It.IsAny(), out totalRecs, It.IsAny(), It.IsAny(), It.IsAny()) - == - allRecs); - } - if (dataTypeService == null) - { - dataTypeService = Mock.Of(); - } - - if (memberService == null) - { - memberService = Mock.Of(); - } - - if (analyzer == null) - { - analyzer = new StandardAnalyzer(Version.LUCENE_29); - } - - var indexSet = new IndexSet(); - var indexCriteria = indexSet.ToIndexCriteria(dataService, UmbracoContentIndexer.IndexFieldPolicies); - - var i = new UmbracoContentIndexer(indexCriteria, - luceneDir, //custom lucene directory - dataService, - contentService, - mediaService, - dataTypeService, - userService, - analyzer, - false); - - //i.IndexSecondsInterval = 1; - - i.IndexingError += IndexingError; - - return i; - } - public static UmbracoExamineSearcher GetUmbracoSearcher(Directory luceneDir, Analyzer analyzer = null) - { - if (analyzer == null) - { - analyzer = new StandardAnalyzer(Version.LUCENE_29); - } - return new UmbracoExamineSearcher(luceneDir, analyzer); - } - - public static LuceneSearcher GetLuceneSearcher(Directory luceneDir) - { - return new LuceneSearcher(luceneDir, new StandardAnalyzer(Version.LUCENE_29)); - } - - public static MultiIndexSearcher GetMultiSearcher(Directory pdfDir, Directory simpleDir, Directory conventionDir, Directory cwsDir) - { - var i = new MultiIndexSearcher(new[] { pdfDir, simpleDir, conventionDir, cwsDir }, new StandardAnalyzer(Version.LUCENE_29)); - return i; - } + var allRecs = dataService.MediaService.GetLatestMediaByXpath("//node") + .Root + .Elements() + .Select(x => Mock.Of( + m => + m.Id == (int)x.Attribute("id") && + m.ParentId == (int)x.Attribute("parentID") && + m.Level == (int)x.Attribute("level") && + m.CreatorId == 0 && + m.SortOrder == (int)x.Attribute("sortOrder") && + m.CreateDate == (DateTime)x.Attribute("createDate") && + m.UpdateDate == (DateTime)x.Attribute("updateDate") && + m.Name == (string)x.Attribute("nodeName") && + m.Path == (string)x.Attribute("path") && + m.Properties == new PropertyCollection() && + m.ContentType == Mock.Of(mt => + mt.Alias == (string)x.Attribute("nodeTypeAlias") && + mt.Id == (int)x.Attribute("nodeType")))) + .ToArray(); - internal static void IndexingError(object sender, IndexingErrorEventArgs e) - { - throw new ApplicationException(e.Message, e.InnerException); - } + mediaService = Mock.Of( + x => x.GetPagedDescendants( + It.IsAny(), It.IsAny(), It.IsAny(), out totalRecs, It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()) + == + allRecs); + } + if (dataTypeService == null) + { + dataTypeService = Mock.Of(); + } + if (memberService == null) + { + memberService = Mock.Of(); + } + if (analyzer == null) + { + analyzer = new StandardAnalyzer(Version.LUCENE_29); + } + + var indexSet = new IndexSet(); + var indexCriteria = indexSet.ToIndexCriteria(dataService, UmbracoContentIndexer.IndexFieldPolicies); + + var i = new UmbracoContentIndexer(indexCriteria, + luceneDir, //custom lucene directory + dataService, + contentService, + mediaService, + dataTypeService, + userService, + analyzer, + false); + + //i.IndexSecondsInterval = 1; + + i.IndexingError += IndexingError; + + return i; } + public static UmbracoExamineSearcher GetUmbracoSearcher(Directory luceneDir, Analyzer analyzer = null) + { + if (analyzer == null) + { + analyzer = new StandardAnalyzer(Version.LUCENE_29); + } + return new UmbracoExamineSearcher(luceneDir, analyzer); + } + + public static LuceneSearcher GetLuceneSearcher(Directory luceneDir) + { + return new LuceneSearcher(luceneDir, new StandardAnalyzer(Version.LUCENE_29)); + } + + public static MultiIndexSearcher GetMultiSearcher(Directory pdfDir, Directory simpleDir, Directory conventionDir, Directory cwsDir) + { + var i = new MultiIndexSearcher(new[] { pdfDir, simpleDir, conventionDir, cwsDir }, new StandardAnalyzer(Version.LUCENE_29)); + return i; + } + + + internal static void IndexingError(object sender, IndexingErrorEventArgs e) + { + throw new ApplicationException(e.Message, e.InnerException); + } + + + } } \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js index 2cf7127707..2688dec37a 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js @@ -25,489 +25,491 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { - /** internal method process the saving of data and post processing the result */ - function saveContentItem(content, action, files) { - return umbRequestHelper.postSaveContent({ - restApiUrl: umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "PostSave"), - content: content, - action: action, - files: files, - dataFormatter: function (c, a) { - return umbDataFormatter.formatContentPostData(c, a); - } - }); - } + /** internal method process the saving of data and post processing the result */ + function saveContentItem(content, action, files) { + return umbRequestHelper.postSaveContent({ + restApiUrl: umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "PostSave"), + content: content, + action: action, + files: files, + dataFormatter: function (c, a) { + return umbDataFormatter.formatContentPostData(c, a); + } + }); + } - return { - - getRecycleBin: function() { - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "GetRecycleBin")), - 'Failed to retrieve data for content recycle bin'); - }, + return { - /** - * @ngdoc method - * @name umbraco.resources.contentResource#sort - * @methodOf umbraco.resources.contentResource - * - * @description - * Sorts all children below a given parent node id, based on a collection of node-ids - * - * ##usage - *
-         * var ids = [123,34533,2334,23434];
-         * contentResource.sort({ parentId: 1244, sortedIds: ids })
-         *    .then(function() {
-         *        $scope.complete = true;
-         *    });
-         * 
- * @param {Object} args arguments object - * @param {Int} args.parentId the ID of the parent node - * @param {Array} options.sortedIds array of node IDs as they should be sorted - * @returns {Promise} resourcePromise object. - * - */ - sort: function (args) { - if (!args) { - throw "args cannot be null"; - } - if (!args.parentId) { - throw "args.parentId cannot be null"; - } - if (!args.sortedIds) { - throw "args.sortedIds cannot be null"; - } + getRecycleBin: function () { + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "GetRecycleBin")), + 'Failed to retrieve data for content recycle bin'); + }, - return umbRequestHelper.resourcePromise( - $http.post(umbRequestHelper.getApiUrl("contentApiBaseUrl", "PostSort"), - { - parentId: args.parentId, - idSortOrder: args.sortedIds - }), - 'Failed to sort content'); - }, + /** + * @ngdoc method + * @name umbraco.resources.contentResource#sort + * @methodOf umbraco.resources.contentResource + * + * @description + * Sorts all children below a given parent node id, based on a collection of node-ids + * + * ##usage + *
+	 * var ids = [123,34533,2334,23434];
+	 * contentResource.sort({ parentId: 1244, sortedIds: ids })
+	 *    .then(function() {
+	 *        $scope.complete = true;
+	 *    });
+	 * 
+ * @param {Object} args arguments object + * @param {Int} args.parentId the ID of the parent node + * @param {Array} options.sortedIds array of node IDs as they should be sorted + * @returns {Promise} resourcePromise object. + * + */ + sort: function (args) { + if (!args) { + throw "args cannot be null"; + } + if (!args.parentId) { + throw "args.parentId cannot be null"; + } + if (!args.sortedIds) { + throw "args.sortedIds cannot be null"; + } - /** - * @ngdoc method - * @name umbraco.resources.contentResource#move - * @methodOf umbraco.resources.contentResource - * - * @description - * Moves a node underneath a new parentId - * - * ##usage - *
-         * contentResource.move({ parentId: 1244, id: 123 })
-         *    .then(function() {
-         *        alert("node was moved");
-         *    }, function(err){
-         *      alert("node didnt move:" + err.data.Message); 
-         *    });
-         * 
- * @param {Object} args arguments object - * @param {Int} args.idd the ID of the node to move - * @param {Int} args.parentId the ID of the parent node to move to - * @returns {Promise} resourcePromise object. - * - */ - move: function (args) { - if (!args) { - throw "args cannot be null"; - } - if (!args.parentId) { - throw "args.parentId cannot be null"; - } - if (!args.id) { - throw "args.id cannot be null"; - } + return umbRequestHelper.resourcePromise( + $http.post(umbRequestHelper.getApiUrl("contentApiBaseUrl", "PostSort"), + { + parentId: args.parentId, + idSortOrder: args.sortedIds + }), + 'Failed to sort content'); + }, - return umbRequestHelper.resourcePromise( - $http.post(umbRequestHelper.getApiUrl("contentApiBaseUrl", "PostMove"), - { - parentId: args.parentId, - id: args.id - }), - 'Failed to move content'); - }, + /** + * @ngdoc method + * @name umbraco.resources.contentResource#move + * @methodOf umbraco.resources.contentResource + * + * @description + * Moves a node underneath a new parentId + * + * ##usage + *
+	 * contentResource.move({ parentId: 1244, id: 123 })
+	 *    .then(function() {
+	 *        alert("node was moved");
+	 *    }, function(err){
+	 *      alert("node didnt move:" + err.data.Message); 
+	 *    });
+	 * 
+ * @param {Object} args arguments object + * @param {Int} args.idd the ID of the node to move + * @param {Int} args.parentId the ID of the parent node to move to + * @returns {Promise} resourcePromise object. + * + */ + move: function (args) { + if (!args) { + throw "args cannot be null"; + } + if (!args.parentId) { + throw "args.parentId cannot be null"; + } + if (!args.id) { + throw "args.id cannot be null"; + } - /** - * @ngdoc method - * @name umbraco.resources.contentResource#copy - * @methodOf umbraco.resources.contentResource - * - * @description - * Copies a node underneath a new parentId - * - * ##usage - *
-         * contentResource.copy({ parentId: 1244, id: 123 })
-         *    .then(function() {
-         *        alert("node was copied");
-         *    }, function(err){
-         *      alert("node wasnt copy:" + err.data.Message); 
-         *    });
-         * 
- * @param {Object} args arguments object - * @param {Int} args.id the ID of the node to copy - * @param {Int} args.parentId the ID of the parent node to copy to - * @param {Boolean} args.relateToOriginal if true, relates the copy to the original through the relation api - * @returns {Promise} resourcePromise object. - * - */ - copy: function (args) { - if (!args) { - throw "args cannot be null"; - } - if (!args.parentId) { - throw "args.parentId cannot be null"; - } - if (!args.id) { - throw "args.id cannot be null"; - } + return umbRequestHelper.resourcePromise( + $http.post(umbRequestHelper.getApiUrl("contentApiBaseUrl", "PostMove"), + { + parentId: args.parentId, + id: args.id + }), + 'Failed to move content'); + }, - return umbRequestHelper.resourcePromise( - $http.post(umbRequestHelper.getApiUrl("contentApiBaseUrl", "PostCopy"), - args), - 'Failed to copy content'); - }, + /** + * @ngdoc method + * @name umbraco.resources.contentResource#copy + * @methodOf umbraco.resources.contentResource + * + * @description + * Copies a node underneath a new parentId + * + * ##usage + *
+	 * contentResource.copy({ parentId: 1244, id: 123 })
+	 *    .then(function() {
+	 *        alert("node was copied");
+	 *    }, function(err){
+	 *      alert("node wasnt copy:" + err.data.Message); 
+	 *    });
+	 * 
+ * @param {Object} args arguments object + * @param {Int} args.id the ID of the node to copy + * @param {Int} args.parentId the ID of the parent node to copy to + * @param {Boolean} args.relateToOriginal if true, relates the copy to the original through the relation api + * @returns {Promise} resourcePromise object. + * + */ + copy: function (args) { + if (!args) { + throw "args cannot be null"; + } + if (!args.parentId) { + throw "args.parentId cannot be null"; + } + if (!args.id) { + throw "args.id cannot be null"; + } - /** - * @ngdoc method - * @name umbraco.resources.contentResource#unPublish - * @methodOf umbraco.resources.contentResource - * - * @description - * Unpublishes a content item with a given Id - * - * ##usage - *
-         * contentResource.unPublish(1234)
-         *    .then(function() {
-         *        alert("node was unpulished");
-         *    }, function(err){
-         *      alert("node wasnt unpublished:" + err.data.Message); 
-         *    });
-         * 
- * @param {Int} id the ID of the node to unpublish - * @returns {Promise} resourcePromise object. - * - */ - unPublish: function (id) { - if (!id) { - throw "id cannot be null"; - } - - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "PostUnPublish", - [{ id: id }])), - 'Failed to publish content with id ' + id); - }, - /** - * @ngdoc method - * @name umbraco.resources.contentResource#emptyRecycleBin - * @methodOf umbraco.resources.contentResource - * - * @description - * Empties the content recycle bin - * - * ##usage - *
-         * contentResource.emptyRecycleBin()
-         *    .then(function() {
-         *        alert('its empty!');
-         *    });
-         * 
- * - * @returns {Promise} resourcePromise object. - * - */ - emptyRecycleBin: function() { - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "EmptyRecycleBin")), - 'Failed to empty the recycle bin'); - }, + return umbRequestHelper.resourcePromise( + $http.post(umbRequestHelper.getApiUrl("contentApiBaseUrl", "PostCopy"), + args), + 'Failed to copy content'); + }, - /** - * @ngdoc method - * @name umbraco.resources.contentResource#deleteById - * @methodOf umbraco.resources.contentResource - * - * @description - * Deletes a content item with a given id - * - * ##usage - *
-         * contentResource.deleteById(1234)
-         *    .then(function() {
-         *        alert('its gone!');
-         *    });
-         * 
- * - * @param {Int} id id of content item to delete - * @returns {Promise} resourcePromise object. - * - */ - deleteById: function(id) { - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "DeleteById", - [{ id: id }])), - 'Failed to delete item ' + id); - }, + /** + * @ngdoc method + * @name umbraco.resources.contentResource#unPublish + * @methodOf umbraco.resources.contentResource + * + * @description + * Unpublishes a content item with a given Id + * + * ##usage + *
+	 * contentResource.unPublish(1234)
+	 *    .then(function() {
+	 *        alert("node was unpulished");
+	 *    }, function(err){
+	 *      alert("node wasnt unpublished:" + err.data.Message); 
+	 *    });
+	 * 
+ * @param {Int} id the ID of the node to unpublish + * @returns {Promise} resourcePromise object. + * + */ + unPublish: function (id) { + if (!id) { + throw "id cannot be null"; + } - /** - * @ngdoc method - * @name umbraco.resources.contentResource#getById - * @methodOf umbraco.resources.contentResource - * - * @description - * Gets a content item with a given id - * - * ##usage - *
-         * contentResource.getById(1234)
-         *    .then(function(content) {
-         *        var myDoc = content; 
-         *        alert('its here!');
-         *    });
-         * 
- * - * @param {Int} id id of content item to return - * @returns {Promise} resourcePromise object containing the content item. - * - */ - getById: function (id) { - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "GetById", - [{ id: id }])), - 'Failed to retrieve data for content id ' + id); - }, - - /** - * @ngdoc method - * @name umbraco.resources.contentResource#getByIds - * @methodOf umbraco.resources.contentResource - * - * @description - * Gets an array of content items, given a collection of ids - * - * ##usage - *
-         * contentResource.getByIds( [1234,2526,28262])
-         *    .then(function(contentArray) {
-         *        var myDoc = contentArray; 
-         *        alert('they are here!');
-         *    });
-         * 
- * - * @param {Array} ids ids of content items to return as an array - * @returns {Promise} resourcePromise object containing the content items array. - * - */ - getByIds: function (ids) { - - var idQuery = ""; - _.each(ids, function(item) { - idQuery += "ids=" + item + "&"; - }); + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "PostUnPublish", + [{ id: id }])), + 'Failed to publish content with id ' + id); + }, + /** + * @ngdoc method + * @name umbraco.resources.contentResource#emptyRecycleBin + * @methodOf umbraco.resources.contentResource + * + * @description + * Empties the content recycle bin + * + * ##usage + *
+	 * contentResource.emptyRecycleBin()
+	 *    .then(function() {
+	 *        alert('its empty!');
+	 *    });
+	 * 
+ * + * @returns {Promise} resourcePromise object. + * + */ + emptyRecycleBin: function () { + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "EmptyRecycleBin")), + 'Failed to empty the recycle bin'); + }, - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "GetByIds", - idQuery)), - 'Failed to retrieve data for content with multiple ids'); - }, + /** + * @ngdoc method + * @name umbraco.resources.contentResource#deleteById + * @methodOf umbraco.resources.contentResource + * + * @description + * Deletes a content item with a given id + * + * ##usage + *
+	 * contentResource.deleteById(1234)
+	 *    .then(function() {
+	 *        alert('its gone!');
+	 *    });
+	 * 
+ * + * @param {Int} id id of content item to delete + * @returns {Promise} resourcePromise object. + * + */ + deleteById: function (id) { + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "DeleteById", + [{ id: id }])), + 'Failed to delete item ' + id); + }, - - /** - * @ngdoc method - * @name umbraco.resources.contentResource#getScaffold - * @methodOf umbraco.resources.contentResource - * - * @description - * Returns a scaffold of an empty content item, given the id of the content item to place it underneath and the content type alias. - * - * - Parent Id must be provided so umbraco knows where to store the content - * - Content Type alias must be provided so umbraco knows which properties to put on the content scaffold - * - * The scaffold is used to build editors for content that has not yet been populated with data. - * - * ##usage - *
-         * contentResource.getScaffold(1234, 'homepage')
-         *    .then(function(scaffold) {
-         *        var myDoc = scaffold;
-         *        myDoc.name = "My new document"; 
-         *
-         *        contentResource.publish(myDoc, true)
-         *            .then(function(content){
-         *                alert("Retrieved, updated and published again");
-         *            });
-         *    });
-         * 
- * - * @param {Int} parentId id of content item to return - * @param {String} alias contenttype alias to base the scaffold on - * @returns {Promise} resourcePromise object containing the content scaffold. - * - */ - getScaffold: function (parentId, alias) { - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "GetEmpty", - [{ contentTypeAlias: alias }, { parentId: parentId }])), - 'Failed to retrieve data for empty content item type ' + alias); - }, + /** + * @ngdoc method + * @name umbraco.resources.contentResource#getById + * @methodOf umbraco.resources.contentResource + * + * @description + * Gets a content item with a given id + * + * ##usage + *
+	 * contentResource.getById(1234)
+	 *    .then(function(content) {
+	 *        var myDoc = content; 
+	 *        alert('its here!');
+	 *    });
+	 * 
+ * + * @param {Int} id id of content item to return + * @returns {Promise} resourcePromise object containing the content item. + * + */ + getById: function (id) { + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "GetById", + [{ id: id }])), + 'Failed to retrieve data for content id ' + id); + }, - /** - * @ngdoc method - * @name umbraco.resources.contentResource#getNiceUrl - * @methodOf umbraco.resources.contentResource - * - * @description - * Returns a url, given a node ID - * - * ##usage - *
-         * contentResource.getNiceUrl(id)
-         *    .then(function(url) {
-         *        alert('its here!');
-         *    });
-         * 
- * - * @param {Int} id Id of node to return the public url to - * @returns {Promise} resourcePromise object containing the url. - * - */ - getNiceUrl: function (id) { - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "GetNiceUrl",[{id: id}])), - 'Failed to retrieve url for id:' + id); - }, + /** + * @ngdoc method + * @name umbraco.resources.contentResource#getByIds + * @methodOf umbraco.resources.contentResource + * + * @description + * Gets an array of content items, given a collection of ids + * + * ##usage + *
+	 * contentResource.getByIds( [1234,2526,28262])
+	 *    .then(function(contentArray) {
+	 *        var myDoc = contentArray; 
+	 *        alert('they are here!');
+	 *    });
+	 * 
+ * + * @param {Array} ids ids of content items to return as an array + * @returns {Promise} resourcePromise object containing the content items array. + * + */ + getByIds: function (ids) { - /** - * @ngdoc method - * @name umbraco.resources.contentResource#getChildren - * @methodOf umbraco.resources.contentResource - * - * @description - * Gets children of a content item with a given id - * - * ##usage - *
-         * contentResource.getChildren(1234, {pageSize: 10, pageNumber: 2})
-         *    .then(function(contentArray) {
-         *        var children = contentArray; 
-         *        alert('they are here!');
-         *    });
-         * 
- * - * @param {Int} parentid id of content item to return children of - * @param {Object} options optional options object - * @param {Int} options.pageSize if paging data, number of nodes per page, default = 0 - * @param {Int} options.pageNumber if paging data, current page index, default = 0 - * @param {String} options.filter if provided, query will only return those with names matching the filter - * @param {String} options.orderDirection can be `Ascending` or `Descending` - Default: `Ascending` - * @param {String} options.orderBy property to order items by, default: `SortOrder` - * @returns {Promise} resourcePromise object containing an array of content items. - * - */ - getChildren: function (parentId, options) { + var idQuery = ""; + _.each(ids, function (item) { + idQuery += "ids=" + item + "&"; + }); - var defaults = { - pageSize: 0, - pageNumber: 0, - filter: '', - orderDirection: "Ascending", - orderBy: "SortOrder" - }; - if (options === undefined) { - options = {}; - } - //overwrite the defaults if there are any specified - angular.extend(defaults, options); - //now copy back to the options we will use - options = defaults; - //change asc/desct - if (options.orderDirection === "asc") { - options.orderDirection = "Ascending"; - } - else if (options.orderDirection === "desc") { - options.orderDirection = "Descending"; - } + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "GetByIds", + idQuery)), + 'Failed to retrieve data for content with multiple ids'); + }, - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "GetChildren", - [ - { id: parentId }, - { pageNumber: options.pageNumber }, - { pageSize: options.pageSize }, - { orderBy: options.orderBy }, - { orderDirection: options.orderDirection }, - { filter: options.filter } - ])), - 'Failed to retrieve children for content item ' + parentId); - }, - /** - * @ngdoc method - * @name umbraco.resources.contentResource#hasPermission - * @methodOf umbraco.resources.contentResource - * - * @description - * Returns true/false given a permission char to check against a nodeID - * for the current user - * - * ##usage - *
-         * contentResource.hasPermission('p',1234)
-         *    .then(function() {
-         *        alert('You are allowed to publish this item');
-         *    });
-         * 
- * - * @param {String} permission char representing the permission to check - * @param {Int} id id of content item to delete - * @returns {Promise} resourcePromise object. - * - */ - checkPermission: function(permission, id) { - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "HasPermission", - [{ permissionToCheck: permission },{ nodeId: id }])), - 'Failed to check permission for item ' + id); - }, + /** + * @ngdoc method + * @name umbraco.resources.contentResource#getScaffold + * @methodOf umbraco.resources.contentResource + * + * @description + * Returns a scaffold of an empty content item, given the id of the content item to place it underneath and the content type alias. + * + * - Parent Id must be provided so umbraco knows where to store the content + * - Content Type alias must be provided so umbraco knows which properties to put on the content scaffold + * + * The scaffold is used to build editors for content that has not yet been populated with data. + * + * ##usage + *
+	 * contentResource.getScaffold(1234, 'homepage')
+	 *    .then(function(scaffold) {
+	 *        var myDoc = scaffold;
+	 *        myDoc.name = "My new document"; 
+	 *
+	 *        contentResource.publish(myDoc, true)
+	 *            .then(function(content){
+	 *                alert("Retrieved, updated and published again");
+	 *            });
+	 *    });
+	 * 
+ * + * @param {Int} parentId id of content item to return + * @param {String} alias contenttype alias to base the scaffold on + * @returns {Promise} resourcePromise object containing the content scaffold. + * + */ + getScaffold: function (parentId, alias) { + + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "GetEmpty", + [{ contentTypeAlias: alias }, { parentId: parentId }])), + 'Failed to retrieve data for empty content item type ' + alias); + }, + + /** + * @ngdoc method + * @name umbraco.resources.contentResource#getNiceUrl + * @methodOf umbraco.resources.contentResource + * + * @description + * Returns a url, given a node ID + * + * ##usage + *
+	 * contentResource.getNiceUrl(id)
+	 *    .then(function(url) {
+	 *        alert('its here!');
+	 *    });
+	 * 
+ * + * @param {Int} id Id of node to return the public url to + * @returns {Promise} resourcePromise object containing the url. + * + */ + getNiceUrl: function (id) { + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "GetNiceUrl", [{ id: id }])), + 'Failed to retrieve url for id:' + id); + }, + + /** + * @ngdoc method + * @name umbraco.resources.contentResource#getChildren + * @methodOf umbraco.resources.contentResource + * + * @description + * Gets children of a content item with a given id + * + * ##usage + *
+	 * contentResource.getChildren(1234, {pageSize: 10, pageNumber: 2})
+	 *    .then(function(contentArray) {
+	 *        var children = contentArray; 
+	 *        alert('they are here!');
+	 *    });
+	 * 
+ * + * @param {Int} parentid id of content item to return children of + * @param {Object} options optional options object + * @param {Int} options.pageSize if paging data, number of nodes per page, default = 0 + * @param {Int} options.pageNumber if paging data, current page index, default = 0 + * @param {String} options.filter if provided, query will only return those with names matching the filter + * @param {String} options.orderDirection can be `Ascending` or `Descending` - Default: `Ascending` + * @param {String} options.orderBy property to order items by, default: `SortOrder` + * @returns {Promise} resourcePromise object containing an array of content items. + * + */ + getChildren: function (parentId, options) { + + var defaults = { + pageSize: 0, + pageNumber: 0, + filter: '', + orderDirection: "Ascending", + orderBy: "SortOrder", + orderBySystemField: true + }; + if (options === undefined) { + options = {}; + } + //overwrite the defaults if there are any specified + angular.extend(defaults, options); + //now copy back to the options we will use + options = defaults; + //change asc/desct + if (options.orderDirection === "asc") { + options.orderDirection = "Ascending"; + } + else if (options.orderDirection === "desc") { + options.orderDirection = "Descending"; + } + + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "GetChildren", + [ + { id: parentId }, + { pageNumber: options.pageNumber }, + { pageSize: options.pageSize }, + { orderBy: options.orderBy }, + { orderDirection: options.orderDirection }, + { orderBySystemField: options.orderBySystemField }, + { filter: options.filter } + ])), + 'Failed to retrieve children for content item ' + parentId); + }, + + /** + * @ngdoc method + * @name umbraco.resources.contentResource#hasPermission + * @methodOf umbraco.resources.contentResource + * + * @description + * Returns true/false given a permission char to check against a nodeID + * for the current user + * + * ##usage + *
+	 * contentResource.hasPermission('p',1234)
+	 *    .then(function() {
+	 *        alert('You are allowed to publish this item');
+	 *    });
+	 * 
+ * + * @param {String} permission char representing the permission to check + * @param {Int} id id of content item to delete + * @returns {Promise} resourcePromise object. + * + */ + checkPermission: function (permission, id) { + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "HasPermission", + [{ permissionToCheck: permission }, { nodeId: id }])), + 'Failed to check permission for item ' + id); + }, getPermissions: function (nodeIds) { return umbRequestHelper.resourcePromise( @@ -519,140 +521,140 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { 'Failed to get permissions'); }, - /** - * @ngdoc method - * @name umbraco.resources.contentResource#save - * @methodOf umbraco.resources.contentResource - * - * @description - * Saves changes made to a content item to its current version, if the content item is new, the isNew paramater must be passed to force creation - * if the content item needs to have files attached, they must be provided as the files param and passed separately - * - * - * ##usage - *
-         * contentResource.getById(1234)
-         *    .then(function(content) {
-         *          content.name = "I want a new name!";
-         *          contentResource.save(content, false)
-         *            .then(function(content){
-         *                alert("Retrieved, updated and saved again");
-         *            });
-         *    });
-         * 
- * - * @param {Object} content The content item object with changes applied - * @param {Bool} isNew set to true to create a new item or to update an existing - * @param {Array} files collection of files for the document - * @returns {Promise} resourcePromise object containing the saved content item. - * - */ - save: function (content, isNew, files) { - return saveContentItem(content, "save" + (isNew ? "New" : ""), files); - }, + /** + * @ngdoc method + * @name umbraco.resources.contentResource#save + * @methodOf umbraco.resources.contentResource + * + * @description + * Saves changes made to a content item to its current version, if the content item is new, the isNew paramater must be passed to force creation + * if the content item needs to have files attached, they must be provided as the files param and passed separately + * + * + * ##usage + *
+	 * contentResource.getById(1234)
+	 *    .then(function(content) {
+	 *          content.name = "I want a new name!";
+	 *          contentResource.save(content, false)
+	 *            .then(function(content){
+	 *                alert("Retrieved, updated and saved again");
+	 *            });
+	 *    });
+	 * 
+ * + * @param {Object} content The content item object with changes applied + * @param {Bool} isNew set to true to create a new item or to update an existing + * @param {Array} files collection of files for the document + * @returns {Promise} resourcePromise object containing the saved content item. + * + */ + save: function (content, isNew, files) { + return saveContentItem(content, "save" + (isNew ? "New" : ""), files); + }, - /** - * @ngdoc method - * @name umbraco.resources.contentResource#publish - * @methodOf umbraco.resources.contentResource - * - * @description - * Saves and publishes changes made to a content item to a new version, if the content item is new, the isNew paramater must be passed to force creation - * if the content item needs to have files attached, they must be provided as the files param and passed separately - * - * - * ##usage - *
-         * contentResource.getById(1234)
-         *    .then(function(content) {
-         *          content.name = "I want a new name, and be published!";
-         *          contentResource.publish(content, false)
-         *            .then(function(content){
-         *                alert("Retrieved, updated and published again");
-         *            });
-         *    });
-         * 
- * - * @param {Object} content The content item object with changes applied - * @param {Bool} isNew set to true to create a new item or to update an existing - * @param {Array} files collection of files for the document - * @returns {Promise} resourcePromise object containing the saved content item. - * - */ - publish: function (content, isNew, files) { - return saveContentItem(content, "publish" + (isNew ? "New" : ""), files); - }, - - - /** - * @ngdoc method - * @name umbraco.resources.contentResource#sendToPublish - * @methodOf umbraco.resources.contentResource - * - * @description - * Saves changes made to a content item, and notifies any subscribers about a pending publication - * - * ##usage - *
-         * contentResource.getById(1234)
-         *    .then(function(content) {
-         *          content.name = "I want a new name, and be published!";
-         *          contentResource.sendToPublish(content, false)
-         *            .then(function(content){
-         *                alert("Retrieved, updated and notication send off");
-         *            });
-         *    });
-         * 
- * - * @param {Object} content The content item object with changes applied - * @param {Bool} isNew set to true to create a new item or to update an existing - * @param {Array} files collection of files for the document - * @returns {Promise} resourcePromise object containing the saved content item. - * - */ - sendToPublish: function (content, isNew, files) { - return saveContentItem(content, "sendPublish" + (isNew ? "New" : ""), files); - }, - - /** - * @ngdoc method - * @name umbraco.resources.contentResource#publishByid - * @methodOf umbraco.resources.contentResource - * - * @description - * Publishes a content item with a given ID - * - * ##usage - *
-         * contentResource.publishById(1234)
-         *    .then(function(content) {
-         *        alert("published");
-         *    });
-         * 
- * - * @param {Int} id The ID of the conten to publish - * @returns {Promise} resourcePromise object containing the published content item. - * - */ - publishById: function(id){ - - if (!id) { - throw "id cannot be null"; - } - - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "PostPublishById", - [{ id: id }])), - 'Failed to publish content with id ' + id); - - } + /** + * @ngdoc method + * @name umbraco.resources.contentResource#publish + * @methodOf umbraco.resources.contentResource + * + * @description + * Saves and publishes changes made to a content item to a new version, if the content item is new, the isNew paramater must be passed to force creation + * if the content item needs to have files attached, they must be provided as the files param and passed separately + * + * + * ##usage + *
+	 * contentResource.getById(1234)
+	 *    .then(function(content) {
+	 *          content.name = "I want a new name, and be published!";
+	 *          contentResource.publish(content, false)
+	 *            .then(function(content){
+	 *                alert("Retrieved, updated and published again");
+	 *            });
+	 *    });
+	 * 
+ * + * @param {Object} content The content item object with changes applied + * @param {Bool} isNew set to true to create a new item or to update an existing + * @param {Array} files collection of files for the document + * @returns {Promise} resourcePromise object containing the saved content item. + * + */ + publish: function (content, isNew, files) { + return saveContentItem(content, "publish" + (isNew ? "New" : ""), files); + }, - }; + /** + * @ngdoc method + * @name umbraco.resources.contentResource#sendToPublish + * @methodOf umbraco.resources.contentResource + * + * @description + * Saves changes made to a content item, and notifies any subscribers about a pending publication + * + * ##usage + *
+	 * contentResource.getById(1234)
+	 *    .then(function(content) {
+	 *          content.name = "I want a new name, and be published!";
+	 *          contentResource.sendToPublish(content, false)
+	 *            .then(function(content){
+	 *                alert("Retrieved, updated and notication send off");
+	 *            });
+	 *    });
+	 * 
+ * + * @param {Object} content The content item object with changes applied + * @param {Bool} isNew set to true to create a new item or to update an existing + * @param {Array} files collection of files for the document + * @returns {Promise} resourcePromise object containing the saved content item. + * + */ + sendToPublish: function (content, isNew, files) { + return saveContentItem(content, "sendPublish" + (isNew ? "New" : ""), files); + }, + + /** + * @ngdoc method + * @name umbraco.resources.contentResource#publishByid + * @methodOf umbraco.resources.contentResource + * + * @description + * Publishes a content item with a given ID + * + * ##usage + *
+	 * contentResource.publishById(1234)
+	 *    .then(function(content) {
+	 *        alert("published");
+	 *    });
+	 * 
+ * + * @param {Int} id The ID of the conten to publish + * @returns {Promise} resourcePromise object containing the published content item. + * + */ + publishById: function (id) { + + if (!id) { + throw "id cannot be null"; + } + + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "PostPublishById", + [{ id: id }])), + 'Failed to publish content with id ' + id); + + } + + + }; } angular.module('umbraco.resources').factory('contentResource', contentResource); diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/media.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/media.resource.js index 9a2310299f..887ed661a7 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/media.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/media.resource.js @@ -4,470 +4,472 @@ * @description Loads in data for media **/ function mediaResource($q, $http, umbDataFormatter, umbRequestHelper) { - - /** internal method process the saving of data and post processing the result */ - function saveMediaItem(content, action, files) { - return umbRequestHelper.postSaveContent({ - restApiUrl: umbRequestHelper.getApiUrl( - "mediaApiBaseUrl", - "PostSave"), - content: content, - action: action, - files: files, - dataFormatter: function (c, a) { - return umbDataFormatter.formatMediaPostData(c, a); - } - }); - } - return { - - getRecycleBin: function () { - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "mediaApiBaseUrl", - "GetRecycleBin")), - 'Failed to retrieve data for media recycle bin'); - }, + /** internal method process the saving of data and post processing the result */ + function saveMediaItem(content, action, files) { + return umbRequestHelper.postSaveContent({ + restApiUrl: umbRequestHelper.getApiUrl( + "mediaApiBaseUrl", + "PostSave"), + content: content, + action: action, + files: files, + dataFormatter: function (c, a) { + return umbDataFormatter.formatMediaPostData(c, a); + } + }); + } - /** - * @ngdoc method - * @name umbraco.resources.mediaResource#sort - * @methodOf umbraco.resources.mediaResource - * - * @description - * Sorts all children below a given parent node id, based on a collection of node-ids - * - * ##usage - *
-         * var ids = [123,34533,2334,23434];
-         * mediaResource.sort({ sortedIds: ids })
-         *    .then(function() {
-         *        $scope.complete = true;
-         *    });
-         * 
- * @param {Object} args arguments object - * @param {Int} args.parentId the ID of the parent node - * @param {Array} options.sortedIds array of node IDs as they should be sorted - * @returns {Promise} resourcePromise object. - * - */ - sort: function (args) { - if (!args) { - throw "args cannot be null"; - } - if (!args.parentId) { - throw "args.parentId cannot be null"; - } - if (!args.sortedIds) { - throw "args.sortedIds cannot be null"; - } + return { - return umbRequestHelper.resourcePromise( - $http.post(umbRequestHelper.getApiUrl("mediaApiBaseUrl", "PostSort"), - { - parentId: args.parentId, - idSortOrder: args.sortedIds - }), - 'Failed to sort media'); - }, + getRecycleBin: function () { + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "mediaApiBaseUrl", + "GetRecycleBin")), + 'Failed to retrieve data for media recycle bin'); + }, - /** - * @ngdoc method - * @name umbraco.resources.mediaResource#move - * @methodOf umbraco.resources.mediaResource - * - * @description - * Moves a node underneath a new parentId - * - * ##usage - *
-         * mediaResource.move({ parentId: 1244, id: 123 })
-         *    .then(function() {
-         *        alert("node was moved");
-         *    }, function(err){
-         *      alert("node didnt move:" + err.data.Message); 
-         *    });
-         * 
- * @param {Object} args arguments object - * @param {Int} args.idd the ID of the node to move - * @param {Int} args.parentId the ID of the parent node to move to - * @returns {Promise} resourcePromise object. - * - */ - move: function (args) { - if (!args) { - throw "args cannot be null"; - } - if (!args.parentId) { - throw "args.parentId cannot be null"; - } - if (!args.id) { - throw "args.id cannot be null"; - } + /** + * @ngdoc method + * @name umbraco.resources.mediaResource#sort + * @methodOf umbraco.resources.mediaResource + * + * @description + * Sorts all children below a given parent node id, based on a collection of node-ids + * + * ##usage + *
+	 * var ids = [123,34533,2334,23434];
+	 * mediaResource.sort({ sortedIds: ids })
+	 *    .then(function() {
+	 *        $scope.complete = true;
+	 *    });
+	 * 
+ * @param {Object} args arguments object + * @param {Int} args.parentId the ID of the parent node + * @param {Array} options.sortedIds array of node IDs as they should be sorted + * @returns {Promise} resourcePromise object. + * + */ + sort: function (args) { + if (!args) { + throw "args cannot be null"; + } + if (!args.parentId) { + throw "args.parentId cannot be null"; + } + if (!args.sortedIds) { + throw "args.sortedIds cannot be null"; + } - return umbRequestHelper.resourcePromise( - $http.post(umbRequestHelper.getApiUrl("mediaApiBaseUrl", "PostMove"), - { - parentId: args.parentId, - id: args.id - }), - 'Failed to move media'); - }, + return umbRequestHelper.resourcePromise( + $http.post(umbRequestHelper.getApiUrl("mediaApiBaseUrl", "PostSort"), + { + parentId: args.parentId, + idSortOrder: args.sortedIds + }), + 'Failed to sort media'); + }, + + /** + * @ngdoc method + * @name umbraco.resources.mediaResource#move + * @methodOf umbraco.resources.mediaResource + * + * @description + * Moves a node underneath a new parentId + * + * ##usage + *
+	 * mediaResource.move({ parentId: 1244, id: 123 })
+	 *    .then(function() {
+	 *        alert("node was moved");
+	 *    }, function(err){
+	 *      alert("node didnt move:" + err.data.Message); 
+	 *    });
+	 * 
+ * @param {Object} args arguments object + * @param {Int} args.idd the ID of the node to move + * @param {Int} args.parentId the ID of the parent node to move to + * @returns {Promise} resourcePromise object. + * + */ + move: function (args) { + if (!args) { + throw "args cannot be null"; + } + if (!args.parentId) { + throw "args.parentId cannot be null"; + } + if (!args.id) { + throw "args.id cannot be null"; + } + + return umbRequestHelper.resourcePromise( + $http.post(umbRequestHelper.getApiUrl("mediaApiBaseUrl", "PostMove"), + { + parentId: args.parentId, + id: args.id + }), + 'Failed to move media'); + }, - /** - * @ngdoc method - * @name umbraco.resources.mediaResource#getById - * @methodOf umbraco.resources.mediaResource - * - * @description - * Gets a media item with a given id - * - * ##usage - *
-         * mediaResource.getById(1234)
-         *    .then(function(media) {
-         *        var myMedia = media; 
-         *        alert('its here!');
-         *    });
-         * 
- * - * @param {Int} id id of media item to return - * @returns {Promise} resourcePromise object containing the media item. - * - */ - getById: function (id) { - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "mediaApiBaseUrl", - "GetById", - [{ id: id }])), - 'Failed to retrieve data for media id ' + id); - }, + /** + * @ngdoc method + * @name umbraco.resources.mediaResource#getById + * @methodOf umbraco.resources.mediaResource + * + * @description + * Gets a media item with a given id + * + * ##usage + *
+	 * mediaResource.getById(1234)
+	 *    .then(function(media) {
+	 *        var myMedia = media; 
+	 *        alert('its here!');
+	 *    });
+	 * 
+ * + * @param {Int} id id of media item to return + * @returns {Promise} resourcePromise object containing the media item. + * + */ + getById: function (id) { - /** - * @ngdoc method - * @name umbraco.resources.mediaResource#deleteById - * @methodOf umbraco.resources.mediaResource - * - * @description - * Deletes a media item with a given id - * - * ##usage - *
-         * mediaResource.deleteById(1234)
-         *    .then(function() {
-         *        alert('its gone!');
-         *    });
-         * 
- * - * @param {Int} id id of media item to delete - * @returns {Promise} resourcePromise object. - * - */ - deleteById: function(id) { - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "mediaApiBaseUrl", - "DeleteById", - [{ id: id }])), - 'Failed to delete item ' + id); - }, + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "mediaApiBaseUrl", + "GetById", + [{ id: id }])), + 'Failed to retrieve data for media id ' + id); + }, - /** - * @ngdoc method - * @name umbraco.resources.mediaResource#getByIds - * @methodOf umbraco.resources.mediaResource - * - * @description - * Gets an array of media items, given a collection of ids - * - * ##usage - *
-         * mediaResource.getByIds( [1234,2526,28262])
-         *    .then(function(mediaArray) {
-         *        var myDoc = contentArray; 
-         *        alert('they are here!');
-         *    });
-         * 
- * - * @param {Array} ids ids of media items to return as an array - * @returns {Promise} resourcePromise object containing the media items array. - * - */ - getByIds: function (ids) { - - var idQuery = ""; - _.each(ids, function(item) { - idQuery += "ids=" + item + "&"; - }); + /** + * @ngdoc method + * @name umbraco.resources.mediaResource#deleteById + * @methodOf umbraco.resources.mediaResource + * + * @description + * Deletes a media item with a given id + * + * ##usage + *
+	 * mediaResource.deleteById(1234)
+	 *    .then(function() {
+	 *        alert('its gone!');
+	 *    });
+	 * 
+ * + * @param {Int} id id of media item to delete + * @returns {Promise} resourcePromise object. + * + */ + deleteById: function (id) { + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "mediaApiBaseUrl", + "DeleteById", + [{ id: id }])), + 'Failed to delete item ' + id); + }, - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "mediaApiBaseUrl", - "GetByIds", - idQuery)), - 'Failed to retrieve data for media ids ' + ids); - }, + /** + * @ngdoc method + * @name umbraco.resources.mediaResource#getByIds + * @methodOf umbraco.resources.mediaResource + * + * @description + * Gets an array of media items, given a collection of ids + * + * ##usage + *
+	 * mediaResource.getByIds( [1234,2526,28262])
+	 *    .then(function(mediaArray) {
+	 *        var myDoc = contentArray; 
+	 *        alert('they are here!');
+	 *    });
+	 * 
+ * + * @param {Array} ids ids of media items to return as an array + * @returns {Promise} resourcePromise object containing the media items array. + * + */ + getByIds: function (ids) { - /** - * @ngdoc method - * @name umbraco.resources.mediaResource#getScaffold - * @methodOf umbraco.resources.mediaResource - * - * @description - * Returns a scaffold of an empty media item, given the id of the media item to place it underneath and the media type alias. - * - * - Parent Id must be provided so umbraco knows where to store the media - * - Media Type alias must be provided so umbraco knows which properties to put on the media scaffold - * - * The scaffold is used to build editors for media that has not yet been populated with data. - * - * ##usage - *
-         * mediaResource.getScaffold(1234, 'folder')
-         *    .then(function(scaffold) {
-         *        var myDoc = scaffold;
-         *        myDoc.name = "My new media item"; 
-         *
-         *        mediaResource.save(myDoc, true)
-         *            .then(function(media){
-         *                alert("Retrieved, updated and saved again");
-         *            });
-         *    });
-         * 
- * - * @param {Int} parentId id of media item to return - * @param {String} alias mediatype alias to base the scaffold on - * @returns {Promise} resourcePromise object containing the media scaffold. - * - */ - getScaffold: function (parentId, alias) { - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "mediaApiBaseUrl", - "GetEmpty", - [{ contentTypeAlias: alias }, { parentId: parentId }])), - 'Failed to retrieve data for empty media item type ' + alias); + var idQuery = ""; + _.each(ids, function (item) { + idQuery += "ids=" + item + "&"; + }); - }, + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "mediaApiBaseUrl", + "GetByIds", + idQuery)), + 'Failed to retrieve data for media ids ' + ids); + }, - rootMedia: function () { - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "mediaApiBaseUrl", - "GetRootMedia")), - 'Failed to retrieve data for root media'); + /** + * @ngdoc method + * @name umbraco.resources.mediaResource#getScaffold + * @methodOf umbraco.resources.mediaResource + * + * @description + * Returns a scaffold of an empty media item, given the id of the media item to place it underneath and the media type alias. + * + * - Parent Id must be provided so umbraco knows where to store the media + * - Media Type alias must be provided so umbraco knows which properties to put on the media scaffold + * + * The scaffold is used to build editors for media that has not yet been populated with data. + * + * ##usage + *
+	 * mediaResource.getScaffold(1234, 'folder')
+	 *    .then(function(scaffold) {
+	 *        var myDoc = scaffold;
+	 *        myDoc.name = "My new media item"; 
+	 *
+	 *        mediaResource.save(myDoc, true)
+	 *            .then(function(media){
+	 *                alert("Retrieved, updated and saved again");
+	 *            });
+	 *    });
+	 * 
+ * + * @param {Int} parentId id of media item to return + * @param {String} alias mediatype alias to base the scaffold on + * @returns {Promise} resourcePromise object containing the media scaffold. + * + */ + getScaffold: function (parentId, alias) { - }, + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "mediaApiBaseUrl", + "GetEmpty", + [{ contentTypeAlias: alias }, { parentId: parentId }])), + 'Failed to retrieve data for empty media item type ' + alias); - /** - * @ngdoc method - * @name umbraco.resources.mediaResource#getChildren - * @methodOf umbraco.resources.mediaResource - * - * @description - * Gets children of a media item with a given id - * - * ##usage - *
-         * mediaResource.getChildren(1234, {pageSize: 10, pageNumber: 2})
-         *    .then(function(contentArray) {
-         *        var children = contentArray; 
-         *        alert('they are here!');
-         *    });
-         * 
- * - * @param {Int} parentid id of content item to return children of - * @param {Object} options optional options object - * @param {Int} options.pageSize if paging data, number of nodes per page, default = 0 - * @param {Int} options.pageNumber if paging data, current page index, default = 0 - * @param {String} options.filter if provided, query will only return those with names matching the filter - * @param {String} options.orderDirection can be `Ascending` or `Descending` - Default: `Ascending` - * @param {String} options.orderBy property to order items by, default: `SortOrder` - * @returns {Promise} resourcePromise object containing an array of content items. - * - */ - getChildren: function (parentId, options) { + }, - var defaults = { - pageSize: 0, - pageNumber: 0, - filter: '', - orderDirection: "Ascending", - orderBy: "SortOrder" - }; - if (options === undefined) { - options = {}; - } - //overwrite the defaults if there are any specified - angular.extend(defaults, options); - //now copy back to the options we will use - options = defaults; - //change asc/desct - if (options.orderDirection === "asc") { - options.orderDirection = "Ascending"; - } - else if (options.orderDirection === "desc") { - options.orderDirection = "Descending"; - } + rootMedia: function () { - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "mediaApiBaseUrl", - "GetChildren", - [ - { id: parentId }, - { pageNumber: options.pageNumber }, - { pageSize: options.pageSize }, - { orderBy: options.orderBy }, - { orderDirection: options.orderDirection }, - { filter: options.filter } - ])), - 'Failed to retrieve children for media item ' + parentId); - }, - - /** - * @ngdoc method - * @name umbraco.resources.mediaResource#save - * @methodOf umbraco.resources.mediaResource - * - * @description - * Saves changes made to a media item, if the media item is new, the isNew paramater must be passed to force creation - * if the media item needs to have files attached, they must be provided as the files param and passed separately - * - * - * ##usage - *
-         * mediaResource.getById(1234)
-         *    .then(function(media) {
-         *          media.name = "I want a new name!";
-         *          mediaResource.save(media, false)
-         *            .then(function(media){
-         *                alert("Retrieved, updated and saved again");
-         *            });
-         *    });
-         * 
- * - * @param {Object} media The media item object with changes applied - * @param {Bool} isNew set to true to create a new item or to update an existing - * @param {Array} files collection of files for the media item - * @returns {Promise} resourcePromise object containing the saved media item. - * - */ - save: function (media, isNew, files) { - return saveMediaItem(media, "save" + (isNew ? "New" : ""), files); - }, + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "mediaApiBaseUrl", + "GetRootMedia")), + 'Failed to retrieve data for root media'); - /** - * @ngdoc method - * @name umbraco.resources.mediaResource#addFolder - * @methodOf umbraco.resources.mediaResource - * - * @description - * Shorthand for adding a media item of the type "Folder" under a given parent ID - * - * ##usage - *
-         * mediaResource.addFolder("My gallery", 1234)
-         *    .then(function(folder) {
-         *        alert('New folder');
-         *    });
-         * 
- * - * @param {string} name Name of the folder to create - * @param {int} parentId Id of the media item to create the folder underneath - * @returns {Promise} resourcePromise object. - * - */ - addFolder: function(name, parentId){ - return umbRequestHelper.resourcePromise( - $http.post(umbRequestHelper - .getApiUrl("mediaApiBaseUrl", "PostAddFolder"), - { - name: name, - parentId: parentId - }), - 'Failed to add folder'); - }, - - /** - * @ngdoc method - * @name umbraco.resources.mediaResource#getChildFolders - * @methodOf umbraco.resources.mediaResource - * - * @description - * Retrieves all media children with types used as folders. - * Uses the convention of looking for media items with mediaTypes ending in - * *Folder so will match "Folder", "bannerFolder", "secureFolder" etc, - * - * ##usage - *
-         * mediaResource.getChildFolders(1234)
-         *    .then(function(data) {
-         *        alert('folders');
-         *    });
-         * 
- * - * @param {int} parentId Id of the media item to query for child folders - * @returns {Promise} resourcePromise object. - * - */ - getChildFolders: function(parentId){ - if(!parentId){ - parentId = -1; - } - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "mediaApiBaseUrl", - "GetChildFolders", - [ - { id: parentId } - ])), - 'Failed to retrieve child folders for media item ' + parentId); - }, - - /** - * @ngdoc method - * @name umbraco.resources.mediaResource#emptyRecycleBin - * @methodOf umbraco.resources.mediaResource - * - * @description - * Empties the media recycle bin - * - * ##usage - *
-         * mediaResource.emptyRecycleBin()
-         *    .then(function() {
-         *        alert('its empty!');
-         *    });
-         * 
- * - * @returns {Promise} resourcePromise object. - * - */ - emptyRecycleBin: function() { - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "mediaApiBaseUrl", - "EmptyRecycleBin")), - 'Failed to empty the recycle bin'); - } - }; + }, + + /** + * @ngdoc method + * @name umbraco.resources.mediaResource#getChildren + * @methodOf umbraco.resources.mediaResource + * + * @description + * Gets children of a media item with a given id + * + * ##usage + *
+	 * mediaResource.getChildren(1234, {pageSize: 10, pageNumber: 2})
+	 *    .then(function(contentArray) {
+	 *        var children = contentArray; 
+	 *        alert('they are here!');
+	 *    });
+	 * 
+ * + * @param {Int} parentid id of content item to return children of + * @param {Object} options optional options object + * @param {Int} options.pageSize if paging data, number of nodes per page, default = 0 + * @param {Int} options.pageNumber if paging data, current page index, default = 0 + * @param {String} options.filter if provided, query will only return those with names matching the filter + * @param {String} options.orderDirection can be `Ascending` or `Descending` - Default: `Ascending` + * @param {String} options.orderBy property to order items by, default: `SortOrder` + * @returns {Promise} resourcePromise object containing an array of content items. + * + */ + getChildren: function (parentId, options) { + + var defaults = { + pageSize: 0, + pageNumber: 0, + filter: '', + orderDirection: "Ascending", + orderBy: "SortOrder", + orderBySystemField: true + }; + if (options === undefined) { + options = {}; + } + //overwrite the defaults if there are any specified + angular.extend(defaults, options); + //now copy back to the options we will use + options = defaults; + //change asc/desct + if (options.orderDirection === "asc") { + options.orderDirection = "Ascending"; + } + else if (options.orderDirection === "desc") { + options.orderDirection = "Descending"; + } + + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "mediaApiBaseUrl", + "GetChildren", + [ + { id: parentId }, + { pageNumber: options.pageNumber }, + { pageSize: options.pageSize }, + { orderBy: options.orderBy }, + { orderDirection: options.orderDirection }, + { orderBySystemField: options.orderBySystemField }, + { filter: options.filter } + ])), + 'Failed to retrieve children for media item ' + parentId); + }, + + /** + * @ngdoc method + * @name umbraco.resources.mediaResource#save + * @methodOf umbraco.resources.mediaResource + * + * @description + * Saves changes made to a media item, if the media item is new, the isNew paramater must be passed to force creation + * if the media item needs to have files attached, they must be provided as the files param and passed separately + * + * + * ##usage + *
+	 * mediaResource.getById(1234)
+	 *    .then(function(media) {
+	 *          media.name = "I want a new name!";
+	 *          mediaResource.save(media, false)
+	 *            .then(function(media){
+	 *                alert("Retrieved, updated and saved again");
+	 *            });
+	 *    });
+	 * 
+ * + * @param {Object} media The media item object with changes applied + * @param {Bool} isNew set to true to create a new item or to update an existing + * @param {Array} files collection of files for the media item + * @returns {Promise} resourcePromise object containing the saved media item. + * + */ + save: function (media, isNew, files) { + return saveMediaItem(media, "save" + (isNew ? "New" : ""), files); + }, + + /** + * @ngdoc method + * @name umbraco.resources.mediaResource#addFolder + * @methodOf umbraco.resources.mediaResource + * + * @description + * Shorthand for adding a media item of the type "Folder" under a given parent ID + * + * ##usage + *
+	 * mediaResource.addFolder("My gallery", 1234)
+	 *    .then(function(folder) {
+	 *        alert('New folder');
+	 *    });
+	 * 
+ * + * @param {string} name Name of the folder to create + * @param {int} parentId Id of the media item to create the folder underneath + * @returns {Promise} resourcePromise object. + * + */ + addFolder: function (name, parentId) { + return umbRequestHelper.resourcePromise( + $http.post(umbRequestHelper + .getApiUrl("mediaApiBaseUrl", "PostAddFolder"), + { + name: name, + parentId: parentId + }), + 'Failed to add folder'); + }, + + /** + * @ngdoc method + * @name umbraco.resources.mediaResource#getChildFolders + * @methodOf umbraco.resources.mediaResource + * + * @description + * Retrieves all media children with types used as folders. + * Uses the convention of looking for media items with mediaTypes ending in + * *Folder so will match "Folder", "bannerFolder", "secureFolder" etc, + * + * ##usage + *
+	 * mediaResource.getChildFolders(1234)
+	 *    .then(function(data) {
+	 *        alert('folders');
+	 *    });
+	 * 
+ * + * @param {int} parentId Id of the media item to query for child folders + * @returns {Promise} resourcePromise object. + * + */ + getChildFolders: function (parentId) { + if (!parentId) { + parentId = -1; + } + + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "mediaApiBaseUrl", + "GetChildFolders", + [ + { id: parentId } + ])), + 'Failed to retrieve child folders for media item ' + parentId); + }, + + /** + * @ngdoc method + * @name umbraco.resources.mediaResource#emptyRecycleBin + * @methodOf umbraco.resources.mediaResource + * + * @description + * Empties the media recycle bin + * + * ##usage + *
+	 * mediaResource.emptyRecycleBin()
+	 *    .then(function() {
+	 *        alert('its empty!');
+	 *    });
+	 * 
+ * + * @returns {Promise} resourcePromise object. + * + */ + emptyRecycleBin: function () { + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "mediaApiBaseUrl", + "EmptyRecycleBin")), + 'Failed to empty the recycle bin'); + } + }; } angular.module('umbraco.resources').factory('mediaResource', mediaResource); diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/member.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/member.resource.js index 42db4f6366..c231a4944c 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/member.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/member.resource.js @@ -4,230 +4,232 @@ * @description Loads in data for members **/ function memberResource($q, $http, umbDataFormatter, umbRequestHelper) { - - /** internal method process the saving of data and post processing the result */ - function saveMember(content, action, files) { - - return umbRequestHelper.postSaveContent({ - restApiUrl: umbRequestHelper.getApiUrl( - "memberApiBaseUrl", - "PostSave"), - content: content, - action: action, - files: files, - dataFormatter: function(c, a) { - return umbDataFormatter.formatMemberPostData(c, a); - } - }); - } - return { - - getPagedResults: function (memberTypeAlias, options) { + /** internal method process the saving of data and post processing the result */ + function saveMember(content, action, files) { - if (memberTypeAlias === 'all-members') { - memberTypeAlias = null; - } + return umbRequestHelper.postSaveContent({ + restApiUrl: umbRequestHelper.getApiUrl( + "memberApiBaseUrl", + "PostSave"), + content: content, + action: action, + files: files, + dataFormatter: function (c, a) { + return umbDataFormatter.formatMemberPostData(c, a); + } + }); + } - var defaults = { - pageSize: 25, - pageNumber: 1, - filter: '', - orderDirection: "Ascending", - orderBy: "LoginName" - }; - if (options === undefined) { - options = {}; - } - //overwrite the defaults if there are any specified - angular.extend(defaults, options); - //now copy back to the options we will use - options = defaults; - //change asc/desct - if (options.orderDirection === "asc") { - options.orderDirection = "Ascending"; - } - else if (options.orderDirection === "desc") { - options.orderDirection = "Descending"; - } + return { - var params = [ - { pageNumber: options.pageNumber }, - { pageSize: options.pageSize }, - { orderBy: options.orderBy }, - { orderDirection: options.orderDirection }, - { filter: options.filter } - ]; - if (memberTypeAlias != null) { - params.push({ memberTypeAlias: memberTypeAlias }); - } + getPagedResults: function (memberTypeAlias, options) { - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "memberApiBaseUrl", - "GetPagedResults", - params)), - 'Failed to retrieve member paged result'); - }, - - getListNode: function (listName) { + if (memberTypeAlias === 'all-members') { + memberTypeAlias = null; + } - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "memberApiBaseUrl", - "GetListNodeDisplay", - [{ listName: listName }])), - 'Failed to retrieve data for member list ' + listName); - }, + var defaults = { + pageSize: 25, + pageNumber: 1, + filter: '', + orderDirection: "Ascending", + orderBy: "LoginName", + orderBySystemField: true + }; + if (options === undefined) { + options = {}; + } + //overwrite the defaults if there are any specified + angular.extend(defaults, options); + //now copy back to the options we will use + options = defaults; + //change asc/desct + if (options.orderDirection === "asc") { + options.orderDirection = "Ascending"; + } + else if (options.orderDirection === "desc") { + options.orderDirection = "Descending"; + } - /** - * @ngdoc method - * @name umbraco.resources.memberResource#getByKey - * @methodOf umbraco.resources.memberResource - * - * @description - * Gets a member item with a given key - * - * ##usage - *
-         * memberResource.getByKey("0000-0000-000-00000-000")
-         *    .then(function(member) {
-         *        var mymember = member; 
-         *        alert('its here!');
-         *    });
-         * 
- * - * @param {Guid} key key of member item to return - * @returns {Promise} resourcePromise object containing the member item. - * - */ - getByKey: function (key) { - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "memberApiBaseUrl", - "GetByKey", - [{ key: key }])), - 'Failed to retrieve data for member id ' + key); - }, + var params = [ + { pageNumber: options.pageNumber }, + { pageSize: options.pageSize }, + { orderBy: options.orderBy }, + { orderDirection: options.orderDirection }, + { orderBySystemField: options.orderBySystemField }, + { filter: options.filter } + ]; + if (memberTypeAlias != null) { + params.push({ memberTypeAlias: memberTypeAlias }); + } - /** - * @ngdoc method - * @name umbraco.resources.memberResource#deleteByKey - * @methodOf umbraco.resources.memberResource - * - * @description - * Deletes a member item with a given key - * - * ##usage - *
-         * memberResource.deleteByKey("0000-0000-000-00000-000")
-         *    .then(function() {
-         *        alert('its gone!');
-         *    });
-         * 
- * - * @param {Guid} key id of member item to delete - * @returns {Promise} resourcePromise object. - * - */ - deleteByKey: function (key) { - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "memberApiBaseUrl", - "DeleteByKey", - [{ key: key }])), - 'Failed to delete item ' + key); - }, + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "memberApiBaseUrl", + "GetPagedResults", + params)), + 'Failed to retrieve member paged result'); + }, - /** - * @ngdoc method - * @name umbraco.resources.memberResource#getScaffold - * @methodOf umbraco.resources.memberResource - * - * @description - * Returns a scaffold of an empty member item, given the id of the member item to place it underneath and the member type alias. - * - * - Member Type alias must be provided so umbraco knows which properties to put on the member scaffold - * - * The scaffold is used to build editors for member that has not yet been populated with data. - * - * ##usage - *
-         * memberResource.getScaffold('client')
-         *    .then(function(scaffold) {
-         *        var myDoc = scaffold;
-         *        myDoc.name = "My new member item"; 
-         *
-         *        memberResource.save(myDoc, true)
-         *            .then(function(member){
-         *                alert("Retrieved, updated and saved again");
-         *            });
-         *    });
-         * 
- * - * @param {String} alias membertype alias to base the scaffold on - * @returns {Promise} resourcePromise object containing the member scaffold. - * - */ - getScaffold: function (alias) { - - if (alias) { - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "memberApiBaseUrl", - "GetEmpty", - [{ contentTypeAlias: alias }])), - 'Failed to retrieve data for empty member item type ' + alias); - } - else { - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "memberApiBaseUrl", - "GetEmpty")), - 'Failed to retrieve data for empty member item type ' + alias); - } + getListNode: function (listName) { - }, - - /** - * @ngdoc method - * @name umbraco.resources.memberResource#save - * @methodOf umbraco.resources.memberResource - * - * @description - * Saves changes made to a member, if the member is new, the isNew paramater must be passed to force creation - * if the member needs to have files attached, they must be provided as the files param and passed separately - * - * - * ##usage - *
-         * memberResource.getBykey("23234-sd8djsd-3h8d3j-sdh8d")
-         *    .then(function(member) {
-         *          member.name = "Bob";
-         *          memberResource.save(member, false)
-         *            .then(function(member){
-         *                alert("Retrieved, updated and saved again");
-         *            });
-         *    });
-         * 
- * - * @param {Object} media The member item object with changes applied - * @param {Bool} isNew set to true to create a new item or to update an existing - * @param {Array} files collection of files for the media item - * @returns {Promise} resourcePromise object containing the saved media item. - * - */ - save: function (member, isNew, files) { - return saveMember(member, "save" + (isNew ? "New" : ""), files); - } - }; + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "memberApiBaseUrl", + "GetListNodeDisplay", + [{ listName: listName }])), + 'Failed to retrieve data for member list ' + listName); + }, + + /** + * @ngdoc method + * @name umbraco.resources.memberResource#getByKey + * @methodOf umbraco.resources.memberResource + * + * @description + * Gets a member item with a given key + * + * ##usage + *
+	 * memberResource.getByKey("0000-0000-000-00000-000")
+	 *    .then(function(member) {
+	 *        var mymember = member; 
+	 *        alert('its here!');
+	 *    });
+	 * 
+ * + * @param {Guid} key key of member item to return + * @returns {Promise} resourcePromise object containing the member item. + * + */ + getByKey: function (key) { + + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "memberApiBaseUrl", + "GetByKey", + [{ key: key }])), + 'Failed to retrieve data for member id ' + key); + }, + + /** + * @ngdoc method + * @name umbraco.resources.memberResource#deleteByKey + * @methodOf umbraco.resources.memberResource + * + * @description + * Deletes a member item with a given key + * + * ##usage + *
+	 * memberResource.deleteByKey("0000-0000-000-00000-000")
+	 *    .then(function() {
+	 *        alert('its gone!');
+	 *    });
+	 * 
+ * + * @param {Guid} key id of member item to delete + * @returns {Promise} resourcePromise object. + * + */ + deleteByKey: function (key) { + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "memberApiBaseUrl", + "DeleteByKey", + [{ key: key }])), + 'Failed to delete item ' + key); + }, + + /** + * @ngdoc method + * @name umbraco.resources.memberResource#getScaffold + * @methodOf umbraco.resources.memberResource + * + * @description + * Returns a scaffold of an empty member item, given the id of the member item to place it underneath and the member type alias. + * + * - Member Type alias must be provided so umbraco knows which properties to put on the member scaffold + * + * The scaffold is used to build editors for member that has not yet been populated with data. + * + * ##usage + *
+	 * memberResource.getScaffold('client')
+	 *    .then(function(scaffold) {
+	 *        var myDoc = scaffold;
+	 *        myDoc.name = "My new member item"; 
+	 *
+	 *        memberResource.save(myDoc, true)
+	 *            .then(function(member){
+	 *                alert("Retrieved, updated and saved again");
+	 *            });
+	 *    });
+	 * 
+ * + * @param {String} alias membertype alias to base the scaffold on + * @returns {Promise} resourcePromise object containing the member scaffold. + * + */ + getScaffold: function (alias) { + + if (alias) { + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "memberApiBaseUrl", + "GetEmpty", + [{ contentTypeAlias: alias }])), + 'Failed to retrieve data for empty member item type ' + alias); + } + else { + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "memberApiBaseUrl", + "GetEmpty")), + 'Failed to retrieve data for empty member item type ' + alias); + } + + }, + + /** + * @ngdoc method + * @name umbraco.resources.memberResource#save + * @methodOf umbraco.resources.memberResource + * + * @description + * Saves changes made to a member, if the member is new, the isNew paramater must be passed to force creation + * if the member needs to have files attached, they must be provided as the files param and passed separately + * + * + * ##usage + *
+	 * memberResource.getBykey("23234-sd8djsd-3h8d3j-sdh8d")
+	 *    .then(function(member) {
+	 *          member.name = "Bob";
+	 *          memberResource.save(member, false)
+	 *            .then(function(member){
+	 *                alert("Retrieved, updated and saved again");
+	 *            });
+	 *    });
+	 * 
+ * + * @param {Object} media The member item object with changes applied + * @param {Bool} isNew set to true to create a new item or to update an existing + * @param {Array} files collection of files for the media item + * @returns {Promise} resourcePromise object containing the saved media item. + * + */ + save: function (member, isNew, files) { + return saveMember(member, "save" + (isNew ? "New" : ""), files); + } + }; } angular.module('umbraco.resources').factory('memberResource', memberResource); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/list/list.listviewlayout.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/list/list.listviewlayout.controller.js index 1d2d361f28..de4902bdd1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/list/list.listviewlayout.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/list/list.listviewlayout.controller.js @@ -44,8 +44,9 @@ return listViewHelper.setSortingDirection(col, direction, $scope.options); } - function sort(field, allow) { + function sort(field, allow, isSystem) { if (allow) { + $scope.options.orderBySystemField = isSystem; listViewHelper.setSorting(field, allow, $scope.options); $scope.getContent($scope.contentId); } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js index 59ea03aa91..fcb738a358 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js @@ -1,64 +1,64 @@ function listViewController($rootScope, $scope, $routeParams, $injector, $cookieStore, notificationsService, iconHelper, dialogService, editorState, localizationService, $location, appState, $timeout, $q, mediaResource, listViewHelper, userService) { - //this is a quick check to see if we're in create mode, if so just exit - we cannot show children for content - // that isn't created yet, if we continue this will use the parent id in the route params which isn't what - // we want. NOTE: This is just a safety check since when we scaffold an empty model on the server we remove - // the list view tab entirely when it's new. - if ($routeParams.create) { - $scope.isNew = true; - return; - } + //this is a quick check to see if we're in create mode, if so just exit - we cannot show children for content + // that isn't created yet, if we continue this will use the parent id in the route params which isn't what + // we want. NOTE: This is just a safety check since when we scaffold an empty model on the server we remove + // the list view tab entirely when it's new. + if ($routeParams.create) { + $scope.isNew = true; + return; + } - //Now we need to check if this is for media, members or content because that will depend on the resources we use - var contentResource, getContentTypesCallback, getListResultsCallback, deleteItemCallback, getIdCallback, createEditUrlCallback; + //Now we need to check if this is for media, members or content because that will depend on the resources we use + var contentResource, getContentTypesCallback, getListResultsCallback, deleteItemCallback, getIdCallback, createEditUrlCallback; - //check the config for the entity type, or the current section name (since the config is only set in c#, not in pre-vals) - if (($scope.model.config.entityType && $scope.model.config.entityType === "member") || (appState.getSectionState("currentSection") === "member")) { - $scope.entityType = "member"; - contentResource = $injector.get('memberResource'); - getContentTypesCallback = $injector.get('memberTypeResource').getTypes; - getListResultsCallback = contentResource.getPagedResults; - deleteItemCallback = contentResource.deleteByKey; - getIdCallback = function(selected) { - var selectedKey = getItemKey(selected.id); - return selectedKey; - }; - createEditUrlCallback = function(item) { - return "/" + $scope.entityType + "/" + $scope.entityType + "/edit/" + item.key + "?page=" + $scope.options.pageNumber + "&listName=" + $scope.contentId; - }; - } - else { - //check the config for the entity type, or the current section name (since the config is only set in c#, not in pre-vals) - if (($scope.model.config.entityType && $scope.model.config.entityType === "media") || (appState.getSectionState("currentSection") === "media")) { - $scope.entityType = "media"; - contentResource = $injector.get('mediaResource'); - getContentTypesCallback = $injector.get('mediaTypeResource').getAllowedTypes; - } - else { - $scope.entityType = "content"; - contentResource = $injector.get('contentResource'); - getContentTypesCallback = $injector.get('contentTypeResource').getAllowedTypes; - } - getListResultsCallback = contentResource.getChildren; - deleteItemCallback = contentResource.deleteById; - getIdCallback = function(selected) { - return selected.id; - }; - createEditUrlCallback = function(item) { - return "/" + $scope.entityType + "/" + $scope.entityType + "/edit/" + item.id + "?page=" + $scope.options.pageNumber; - }; - } + //check the config for the entity type, or the current section name (since the config is only set in c#, not in pre-vals) + if (($scope.model.config.entityType && $scope.model.config.entityType === "member") || (appState.getSectionState("currentSection") === "member")) { + $scope.entityType = "member"; + contentResource = $injector.get('memberResource'); + getContentTypesCallback = $injector.get('memberTypeResource').getTypes; + getListResultsCallback = contentResource.getPagedResults; + deleteItemCallback = contentResource.deleteByKey; + getIdCallback = function (selected) { + var selectedKey = getItemKey(selected.id); + return selectedKey; + }; + createEditUrlCallback = function (item) { + return "/" + $scope.entityType + "/" + $scope.entityType + "/edit/" + item.key + "?page=" + $scope.options.pageNumber + "&listName=" + $scope.contentId; + }; + } + else { + //check the config for the entity type, or the current section name (since the config is only set in c#, not in pre-vals) + if (($scope.model.config.entityType && $scope.model.config.entityType === "media") || (appState.getSectionState("currentSection") === "media")) { + $scope.entityType = "media"; + contentResource = $injector.get('mediaResource'); + getContentTypesCallback = $injector.get('mediaTypeResource').getAllowedTypes; + } + else { + $scope.entityType = "content"; + contentResource = $injector.get('contentResource'); + getContentTypesCallback = $injector.get('contentTypeResource').getAllowedTypes; + } + getListResultsCallback = contentResource.getChildren; + deleteItemCallback = contentResource.deleteById; + getIdCallback = function (selected) { + return selected.id; + }; + createEditUrlCallback = function (item) { + return "/" + $scope.entityType + "/" + $scope.entityType + "/edit/" + item.id + "?page=" + $scope.options.pageNumber; + }; + } + + $scope.pagination = []; + $scope.isNew = false; + $scope.actionInProgress = false; + $scope.selection = []; + $scope.folders = []; + $scope.listViewResultSet = { + totalPages: 0, + items: [] + }; - $scope.pagination = []; - $scope.isNew = false; - $scope.actionInProgress = false; - $scope.selection = []; - $scope.folders = []; - $scope.listViewResultSet = { - totalPages: 0, - items: [] - }; - $scope.currentNodePermissions = {} //Just ensure we do have an editorState @@ -135,433 +135,444 @@ function listViewController($rootScope, $scope, $routeParams, $injector, $cookie } - $scope.options = { - displayAtTabNumber: $scope.model.config.displayAtTabNumber ? $scope.model.config.displayAtTabNumber : 1, - pageSize: $scope.model.config.pageSize ? $scope.model.config.pageSize : 10, - pageNumber: ($routeParams.page && Number($routeParams.page) != NaN && Number($routeParams.page) > 0) ? $routeParams.page : 1, - filter: '', - orderBy: ($scope.model.config.orderBy ? $scope.model.config.orderBy : 'VersionDate').trim(), - orderDirection: $scope.model.config.orderDirection ? $scope.model.config.orderDirection.trim() : "desc", - includeProperties: $scope.model.config.includeProperties ? $scope.model.config.includeProperties : [ - { alias: 'updateDate', header: 'Last edited', isSystem: 1 }, - { alias: 'updater', header: 'Last edited by', isSystem: 1 } - ], - layout: { - layouts: $scope.model.config.layouts, - activeLayout: listViewHelper.getLayout($routeParams.id, $scope.model.config.layouts) - }, - allowBulkPublish: $scope.entityType === 'content' && $scope.model.config.bulkActionPermissions.allowBulkPublish, - allowBulkUnpublish: $scope.entityType === 'content' && $scope.model.config.bulkActionPermissions.allowBulkUnpublish, - allowBulkCopy: $scope.entityType === 'content' && $scope.model.config.bulkActionPermissions.allowBulkCopy, - allowBulkMove: $scope.model.config.bulkActionPermissions.allowBulkMove, - allowBulkDelete: $scope.model.config.bulkActionPermissions.allowBulkDelete - }; + $scope.options = { + displayAtTabNumber: $scope.model.config.displayAtTabNumber ? $scope.model.config.displayAtTabNumber : 1, + pageSize: $scope.model.config.pageSize ? $scope.model.config.pageSize : 10, + pageNumber: ($routeParams.page && Number($routeParams.page) != NaN && Number($routeParams.page) > 0) ? $routeParams.page : 1, + filter: '', + orderBy: ($scope.model.config.orderBy ? $scope.model.config.orderBy : 'VersionDate').trim(), + orderDirection: $scope.model.config.orderDirection ? $scope.model.config.orderDirection.trim() : "desc", + orderBySystemField: true, + includeProperties: $scope.model.config.includeProperties ? $scope.model.config.includeProperties : [ + { alias: 'updateDate', header: 'Last edited', isSystem: 1 }, + { alias: 'updater', header: 'Last edited by', isSystem: 1 } + ], + layout: { + layouts: $scope.model.config.layouts, + activeLayout: listViewHelper.getLayout($routeParams.id, $scope.model.config.layouts) + }, + allowBulkPublish: $scope.entityType === 'content' && $scope.model.config.bulkActionPermissions.allowBulkPublish, + allowBulkUnpublish: $scope.entityType === 'content' && $scope.model.config.bulkActionPermissions.allowBulkUnpublish, + allowBulkCopy: $scope.entityType === 'content' && $scope.model.config.bulkActionPermissions.allowBulkCopy, + allowBulkMove: $scope.model.config.bulkActionPermissions.allowBulkMove, + allowBulkDelete: $scope.model.config.bulkActionPermissions.allowBulkDelete + }; - //update all of the system includeProperties to enable sorting - _.each($scope.options.includeProperties, function(e, i) { + //update all of the system includeProperties to enable sorting + _.each($scope.options.includeProperties, function (e, i) { - if (e.isSystem) { - //NOTE: special case for contentTypeAlias, it's a system property that cannot be sorted - // to do that, we'd need to update the base query for content to include the content type alias column - // which requires another join and would be slower. BUT We are doing this for members so not sure it makes a diff? - if (e.alias != "contentTypeAlias") { - e.allowSorting = true; - } + //NOTE: special case for contentTypeAlias, it's a system property that cannot be sorted + // to do that, we'd need to update the base query for content to include the content type alias column + // which requires another join and would be slower. BUT We are doing this for members so not sure it makes a diff? + if (e.alias != "contentTypeAlias") { + e.allowSorting = true; + } - //localize the header - var key = getLocalizedKey(e.alias); - localizationService.localize(key).then(function (v) { - e.header = v; - }); - } + // Another special case for lasted edited data/update date for media, again this field isn't available on the base table so we can't sort by it + if (e.isSystem && $scope.entityType == "media") { + e.allowSorting = e.alias != 'updateDate'; + } + + // Another special case for members, only fields on the base table (cmsMember) can be used for sorting + if (e.isSystem && $scope.entityType == "member") { + e.allowSorting = e.alias == 'username' || e.alias == 'email'; + } + if (e.isSystem) { + + //localize the header + var key = getLocalizedKey(e.alias); + localizationService.localize(key).then(function (v) { + e.header = v; + }); + } + }); + + $scope.selectLayout = function (selectedLayout) { + $scope.options.layout.activeLayout = listViewHelper.setLayout($routeParams.id, selectedLayout, $scope.model.config.layouts); + }; + + function showNotificationsAndReset(err, reload, successMsg) { + + //check if response is ysod + if (err.status && err.status >= 500) { + + // Open ysod overlay + $scope.ysodOverlay = { + view: "ysod", + error: err, + show: true + }; + } + + $timeout(function () { + $scope.bulkStatus = ""; + $scope.actionInProgress = false; + }, 500); + + if (reload === true) { + $scope.reloadView($scope.contentId); + } + + if (err.data && angular.isArray(err.data.notifications)) { + for (var i = 0; i < err.data.notifications.length; i++) { + notificationsService.showNotification(err.data.notifications[i]); + } + } + else if (successMsg) { + notificationsService.success("Done", successMsg); + } + } + + $scope.next = function (pageNumber) { + $scope.options.pageNumber = pageNumber; + $scope.reloadView($scope.contentId); + }; + + $scope.goToPage = function (pageNumber) { + $scope.options.pageNumber = pageNumber; + $scope.reloadView($scope.contentId); + }; + + $scope.prev = function (pageNumber) { + $scope.options.pageNumber = pageNumber; + $scope.reloadView($scope.contentId); + }; + + + /*Loads the search results, based on parameters set in prev,next,sort and so on*/ + /*Pagination is done by an array of objects, due angularJS's funky way of monitoring state + with simple values */ + + $scope.reloadView = function (id) { + + $scope.viewLoaded = false; + + listViewHelper.clearSelection($scope.listViewResultSet.items, $scope.folders, $scope.selection); + + getListResultsCallback(id, $scope.options).then(function (data) { + + $scope.actionInProgress = false; + $scope.listViewResultSet = data; + + //update all values for display + if ($scope.listViewResultSet.items) { + _.each($scope.listViewResultSet.items, function (e, index) { + setPropertyValues(e); }); - - $scope.selectLayout = function(selectedLayout) { - $scope.options.layout.activeLayout = listViewHelper.setLayout($routeParams.id, selectedLayout, $scope.model.config.layouts); - }; - - function showNotificationsAndReset(err, reload, successMsg) { - - //check if response is ysod - if(err.status && err.status >= 500) { - - // Open ysod overlay - $scope.ysodOverlay = { - view : "ysod", - error : err, - show : true - }; - } - - $timeout(function() { - $scope.bulkStatus = ""; - $scope.actionInProgress = false; - }, 500); - - if (reload === true) { - $scope.reloadView($scope.contentId); - } - - if (err.data && angular.isArray(err.data.notifications)) { - for (var i = 0; i < err.data.notifications.length; i++) { - notificationsService.showNotification(err.data.notifications[i]); - } - } - else if (successMsg) { - notificationsService.success("Done", successMsg); - } - } - - $scope.next = function(pageNumber) { - $scope.options.pageNumber = pageNumber; - $scope.reloadView($scope.contentId); - }; - - $scope.goToPage = function(pageNumber) { - $scope.options.pageNumber = pageNumber; - $scope.reloadView($scope.contentId); - }; - - $scope.prev = function(pageNumber) { - $scope.options.pageNumber = pageNumber; - $scope.reloadView($scope.contentId); - }; - - - /*Loads the search results, based on parameters set in prev,next,sort and so on*/ - /*Pagination is done by an array of objects, due angularJS's funky way of monitoring state - with simple values */ - - $scope.reloadView = function(id) { - - $scope.viewLoaded = false; - - listViewHelper.clearSelection($scope.listViewResultSet.items, $scope.folders, $scope.selection); - - getListResultsCallback(id, $scope.options).then(function(data) { - - $scope.actionInProgress = false; - $scope.listViewResultSet = data; - - //update all values for display - if ($scope.listViewResultSet.items) { - _.each($scope.listViewResultSet.items, function(e, index) { - setPropertyValues(e); - }); - } - - if ($scope.entityType === 'media') { - - mediaResource.getChildFolders($scope.contentId) - .then(function(folders) { - $scope.folders = folders; - $scope.viewLoaded = true; - }); - - } else { - $scope.viewLoaded = true; - } - - //NOTE: This might occur if we are requesting a higher page number than what is actually available, for example - // if you have more than one page and you delete all items on the last page. In this case, we need to reset to the last - // available page and then re-load again - if ($scope.options.pageNumber > $scope.listViewResultSet.totalPages) { - $scope.options.pageNumber = $scope.listViewResultSet.totalPages; - - //reload! - $scope.reloadView(id); - } - - }); - }; - - var searchListView = _.debounce(function(){ - $scope.$apply(function() { - makeSearch(); - }); - }, 500); - - $scope.forceSearch = function (ev) { - //13: enter - switch (ev.keyCode) { - case 13: - makeSearch(); - break; - } - }; - - $scope.enterSearch = function() { - $scope.viewLoaded = false; - searchListView(); - }; - - function makeSearch() { - if ($scope.options.filter !== null && $scope.options.filter !== undefined) { - $scope.options.pageNumber = 1; - //$scope.actionInProgress = true; - $scope.reloadView($scope.contentId); - } - } - - $scope.isAnythingSelected = function() { - if ($scope.selection.length === 0) { - return false; - } else { - return true; - } - }; - - $scope.selectedItemsCount = function() { - return $scope.selection.length; - }; - - $scope.clearSelection = function() { - listViewHelper.clearSelection($scope.listViewResultSet.items, $scope.folders, $scope.selection); - }; - - $scope.getIcon = function(entry) { - return iconHelper.convertFromLegacyIcon(entry.icon); - }; - - function serial(selected, fn, getStatusMsg, index) { - return fn(selected, index).then(function (content) { - index++; - $scope.bulkStatus = getStatusMsg(index, selected.length); - return index < selected.length ? serial(selected, fn, getStatusMsg, index) : content; - }, function (err) { - var reload = index > 0; - showNotificationsAndReset(err, reload); - return err; - }); - } - - function applySelected(fn, getStatusMsg, getSuccessMsg, confirmMsg) { - var selected = $scope.selection; - if (selected.length === 0) - return; - if (confirmMsg && !confirm(confirmMsg)) - return; - - $scope.actionInProgress = true; - $scope.bulkStatus = getStatusMsg(0, selected.length); - - serial(selected, fn, getStatusMsg, 0).then(function (result) { - // executes once the whole selection has been processed - // in case of an error (caught by serial), result will be the error - if (!(result.data && angular.isArray(result.data.notifications))) - showNotificationsAndReset(result, true, getSuccessMsg(selected.length)); - }); - } - - $scope.delete = function () { - applySelected( - function (selected, index) { return deleteItemCallback(getIdCallback(selected[index])); }, - function (count, total) { return "Deleted " + count + " out of " + total + " item" + (total > 1 ? "s" : ""); }, - function (total) { return "Deleted " + total + " item" + (total > 1 ? "s" : ""); }, - "Sure you want to delete?"); - }; - - $scope.publish = function () { - applySelected( - function (selected, index) { return contentResource.publishById(getIdCallback(selected[index])); }, - function (count, total) { return "Published " + count + " out of " + total + " item" + (total > 1 ? "s" : ""); }, - function (total) { return "Published " + total + " item" + (total > 1 ? "s" : ""); }); - }; - - $scope.unpublish = function() { - applySelected( - function (selected, index) { return contentResource.unPublish(getIdCallback(selected[index])); }, - function (count, total) { return "Unpublished " + count + " out of " + total + " item" + (total > 1 ? "s" : ""); }, - function (total) { return "Unpublished " + total + " item" + (total > 1 ? "s" : ""); }); - }; - - $scope.move = function() { - $scope.moveDialog = {}; - $scope.moveDialog.title = "Move"; - $scope.moveDialog.section = $scope.entityType; - $scope.moveDialog.currentNode = $scope.contentId; - $scope.moveDialog.view = "move"; - $scope.moveDialog.show = true; - - $scope.moveDialog.submit = function(model) { - - if(model.target) { - performMove(model.target); - } - - $scope.moveDialog.show = false; - $scope.moveDialog = null; - }; - - $scope.moveDialog.close = function(oldModel) { - $scope.moveDialog.show = false; - $scope.moveDialog = null; - }; - - }; - - function performMove(target) { - - applySelected( - function(selected, index) {return contentResource.move({parentId: target.id, id: getIdCallback(selected[index])}); }, - function(count, total) {return "Moved " + count + " out of " + total + " item" + (total > 1 ? "s" : ""); }, - function(total) {return "Moved " + total + " item" + (total > 1 ? "s" : ""); }); - } - - $scope.copy = function() { - $scope.copyDialog = {}; - $scope.copyDialog.title = "Copy"; - $scope.copyDialog.section = $scope.entityType; - $scope.copyDialog.currentNode = $scope.contentId; - $scope.copyDialog.view = "copy"; - $scope.copyDialog.show = true; - - $scope.copyDialog.submit = function(model) { - if(model.target) { - performCopy(model.target, model.relateToOriginal); - } - - $scope.copyDialog.show = false; - $scope.copyDialog = null; - }; - - $scope.copyDialog.close = function(oldModel) { - $scope.copyDialog.show = false; - $scope.copyDialog = null; - }; - - }; - - function performCopy(target, relateToOriginal) { - applySelected( - function(selected, index) {return contentResource.copy({parentId: target.id, id: getIdCallback(selected[index]), relateToOriginal: relateToOriginal}); }, - function(count, total) {return "Copied " + count + " out of " + total + " item" + (total > 1 ? "s" : ""); }, - function(total) {return "Copied " + total + " item" + (total > 1 ? "s" : ""); }); - } - - function getCustomPropertyValue(alias, properties) { - var value = ''; - var index = 0; - var foundAlias = false; - for (var i = 0; i < properties.length; i++) { - if (properties[i].alias == alias) { - foundAlias = true; - break; - } - index++; - } - - if (foundAlias) { - value = properties[index].value; - } - - return value; - } - - /** This ensures that the correct value is set for each item in a row, we don't want to call a function during interpolation or ng-bind as performance is really bad that way */ - function setPropertyValues(result) { - - //set the edit url - result.editPath = createEditUrlCallback(result); - - _.each($scope.options.includeProperties, function (e, i) { - - var alias = e.alias; - - // First try to pull the value directly from the alias (e.g. updatedBy) - var value = result[alias]; - - // If this returns an object, look for the name property of that (e.g. owner.name) - if (value === Object(value)) { - value = value['name']; - } - - // If we've got nothing yet, look at a user defined property - if (typeof value === 'undefined') { - value = getCustomPropertyValue(alias, result.properties); - } - - // If we have a date, format it - if (isDate(value)) { - value = value.substring(0, value.length - 3); - } - - // set what we've got on the result - result[alias] = value; - }); - - - } - - function isDate(val) { - if (angular.isString(val)) { - return val.match(/^(\d{4})\-(\d{2})\-(\d{2})\ (\d{2})\:(\d{2})\:(\d{2})$/); - } - return false; - } - - function initView() { - //default to root id if the id is undefined - var id = $routeParams.id; - if(id === undefined){ - id = -1; - } - - $scope.listViewAllowedTypes = getContentTypesCallback(id); - - $scope.contentId = id; - $scope.isTrashed = id === "-20" || id === "-21"; - - $scope.options.allowBulkPublish = $scope.options.allowBulkPublish && !$scope.isTrashed; - $scope.options.allowBulkUnpublish = $scope.options.allowBulkUnpublish && !$scope.isTrashed; - - $scope.options.bulkActionsAllowed = $scope.options.allowBulkPublish || - $scope.options.allowBulkUnpublish || - $scope.options.allowBulkCopy || - $scope.options.allowBulkMove || - $scope.options.allowBulkDelete; - - $scope.reloadView($scope.contentId); - } - - function getLocalizedKey(alias) { - - switch (alias) { - case "sortOrder": - return "general_sort"; - case "updateDate": - return "content_updateDate"; - case "updater": - return "content_updatedBy"; - case "createDate": - return "content_createDate"; - case "owner": - return "content_createBy"; - case "published": - return "content_isPublished"; - case "contentTypeAlias": - //TODO: Check for members - return $scope.entityType === "content" ? "content_documentType" : "content_mediatype"; - case "email": - return "general_email"; - case "username": - return "general_username"; - } - return alias; - } - - function getItemKey(itemId) { - for (var i = 0; i < $scope.listViewResultSet.items.length; i++) { - var item = $scope.listViewResultSet.items[i]; - if (item.id === itemId) { - return item.key; - } - } - } - - //GO! - initView(); + } + + if ($scope.entityType === 'media') { + + mediaResource.getChildFolders($scope.contentId) + .then(function (folders) { + $scope.folders = folders; + $scope.viewLoaded = true; + }); + + } else { + $scope.viewLoaded = true; + } + + //NOTE: This might occur if we are requesting a higher page number than what is actually available, for example + // if you have more than one page and you delete all items on the last page. In this case, we need to reset to the last + // available page and then re-load again + if ($scope.options.pageNumber > $scope.listViewResultSet.totalPages) { + $scope.options.pageNumber = $scope.listViewResultSet.totalPages; + + //reload! + $scope.reloadView(id); + } + + }); + }; + + var searchListView = _.debounce(function () { + $scope.$apply(function () { + makeSearch(); + }); + }, 500); + + $scope.forceSearch = function (ev) { + //13: enter + switch (ev.keyCode) { + case 13: + makeSearch(); + break; + } + }; + + $scope.enterSearch = function () { + $scope.viewLoaded = false; + searchListView(); + }; + + function makeSearch() { + if ($scope.options.filter !== null && $scope.options.filter !== undefined) { + $scope.options.pageNumber = 1; + //$scope.actionInProgress = true; + $scope.reloadView($scope.contentId); + } + } + + $scope.isAnythingSelected = function () { + if ($scope.selection.length === 0) { + return false; + } else { + return true; + } + }; + + $scope.selectedItemsCount = function () { + return $scope.selection.length; + }; + + $scope.clearSelection = function () { + listViewHelper.clearSelection($scope.listViewResultSet.items, $scope.folders, $scope.selection); + }; + + $scope.getIcon = function (entry) { + return iconHelper.convertFromLegacyIcon(entry.icon); + }; + + function serial(selected, fn, getStatusMsg, index) { + return fn(selected, index).then(function (content) { + index++; + $scope.bulkStatus = getStatusMsg(index, selected.length); + return index < selected.length ? serial(selected, fn, getStatusMsg, index) : content; + }, function (err) { + var reload = index > 0; + showNotificationsAndReset(err, reload); + return err; + }); + } + + function applySelected(fn, getStatusMsg, getSuccessMsg, confirmMsg) { + var selected = $scope.selection; + if (selected.length === 0) + return; + if (confirmMsg && !confirm(confirmMsg)) + return; + + $scope.actionInProgress = true; + $scope.bulkStatus = getStatusMsg(0, selected.length); + + serial(selected, fn, getStatusMsg, 0).then(function (result) { + // executes once the whole selection has been processed + // in case of an error (caught by serial), result will be the error + if (!(result.data && angular.isArray(result.data.notifications))) + showNotificationsAndReset(result, true, getSuccessMsg(selected.length)); + }); + } + + $scope.delete = function () { + applySelected( + function (selected, index) { return deleteItemCallback(getIdCallback(selected[index])); }, + function (count, total) { return "Deleted " + count + " out of " + total + " item" + (total > 1 ? "s" : ""); }, + function (total) { return "Deleted " + total + " item" + (total > 1 ? "s" : ""); }, + "Sure you want to delete?"); + }; + + $scope.publish = function () { + applySelected( + function (selected, index) { return contentResource.publishById(getIdCallback(selected[index])); }, + function (count, total) { return "Published " + count + " out of " + total + " item" + (total > 1 ? "s" : ""); }, + function (total) { return "Published " + total + " item" + (total > 1 ? "s" : ""); }); + }; + + $scope.unpublish = function () { + applySelected( + function (selected, index) { return contentResource.unPublish(getIdCallback(selected[index])); }, + function (count, total) { return "Unpublished " + count + " out of " + total + " item" + (total > 1 ? "s" : ""); }, + function (total) { return "Unpublished " + total + " item" + (total > 1 ? "s" : ""); }); + }; + + $scope.move = function () { + $scope.moveDialog = {}; + $scope.moveDialog.title = "Move"; + $scope.moveDialog.section = $scope.entityType; + $scope.moveDialog.currentNode = $scope.contentId; + $scope.moveDialog.view = "move"; + $scope.moveDialog.show = true; + + $scope.moveDialog.submit = function (model) { + + if (model.target) { + performMove(model.target); + } + + $scope.moveDialog.show = false; + $scope.moveDialog = null; + }; + + $scope.moveDialog.close = function (oldModel) { + $scope.moveDialog.show = false; + $scope.moveDialog = null; + }; + + }; + + function performMove(target) { + + applySelected( + function (selected, index) { return contentResource.move({ parentId: target.id, id: getIdCallback(selected[index]) }); }, + function (count, total) { return "Moved " + count + " out of " + total + " item" + (total > 1 ? "s" : ""); }, + function (total) { return "Moved " + total + " item" + (total > 1 ? "s" : ""); }); + } + + $scope.copy = function () { + $scope.copyDialog = {}; + $scope.copyDialog.title = "Copy"; + $scope.copyDialog.section = $scope.entityType; + $scope.copyDialog.currentNode = $scope.contentId; + $scope.copyDialog.view = "copy"; + $scope.copyDialog.show = true; + + $scope.copyDialog.submit = function (model) { + if (model.target) { + performCopy(model.target, model.relateToOriginal); + } + + $scope.copyDialog.show = false; + $scope.copyDialog = null; + }; + + $scope.copyDialog.close = function (oldModel) { + $scope.copyDialog.show = false; + $scope.copyDialog = null; + }; + + }; + + function performCopy(target, relateToOriginal) { + applySelected( + function (selected, index) { return contentResource.copy({ parentId: target.id, id: getIdCallback(selected[index]), relateToOriginal: relateToOriginal }); }, + function (count, total) { return "Copied " + count + " out of " + total + " item" + (total > 1 ? "s" : ""); }, + function (total) { return "Copied " + total + " item" + (total > 1 ? "s" : ""); }); + } + + function getCustomPropertyValue(alias, properties) { + var value = ''; + var index = 0; + var foundAlias = false; + for (var i = 0; i < properties.length; i++) { + if (properties[i].alias == alias) { + foundAlias = true; + break; + } + index++; + } + + if (foundAlias) { + value = properties[index].value; + } + + return value; + } + + /** This ensures that the correct value is set for each item in a row, we don't want to call a function during interpolation or ng-bind as performance is really bad that way */ + function setPropertyValues(result) { + + //set the edit url + result.editPath = createEditUrlCallback(result); + + _.each($scope.options.includeProperties, function (e, i) { + + var alias = e.alias; + + // First try to pull the value directly from the alias (e.g. updatedBy) + var value = result[alias]; + + // If this returns an object, look for the name property of that (e.g. owner.name) + if (value === Object(value)) { + value = value['name']; + } + + // If we've got nothing yet, look at a user defined property + if (typeof value === 'undefined') { + value = getCustomPropertyValue(alias, result.properties); + } + + // If we have a date, format it + if (isDate(value)) { + value = value.substring(0, value.length - 3); + } + + // set what we've got on the result + result[alias] = value; + }); + + + } + + function isDate(val) { + if (angular.isString(val)) { + return val.match(/^(\d{4})\-(\d{2})\-(\d{2})\ (\d{2})\:(\d{2})\:(\d{2})$/); + } + return false; + } + + function initView() { + //default to root id if the id is undefined + var id = $routeParams.id; + if (id === undefined) { + id = -1; + } + + $scope.listViewAllowedTypes = getContentTypesCallback(id); + + $scope.contentId = id; + $scope.isTrashed = id === "-20" || id === "-21"; + + $scope.options.allowBulkPublish = $scope.options.allowBulkPublish && !$scope.isTrashed; + $scope.options.allowBulkUnpublish = $scope.options.allowBulkUnpublish && !$scope.isTrashed; + + $scope.options.bulkActionsAllowed = $scope.options.allowBulkPublish || + $scope.options.allowBulkUnpublish || + $scope.options.allowBulkCopy || + $scope.options.allowBulkMove || + $scope.options.allowBulkDelete; + + $scope.reloadView($scope.contentId); + } + + function getLocalizedKey(alias) { + + switch (alias) { + case "sortOrder": + return "general_sort"; + case "updateDate": + return "content_updateDate"; + case "updater": + return "content_updatedBy"; + case "createDate": + return "content_createDate"; + case "owner": + return "content_createBy"; + case "published": + return "content_isPublished"; + case "contentTypeAlias": + //TODO: Check for members + return $scope.entityType === "content" ? "content_documentType" : "content_mediatype"; + case "email": + return "general_email"; + case "username": + return "general_username"; + } + return alias; + } + + function getItemKey(itemId) { + for (var i = 0; i < $scope.listViewResultSet.items.length; i++) { + var item = $scope.listViewResultSet.items[i]; + if (item.id === itemId) { + return item.key; + } + } + } + + //GO! + initView(); } diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index a65821cfa0..ff811ce4b2 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -38,183 +38,186 @@ using Notification = Umbraco.Web.Models.ContentEditing.Notification; namespace Umbraco.Web.Editors { - /// - /// The API controller used for editing content - /// - /// - /// This controller is decorated with the UmbracoApplicationAuthorizeAttribute which means that any user requesting - /// access to ALL of the methods on this controller will need access to the content application. - /// - [PluginController("UmbracoApi")] - [UmbracoApplicationAuthorizeAttribute(Constants.Applications.Content)] - public class ContentController : ContentControllerBase - { - /// - /// Constructor - /// - public ContentController() - : this(UmbracoContext.Current) - { - } + /// + /// The API controller used for editing content + /// + /// + /// This controller is decorated with the UmbracoApplicationAuthorizeAttribute which means that any user requesting + /// access to ALL of the methods on this controller will need access to the content application. + /// + [PluginController("UmbracoApi")] + [UmbracoApplicationAuthorizeAttribute(Constants.Applications.Content)] + public class ContentController : ContentControllerBase + { + /// + /// Constructor + /// + public ContentController() + : this(UmbracoContext.Current) + { + } - /// - /// Constructor - /// - /// - public ContentController(UmbracoContext umbracoContext) - : base(umbracoContext) - { - } + /// + /// Constructor + /// + /// + public ContentController(UmbracoContext umbracoContext) + : base(umbracoContext) + { + } - /// - /// Return content for the specified ids - /// - /// - /// - [FilterAllowedOutgoingContent(typeof(IEnumerable))] - public IEnumerable GetByIds([FromUri]int[] ids) - { - var foundContent = Services.ContentService.GetByIds(ids); - return foundContent.Select(Mapper.Map); - } + /// + /// Return content for the specified ids + /// + /// + /// + [FilterAllowedOutgoingContent(typeof(IEnumerable))] + public IEnumerable GetByIds([FromUri]int[] ids) + { + var foundContent = Services.ContentService.GetByIds(ids); + return foundContent.Select(Mapper.Map); + } - /// - /// Returns an item to be used to display the recycle bin for content - /// - /// - public ContentItemDisplay GetRecycleBin() - { - var display = new ContentItemDisplay - { - Id = Constants.System.RecycleBinContent, - Alias = "recycleBin", - ParentId = -1, - Name = Services.TextService.Localize("general/recycleBin"), - ContentTypeAlias = "recycleBin", - CreateDate = DateTime.Now, - IsContainer = true, - Path = "-1," + Constants.System.RecycleBinContent - }; + /// + /// Returns an item to be used to display the recycle bin for content + /// + /// + public ContentItemDisplay GetRecycleBin() + { + var display = new ContentItemDisplay + { + Id = Constants.System.RecycleBinContent, + Alias = "recycleBin", + ParentId = -1, + Name = Services.TextService.Localize("general/recycleBin"), + ContentTypeAlias = "recycleBin", + CreateDate = DateTime.Now, + IsContainer = true, + Path = "-1," + Constants.System.RecycleBinContent + }; - TabsAndPropertiesResolver.AddListView(display, "content", Services.DataTypeService, Services.TextService); + TabsAndPropertiesResolver.AddListView(display, "content", Services.DataTypeService, Services.TextService); - return display; - } + return display; + } - /// - /// Gets the content json for the content id - /// - /// - /// - [OutgoingEditorModelEvent] - [EnsureUserPermissionForContent("id")] - public ContentItemDisplay GetById(int id) - { - var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); - if (foundContent == null) - { - HandleContentNotFound(id); - } - - var content = Mapper.Map(foundContent); - return content; - } + /// + /// Gets the content json for the content id + /// + /// + /// + [OutgoingEditorModelEvent] + [EnsureUserPermissionForContent("id")] + public ContentItemDisplay GetById(int id) + { + var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); + if (foundContent == null) + { + HandleContentNotFound(id); + } - [EnsureUserPermissionForContent("id")] - public ContentItemDisplay GetWithTreeDefinition(int id) - { - var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); - if (foundContent == null) - { - HandleContentNotFound(id); - } + var content = Mapper.Map(foundContent); + return content; + } - var content = Mapper.Map(foundContent); - return content; - } + [EnsureUserPermissionForContent("id")] + public ContentItemDisplay GetWithTreeDefinition(int id) + { + var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); + if (foundContent == null) + { + HandleContentNotFound(id); + } - /// - /// Gets an empty content item for the - /// - /// - /// - /// - /// If this is a container type, we'll remove the umbContainerView tab for a new item since - /// it cannot actually list children if it doesn't exist yet. - /// - [OutgoingEditorModelEvent] - public ContentItemDisplay GetEmpty(string contentTypeAlias, int parentId) - { - var contentType = Services.ContentTypeService.GetContentType(contentTypeAlias); - if (contentType == null) - { - throw new HttpResponseException(HttpStatusCode.NotFound); - } + var content = Mapper.Map(foundContent); + return content; + } - var emptyContent = Services.ContentService.CreateContent("", parentId, contentType.Alias, UmbracoUser.Id); - var mapped = Mapper.Map(emptyContent); + /// + /// Gets an empty content item for the + /// + /// + /// + /// + /// If this is a container type, we'll remove the umbContainerView tab for a new item since + /// it cannot actually list children if it doesn't exist yet. + /// + [OutgoingEditorModelEvent] + public ContentItemDisplay GetEmpty(string contentTypeAlias, int parentId) + { + var contentType = Services.ContentTypeService.GetContentType(contentTypeAlias); + if (contentType == null) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } - //remove this tab if it exists: umbContainerView - var containerTab = mapped.Tabs.FirstOrDefault(x => x.Alias == Constants.Conventions.PropertyGroups.ListViewGroupName); - mapped.Tabs = mapped.Tabs.Except(new[] {containerTab}); - return mapped; - } + var emptyContent = Services.ContentService.CreateContent("", parentId, contentType.Alias, UmbracoUser.Id); + var mapped = Mapper.Map(emptyContent); - /// - /// Gets the Url for a given node ID - /// - /// - /// - public HttpResponseMessage GetNiceUrl(int id) - { - var url = Umbraco.NiceUrl(id); - var response = Request.CreateResponse(HttpStatusCode.OK); - response.Content = new StringContent(url, Encoding.UTF8, "application/json"); - return response; - } + //remove this tab if it exists: umbContainerView + var containerTab = mapped.Tabs.FirstOrDefault(x => x.Alias == Constants.Conventions.PropertyGroups.ListViewGroupName); + mapped.Tabs = mapped.Tabs.Except(new[] { containerTab }); + return mapped; + } - /// - /// Gets the children for the content id passed in - /// - /// - [FilterAllowedOutgoingContent(typeof(IEnumerable>), "Items")] - public PagedResult> GetChildren( - int id, - int pageNumber = 0, //TODO: This should be '1' as it's not the index - int pageSize = 0, - string orderBy = "SortOrder", - Direction orderDirection = Direction.Ascending, - string filter = "") - { - long totalChildren; - IContent[] children; - if (pageNumber > 0 && pageSize > 0) - { - children = Services.ContentService.GetPagedChildren(id, (pageNumber - 1), pageSize, out totalChildren, orderBy, orderDirection, filter).ToArray(); - } - else - { - children = Services.ContentService.GetChildren(id).ToArray(); - totalChildren = children.Length; - } + /// + /// Gets the Url for a given node ID + /// + /// + /// + public HttpResponseMessage GetNiceUrl(int id) + { + var url = Umbraco.NiceUrl(id); + var response = Request.CreateResponse(HttpStatusCode.OK); + response.Content = new StringContent(url, Encoding.UTF8, "application/json"); + return response; + } - if (totalChildren == 0) - { - return new PagedResult>(0, 0, 0); - } + /// + /// Gets the children for the content id passed in + /// + /// + [FilterAllowedOutgoingContent(typeof(IEnumerable>), "Items")] + public PagedResult> GetChildren( + int id, + int pageNumber = 0, //TODO: This should be '1' as it's not the index + int pageSize = 0, + string orderBy = "SortOrder", + Direction orderDirection = Direction.Ascending, + bool orderBySystemField = true, + string filter = "") + { + long totalChildren; + IContent[] children; + if (pageNumber > 0 && pageSize > 0) + { + children = Services.ContentService + .GetPagedChildren(id, (pageNumber - 1), pageSize, out totalChildren + , orderBy, orderDirection, orderBySystemField, filter).ToArray(); + } + else + { + children = Services.ContentService.GetChildren(id).ToArray(); + totalChildren = children.Length; + } - var pagedResult = new PagedResult>(totalChildren, pageNumber, pageSize); - pagedResult.Items = children - .Select(Mapper.Map>); + if (totalChildren == 0) + { + return new PagedResult>(0, 0, 0); + } - return pagedResult; - } + var pagedResult = new PagedResult>(totalChildren, pageNumber, pageSize); + pagedResult.Items = children + .Select(Mapper.Map>); - [Obsolete("Dont use this, it is incorrectly named, use HasPermission instead")] - public bool GetHasPermission(string permissionToCheck, int nodeId) - { - return HasPermission(permissionToCheck, nodeId); - } + return pagedResult; + } + + [Obsolete("Dont use this, it is incorrectly named, use HasPermission instead")] + public bool GetHasPermission(string permissionToCheck, int nodeId) + { + return HasPermission(permissionToCheck, nodeId); + } /// /// Returns permissions for all nodes passed in for the current user @@ -229,574 +232,574 @@ namespace Umbraco.Web.Editors .ToDictionary(x => x.EntityId, x => x.AssignedPermissions); } - [HttpGet] - public bool HasPermission(string permissionToCheck, int nodeId) - { - var p = Services.UserService.GetPermissions(Security.CurrentUser, nodeId).FirstOrDefault(); - if (p != null && p.AssignedPermissions.Contains(permissionToCheck.ToString(CultureInfo.InvariantCulture))) - { - return true; - } + [HttpGet] + public bool HasPermission(string permissionToCheck, int nodeId) + { + var p = Services.UserService.GetPermissions(Security.CurrentUser, nodeId).FirstOrDefault(); + if (p != null && p.AssignedPermissions.Contains(permissionToCheck.ToString(CultureInfo.InvariantCulture))) + { + return true; + } - return false; - } - - /// - /// Saves content - /// - /// - [FileUploadCleanupFilter] - [ContentPostValidate] - public ContentItemDisplay PostSave( - [ModelBinder(typeof(ContentItemBinder))] - ContentItemSave contentItem) - { - //If we've reached here it means: - // * Our model has been bound - // * and validated - // * any file attachments have been saved to their temporary location for us to use - // * we have a reference to the DTO object and the persisted object - // * Permissions are valid - - MapPropertyValues(contentItem); + return false; + } - //We need to manually check the validation results here because: - // * We still need to save the entity even if there are validation value errors - // * Depending on if the entity is new, and if there are non property validation errors (i.e. the name is null) - // then we cannot continue saving, we can only display errors - // * If there are validation errors and they were attempting to publish, we can only save, NOT publish and display - // a message indicating this - if (ModelState.IsValid == false) - { - if (ValidationHelper.ModelHasRequiredForPersistenceErrors(contentItem) && IsCreatingAction(contentItem.Action)) - { - //ok, so the absolute mandatory data is invalid and it's new, we cannot actually continue! - // add the modelstate to the outgoing object and throw a validation message - var forDisplay = Mapper.Map(contentItem.PersistedContent); - forDisplay.Errors = ModelState.ToErrorDictionary(); - throw new HttpResponseException(Request.CreateValidationErrorResponse(forDisplay)); - - } + /// + /// Saves content + /// + /// + [FileUploadCleanupFilter] + [ContentPostValidate] + public ContentItemDisplay PostSave( + [ModelBinder(typeof(ContentItemBinder))] + ContentItemSave contentItem) + { + //If we've reached here it means: + // * Our model has been bound + // * and validated + // * any file attachments have been saved to their temporary location for us to use + // * we have a reference to the DTO object and the persisted object + // * Permissions are valid - //if the model state is not valid we cannot publish so change it to save - switch (contentItem.Action) - { - case ContentSaveAction.Publish: - contentItem.Action = ContentSaveAction.Save; - break; - case ContentSaveAction.PublishNew: - contentItem.Action = ContentSaveAction.SaveNew; - break; - } - } + MapPropertyValues(contentItem); - //initialize this to successful - var publishStatus = Attempt.Succeed(); - var wasCancelled = false; + //We need to manually check the validation results here because: + // * We still need to save the entity even if there are validation value errors + // * Depending on if the entity is new, and if there are non property validation errors (i.e. the name is null) + // then we cannot continue saving, we can only display errors + // * If there are validation errors and they were attempting to publish, we can only save, NOT publish and display + // a message indicating this + if (ModelState.IsValid == false) + { + if (ValidationHelper.ModelHasRequiredForPersistenceErrors(contentItem) && IsCreatingAction(contentItem.Action)) + { + //ok, so the absolute mandatory data is invalid and it's new, we cannot actually continue! + // add the modelstate to the outgoing object and throw a validation message + var forDisplay = Mapper.Map(contentItem.PersistedContent); + forDisplay.Errors = ModelState.ToErrorDictionary(); + throw new HttpResponseException(Request.CreateValidationErrorResponse(forDisplay)); - if (contentItem.Action == ContentSaveAction.Save || contentItem.Action == ContentSaveAction.SaveNew) - { - //save the item - var saveResult = Services.ContentService.WithResult().Save(contentItem.PersistedContent, Security.CurrentUser.Id); + } - wasCancelled = saveResult.Success == false && saveResult.Result.StatusType == OperationStatusType.FailedCancelledByEvent; - } - else if (contentItem.Action == ContentSaveAction.SendPublish || contentItem.Action == ContentSaveAction.SendPublishNew) - { - var sendResult = Services.ContentService.SendToPublication(contentItem.PersistedContent, Security.CurrentUser.Id); - wasCancelled = sendResult == false; - } - else - { - //publish the item and check if it worked, if not we will show a diff msg below - publishStatus = Services.ContentService.SaveAndPublishWithStatus(contentItem.PersistedContent, Security.CurrentUser.Id); - wasCancelled = publishStatus.Result.StatusType == PublishStatusType.FailedCancelledByEvent; - } + //if the model state is not valid we cannot publish so change it to save + switch (contentItem.Action) + { + case ContentSaveAction.Publish: + contentItem.Action = ContentSaveAction.Save; + break; + case ContentSaveAction.PublishNew: + contentItem.Action = ContentSaveAction.SaveNew; + break; + } + } - //return the updated model - var display = Mapper.Map(contentItem.PersistedContent); + //initialize this to successful + var publishStatus = Attempt.Succeed(); + var wasCancelled = false; - //lasty, if it is not valid, add the modelstate to the outgoing object and throw a 403 - HandleInvalidModelState(display); + if (contentItem.Action == ContentSaveAction.Save || contentItem.Action == ContentSaveAction.SaveNew) + { + //save the item + var saveResult = Services.ContentService.WithResult().Save(contentItem.PersistedContent, Security.CurrentUser.Id); - //put the correct msgs in - switch (contentItem.Action) - { - case ContentSaveAction.Save: - case ContentSaveAction.SaveNew: - if (wasCancelled == false) - { - display.AddSuccessNotification( - Services.TextService.Localize("speechBubbles/editContentSavedHeader"), - Services.TextService.Localize("speechBubbles/editContentSavedText")); - } - else - { - AddCancelMessage(display); - } - break; - case ContentSaveAction.SendPublish: - case ContentSaveAction.SendPublishNew: - if (wasCancelled == false) - { - display.AddSuccessNotification( - Services.TextService.Localize("speechBubbles/editContentSendToPublish"), - Services.TextService.Localize("speechBubbles/editContentSendToPublishText")); - } - else - { - AddCancelMessage(display); - } - break; - case ContentSaveAction.Publish: - case ContentSaveAction.PublishNew: - ShowMessageForPublishStatus(publishStatus.Result, display); - break; - } + wasCancelled = saveResult.Success == false && saveResult.Result.StatusType == OperationStatusType.FailedCancelledByEvent; + } + else if (contentItem.Action == ContentSaveAction.SendPublish || contentItem.Action == ContentSaveAction.SendPublishNew) + { + var sendResult = Services.ContentService.SendToPublication(contentItem.PersistedContent, Security.CurrentUser.Id); + wasCancelled = sendResult == false; + } + else + { + //publish the item and check if it worked, if not we will show a diff msg below + publishStatus = Services.ContentService.SaveAndPublishWithStatus(contentItem.PersistedContent, Security.CurrentUser.Id); + wasCancelled = publishStatus.Result.StatusType == PublishStatusType.FailedCancelledByEvent; + } - UpdatePreviewContext(contentItem.PersistedContent.Id); + //return the updated model + var display = Mapper.Map(contentItem.PersistedContent); - //If the item is new and the operation was cancelled, we need to return a different - // status code so the UI can handle it since it won't be able to redirect since there - // is no Id to redirect to! - if (wasCancelled && IsCreatingAction(contentItem.Action)) - { - throw new HttpResponseException(Request.CreateValidationErrorResponse(display)); - } + //lasty, if it is not valid, add the modelstate to the outgoing object and throw a 403 + HandleInvalidModelState(display); - return display; - } - - /// - /// Publishes a document with a given ID - /// - /// - /// - /// - /// The CanAccessContentAuthorize attribute will deny access to this method if the current user - /// does not have Publish access to this node. - /// - /// - [EnsureUserPermissionForContent("id", 'U')] - public HttpResponseMessage PostPublishById(int id) - { - var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); + //put the correct msgs in + switch (contentItem.Action) + { + case ContentSaveAction.Save: + case ContentSaveAction.SaveNew: + if (wasCancelled == false) + { + display.AddSuccessNotification( + Services.TextService.Localize("speechBubbles/editContentSavedHeader"), + Services.TextService.Localize("speechBubbles/editContentSavedText")); + } + else + { + AddCancelMessage(display); + } + break; + case ContentSaveAction.SendPublish: + case ContentSaveAction.SendPublishNew: + if (wasCancelled == false) + { + display.AddSuccessNotification( + Services.TextService.Localize("speechBubbles/editContentSendToPublish"), + Services.TextService.Localize("speechBubbles/editContentSendToPublishText")); + } + else + { + AddCancelMessage(display); + } + break; + case ContentSaveAction.Publish: + case ContentSaveAction.PublishNew: + ShowMessageForPublishStatus(publishStatus.Result, display); + break; + } - if (foundContent == null) - { - return HandleContentNotFound(id, false); - } + UpdatePreviewContext(contentItem.PersistedContent.Id); - var publishResult = Services.ContentService.PublishWithStatus(foundContent, Security.GetUserId()); - if (publishResult.Success == false) - { - var notificationModel = new SimpleNotificationModel(); - ShowMessageForPublishStatus(publishResult.Result, notificationModel); - return Request.CreateValidationErrorResponse(notificationModel); - } + //If the item is new and the operation was cancelled, we need to return a different + // status code so the UI can handle it since it won't be able to redirect since there + // is no Id to redirect to! + if (wasCancelled && IsCreatingAction(contentItem.Action)) + { + throw new HttpResponseException(Request.CreateValidationErrorResponse(display)); + } - //return ok - return Request.CreateResponse(HttpStatusCode.OK); + return display; + } - } + /// + /// Publishes a document with a given ID + /// + /// + /// + /// + /// The CanAccessContentAuthorize attribute will deny access to this method if the current user + /// does not have Publish access to this node. + /// + /// + [EnsureUserPermissionForContent("id", 'U')] + public HttpResponseMessage PostPublishById(int id) + { + var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); - /// - /// Moves an item to the recycle bin, if it is already there then it will permanently delete it - /// - /// - /// - /// - /// The CanAccessContentAuthorize attribute will deny access to this method if the current user - /// does not have Delete access to this node. - /// - [EnsureUserPermissionForContent("id", 'D')] - [HttpDelete] - [HttpPost] - public HttpResponseMessage DeleteById(int id) - { - var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); + if (foundContent == null) + { + return HandleContentNotFound(id, false); + } - if (foundContent == null) - { - return HandleContentNotFound(id, false); - } + var publishResult = Services.ContentService.PublishWithStatus(foundContent, Security.GetUserId()); + if (publishResult.Success == false) + { + var notificationModel = new SimpleNotificationModel(); + ShowMessageForPublishStatus(publishResult.Result, notificationModel); + return Request.CreateValidationErrorResponse(notificationModel); + } - //if the current item is in the recycle bin - if (foundContent.IsInRecycleBin() == false) - { - var moveResult = Services.ContentService.WithResult().MoveToRecycleBin(foundContent, Security.GetUserId()); - if (moveResult == false) - { - //returning an object of INotificationModel will ensure that any pending - // notification messages are added to the response. - return Request.CreateValidationErrorResponse(new SimpleNotificationModel()); - } - } - else - { - var deleteResult = Services.ContentService.WithResult().Delete(foundContent, Security.GetUserId()); - if (deleteResult == false) - { - //returning an object of INotificationModel will ensure that any pending - // notification messages are added to the response. - return Request.CreateValidationErrorResponse(new SimpleNotificationModel()); - } - } + //return ok + return Request.CreateResponse(HttpStatusCode.OK); - return Request.CreateResponse(HttpStatusCode.OK); - } + } - /// - /// Empties the recycle bin - /// - /// - /// - /// attributed with EnsureUserPermissionForContent to verify the user has access to the recycle bin - /// - [HttpDelete] - [HttpPost] - [EnsureUserPermissionForContent(Constants.System.RecycleBinContent)] - public HttpResponseMessage EmptyRecycleBin() - { - Services.ContentService.EmptyRecycleBin(); - return Request.CreateResponse(HttpStatusCode.OK); - } + /// + /// Moves an item to the recycle bin, if it is already there then it will permanently delete it + /// + /// + /// + /// + /// The CanAccessContentAuthorize attribute will deny access to this method if the current user + /// does not have Delete access to this node. + /// + [EnsureUserPermissionForContent("id", 'D')] + [HttpDelete] + [HttpPost] + public HttpResponseMessage DeleteById(int id) + { + var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); - /// - /// Change the sort order for media - /// - /// - /// - [EnsureUserPermissionForContent("sorted.ParentId", 'S')] - public HttpResponseMessage PostSort(ContentSortOrder sorted) - { - if (sorted == null) - { - return Request.CreateResponse(HttpStatusCode.NotFound); - } + if (foundContent == null) + { + return HandleContentNotFound(id, false); + } - //if there's nothing to sort just return ok - if (sorted.IdSortOrder.Length == 0) - { - return Request.CreateResponse(HttpStatusCode.OK); - } + //if the current item is in the recycle bin + if (foundContent.IsInRecycleBin() == false) + { + var moveResult = Services.ContentService.WithResult().MoveToRecycleBin(foundContent, Security.GetUserId()); + if (moveResult == false) + { + //returning an object of INotificationModel will ensure that any pending + // notification messages are added to the response. + return Request.CreateValidationErrorResponse(new SimpleNotificationModel()); + } + } + else + { + var deleteResult = Services.ContentService.WithResult().Delete(foundContent, Security.GetUserId()); + if (deleteResult == false) + { + //returning an object of INotificationModel will ensure that any pending + // notification messages are added to the response. + return Request.CreateValidationErrorResponse(new SimpleNotificationModel()); + } + } - var contentService = Services.ContentService; - var sortedContent = new List(); - try - { - sortedContent.AddRange(Services.ContentService.GetByIds(sorted.IdSortOrder)); + return Request.CreateResponse(HttpStatusCode.OK); + } - // Save content with new sort order and update content xml in db accordingly - if (contentService.Sort(sortedContent) == false) - { - LogHelper.Warn("Content sorting failed, this was probably caused by an event being cancelled"); - return Request.CreateValidationErrorResponse("Content sorting failed, this was probably caused by an event being cancelled"); - } - return Request.CreateResponse(HttpStatusCode.OK); - } - catch (Exception ex) - { - LogHelper.Error("Could not update content sort order", ex); - throw; - } - } + /// + /// Empties the recycle bin + /// + /// + /// + /// attributed with EnsureUserPermissionForContent to verify the user has access to the recycle bin + /// + [HttpDelete] + [HttpPost] + [EnsureUserPermissionForContent(Constants.System.RecycleBinContent)] + public HttpResponseMessage EmptyRecycleBin() + { + Services.ContentService.EmptyRecycleBin(); + return Request.CreateResponse(HttpStatusCode.OK); + } - /// - /// Change the sort order for media - /// - /// - /// - [EnsureUserPermissionForContent("move.ParentId", 'M')] - public HttpResponseMessage PostMove(MoveOrCopy move) - { - var toMove = ValidateMoveOrCopy(move); + /// + /// Change the sort order for media + /// + /// + /// + [EnsureUserPermissionForContent("sorted.ParentId", 'S')] + public HttpResponseMessage PostSort(ContentSortOrder sorted) + { + if (sorted == null) + { + return Request.CreateResponse(HttpStatusCode.NotFound); + } - Services.ContentService.Move(toMove, move.ParentId); + //if there's nothing to sort just return ok + if (sorted.IdSortOrder.Length == 0) + { + return Request.CreateResponse(HttpStatusCode.OK); + } - var response = Request.CreateResponse(HttpStatusCode.OK); - response.Content = new StringContent(toMove.Path, Encoding.UTF8, "application/json"); - return response; - } + var contentService = Services.ContentService; + var sortedContent = new List(); + try + { + sortedContent.AddRange(Services.ContentService.GetByIds(sorted.IdSortOrder)); - /// - /// Copies a content item and places the copy as a child of a given parent Id - /// - /// - /// - [EnsureUserPermissionForContent("copy.ParentId", 'C')] - public HttpResponseMessage PostCopy(MoveOrCopy copy) - { - var toCopy = ValidateMoveOrCopy(copy); + // Save content with new sort order and update content xml in db accordingly + if (contentService.Sort(sortedContent) == false) + { + LogHelper.Warn("Content sorting failed, this was probably caused by an event being cancelled"); + return Request.CreateValidationErrorResponse("Content sorting failed, this was probably caused by an event being cancelled"); + } + return Request.CreateResponse(HttpStatusCode.OK); + } + catch (Exception ex) + { + LogHelper.Error("Could not update content sort order", ex); + throw; + } + } - var c = Services.ContentService.Copy(toCopy, copy.ParentId, copy.RelateToOriginal, copy.Recursive); + /// + /// Change the sort order for media + /// + /// + /// + [EnsureUserPermissionForContent("move.ParentId", 'M')] + public HttpResponseMessage PostMove(MoveOrCopy move) + { + var toMove = ValidateMoveOrCopy(move); - var response = Request.CreateResponse(HttpStatusCode.OK); - response.Content = new StringContent(c.Path, Encoding.UTF8, "application/json"); - return response; - } + Services.ContentService.Move(toMove, move.ParentId); - /// - /// Unpublishes a node with a given Id and returns the unpublished entity - /// - /// - /// - [EnsureUserPermissionForContent("id", 'U')] - public ContentItemDisplay PostUnPublish(int id) - { - var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); + var response = Request.CreateResponse(HttpStatusCode.OK); + response.Content = new StringContent(toMove.Path, Encoding.UTF8, "application/json"); + return response; + } - if (foundContent == null) - HandleContentNotFound(id); - - var unpublishResult = Services.ContentService.WithResult().UnPublish(foundContent, Security.CurrentUser.Id); + /// + /// Copies a content item and places the copy as a child of a given parent Id + /// + /// + /// + [EnsureUserPermissionForContent("copy.ParentId", 'C')] + public HttpResponseMessage PostCopy(MoveOrCopy copy) + { + var toCopy = ValidateMoveOrCopy(copy); - var content = Mapper.Map(foundContent); + var c = Services.ContentService.Copy(toCopy, copy.ParentId, copy.RelateToOriginal, copy.Recursive); - if (unpublishResult == false) - { - AddCancelMessage(content); - throw new HttpResponseException(Request.CreateValidationErrorResponse(content)); - } - else - { - content.AddSuccessNotification(Services.TextService.Localize("content/unPublish"), Services.TextService.Localize("speechBubbles/contentUnpublished")); - return content; - } - } + var response = Request.CreateResponse(HttpStatusCode.OK); + response.Content = new StringContent(c.Path, Encoding.UTF8, "application/json"); + return response; + } - /// - /// Checks if the user is currently in preview mode and if so will update the preview content for this item - /// - /// - private void UpdatePreviewContext(int contentId) - { - var previewId = Request.GetPreviewCookieValue(); - if (previewId.IsNullOrWhiteSpace()) return; - Guid id; - if (Guid.TryParse(previewId, out id)) - { - var d = new Document(contentId); - var pc = new PreviewContent(UmbracoUser, id, false); - pc.PrepareDocument(UmbracoUser, d, true); - pc.SavePreviewSet(); - } - } + /// + /// Unpublishes a node with a given Id and returns the unpublished entity + /// + /// + /// + [EnsureUserPermissionForContent("id", 'U')] + public ContentItemDisplay PostUnPublish(int id) + { + var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); - /// - /// Maps the dto property values to the persisted model - /// - /// - private void MapPropertyValues(ContentItemSave contentItem) - { - UpdateName(contentItem); + if (foundContent == null) + HandleContentNotFound(id); - //TODO: We need to support 'send to publish' + var unpublishResult = Services.ContentService.WithResult().UnPublish(foundContent, Security.CurrentUser.Id); - contentItem.PersistedContent.ExpireDate = contentItem.ExpireDate; - contentItem.PersistedContent.ReleaseDate = contentItem.ReleaseDate; - //only set the template if it didn't change - var templateChanged = (contentItem.PersistedContent.Template == null && contentItem.TemplateAlias.IsNullOrWhiteSpace() == false) - || (contentItem.PersistedContent.Template != null && contentItem.PersistedContent.Template.Alias != contentItem.TemplateAlias) - || (contentItem.PersistedContent.Template != null && contentItem.TemplateAlias.IsNullOrWhiteSpace()); - if (templateChanged) - { - var template = Services.FileService.GetTemplate(contentItem.TemplateAlias); - if (template == null && contentItem.TemplateAlias.IsNullOrWhiteSpace() == false) - { - //ModelState.AddModelError("Template", "No template exists with the specified alias: " + contentItem.TemplateAlias); - LogHelper.Warn("No template exists with the specified alias: " + contentItem.TemplateAlias); - } - else - { - //NOTE: this could be null if there was a template and the posted template is null, this should remove the assigned template - contentItem.PersistedContent.Template = template; - } - } + var content = Mapper.Map(foundContent); - base.MapPropertyValues(contentItem); - } + if (unpublishResult == false) + { + AddCancelMessage(content); + throw new HttpResponseException(Request.CreateValidationErrorResponse(content)); + } + else + { + content.AddSuccessNotification(Services.TextService.Localize("content/unPublish"), Services.TextService.Localize("speechBubbles/contentUnpublished")); + return content; + } + } - /// - /// Ensures the item can be moved/copied to the new location - /// - /// - /// - private IContent ValidateMoveOrCopy(MoveOrCopy model) - { - if (model == null) - { - throw new HttpResponseException(HttpStatusCode.NotFound); - } + /// + /// Checks if the user is currently in preview mode and if so will update the preview content for this item + /// + /// + private void UpdatePreviewContext(int contentId) + { + var previewId = Request.GetPreviewCookieValue(); + if (previewId.IsNullOrWhiteSpace()) return; + Guid id; + if (Guid.TryParse(previewId, out id)) + { + var d = new Document(contentId); + var pc = new PreviewContent(UmbracoUser, id, false); + pc.PrepareDocument(UmbracoUser, d, true); + pc.SavePreviewSet(); + } + } - var contentService = Services.ContentService; - var toMove = contentService.GetById(model.Id); - if (toMove == null) - { - throw new HttpResponseException(HttpStatusCode.NotFound); - } - if (model.ParentId < 0) - { - //cannot move if the content item is not allowed at the root - if (toMove.ContentType.AllowedAsRoot == false) - { - throw new HttpResponseException( - Request.CreateNotificationValidationErrorResponse( - Services.TextService.Localize("moveOrCopy/notAllowedAtRoot"))); - } - } - else - { - var parent = contentService.GetById(model.ParentId); - if (parent == null) - { - throw new HttpResponseException(HttpStatusCode.NotFound); - } + /// + /// Maps the dto property values to the persisted model + /// + /// + private void MapPropertyValues(ContentItemSave contentItem) + { + UpdateName(contentItem); - //check if the item is allowed under this one - if (parent.ContentType.AllowedContentTypes.Select(x => x.Id).ToArray() - .Any(x => x.Value == toMove.ContentType.Id) == false) - { - throw new HttpResponseException( - Request.CreateNotificationValidationErrorResponse( - Services.TextService.Localize("moveOrCopy/notAllowedByContentType"))); - } + //TODO: We need to support 'send to publish' - // Check on paths - if ((string.Format(",{0},", parent.Path)).IndexOf(string.Format(",{0},", toMove.Id), StringComparison.Ordinal) > -1) - { - throw new HttpResponseException( - Request.CreateNotificationValidationErrorResponse( - Services.TextService.Localize("moveOrCopy/notAllowedByPath"))); - } - } + contentItem.PersistedContent.ExpireDate = contentItem.ExpireDate; + contentItem.PersistedContent.ReleaseDate = contentItem.ReleaseDate; + //only set the template if it didn't change + var templateChanged = (contentItem.PersistedContent.Template == null && contentItem.TemplateAlias.IsNullOrWhiteSpace() == false) + || (contentItem.PersistedContent.Template != null && contentItem.PersistedContent.Template.Alias != contentItem.TemplateAlias) + || (contentItem.PersistedContent.Template != null && contentItem.TemplateAlias.IsNullOrWhiteSpace()); + if (templateChanged) + { + var template = Services.FileService.GetTemplate(contentItem.TemplateAlias); + if (template == null && contentItem.TemplateAlias.IsNullOrWhiteSpace() == false) + { + //ModelState.AddModelError("Template", "No template exists with the specified alias: " + contentItem.TemplateAlias); + LogHelper.Warn("No template exists with the specified alias: " + contentItem.TemplateAlias); + } + else + { + //NOTE: this could be null if there was a template and the posted template is null, this should remove the assigned template + contentItem.PersistedContent.Template = template; + } + } - return toMove; - } + base.MapPropertyValues(contentItem); + } - private void ShowMessageForPublishStatus(PublishStatus status, INotificationModel display) - { - switch (status.StatusType) - { - case PublishStatusType.Success: - case PublishStatusType.SuccessAlreadyPublished: - display.AddSuccessNotification( - Services.TextService.Localize("speechBubbles/editContentPublishedHeader"), - Services.TextService.Localize("speechBubbles/editContentPublishedText")); - break; - case PublishStatusType.FailedPathNotPublished: - display.AddWarningNotification( - Services.TextService.Localize("publish"), - Services.TextService.Localize("publish/contentPublishedFailedByParent", - new[] {string.Format("{0} ({1})", status.ContentItem.Name, status.ContentItem.Id)}).Trim()); - break; - case PublishStatusType.FailedCancelledByEvent: - AddCancelMessage(display, "publish", "speechBubbles/contentPublishedFailedByEvent"); - break; - case PublishStatusType.FailedAwaitingRelease: - display.AddWarningNotification( - Services.TextService.Localize("publish"), - Services.TextService.Localize("publish/contentPublishedFailedAwaitingRelease", - new[] {string.Format("{0} ({1})", status.ContentItem.Name, status.ContentItem.Id)}).Trim()); - break; - case PublishStatusType.FailedHasExpired: - display.AddWarningNotification( - Services.TextService.Localize("publish"), - Services.TextService.Localize("publish/contentPublishedFailedExpired", - new[] - { - string.Format("{0} ({1})", status.ContentItem.Name, status.ContentItem.Id), - }).Trim()); - break; - case PublishStatusType.FailedIsTrashed: - //TODO: We should add proper error messaging for this! - break; - case PublishStatusType.FailedContentInvalid: - display.AddWarningNotification( - Services.TextService.Localize("publish"), - Services.TextService.Localize("publish/contentPublishedFailedInvalid", - new[] - { - string.Format("{0} ({1})", status.ContentItem.Name, status.ContentItem.Id), - string.Join(",", status.InvalidProperties.Select(x => x.Alias)) - }).Trim()); - break; - default: - throw new IndexOutOfRangeException(); - } - } + /// + /// Ensures the item can be moved/copied to the new location + /// + /// + /// + private IContent ValidateMoveOrCopy(MoveOrCopy model) + { + if (model == null) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } - + var contentService = Services.ContentService; + var toMove = contentService.GetById(model.Id); + if (toMove == null) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } + if (model.ParentId < 0) + { + //cannot move if the content item is not allowed at the root + if (toMove.ContentType.AllowedAsRoot == false) + { + throw new HttpResponseException( + Request.CreateNotificationValidationErrorResponse( + Services.TextService.Localize("moveOrCopy/notAllowedAtRoot"))); + } + } + else + { + var parent = contentService.GetById(model.ParentId); + if (parent == null) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } - /// - /// Performs a permissions check for the user to check if it has access to the node based on - /// start node and/or permissions for the node - /// - /// The storage to add the content item to so it can be reused - /// - /// - /// - /// The content to lookup, if the contentItem is not specified - /// - /// Specifies the already resolved content item to check against - /// - internal static bool CheckPermissions( - IDictionary storage, - IUser user, - IUserService userService, - IContentService contentService, - int nodeId, - char[] permissionsToCheck = null, - IContent contentItem = null) - { - - if (contentItem == null && nodeId != Constants.System.Root && nodeId != Constants.System.RecycleBinContent) - { - contentItem = contentService.GetById(nodeId); - //put the content item into storage so it can be retreived - // in the controller (saves a lookup) - storage[typeof(IContent).ToString()] = contentItem; - } + //check if the item is allowed under this one + if (parent.ContentType.AllowedContentTypes.Select(x => x.Id).ToArray() + .Any(x => x.Value == toMove.ContentType.Id) == false) + { + throw new HttpResponseException( + Request.CreateNotificationValidationErrorResponse( + Services.TextService.Localize("moveOrCopy/notAllowedByContentType"))); + } - if (contentItem == null && nodeId != Constants.System.Root && nodeId != Constants.System.RecycleBinContent) - { - throw new HttpResponseException(HttpStatusCode.NotFound); - } + // Check on paths + if ((string.Format(",{0},", parent.Path)).IndexOf(string.Format(",{0},", toMove.Id), StringComparison.Ordinal) > -1) + { + throw new HttpResponseException( + Request.CreateNotificationValidationErrorResponse( + Services.TextService.Localize("moveOrCopy/notAllowedByPath"))); + } + } - var hasPathAccess = (nodeId == Constants.System.Root) - ? UserExtensions.HasPathAccess( - Constants.System.Root.ToInvariantString(), - user.StartContentId, - Constants.System.RecycleBinContent) - : (nodeId == Constants.System.RecycleBinContent) - ? UserExtensions.HasPathAccess( - Constants.System.RecycleBinContent.ToInvariantString(), - user.StartContentId, - Constants.System.RecycleBinContent) - : user.HasPathAccess(contentItem); + return toMove; + } - if (hasPathAccess == false) - { - return false; - } + private void ShowMessageForPublishStatus(PublishStatus status, INotificationModel display) + { + switch (status.StatusType) + { + case PublishStatusType.Success: + case PublishStatusType.SuccessAlreadyPublished: + display.AddSuccessNotification( + Services.TextService.Localize("speechBubbles/editContentPublishedHeader"), + Services.TextService.Localize("speechBubbles/editContentPublishedText")); + break; + case PublishStatusType.FailedPathNotPublished: + display.AddWarningNotification( + Services.TextService.Localize("publish"), + Services.TextService.Localize("publish/contentPublishedFailedByParent", + new[] { string.Format("{0} ({1})", status.ContentItem.Name, status.ContentItem.Id) }).Trim()); + break; + case PublishStatusType.FailedCancelledByEvent: + AddCancelMessage(display, "publish", "speechBubbles/contentPublishedFailedByEvent"); + break; + case PublishStatusType.FailedAwaitingRelease: + display.AddWarningNotification( + Services.TextService.Localize("publish"), + Services.TextService.Localize("publish/contentPublishedFailedAwaitingRelease", + new[] { string.Format("{0} ({1})", status.ContentItem.Name, status.ContentItem.Id) }).Trim()); + break; + case PublishStatusType.FailedHasExpired: + display.AddWarningNotification( + Services.TextService.Localize("publish"), + Services.TextService.Localize("publish/contentPublishedFailedExpired", + new[] + { + string.Format("{0} ({1})", status.ContentItem.Name, status.ContentItem.Id), + }).Trim()); + break; + case PublishStatusType.FailedIsTrashed: + //TODO: We should add proper error messaging for this! + break; + case PublishStatusType.FailedContentInvalid: + display.AddWarningNotification( + Services.TextService.Localize("publish"), + Services.TextService.Localize("publish/contentPublishedFailedInvalid", + new[] + { + string.Format("{0} ({1})", status.ContentItem.Name, status.ContentItem.Id), + string.Join(",", status.InvalidProperties.Select(x => x.Alias)) + }).Trim()); + break; + default: + throw new IndexOutOfRangeException(); + } + } - if (permissionsToCheck == null || permissionsToCheck.Any() == false) - { - return true; - } - var permission = userService.GetPermissions(user, nodeId).FirstOrDefault(); - var allowed = true; - foreach (var p in permissionsToCheck) - { - if (permission == null || permission.AssignedPermissions.Contains(p.ToString(CultureInfo.InvariantCulture)) == false) - { - allowed = false; - } - } - return allowed; - } + /// + /// Performs a permissions check for the user to check if it has access to the node based on + /// start node and/or permissions for the node + /// + /// The storage to add the content item to so it can be reused + /// + /// + /// + /// The content to lookup, if the contentItem is not specified + /// + /// Specifies the already resolved content item to check against + /// + internal static bool CheckPermissions( + IDictionary storage, + IUser user, + IUserService userService, + IContentService contentService, + int nodeId, + char[] permissionsToCheck = null, + IContent contentItem = null) + { - } + if (contentItem == null && nodeId != Constants.System.Root && nodeId != Constants.System.RecycleBinContent) + { + contentItem = contentService.GetById(nodeId); + //put the content item into storage so it can be retreived + // in the controller (saves a lookup) + storage[typeof(IContent).ToString()] = contentItem; + } + + if (contentItem == null && nodeId != Constants.System.Root && nodeId != Constants.System.RecycleBinContent) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + var hasPathAccess = (nodeId == Constants.System.Root) + ? UserExtensions.HasPathAccess( + Constants.System.Root.ToInvariantString(), + user.StartContentId, + Constants.System.RecycleBinContent) + : (nodeId == Constants.System.RecycleBinContent) + ? UserExtensions.HasPathAccess( + Constants.System.RecycleBinContent.ToInvariantString(), + user.StartContentId, + Constants.System.RecycleBinContent) + : user.HasPathAccess(contentItem); + + if (hasPathAccess == false) + { + return false; + } + + if (permissionsToCheck == null || permissionsToCheck.Any() == false) + { + return true; + } + + var permission = userService.GetPermissions(user, nodeId).FirstOrDefault(); + + var allowed = true; + foreach (var p in permissionsToCheck) + { + if (permission == null || permission.AssignedPermissions.Contains(p.ToString(CultureInfo.InvariantCulture)) == false) + { + allowed = false; + } + } + return allowed; + } + + } } \ No newline at end of file diff --git a/src/Umbraco.Web/Editors/MediaController.cs b/src/Umbraco.Web/Editors/MediaController.cs index 66889e2206..da7c72a289 100644 --- a/src/Umbraco.Web/Editors/MediaController.cs +++ b/src/Umbraco.Web/Editors/MediaController.cs @@ -39,672 +39,675 @@ using Notification = Umbraco.Web.Models.ContentEditing.Notification; namespace Umbraco.Web.Editors { - /// - /// This controller is decorated with the UmbracoApplicationAuthorizeAttribute which means that any user requesting - /// access to ALL of the methods on this controller will need access to the media application. - /// - [PluginController("UmbracoApi")] - [UmbracoApplicationAuthorizeAttribute(Constants.Applications.Media)] - public class MediaController : ContentControllerBase - { - /// - /// Constructor - /// - public MediaController() - : this(UmbracoContext.Current) - { - } - - /// - /// Constructor - /// - /// - public MediaController(UmbracoContext umbracoContext) - : base(umbracoContext) - { - } - - /// - /// Gets an empty content item for the - /// - /// - /// - /// - [OutgoingEditorModelEvent] - public MediaItemDisplay GetEmpty(string contentTypeAlias, int parentId) - { - var contentType = Services.ContentTypeService.GetMediaType(contentTypeAlias); - if (contentType == null) - { - throw new HttpResponseException(HttpStatusCode.NotFound); - } - - var emptyContent = Services.MediaService.CreateMedia("", parentId, contentType.Alias, UmbracoUser.Id); - var mapped = Mapper.Map(emptyContent); - - //remove this tab if it exists: umbContainerView - var containerTab = mapped.Tabs.FirstOrDefault(x => x.Alias == Constants.Conventions.PropertyGroups.ListViewGroupName); - mapped.Tabs = mapped.Tabs.Except(new[] { containerTab }); - return mapped; - } - - /// - /// Returns an item to be used to display the recycle bin for media - /// - /// - public ContentItemDisplay GetRecycleBin() - { - var display = new ContentItemDisplay - { - Id = Constants.System.RecycleBinMedia, - Alias = "recycleBin", - ParentId = -1, - Name = Services.TextService.Localize("general/recycleBin"), - ContentTypeAlias = "recycleBin", - CreateDate = DateTime.Now, - IsContainer = true, - Path = "-1," + Constants.System.RecycleBinMedia - }; - - TabsAndPropertiesResolver.AddListView(display, "media", Services.DataTypeService, Services.TextService); - - return display; - } - - /// - /// Gets the content json for the content id - /// - /// - /// - [OutgoingEditorModelEvent] - [EnsureUserPermissionForMedia("id")] - public MediaItemDisplay GetById(int id) - { - var foundContent = GetObjectFromRequest(() => Services.MediaService.GetById(id)); - - if (foundContent == null) - { - HandleContentNotFound(id); - //HandleContentNotFound will throw an exception - return null; - } - return Mapper.Map(foundContent); - } - - /// - /// Return media for the specified ids - /// - /// - /// - [FilterAllowedOutgoingMedia(typeof(IEnumerable))] - public IEnumerable GetByIds([FromUri]int[] ids) - { - var foundMedia = Services.MediaService.GetByIds(ids); - return foundMedia.Select(Mapper.Map); - } - - /// - /// Returns media items known to be a container of other media items - /// - /// - /// - [FilterAllowedOutgoingMedia(typeof(IEnumerable>))] - public IEnumerable> GetChildFolders(int id = -1) - { - //Suggested convention for folder mediatypes - we can make this more or less complicated as long as we document it... - //if you create a media type, which has an alias that ends with ...Folder then its a folder: ex: "secureFolder", "bannerFolder", "Folder" - var folderTypes = Services.ContentTypeService.GetAllMediaTypes().ToArray().Where(x => x.Alias.EndsWith("Folder")).Select(x => x.Id); - - var children = (id < 0) ? Services.MediaService.GetRootMedia() : Services.MediaService.GetById(id).Children(); - return children.Where(x => folderTypes.Contains(x.ContentTypeId)).Select(Mapper.Map>); - } - - /// - /// Returns the root media objects - /// - [FilterAllowedOutgoingMedia(typeof(IEnumerable>))] - public IEnumerable> GetRootMedia() - { - //TODO: Add permissions check! - - return Services.MediaService.GetRootMedia() - .Select(Mapper.Map>); - } - - /// - /// Returns the child media objects - /// - [FilterAllowedOutgoingMedia(typeof(IEnumerable>), "Items")] - public PagedResult> GetChildren(int id, - int pageNumber = 0, - int pageSize = 0, - string orderBy = "SortOrder", - Direction orderDirection = Direction.Ascending, - string filter = "") - { - int totalChildren; - IMedia[] children; - if (pageNumber > 0 && pageSize > 0) - { - children = Services.MediaService.GetPagedChildren(id, (pageNumber - 1), pageSize, out totalChildren, orderBy, orderDirection, filter).ToArray(); - } - else - { - children = Services.MediaService.GetChildren(id).ToArray(); - totalChildren = children.Length; - } - - if (totalChildren == 0) - { - return new PagedResult>(0, 0, 0); - } - - var pagedResult = new PagedResult>(totalChildren, pageNumber, pageSize); - pagedResult.Items = children - .Select(Mapper.Map>); - - return pagedResult; - } - - /// - /// Moves an item to the recycle bin, if it is already there then it will permanently delete it - /// - /// - /// - [EnsureUserPermissionForMedia("id")] - [HttpPost] - public HttpResponseMessage DeleteById(int id) - { - var foundMedia = GetObjectFromRequest(() => Services.MediaService.GetById(id)); - - if (foundMedia == null) - { - return HandleContentNotFound(id, false); - } - - //if the current item is in the recycle bin - if (foundMedia.IsInRecycleBin() == false) - { - var moveResult = Services.MediaService.WithResult().MoveToRecycleBin(foundMedia, (int)Security.CurrentUser.Id); - if (moveResult == false) - { - //returning an object of INotificationModel will ensure that any pending - // notification messages are added to the response. - return Request.CreateValidationErrorResponse(new SimpleNotificationModel()); - } - } - else - { - var deleteResult = Services.MediaService.WithResult().Delete(foundMedia, (int)Security.CurrentUser.Id); - if (deleteResult == false) - { - //returning an object of INotificationModel will ensure that any pending - // notification messages are added to the response. - return Request.CreateValidationErrorResponse(new SimpleNotificationModel()); - } - } - - return Request.CreateResponse(HttpStatusCode.OK); - } - - /// - /// Change the sort order for media - /// - /// - /// - [EnsureUserPermissionForMedia("move.Id")] - public HttpResponseMessage PostMove(MoveOrCopy move) - { - var toMove = ValidateMoveOrCopy(move); - - Services.MediaService.Move(toMove, move.ParentId); - - var response = Request.CreateResponse(HttpStatusCode.OK); - response.Content = new StringContent(toMove.Path, Encoding.UTF8, "application/json"); - return response; - } - - /// - /// Saves content - /// - /// - [FileUploadCleanupFilter] - [MediaPostValidate] - public MediaItemDisplay PostSave( - [ModelBinder(typeof(MediaItemBinder))] - MediaItemSave contentItem) - { - //If we've reached here it means: - // * Our model has been bound - // * and validated - // * any file attachments have been saved to their temporary location for us to use - // * we have a reference to the DTO object and the persisted object - // * Permissions are valid - - MapPropertyValues(contentItem); - - //We need to manually check the validation results here because: - // * We still need to save the entity even if there are validation value errors - // * Depending on if the entity is new, and if there are non property validation errors (i.e. the name is null) - // then we cannot continue saving, we can only display errors - // * If there are validation errors and they were attempting to publish, we can only save, NOT publish and display - // a message indicating this - if (ModelState.IsValid == false) - { - if (ValidationHelper.ModelHasRequiredForPersistenceErrors(contentItem) - && (contentItem.Action == ContentSaveAction.SaveNew)) - { - //ok, so the absolute mandatory data is invalid and it's new, we cannot actually continue! - // add the modelstate to the outgoing object and throw validation response - var forDisplay = Mapper.Map(contentItem.PersistedContent); - forDisplay.Errors = ModelState.ToErrorDictionary(); - throw new HttpResponseException(Request.CreateValidationErrorResponse(forDisplay)); - } - } - - //save the item - var saveStatus = Services.MediaService.WithResult().Save(contentItem.PersistedContent, (int)Security.CurrentUser.Id); - - //return the updated model - var display = Mapper.Map(contentItem.PersistedContent); - - //lasty, if it is not valid, add the modelstate to the outgoing object and throw a 403 - HandleInvalidModelState(display); - - //put the correct msgs in - switch (contentItem.Action) - { - case ContentSaveAction.Save: - case ContentSaveAction.SaveNew: - if (saveStatus.Success) - { - display.AddSuccessNotification( - Services.TextService.Localize("speechBubbles/editMediaSaved"), - Services.TextService.Localize("speechBubbles/editMediaSavedText")); - } - else - { - AddCancelMessage(display); - - //If the item is new and the operation was cancelled, we need to return a different - // status code so the UI can handle it since it won't be able to redirect since there - // is no Id to redirect to! - if (saveStatus.Result.StatusType == OperationStatusType.FailedCancelledByEvent && IsCreatingAction(contentItem.Action)) - { - throw new HttpResponseException(Request.CreateValidationErrorResponse(display)); - } - } - - break; - } - - return display; - } - - /// - /// Maps the property values to the persisted entity - /// - /// - protected override void MapPropertyValues(ContentBaseItemSave contentItem) - { - UpdateName(contentItem); - - //use the base method to map the rest of the properties - base.MapPropertyValues(contentItem); - } - - /// - /// Empties the recycle bin - /// - /// - [HttpDelete] - [HttpPost] - public HttpResponseMessage EmptyRecycleBin() - { - Services.MediaService.EmptyRecycleBin(); - return Request.CreateResponse(HttpStatusCode.OK); - } - - /// - /// Change the sort order for media - /// - /// - /// - [EnsureUserPermissionForMedia("sorted.ParentId")] - public HttpResponseMessage PostSort(ContentSortOrder sorted) - { - if (sorted == null) - { - return Request.CreateResponse(HttpStatusCode.NotFound); - } - - //if there's nothing to sort just return ok - if (sorted.IdSortOrder.Length == 0) - { - return Request.CreateResponse(HttpStatusCode.OK); - } - - var mediaService = base.ApplicationContext.Services.MediaService; - var sortedMedia = new List(); - try - { - sortedMedia.AddRange(sorted.IdSortOrder.Select(mediaService.GetById)); - - // Save Media with new sort order and update content xml in db accordingly - if (mediaService.Sort(sortedMedia) == false) - { - LogHelper.Warn("Media sorting failed, this was probably caused by an event being cancelled"); - return Request.CreateValidationErrorResponse("Media sorting failed, this was probably caused by an event being cancelled"); - } - return Request.CreateResponse(HttpStatusCode.OK); - } - catch (Exception ex) - { - LogHelper.Error("Could not update media sort order", ex); - throw; - } - } - - [EnsureUserPermissionForMedia("folder.ParentId")] - public MediaItemDisplay PostAddFolder(EntityBasic folder) - { - var mediaService = ApplicationContext.Services.MediaService; - var f = mediaService.CreateMedia(folder.Name, folder.ParentId, Constants.Conventions.MediaTypes.Folder); - mediaService.Save(f, Security.CurrentUser.Id); - - return Mapper.Map(f); - } - - /// - /// Used to submit a media file - /// - /// - /// - /// We cannot validate this request with attributes (nicely) due to the nature of the multi-part for data. - /// - [FileUploadCleanupFilter(false)] - public async Task PostAddFile() - { - if (Request.Content.IsMimeMultipartContent() == false) - { - throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType); - } - - var root = IOHelper.MapPath("~/App_Data/TEMP/FileUploads"); - //ensure it exists - Directory.CreateDirectory(root); - var provider = new MultipartFormDataStreamProvider(root); - - var result = await Request.Content.ReadAsMultipartAsync(provider); - - //must have a file - if (result.FileData.Count == 0) - { - return Request.CreateResponse(HttpStatusCode.NotFound); - } - - //get the string json from the request - int parentId; - if (int.TryParse(result.FormData["currentFolder"], out parentId) == false) - { - return Request.CreateValidationErrorResponse("The request was not formatted correctly, the currentFolder is not an integer"); - } - - //ensure the user has access to this folder by parent id! - if (CheckPermissions( - new Dictionary(), - Security.CurrentUser, - Services.MediaService, parentId) == false) - { - return Request.CreateResponse( - HttpStatusCode.Unauthorized, - new SimpleNotificationModel(new Notification( - Services.TextService.Localize("speechBubbles/operationFailedHeader"), - Services.TextService.Localize("speechBubbles/invalidUserPermissionsText"), - SpeechBubbleIcon.Warning))); - } - - var tempFiles = new PostedFiles(); - var mediaService = ApplicationContext.Services.MediaService; - - - //in case we pass a path with a folder in it, we will create it and upload media to it. - if (result.FormData.ContainsKey("path")) - { - - var folders = result.FormData["path"].Split('/'); - - for (int i = 0; i < folders.Length - 1; i++) - { - var folderName = folders[i]; - IMedia folderMediaItem; - - //if uploading directly to media root and not a subfolder - if (parentId == -1) - { - //look for matching folder - folderMediaItem = - mediaService.GetRootMedia().FirstOrDefault(x => x.Name == folderName && x.ContentType.Alias == Constants.Conventions.MediaTypes.Folder); - if (folderMediaItem == null) - { - //if null, create a folder - folderMediaItem = mediaService.CreateMedia(folderName, -1, Constants.Conventions.MediaTypes.Folder); - mediaService.Save(folderMediaItem); - } - } - else - { - //get current parent - var mediaRoot = mediaService.GetById(parentId); - - //if the media root is null, something went wrong, we'll abort - if (mediaRoot == null) - return Request.CreateErrorResponse(HttpStatusCode.InternalServerError, - "The folder: " + folderName + " could not be used for storing images, its ID: " + parentId + - " returned null"); - - //look for matching folder - folderMediaItem = mediaRoot.Children().FirstOrDefault(x => x.Name == folderName && x.ContentType.Alias == Constants.Conventions.MediaTypes.Folder); - if (folderMediaItem == null) - { - //if null, create a folder - folderMediaItem = mediaService.CreateMedia(folderName, mediaRoot, Constants.Conventions.MediaTypes.Folder); - mediaService.Save(folderMediaItem); - } - } - //set the media root to the folder id so uploaded files will end there. - parentId = folderMediaItem.Id; - } - } - - //get the files - foreach (var file in result.FileData) - { - var fileName = file.Headers.ContentDisposition.FileName.Trim(new[] { '\"' }); - var ext = fileName.Substring(fileName.LastIndexOf('.')+1).ToLower(); - - if (UmbracoConfig.For.UmbracoSettings().Content.DisallowedUploadFiles.Contains(ext) == false) - { - var mediaType = Constants.Conventions.MediaTypes.File; - - if (UmbracoConfig.For.UmbracoSettings().Content.ImageFileTypes.Contains(ext)) - mediaType = Constants.Conventions.MediaTypes.Image; - - //TODO: make the media item name "nice" since file names could be pretty ugly, we have - // string extensions to do much of this but we'll need: - // * Pascalcase the name (use string extensions) - // * strip the file extension - // * underscores to spaces - // * probably remove 'ugly' characters - let's discuss - // All of this logic should exist in a string extensions method and be unit tested - // http://issues.umbraco.org/issue/U4-5572 - var mediaItemName = fileName; - - var f = mediaService.CreateMedia(mediaItemName, parentId, mediaType, Security.CurrentUser.Id); - - var fileInfo = new FileInfo(file.LocalFileName); - var fs = fileInfo.OpenReadWithRetry(); - if (fs == null) throw new InvalidOperationException("Could not acquire file stream"); - using (fs) - { - f.SetValue(Constants.Conventions.Media.File, fileName, fs); - } - - var saveResult = mediaService.WithResult().Save(f, Security.CurrentUser.Id); - if (saveResult == false) - { - AddCancelMessage(tempFiles, - message: Services.TextService.Localize("speechBubbles/operationCancelledText") + " -- " + mediaItemName, - localizeMessage: false); - } - else - { - tempFiles.UploadedFiles.Add(new ContentItemFile - { - FileName = fileName, - PropertyAlias = Constants.Conventions.Media.File, - TempFilePath = file.LocalFileName - }); - } - } - else - { - tempFiles.Notifications.Add(new Notification( - Services.TextService.Localize("speechBubbles/operationFailedHeader"), - "Cannot upload file " + file.Headers.ContentDisposition.FileName + ", it is not an approved file type", - SpeechBubbleIcon.Warning)); - } - } - - //Different response if this is a 'blueimp' request - if (Request.GetQueryNameValuePairs().Any(x => x.Key == "origin")) - { - var origin = Request.GetQueryNameValuePairs().First(x => x.Key == "origin"); - if (origin.Value == "blueimp") - { - return Request.CreateResponse(HttpStatusCode.OK, - tempFiles, - //Don't output the angular xsrf stuff, blue imp doesn't like that - new JsonMediaTypeFormatter()); - } - } - - return Request.CreateResponse(HttpStatusCode.OK, tempFiles); - } - - /// - /// This is used for the response of PostAddFile so that we can analyze the response in a filter and remove the - /// temporary files that were created. - /// - [DataContract] - private class PostedFiles : IHaveUploadedFiles, INotificationModel - { - public PostedFiles() - { - UploadedFiles = new List(); - Notifications = new List(); - } - public List UploadedFiles { get; private set; } - - [DataMember(Name = "notifications")] - public List Notifications { get; private set; } - } - - /// - /// Ensures the item can be moved/copied to the new location - /// - /// - /// - private IMedia ValidateMoveOrCopy(MoveOrCopy model) - { - if (model == null) - { - throw new HttpResponseException(HttpStatusCode.NotFound); - } - - var mediaService = Services.MediaService; - var toMove = mediaService.GetById(model.Id); - if (toMove == null) - { - throw new HttpResponseException(HttpStatusCode.NotFound); - } - if (model.ParentId < 0) - { - //cannot move if the content item is not allowed at the root - if (toMove.ContentType.AllowedAsRoot == false) - { - var notificationModel = new SimpleNotificationModel(); - notificationModel.AddErrorNotification(Services.TextService.Localize("moveOrCopy/notAllowedAtRoot"), ""); - throw new HttpResponseException(Request.CreateValidationErrorResponse(notificationModel)); - } - } - else - { - var parent = mediaService.GetById(model.ParentId); - if (parent == null) - { - throw new HttpResponseException(HttpStatusCode.NotFound); - } - - //check if the item is allowed under this one - if (parent.ContentType.AllowedContentTypes.Select(x => x.Id).ToArray() - .Any(x => x.Value == toMove.ContentType.Id) == false) - { - var notificationModel = new SimpleNotificationModel(); - notificationModel.AddErrorNotification(Services.TextService.Localize("moveOrCopy/notAllowedByContentType"), ""); - throw new HttpResponseException(Request.CreateValidationErrorResponse(notificationModel)); - } - - // Check on paths - if ((string.Format(",{0},", parent.Path)).IndexOf(string.Format(",{0},", toMove.Id), StringComparison.Ordinal) > -1) - { - var notificationModel = new SimpleNotificationModel(); - notificationModel.AddErrorNotification(Services.TextService.Localize("moveOrCopy/notAllowedByPath"), ""); - throw new HttpResponseException(Request.CreateValidationErrorResponse(notificationModel)); - } - } - - return toMove; - } - - /// - /// Performs a permissions check for the user to check if it has access to the node based on - /// start node and/or permissions for the node - /// - /// The storage to add the content item to so it can be reused - /// - /// - /// The content to lookup, if the contentItem is not specified - /// Specifies the already resolved content item to check against, setting this ignores the nodeId - /// - internal static bool CheckPermissions(IDictionary storage, IUser user, IMediaService mediaService, int nodeId, IMedia media = null) - { - if (media == null && nodeId != Constants.System.Root && nodeId != Constants.System.RecycleBinMedia) - { - media = mediaService.GetById(nodeId); - //put the content item into storage so it can be retreived - // in the controller (saves a lookup) - storage[typeof(IMedia).ToString()] = media; - } - - if (media == null && nodeId != Constants.System.Root && nodeId != Constants.System.RecycleBinMedia) - { - throw new HttpResponseException(HttpStatusCode.NotFound); - } - - var hasPathAccess = (nodeId == Constants.System.Root) - ? UserExtensions.HasPathAccess( - Constants.System.Root.ToInvariantString(), - user.StartMediaId, - Constants.System.RecycleBinMedia) - : (nodeId == Constants.System.RecycleBinMedia) - ? UserExtensions.HasPathAccess( - Constants.System.RecycleBinMedia.ToInvariantString(), - user.StartMediaId, - Constants.System.RecycleBinMedia) - : user.HasPathAccess(media); - - return hasPathAccess; - } - } + /// + /// This controller is decorated with the UmbracoApplicationAuthorizeAttribute which means that any user requesting + /// access to ALL of the methods on this controller will need access to the media application. + /// + [PluginController("UmbracoApi")] + [UmbracoApplicationAuthorizeAttribute(Constants.Applications.Media)] + public class MediaController : ContentControllerBase + { + /// + /// Constructor + /// + public MediaController() + : this(UmbracoContext.Current) + { + } + + /// + /// Constructor + /// + /// + public MediaController(UmbracoContext umbracoContext) + : base(umbracoContext) + { + } + + /// + /// Gets an empty content item for the + /// + /// + /// + /// + [OutgoingEditorModelEvent] + public MediaItemDisplay GetEmpty(string contentTypeAlias, int parentId) + { + var contentType = Services.ContentTypeService.GetMediaType(contentTypeAlias); + if (contentType == null) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + var emptyContent = Services.MediaService.CreateMedia("", parentId, contentType.Alias, UmbracoUser.Id); + var mapped = Mapper.Map(emptyContent); + + //remove this tab if it exists: umbContainerView + var containerTab = mapped.Tabs.FirstOrDefault(x => x.Alias == Constants.Conventions.PropertyGroups.ListViewGroupName); + mapped.Tabs = mapped.Tabs.Except(new[] { containerTab }); + return mapped; + } + + /// + /// Returns an item to be used to display the recycle bin for media + /// + /// + public ContentItemDisplay GetRecycleBin() + { + var display = new ContentItemDisplay + { + Id = Constants.System.RecycleBinMedia, + Alias = "recycleBin", + ParentId = -1, + Name = Services.TextService.Localize("general/recycleBin"), + ContentTypeAlias = "recycleBin", + CreateDate = DateTime.Now, + IsContainer = true, + Path = "-1," + Constants.System.RecycleBinMedia + }; + + TabsAndPropertiesResolver.AddListView(display, "media", Services.DataTypeService, Services.TextService); + + return display; + } + + /// + /// Gets the content json for the content id + /// + /// + /// + [OutgoingEditorModelEvent] + [EnsureUserPermissionForMedia("id")] + public MediaItemDisplay GetById(int id) + { + var foundContent = GetObjectFromRequest(() => Services.MediaService.GetById(id)); + + if (foundContent == null) + { + HandleContentNotFound(id); + //HandleContentNotFound will throw an exception + return null; + } + return Mapper.Map(foundContent); + } + + /// + /// Return media for the specified ids + /// + /// + /// + [FilterAllowedOutgoingMedia(typeof(IEnumerable))] + public IEnumerable GetByIds([FromUri]int[] ids) + { + var foundMedia = Services.MediaService.GetByIds(ids); + return foundMedia.Select(Mapper.Map); + } + + /// + /// Returns media items known to be a container of other media items + /// + /// + /// + [FilterAllowedOutgoingMedia(typeof(IEnumerable>))] + public IEnumerable> GetChildFolders(int id = -1) + { + //Suggested convention for folder mediatypes - we can make this more or less complicated as long as we document it... + //if you create a media type, which has an alias that ends with ...Folder then its a folder: ex: "secureFolder", "bannerFolder", "Folder" + var folderTypes = Services.ContentTypeService.GetAllMediaTypes().ToArray().Where(x => x.Alias.EndsWith("Folder")).Select(x => x.Id); + + var children = (id < 0) ? Services.MediaService.GetRootMedia() : Services.MediaService.GetById(id).Children(); + return children.Where(x => folderTypes.Contains(x.ContentTypeId)).Select(Mapper.Map>); + } + + /// + /// Returns the root media objects + /// + [FilterAllowedOutgoingMedia(typeof(IEnumerable>))] + public IEnumerable> GetRootMedia() + { + //TODO: Add permissions check! + + return Services.MediaService.GetRootMedia() + .Select(Mapper.Map>); + } + + /// + /// Returns the child media objects + /// + [FilterAllowedOutgoingMedia(typeof(IEnumerable>), "Items")] + public PagedResult> GetChildren(int id, + int pageNumber = 0, + int pageSize = 0, + string orderBy = "SortOrder", + Direction orderDirection = Direction.Ascending, + bool orderBySystemField = true, + string filter = "") + { + int totalChildren; + IMedia[] children; + if (pageNumber > 0 && pageSize > 0) + { + children = Services.MediaService + .GetPagedChildren(id, (pageNumber - 1), pageSize, out totalChildren + , orderBy, orderDirection, orderBySystemField, filter).ToArray(); + } + else + { + children = Services.MediaService.GetChildren(id).ToArray(); + totalChildren = children.Length; + } + + if (totalChildren == 0) + { + return new PagedResult>(0, 0, 0); + } + + var pagedResult = new PagedResult>(totalChildren, pageNumber, pageSize); + pagedResult.Items = children + .Select(Mapper.Map>); + + return pagedResult; + } + + /// + /// Moves an item to the recycle bin, if it is already there then it will permanently delete it + /// + /// + /// + [EnsureUserPermissionForMedia("id")] + [HttpPost] + public HttpResponseMessage DeleteById(int id) + { + var foundMedia = GetObjectFromRequest(() => Services.MediaService.GetById(id)); + + if (foundMedia == null) + { + return HandleContentNotFound(id, false); + } + + //if the current item is in the recycle bin + if (foundMedia.IsInRecycleBin() == false) + { + var moveResult = Services.MediaService.WithResult().MoveToRecycleBin(foundMedia, (int)Security.CurrentUser.Id); + if (moveResult == false) + { + //returning an object of INotificationModel will ensure that any pending + // notification messages are added to the response. + return Request.CreateValidationErrorResponse(new SimpleNotificationModel()); + } + } + else + { + var deleteResult = Services.MediaService.WithResult().Delete(foundMedia, (int)Security.CurrentUser.Id); + if (deleteResult == false) + { + //returning an object of INotificationModel will ensure that any pending + // notification messages are added to the response. + return Request.CreateValidationErrorResponse(new SimpleNotificationModel()); + } + } + + return Request.CreateResponse(HttpStatusCode.OK); + } + + /// + /// Change the sort order for media + /// + /// + /// + [EnsureUserPermissionForMedia("move.Id")] + public HttpResponseMessage PostMove(MoveOrCopy move) + { + var toMove = ValidateMoveOrCopy(move); + + Services.MediaService.Move(toMove, move.ParentId); + + var response = Request.CreateResponse(HttpStatusCode.OK); + response.Content = new StringContent(toMove.Path, Encoding.UTF8, "application/json"); + return response; + } + + /// + /// Saves content + /// + /// + [FileUploadCleanupFilter] + [MediaPostValidate] + public MediaItemDisplay PostSave( + [ModelBinder(typeof(MediaItemBinder))] + MediaItemSave contentItem) + { + //If we've reached here it means: + // * Our model has been bound + // * and validated + // * any file attachments have been saved to their temporary location for us to use + // * we have a reference to the DTO object and the persisted object + // * Permissions are valid + + MapPropertyValues(contentItem); + + //We need to manually check the validation results here because: + // * We still need to save the entity even if there are validation value errors + // * Depending on if the entity is new, and if there are non property validation errors (i.e. the name is null) + // then we cannot continue saving, we can only display errors + // * If there are validation errors and they were attempting to publish, we can only save, NOT publish and display + // a message indicating this + if (ModelState.IsValid == false) + { + if (ValidationHelper.ModelHasRequiredForPersistenceErrors(contentItem) + && (contentItem.Action == ContentSaveAction.SaveNew)) + { + //ok, so the absolute mandatory data is invalid and it's new, we cannot actually continue! + // add the modelstate to the outgoing object and throw validation response + var forDisplay = Mapper.Map(contentItem.PersistedContent); + forDisplay.Errors = ModelState.ToErrorDictionary(); + throw new HttpResponseException(Request.CreateValidationErrorResponse(forDisplay)); + } + } + + //save the item + var saveStatus = Services.MediaService.WithResult().Save(contentItem.PersistedContent, (int)Security.CurrentUser.Id); + + //return the updated model + var display = Mapper.Map(contentItem.PersistedContent); + + //lasty, if it is not valid, add the modelstate to the outgoing object and throw a 403 + HandleInvalidModelState(display); + + //put the correct msgs in + switch (contentItem.Action) + { + case ContentSaveAction.Save: + case ContentSaveAction.SaveNew: + if (saveStatus.Success) + { + display.AddSuccessNotification( + Services.TextService.Localize("speechBubbles/editMediaSaved"), + Services.TextService.Localize("speechBubbles/editMediaSavedText")); + } + else + { + AddCancelMessage(display); + + //If the item is new and the operation was cancelled, we need to return a different + // status code so the UI can handle it since it won't be able to redirect since there + // is no Id to redirect to! + if (saveStatus.Result.StatusType == OperationStatusType.FailedCancelledByEvent && IsCreatingAction(contentItem.Action)) + { + throw new HttpResponseException(Request.CreateValidationErrorResponse(display)); + } + } + + break; + } + + return display; + } + + /// + /// Maps the property values to the persisted entity + /// + /// + protected override void MapPropertyValues(ContentBaseItemSave contentItem) + { + UpdateName(contentItem); + + //use the base method to map the rest of the properties + base.MapPropertyValues(contentItem); + } + + /// + /// Empties the recycle bin + /// + /// + [HttpDelete] + [HttpPost] + public HttpResponseMessage EmptyRecycleBin() + { + Services.MediaService.EmptyRecycleBin(); + return Request.CreateResponse(HttpStatusCode.OK); + } + + /// + /// Change the sort order for media + /// + /// + /// + [EnsureUserPermissionForMedia("sorted.ParentId")] + public HttpResponseMessage PostSort(ContentSortOrder sorted) + { + if (sorted == null) + { + return Request.CreateResponse(HttpStatusCode.NotFound); + } + + //if there's nothing to sort just return ok + if (sorted.IdSortOrder.Length == 0) + { + return Request.CreateResponse(HttpStatusCode.OK); + } + + var mediaService = base.ApplicationContext.Services.MediaService; + var sortedMedia = new List(); + try + { + sortedMedia.AddRange(sorted.IdSortOrder.Select(mediaService.GetById)); + + // Save Media with new sort order and update content xml in db accordingly + if (mediaService.Sort(sortedMedia) == false) + { + LogHelper.Warn("Media sorting failed, this was probably caused by an event being cancelled"); + return Request.CreateValidationErrorResponse("Media sorting failed, this was probably caused by an event being cancelled"); + } + return Request.CreateResponse(HttpStatusCode.OK); + } + catch (Exception ex) + { + LogHelper.Error("Could not update media sort order", ex); + throw; + } + } + + [EnsureUserPermissionForMedia("folder.ParentId")] + public MediaItemDisplay PostAddFolder(EntityBasic folder) + { + var mediaService = ApplicationContext.Services.MediaService; + var f = mediaService.CreateMedia(folder.Name, folder.ParentId, Constants.Conventions.MediaTypes.Folder); + mediaService.Save(f, Security.CurrentUser.Id); + + return Mapper.Map(f); + } + + /// + /// Used to submit a media file + /// + /// + /// + /// We cannot validate this request with attributes (nicely) due to the nature of the multi-part for data. + /// + [FileUploadCleanupFilter(false)] + public async Task PostAddFile() + { + if (Request.Content.IsMimeMultipartContent() == false) + { + throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType); + } + + var root = IOHelper.MapPath("~/App_Data/TEMP/FileUploads"); + //ensure it exists + Directory.CreateDirectory(root); + var provider = new MultipartFormDataStreamProvider(root); + + var result = await Request.Content.ReadAsMultipartAsync(provider); + + //must have a file + if (result.FileData.Count == 0) + { + return Request.CreateResponse(HttpStatusCode.NotFound); + } + + //get the string json from the request + int parentId; + if (int.TryParse(result.FormData["currentFolder"], out parentId) == false) + { + return Request.CreateValidationErrorResponse("The request was not formatted correctly, the currentFolder is not an integer"); + } + + //ensure the user has access to this folder by parent id! + if (CheckPermissions( + new Dictionary(), + Security.CurrentUser, + Services.MediaService, parentId) == false) + { + return Request.CreateResponse( + HttpStatusCode.Unauthorized, + new SimpleNotificationModel(new Notification( + Services.TextService.Localize("speechBubbles/operationFailedHeader"), + Services.TextService.Localize("speechBubbles/invalidUserPermissionsText"), + SpeechBubbleIcon.Warning))); + } + + var tempFiles = new PostedFiles(); + var mediaService = ApplicationContext.Services.MediaService; + + + //in case we pass a path with a folder in it, we will create it and upload media to it. + if (result.FormData.ContainsKey("path")) + { + + var folders = result.FormData["path"].Split('/'); + + for (int i = 0; i < folders.Length - 1; i++) + { + var folderName = folders[i]; + IMedia folderMediaItem; + + //if uploading directly to media root and not a subfolder + if (parentId == -1) + { + //look for matching folder + folderMediaItem = + mediaService.GetRootMedia().FirstOrDefault(x => x.Name == folderName && x.ContentType.Alias == Constants.Conventions.MediaTypes.Folder); + if (folderMediaItem == null) + { + //if null, create a folder + folderMediaItem = mediaService.CreateMedia(folderName, -1, Constants.Conventions.MediaTypes.Folder); + mediaService.Save(folderMediaItem); + } + } + else + { + //get current parent + var mediaRoot = mediaService.GetById(parentId); + + //if the media root is null, something went wrong, we'll abort + if (mediaRoot == null) + return Request.CreateErrorResponse(HttpStatusCode.InternalServerError, + "The folder: " + folderName + " could not be used for storing images, its ID: " + parentId + + " returned null"); + + //look for matching folder + folderMediaItem = mediaRoot.Children().FirstOrDefault(x => x.Name == folderName && x.ContentType.Alias == Constants.Conventions.MediaTypes.Folder); + if (folderMediaItem == null) + { + //if null, create a folder + folderMediaItem = mediaService.CreateMedia(folderName, mediaRoot, Constants.Conventions.MediaTypes.Folder); + mediaService.Save(folderMediaItem); + } + } + //set the media root to the folder id so uploaded files will end there. + parentId = folderMediaItem.Id; + } + } + + //get the files + foreach (var file in result.FileData) + { + var fileName = file.Headers.ContentDisposition.FileName.Trim(new[] { '\"' }); + var ext = fileName.Substring(fileName.LastIndexOf('.') + 1).ToLower(); + + if (UmbracoConfig.For.UmbracoSettings().Content.DisallowedUploadFiles.Contains(ext) == false) + { + var mediaType = Constants.Conventions.MediaTypes.File; + + if (UmbracoConfig.For.UmbracoSettings().Content.ImageFileTypes.Contains(ext)) + mediaType = Constants.Conventions.MediaTypes.Image; + + //TODO: make the media item name "nice" since file names could be pretty ugly, we have + // string extensions to do much of this but we'll need: + // * Pascalcase the name (use string extensions) + // * strip the file extension + // * underscores to spaces + // * probably remove 'ugly' characters - let's discuss + // All of this logic should exist in a string extensions method and be unit tested + // http://issues.umbraco.org/issue/U4-5572 + var mediaItemName = fileName; + + var f = mediaService.CreateMedia(mediaItemName, parentId, mediaType, Security.CurrentUser.Id); + + var fileInfo = new FileInfo(file.LocalFileName); + var fs = fileInfo.OpenReadWithRetry(); + if (fs == null) throw new InvalidOperationException("Could not acquire file stream"); + using (fs) + { + f.SetValue(Constants.Conventions.Media.File, fileName, fs); + } + + var saveResult = mediaService.WithResult().Save(f, Security.CurrentUser.Id); + if (saveResult == false) + { + AddCancelMessage(tempFiles, + message: Services.TextService.Localize("speechBubbles/operationCancelledText") + " -- " + mediaItemName, + localizeMessage: false); + } + else + { + tempFiles.UploadedFiles.Add(new ContentItemFile + { + FileName = fileName, + PropertyAlias = Constants.Conventions.Media.File, + TempFilePath = file.LocalFileName + }); + } + } + else + { + tempFiles.Notifications.Add(new Notification( + Services.TextService.Localize("speechBubbles/operationFailedHeader"), + "Cannot upload file " + file.Headers.ContentDisposition.FileName + ", it is not an approved file type", + SpeechBubbleIcon.Warning)); + } + } + + //Different response if this is a 'blueimp' request + if (Request.GetQueryNameValuePairs().Any(x => x.Key == "origin")) + { + var origin = Request.GetQueryNameValuePairs().First(x => x.Key == "origin"); + if (origin.Value == "blueimp") + { + return Request.CreateResponse(HttpStatusCode.OK, + tempFiles, + //Don't output the angular xsrf stuff, blue imp doesn't like that + new JsonMediaTypeFormatter()); + } + } + + return Request.CreateResponse(HttpStatusCode.OK, tempFiles); + } + + /// + /// This is used for the response of PostAddFile so that we can analyze the response in a filter and remove the + /// temporary files that were created. + /// + [DataContract] + private class PostedFiles : IHaveUploadedFiles, INotificationModel + { + public PostedFiles() + { + UploadedFiles = new List(); + Notifications = new List(); + } + public List UploadedFiles { get; private set; } + + [DataMember(Name = "notifications")] + public List Notifications { get; private set; } + } + + /// + /// Ensures the item can be moved/copied to the new location + /// + /// + /// + private IMedia ValidateMoveOrCopy(MoveOrCopy model) + { + if (model == null) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + var mediaService = Services.MediaService; + var toMove = mediaService.GetById(model.Id); + if (toMove == null) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } + if (model.ParentId < 0) + { + //cannot move if the content item is not allowed at the root + if (toMove.ContentType.AllowedAsRoot == false) + { + var notificationModel = new SimpleNotificationModel(); + notificationModel.AddErrorNotification(Services.TextService.Localize("moveOrCopy/notAllowedAtRoot"), ""); + throw new HttpResponseException(Request.CreateValidationErrorResponse(notificationModel)); + } + } + else + { + var parent = mediaService.GetById(model.ParentId); + if (parent == null) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + //check if the item is allowed under this one + if (parent.ContentType.AllowedContentTypes.Select(x => x.Id).ToArray() + .Any(x => x.Value == toMove.ContentType.Id) == false) + { + var notificationModel = new SimpleNotificationModel(); + notificationModel.AddErrorNotification(Services.TextService.Localize("moveOrCopy/notAllowedByContentType"), ""); + throw new HttpResponseException(Request.CreateValidationErrorResponse(notificationModel)); + } + + // Check on paths + if ((string.Format(",{0},", parent.Path)).IndexOf(string.Format(",{0},", toMove.Id), StringComparison.Ordinal) > -1) + { + var notificationModel = new SimpleNotificationModel(); + notificationModel.AddErrorNotification(Services.TextService.Localize("moveOrCopy/notAllowedByPath"), ""); + throw new HttpResponseException(Request.CreateValidationErrorResponse(notificationModel)); + } + } + + return toMove; + } + + /// + /// Performs a permissions check for the user to check if it has access to the node based on + /// start node and/or permissions for the node + /// + /// The storage to add the content item to so it can be reused + /// + /// + /// The content to lookup, if the contentItem is not specified + /// Specifies the already resolved content item to check against, setting this ignores the nodeId + /// + internal static bool CheckPermissions(IDictionary storage, IUser user, IMediaService mediaService, int nodeId, IMedia media = null) + { + if (media == null && nodeId != Constants.System.Root && nodeId != Constants.System.RecycleBinMedia) + { + media = mediaService.GetById(nodeId); + //put the content item into storage so it can be retreived + // in the controller (saves a lookup) + storage[typeof(IMedia).ToString()] = media; + } + + if (media == null && nodeId != Constants.System.Root && nodeId != Constants.System.RecycleBinMedia) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + var hasPathAccess = (nodeId == Constants.System.Root) + ? UserExtensions.HasPathAccess( + Constants.System.Root.ToInvariantString(), + user.StartMediaId, + Constants.System.RecycleBinMedia) + : (nodeId == Constants.System.RecycleBinMedia) + ? UserExtensions.HasPathAccess( + Constants.System.RecycleBinMedia.ToInvariantString(), + user.StartMediaId, + Constants.System.RecycleBinMedia) + : user.HasPathAccess(media); + + return hasPathAccess; + } + } } diff --git a/src/Umbraco.Web/Editors/MemberController.cs b/src/Umbraco.Web/Editors/MemberController.cs index 909189b7ad..a2f1090a63 100644 --- a/src/Umbraco.Web/Editors/MemberController.cs +++ b/src/Umbraco.Web/Editors/MemberController.cs @@ -34,248 +34,251 @@ using Examine; namespace Umbraco.Web.Editors { - /// - /// This controller is decorated with the UmbracoApplicationAuthorizeAttribute which means that any user requesting - /// access to ALL of the methods on this controller will need access to the member application. - /// - [PluginController("UmbracoApi")] - [UmbracoApplicationAuthorizeAttribute(Constants.Applications.Members)] - [OutgoingNoHyphenGuidFormat] - public class MemberController : ContentControllerBase - { - /// - /// Constructor - /// - public MemberController() - : this(UmbracoContext.Current) - { - _provider = Core.Security.MembershipProviderExtensions.GetMembersMembershipProvider(); - } + /// + /// This controller is decorated with the UmbracoApplicationAuthorizeAttribute which means that any user requesting + /// access to ALL of the methods on this controller will need access to the member application. + /// + [PluginController("UmbracoApi")] + [UmbracoApplicationAuthorizeAttribute(Constants.Applications.Members)] + [OutgoingNoHyphenGuidFormat] + public class MemberController : ContentControllerBase + { + /// + /// Constructor + /// + public MemberController() + : this(UmbracoContext.Current) + { + _provider = Core.Security.MembershipProviderExtensions.GetMembersMembershipProvider(); + } - /// - /// Constructor - /// - /// - public MemberController(UmbracoContext umbracoContext) - : base(umbracoContext) - { - _provider = Core.Security.MembershipProviderExtensions.GetMembersMembershipProvider(); - } + /// + /// Constructor + /// + /// + public MemberController(UmbracoContext umbracoContext) + : base(umbracoContext) + { + _provider = Core.Security.MembershipProviderExtensions.GetMembersMembershipProvider(); + } - private readonly MembershipProvider _provider; + private readonly MembershipProvider _provider; - /// - /// Returns the currently configured membership scenario for members in umbraco - /// - /// - protected MembershipScenario MembershipScenario - { - get { return Services.MemberService.GetMembershipScenario(); } - } + /// + /// Returns the currently configured membership scenario for members in umbraco + /// + /// + protected MembershipScenario MembershipScenario + { + get { return Services.MemberService.GetMembershipScenario(); } + } - public PagedResult GetPagedResults( - int pageNumber = 1, - int pageSize = 100, - string orderBy = "Name", - Direction orderDirection = Direction.Ascending, - string filter = "", - string memberTypeAlias = null) - { - - if (pageNumber <= 0 || pageSize <= 0) - { - throw new NotSupportedException("Both pageNumber and pageSize must be greater than zero"); - } + public PagedResult GetPagedResults( + int pageNumber = 1, + int pageSize = 100, + string orderBy = "Name", + Direction orderDirection = Direction.Ascending, + bool orderBySystemField = true, + string filter = "", + string memberTypeAlias = null) + { - if (MembershipScenario == MembershipScenario.NativeUmbraco) - { - long totalRecords; - var members = Services.MemberService.GetAll((pageNumber - 1), pageSize, out totalRecords, orderBy, orderDirection, memberTypeAlias, filter).ToArray(); - if (totalRecords == 0) - { - return new PagedResult(0, 0, 0); - } - var pagedResult = new PagedResult(totalRecords, pageNumber, pageSize); - pagedResult.Items = members - .Select(Mapper.Map); - return pagedResult; - } - else - { - int totalRecords; - var members = _provider.GetAllUsers((pageNumber - 1), pageSize, out totalRecords); - if (totalRecords == 0) - { - return new PagedResult(0, 0, 0); - } - var pagedResult = new PagedResult(totalRecords, pageNumber, pageSize); - pagedResult.Items = members - .Cast() - .Select(Mapper.Map); - return pagedResult; - } - - } + if (pageNumber <= 0 || pageSize <= 0) + { + throw new NotSupportedException("Both pageNumber and pageSize must be greater than zero"); + } - /// - /// Returns a display node with a list view to render members - /// - /// - /// - public MemberListDisplay GetListNodeDisplay(string listName) - { - var display = new MemberListDisplay - { - ContentTypeAlias = listName, - ContentTypeName = listName, - Id = listName, - IsContainer = true, - Name = listName == Constants.Conventions.MemberTypes.AllMembersListId ? "All Members" : listName, - Path = "-1," + listName, - ParentId = -1 - }; + if (MembershipScenario == MembershipScenario.NativeUmbraco) + { + long totalRecords; + var members = Services.MemberService +.GetAll((pageNumber - 1), pageSize, out totalRecords, orderBy, orderDirection, orderBySystemField +, memberTypeAlias, filter).ToArray(); + if (totalRecords == 0) + { + return new PagedResult(0, 0, 0); + } + var pagedResult = new PagedResult(totalRecords, pageNumber, pageSize); + pagedResult.Items = members + .Select(Mapper.Map); + return pagedResult; + } + else + { + int totalRecords; + var members = _provider.GetAllUsers((pageNumber - 1), pageSize, out totalRecords); + if (totalRecords == 0) + { + return new PagedResult(0, 0, 0); + } + var pagedResult = new PagedResult(totalRecords, pageNumber, pageSize); + pagedResult.Items = members + .Cast() + .Select(Mapper.Map); + return pagedResult; + } - TabsAndPropertiesResolver.AddListView(display, "member", Services.DataTypeService, Services.TextService); + } - return display; - } + /// + /// Returns a display node with a list view to render members + /// + /// + /// + public MemberListDisplay GetListNodeDisplay(string listName) + { + var display = new MemberListDisplay + { + ContentTypeAlias = listName, + ContentTypeName = listName, + Id = listName, + IsContainer = true, + Name = listName == Constants.Conventions.MemberTypes.AllMembersListId ? "All Members" : listName, + Path = "-1," + listName, + ParentId = -1 + }; - /// - /// Gets the content json for the member - /// - /// - /// - [OutgoingEditorModelEvent] - public MemberDisplay GetByKey(Guid key) - { - MembershipUser foundMembershipMember; - MemberDisplay display; - IMember foundMember; - switch (MembershipScenario) - { - case MembershipScenario.NativeUmbraco: - foundMember = Services.MemberService.GetByKey(key); - if (foundMember == null) - { - HandleContentNotFound(key); - } - return Mapper.Map(foundMember); - case MembershipScenario.CustomProviderWithUmbracoLink: + TabsAndPropertiesResolver.AddListView(display, "member", Services.DataTypeService, Services.TextService); - //TODO: Support editing custom properties for members with a custom membership provider here. + return display; + } - //foundMember = Services.MemberService.GetByKey(key); - //if (foundMember == null) - //{ - // HandleContentNotFound(key); - //} - //foundMembershipMember = Membership.GetUser(key, false); - //if (foundMembershipMember == null) - //{ - // HandleContentNotFound(key); - //} + /// + /// Gets the content json for the member + /// + /// + /// + [OutgoingEditorModelEvent] + public MemberDisplay GetByKey(Guid key) + { + MembershipUser foundMembershipMember; + MemberDisplay display; + IMember foundMember; + switch (MembershipScenario) + { + case MembershipScenario.NativeUmbraco: + foundMember = Services.MemberService.GetByKey(key); + if (foundMember == null) + { + HandleContentNotFound(key); + } + return Mapper.Map(foundMember); + case MembershipScenario.CustomProviderWithUmbracoLink: - //display = Mapper.Map(foundMembershipMember); - ////map the name over - //display.Name = foundMember.Name; - //return display; + //TODO: Support editing custom properties for members with a custom membership provider here. - case MembershipScenario.StandaloneCustomProvider: - default: - foundMembershipMember = _provider.GetUser(key, false); - if (foundMembershipMember == null) - { - HandleContentNotFound(key); - } - display = Mapper.Map(foundMembershipMember); - return display; - } - } + //foundMember = Services.MemberService.GetByKey(key); + //if (foundMember == null) + //{ + // HandleContentNotFound(key); + //} + //foundMembershipMember = Membership.GetUser(key, false); + //if (foundMembershipMember == null) + //{ + // HandleContentNotFound(key); + //} - /// - /// Gets an empty content item for the - /// - /// - /// - [OutgoingEditorModelEvent] - public MemberDisplay GetEmpty(string contentTypeAlias = null) - { - IMember emptyContent; - switch (MembershipScenario) - { - case MembershipScenario.NativeUmbraco: - if (contentTypeAlias == null) - { - throw new HttpResponseException(HttpStatusCode.NotFound); - } + //display = Mapper.Map(foundMembershipMember); + ////map the name over + //display.Name = foundMember.Name; + //return display; - var contentType = Services.MemberTypeService.Get(contentTypeAlias); - if (contentType == null) - { - throw new HttpResponseException(HttpStatusCode.NotFound); - } + case MembershipScenario.StandaloneCustomProvider: + default: + foundMembershipMember = _provider.GetUser(key, false); + if (foundMembershipMember == null) + { + HandleContentNotFound(key); + } + display = Mapper.Map(foundMembershipMember); + return display; + } + } - emptyContent = new Member(contentType); - emptyContent.AdditionalData["NewPassword"] = Membership.GeneratePassword(Membership.MinRequiredPasswordLength, Membership.MinRequiredNonAlphanumericCharacters); - return Mapper.Map(emptyContent); - case MembershipScenario.CustomProviderWithUmbracoLink: - //TODO: Support editing custom properties for members with a custom membership provider here. + /// + /// Gets an empty content item for the + /// + /// + /// + [OutgoingEditorModelEvent] + public MemberDisplay GetEmpty(string contentTypeAlias = null) + { + IMember emptyContent; + switch (MembershipScenario) + { + case MembershipScenario.NativeUmbraco: + if (contentTypeAlias == null) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } - case MembershipScenario.StandaloneCustomProvider: - default: - //we need to return a scaffold of a 'simple' member - basically just what a membership provider can edit - emptyContent = MemberService.CreateGenericMembershipProviderMember("", "", "", ""); - emptyContent.AdditionalData["NewPassword"] = Membership.GeneratePassword(Membership.MinRequiredPasswordLength, Membership.MinRequiredNonAlphanumericCharacters); - return Mapper.Map(emptyContent); - } - } + var contentType = Services.MemberTypeService.Get(contentTypeAlias); + if (contentType == null) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } - /// - /// Saves member - /// - /// - [FileUploadCleanupFilter] - public MemberDisplay PostSave( - [ModelBinder(typeof(MemberBinder))] - MemberSave contentItem) - { + emptyContent = new Member(contentType); + emptyContent.AdditionalData["NewPassword"] = Membership.GeneratePassword(Membership.MinRequiredPasswordLength, Membership.MinRequiredNonAlphanumericCharacters); + return Mapper.Map(emptyContent); + case MembershipScenario.CustomProviderWithUmbracoLink: + //TODO: Support editing custom properties for members with a custom membership provider here. - //If we've reached here it means: - // * Our model has been bound - // * and validated - // * any file attachments have been saved to their temporary location for us to use - // * we have a reference to the DTO object and the persisted object - // * Permissions are valid + case MembershipScenario.StandaloneCustomProvider: + default: + //we need to return a scaffold of a 'simple' member - basically just what a membership provider can edit + emptyContent = MemberService.CreateGenericMembershipProviderMember("", "", "", ""); + emptyContent.AdditionalData["NewPassword"] = Membership.GeneratePassword(Membership.MinRequiredPasswordLength, Membership.MinRequiredNonAlphanumericCharacters); + return Mapper.Map(emptyContent); + } + } - //This is a special case for when we're not using the umbraco membership provider - when this is the case - // we will not have a ContentTypeAlias set which means the model state will be invalid but we don't care about that - // so we'll remove that model state value - if (MembershipScenario != MembershipScenario.NativeUmbraco) - { - ModelState.Remove("ContentTypeAlias"); + /// + /// Saves member + /// + /// + [FileUploadCleanupFilter] + public MemberDisplay PostSave( + [ModelBinder(typeof(MemberBinder))] + MemberSave contentItem) + { - //TODO: We're removing this because we are not displaying it but when we support the CustomProviderWithUmbracoLink scenario - // we will be able to have a real name associated so do not remove this state once that is implemented! - ModelState.Remove("Name"); - } + //If we've reached here it means: + // * Our model has been bound + // * and validated + // * any file attachments have been saved to their temporary location for us to use + // * we have a reference to the DTO object and the persisted object + // * Permissions are valid - //map the properties to the persisted entity - MapPropertyValues(contentItem); + //This is a special case for when we're not using the umbraco membership provider - when this is the case + // we will not have a ContentTypeAlias set which means the model state will be invalid but we don't care about that + // so we'll remove that model state value + if (MembershipScenario != MembershipScenario.NativeUmbraco) + { + ModelState.Remove("ContentTypeAlias"); - //Unlike content/media - if there are errors for a member, we do NOT proceed to save them, we cannot so return the errors - if (ModelState.IsValid == false) - { - var forDisplay = Mapper.Map(contentItem.PersistedContent); - forDisplay.Errors = ModelState.ToErrorDictionary(); - throw new HttpResponseException(Request.CreateValidationErrorResponse(forDisplay)); - } + //TODO: We're removing this because we are not displaying it but when we support the CustomProviderWithUmbracoLink scenario + // we will be able to have a real name associated so do not remove this state once that is implemented! + ModelState.Remove("Name"); + } + + //map the properties to the persisted entity + MapPropertyValues(contentItem); + + //Unlike content/media - if there are errors for a member, we do NOT proceed to save them, we cannot so return the errors + if (ModelState.IsValid == false) + { + var forDisplay = Mapper.Map(contentItem.PersistedContent); + forDisplay.Errors = ModelState.ToErrorDictionary(); + throw new HttpResponseException(Request.CreateValidationErrorResponse(forDisplay)); + } + + //TODO: WE need to support this! - requires UI updates, etc... + if (_provider.RequiresQuestionAndAnswer) + { + throw new NotSupportedException("Currently the member editor does not support providers that have RequiresQuestionAndAnswer specified"); + } - //TODO: WE need to support this! - requires UI updates, etc... - if (_provider.RequiresQuestionAndAnswer) - { - throw new NotSupportedException("Currently the member editor does not support providers that have RequiresQuestionAndAnswer specified"); - } - //We're gonna look up the current roles now because the below code can cause // events to be raised and developers could be manually adding roles to members in // their handlers. If we don't look this up now there's a chance we'll just end up @@ -284,473 +287,473 @@ namespace Umbraco.Web.Editors //find the ones to remove and remove them var rolesToRemove = currRoles.Except(contentItem.Groups).ToArray(); - string generatedPassword = null; - //Depending on the action we need to first do a create or update using the membership provider - // this ensures that passwords are formatted correclty and also performs the validation on the provider itself. - switch (contentItem.Action) - { - case ContentSaveAction.Save: - generatedPassword = UpdateWithMembershipProvider(contentItem); - break; - case ContentSaveAction.SaveNew: - MembershipCreateStatus status; - CreateWithMembershipProvider(contentItem, out status); + string generatedPassword = null; + //Depending on the action we need to first do a create or update using the membership provider + // this ensures that passwords are formatted correclty and also performs the validation on the provider itself. + switch (contentItem.Action) + { + case ContentSaveAction.Save: + generatedPassword = UpdateWithMembershipProvider(contentItem); + break; + case ContentSaveAction.SaveNew: + MembershipCreateStatus status; + CreateWithMembershipProvider(contentItem, out status); - // save the ID of the creator - contentItem.PersistedContent.CreatorId = Security.CurrentUser.Id; - break; - default: - //we don't support anything else for members - throw new HttpResponseException(HttpStatusCode.NotFound); - } + // save the ID of the creator + contentItem.PersistedContent.CreatorId = Security.CurrentUser.Id; + break; + default: + //we don't support anything else for members + throw new HttpResponseException(HttpStatusCode.NotFound); + } - //If we've had problems creating/updating the user with the provider then return the error - if (ModelState.IsValid == false) - { - var forDisplay = Mapper.Map(contentItem.PersistedContent); - forDisplay.Errors = ModelState.ToErrorDictionary(); - throw new HttpResponseException(Request.CreateValidationErrorResponse(forDisplay)); - } + //If we've had problems creating/updating the user with the provider then return the error + if (ModelState.IsValid == false) + { + var forDisplay = Mapper.Map(contentItem.PersistedContent); + forDisplay.Errors = ModelState.ToErrorDictionary(); + throw new HttpResponseException(Request.CreateValidationErrorResponse(forDisplay)); + } - //save the IMember - - //TODO: When we support the CustomProviderWithUmbracoLink scenario, we'll need to save the custom properties for that here too - if (MembershipScenario == MembershipScenario.NativeUmbraco) - { - //save the item - //NOTE: We are setting the password to NULL - this indicates to the system to not actually save the password - // so it will not get overwritten! - contentItem.PersistedContent.RawPasswordValue = null; + //save the IMember - + //TODO: When we support the CustomProviderWithUmbracoLink scenario, we'll need to save the custom properties for that here too + if (MembershipScenario == MembershipScenario.NativeUmbraco) + { + //save the item + //NOTE: We are setting the password to NULL - this indicates to the system to not actually save the password + // so it will not get overwritten! + contentItem.PersistedContent.RawPasswordValue = null; - //create/save the IMember - Services.MemberService.Save(contentItem.PersistedContent); - } - - //Now let's do the role provider stuff - now that we've saved the content item (that is important since - // if we are changing the username, it must be persisted before looking up the member roles). + //create/save the IMember + Services.MemberService.Save(contentItem.PersistedContent); + } + + //Now let's do the role provider stuff - now that we've saved the content item (that is important since + // if we are changing the username, it must be persisted before looking up the member roles). if (rolesToRemove.Any()) - { + { Roles.RemoveUserFromRoles(contentItem.PersistedContent.Username, rolesToRemove); - } - //find the ones to add and add them + } + //find the ones to add and add them var toAdd = contentItem.Groups.Except(currRoles).ToArray(); - if (toAdd.Any()) - { - //add the ones submitted - Roles.AddUserToRoles(contentItem.PersistedContent.Username, toAdd); - } + if (toAdd.Any()) + { + //add the ones submitted + Roles.AddUserToRoles(contentItem.PersistedContent.Username, toAdd); + } - //set the generated password (if there was one) - in order to do this we'll chuck the gen'd password into the - // additional data of the IUmbracoEntity of the persisted item - then we can retrieve this in the model mapper and set - // the value to be given to the UI. Hooray for AdditionalData :) - contentItem.PersistedContent.AdditionalData["GeneratedPassword"] = generatedPassword; + //set the generated password (if there was one) - in order to do this we'll chuck the gen'd password into the + // additional data of the IUmbracoEntity of the persisted item - then we can retrieve this in the model mapper and set + // the value to be given to the UI. Hooray for AdditionalData :) + contentItem.PersistedContent.AdditionalData["GeneratedPassword"] = generatedPassword; - //return the updated model - var display = Mapper.Map(contentItem.PersistedContent); + //return the updated model + var display = Mapper.Map(contentItem.PersistedContent); - //lasty, if it is not valid, add the modelstate to the outgoing object and throw a 403 - HandleInvalidModelState(display); + //lasty, if it is not valid, add the modelstate to the outgoing object and throw a 403 + HandleInvalidModelState(display); - var localizedTextService = Services.TextService; - //put the correct msgs in - switch (contentItem.Action) - { - case ContentSaveAction.Save: - case ContentSaveAction.SaveNew: - display.AddSuccessNotification(localizedTextService.Localize("speechBubbles/editMemberSaved"), localizedTextService.Localize("speechBubbles/editMemberSaved")); - break; - } + var localizedTextService = Services.TextService; + //put the correct msgs in + switch (contentItem.Action) + { + case ContentSaveAction.Save: + case ContentSaveAction.SaveNew: + display.AddSuccessNotification(localizedTextService.Localize("speechBubbles/editMemberSaved"), localizedTextService.Localize("speechBubbles/editMemberSaved")); + break; + } - return display; - } + return display; + } - /// - /// Maps the property values to the persisted entity - /// - /// - private void MapPropertyValues(MemberSave contentItem) - { - UpdateName(contentItem); + /// + /// Maps the property values to the persisted entity + /// + /// + private void MapPropertyValues(MemberSave contentItem) + { + UpdateName(contentItem); - //map the custom properties - this will already be set for new entities in our member binder - contentItem.PersistedContent.Email = contentItem.Email; - contentItem.PersistedContent.Username = contentItem.Username; + //map the custom properties - this will already be set for new entities in our member binder + contentItem.PersistedContent.Email = contentItem.Email; + contentItem.PersistedContent.Username = contentItem.Username; - //use the base method to map the rest of the properties - base.MapPropertyValues(contentItem); - } + //use the base method to map the rest of the properties + base.MapPropertyValues(contentItem); + } - /// - /// Update the membership user using the membership provider (for things like email, etc...) - /// If a password change is detected then we'll try that too. - /// - /// - /// - /// If the password has been reset then this method will return the reset/generated password, otherwise will return null. - /// - private string UpdateWithMembershipProvider(MemberSave contentItem) - { - //Get the member from the provider + /// + /// Update the membership user using the membership provider (for things like email, etc...) + /// If a password change is detected then we'll try that too. + /// + /// + /// + /// If the password has been reset then this method will return the reset/generated password, otherwise will return null. + /// + private string UpdateWithMembershipProvider(MemberSave contentItem) + { + //Get the member from the provider - var membershipUser = _provider.GetUser(contentItem.PersistedContent.Key, false); - if (membershipUser == null) - { - //This should never happen! so we'll let it YSOD if it does. - throw new InvalidOperationException("Could not get member from membership provider " + _provider.Name + " with key " + contentItem.PersistedContent.Key); - } + var membershipUser = _provider.GetUser(contentItem.PersistedContent.Key, false); + if (membershipUser == null) + { + //This should never happen! so we'll let it YSOD if it does. + throw new InvalidOperationException("Could not get member from membership provider " + _provider.Name + " with key " + contentItem.PersistedContent.Key); + } - var shouldReFetchMember = false; - var providedUserName = contentItem.PersistedContent.Username; + var shouldReFetchMember = false; + var providedUserName = contentItem.PersistedContent.Username; - //Update the membership user if it has changed - try - { - var requiredUpdating = Members.UpdateMember(membershipUser, _provider, - contentItem.Email.Trim(), - contentItem.IsApproved, - comment: contentItem.Comments); + //Update the membership user if it has changed + try + { + var requiredUpdating = Members.UpdateMember(membershipUser, _provider, + contentItem.Email.Trim(), + contentItem.IsApproved, + comment: contentItem.Comments); - if (requiredUpdating.Success) - { - //re-map these values - shouldReFetchMember = true; - } - } - catch (Exception ex) - { - LogHelper.WarnWithException("Could not update member, the provider returned an error", ex); - ModelState.AddPropertyError( - //specify 'default' just so that it shows up as a notification - is not assigned to a property - new ValidationResult("Could not update member, the provider returned an error: " + ex.Message + " (see log for full details)"), "default"); - } + if (requiredUpdating.Success) + { + //re-map these values + shouldReFetchMember = true; + } + } + catch (Exception ex) + { + LogHelper.WarnWithException("Could not update member, the provider returned an error", ex); + ModelState.AddPropertyError( + //specify 'default' just so that it shows up as a notification - is not assigned to a property + new ValidationResult("Could not update member, the provider returned an error: " + ex.Message + " (see log for full details)"), "default"); + } - //if they were locked but now they are trying to be unlocked - if (membershipUser.IsLockedOut && contentItem.IsLockedOut == false) - { - try - { - var result = _provider.UnlockUser(membershipUser.UserName); - if (result == false) - { - //it wasn't successful - but it won't really tell us why. - ModelState.AddModelError("custom", "Could not unlock the user"); - } - else - { - shouldReFetchMember = true; - } - } - catch (Exception ex) - { - ModelState.AddModelError("custom", ex); - } - } - else if (membershipUser.IsLockedOut == false && contentItem.IsLockedOut) - { - //NOTE: This should not ever happen unless someone is mucking around with the request data. - //An admin cannot simply lock a user, they get locked out by password attempts, but an admin can un-approve them - ModelState.AddModelError("custom", "An admin cannot lock a user"); - } + //if they were locked but now they are trying to be unlocked + if (membershipUser.IsLockedOut && contentItem.IsLockedOut == false) + { + try + { + var result = _provider.UnlockUser(membershipUser.UserName); + if (result == false) + { + //it wasn't successful - but it won't really tell us why. + ModelState.AddModelError("custom", "Could not unlock the user"); + } + else + { + shouldReFetchMember = true; + } + } + catch (Exception ex) + { + ModelState.AddModelError("custom", ex); + } + } + else if (membershipUser.IsLockedOut == false && contentItem.IsLockedOut) + { + //NOTE: This should not ever happen unless someone is mucking around with the request data. + //An admin cannot simply lock a user, they get locked out by password attempts, but an admin can un-approve them + ModelState.AddModelError("custom", "An admin cannot lock a user"); + } - //password changes ? - if (contentItem.Password == null) - { - //If the provider has changed some values, these values need to be reflected in the member object - //that will get mapped to the display object - if (shouldReFetchMember) - { - RefetchMemberData(contentItem, LookupType.ByKey); - RestoreProvidedUserName(contentItem, providedUserName); - } + //password changes ? + if (contentItem.Password == null) + { + //If the provider has changed some values, these values need to be reflected in the member object + //that will get mapped to the display object + if (shouldReFetchMember) + { + RefetchMemberData(contentItem, LookupType.ByKey); + RestoreProvidedUserName(contentItem, providedUserName); + } - return null; - } + return null; + } - var passwordChangeResult = Members.ChangePassword(membershipUser.UserName, contentItem.Password, _provider); - if (passwordChangeResult.Success) - { - //If the provider has changed some values, these values need to be reflected in the member object - //that will get mapped to the display object - if (shouldReFetchMember) - { - RefetchMemberData(contentItem, LookupType.ByKey); - RestoreProvidedUserName(contentItem, providedUserName); - } + var passwordChangeResult = Members.ChangePassword(membershipUser.UserName, contentItem.Password, _provider); + if (passwordChangeResult.Success) + { + //If the provider has changed some values, these values need to be reflected in the member object + //that will get mapped to the display object + if (shouldReFetchMember) + { + RefetchMemberData(contentItem, LookupType.ByKey); + RestoreProvidedUserName(contentItem, providedUserName); + } - //even if we weren't resetting this, it is the correct value (null), otherwise if we were resetting then it will contain the new pword - return passwordChangeResult.Result.ResetPassword; - } + //even if we weren't resetting this, it is the correct value (null), otherwise if we were resetting then it will contain the new pword + return passwordChangeResult.Result.ResetPassword; + } - //it wasn't successful, so add the change error to the model state - ModelState.AddPropertyError( - passwordChangeResult.Result.ChangeError, - string.Format("{0}password", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); + //it wasn't successful, so add the change error to the model state + ModelState.AddPropertyError( + passwordChangeResult.Result.ChangeError, + string.Format("{0}password", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); - return null; - } + return null; + } - private enum LookupType - { - ByKey, - ByUserName - } + private enum LookupType + { + ByKey, + ByUserName + } - /// - /// Re-fetches the database data to map to the PersistedContent object and re-assigns the already mapped the posted properties so that the display object is up-to-date - /// - /// - /// - /// This is done during an update if the membership provider has changed some underlying data - we need to ensure that our model is consistent with that data - /// - private void RefetchMemberData(MemberSave contentItem, LookupType lookup) - { - var currProps = contentItem.PersistedContent.Properties.ToArray(); - - switch (MembershipScenario) - { - case MembershipScenario.NativeUmbraco: - switch (lookup) - { - case LookupType.ByKey: - //Go and re-fetch the persisted item - contentItem.PersistedContent = Services.MemberService.GetByKey(contentItem.Key); - break; - case LookupType.ByUserName: - contentItem.PersistedContent = Services.MemberService.GetByUsername(contentItem.Username.Trim()); - break; - } - break; - case MembershipScenario.CustomProviderWithUmbracoLink: - case MembershipScenario.StandaloneCustomProvider: - default: - var membershipUser = _provider.GetUser(contentItem.Key, false); - //Go and re-fetch the persisted item - contentItem.PersistedContent = Mapper.Map(membershipUser); - break; - } + /// + /// Re-fetches the database data to map to the PersistedContent object and re-assigns the already mapped the posted properties so that the display object is up-to-date + /// + /// + /// + /// This is done during an update if the membership provider has changed some underlying data - we need to ensure that our model is consistent with that data + /// + private void RefetchMemberData(MemberSave contentItem, LookupType lookup) + { + var currProps = contentItem.PersistedContent.Properties.ToArray(); - UpdateName(contentItem); + switch (MembershipScenario) + { + case MembershipScenario.NativeUmbraco: + switch (lookup) + { + case LookupType.ByKey: + //Go and re-fetch the persisted item + contentItem.PersistedContent = Services.MemberService.GetByKey(contentItem.Key); + break; + case LookupType.ByUserName: + contentItem.PersistedContent = Services.MemberService.GetByUsername(contentItem.Username.Trim()); + break; + } + break; + case MembershipScenario.CustomProviderWithUmbracoLink: + case MembershipScenario.StandaloneCustomProvider: + default: + var membershipUser = _provider.GetUser(contentItem.Key, false); + //Go and re-fetch the persisted item + contentItem.PersistedContent = Mapper.Map(membershipUser); + break; + } - //re-assign the mapped values that are not part of the membership provider properties. - var builtInAliases = Constants.Conventions.Member.GetStandardPropertyTypeStubs().Select(x => x.Key).ToArray(); - foreach (var p in contentItem.PersistedContent.Properties) - { - var valueMapped = currProps.SingleOrDefault(x => x.Alias == p.Alias); - if (builtInAliases.Contains(p.Alias) == false && valueMapped != null) - { - p.Value = valueMapped.Value; - p.TagSupport.Behavior = valueMapped.TagSupport.Behavior; - p.TagSupport.Enable = valueMapped.TagSupport.Enable; - p.TagSupport.Tags = valueMapped.TagSupport.Tags; - } - } - } + UpdateName(contentItem); - /// - /// Following a refresh of member data called during an update if the membership provider has changed some underlying data, - /// we don't want to lose the provided, and potentiallly changed, username - /// - /// - /// - private static void RestoreProvidedUserName(MemberSave contentItem, string providedUserName) - { - contentItem.PersistedContent.Username = providedUserName; - } + //re-assign the mapped values that are not part of the membership provider properties. + var builtInAliases = Constants.Conventions.Member.GetStandardPropertyTypeStubs().Select(x => x.Key).ToArray(); + foreach (var p in contentItem.PersistedContent.Properties) + { + var valueMapped = currProps.SingleOrDefault(x => x.Alias == p.Alias); + if (builtInAliases.Contains(p.Alias) == false && valueMapped != null) + { + p.Value = valueMapped.Value; + p.TagSupport.Behavior = valueMapped.TagSupport.Behavior; + p.TagSupport.Enable = valueMapped.TagSupport.Enable; + p.TagSupport.Tags = valueMapped.TagSupport.Tags; + } + } + } - /// - /// This is going to create the user with the membership provider and check for validation - /// - /// - /// - /// - /// - /// Depending on if the Umbraco membership provider is active or not, the process differs slightly: - /// - /// * If the umbraco membership provider is used - we create the membership user first with the membership provider, since - /// it's the umbraco membership provider, this writes to the umbraco tables. When that is complete we re-fetch the IMember - /// model data from the db. In this case we don't care what the provider user key is. - /// * If we're using a non-umbraco membership provider - we check if there is a 'Member' member type - if so - /// we create an empty IMember instance first (of type 'Member'), this gives us a unique ID (GUID) - /// that we then use to create the member in the custom membership provider. This acts as the link between Umbraco data and - /// the custom membership provider data. This gives us the ability to eventually have custom membership properties but still use - /// a custom memberhip provider. If there is no 'Member' member type, then we will simply just create the membership provider member - /// with no link to our data. - /// - /// If this is successful, it will go and re-fetch the IMember from the db because it will now have an ID because the Umbraco provider - /// uses the umbraco data store - then of course we need to re-map it to the saved property values. - /// - private MembershipUser CreateWithMembershipProvider(MemberSave contentItem, out MembershipCreateStatus status) - { - MembershipUser membershipUser; + /// + /// Following a refresh of member data called during an update if the membership provider has changed some underlying data, + /// we don't want to lose the provided, and potentiallly changed, username + /// + /// + /// + private static void RestoreProvidedUserName(MemberSave contentItem, string providedUserName) + { + contentItem.PersistedContent.Username = providedUserName; + } - switch (MembershipScenario) - { - case MembershipScenario.NativeUmbraco: - //We are using the umbraco membership provider, create the member using the membership provider first. - var umbracoMembershipProvider = (UmbracoMembershipProviderBase)_provider; - //TODO: We are not supporting q/a - passing in empty here - membershipUser = umbracoMembershipProvider.CreateUser( - contentItem.ContentTypeAlias, contentItem.Username, - contentItem.Password.NewPassword, - contentItem.Email, "", "", - contentItem.IsApproved, - Guid.NewGuid(), //since it's the umbraco provider, the user key here doesn't make any difference - out status); - - break; - case MembershipScenario.CustomProviderWithUmbracoLink: - //We are using a custom membership provider, we'll create an empty IMember first to get the unique id to use - // as the provider user key. - //create it - this persisted item has already been set in the MemberBinder based on the 'Member' member type: - Services.MemberService.Save(contentItem.PersistedContent); + /// + /// This is going to create the user with the membership provider and check for validation + /// + /// + /// + /// + /// + /// Depending on if the Umbraco membership provider is active or not, the process differs slightly: + /// + /// * If the umbraco membership provider is used - we create the membership user first with the membership provider, since + /// it's the umbraco membership provider, this writes to the umbraco tables. When that is complete we re-fetch the IMember + /// model data from the db. In this case we don't care what the provider user key is. + /// * If we're using a non-umbraco membership provider - we check if there is a 'Member' member type - if so + /// we create an empty IMember instance first (of type 'Member'), this gives us a unique ID (GUID) + /// that we then use to create the member in the custom membership provider. This acts as the link between Umbraco data and + /// the custom membership provider data. This gives us the ability to eventually have custom membership properties but still use + /// a custom memberhip provider. If there is no 'Member' member type, then we will simply just create the membership provider member + /// with no link to our data. + /// + /// If this is successful, it will go and re-fetch the IMember from the db because it will now have an ID because the Umbraco provider + /// uses the umbraco data store - then of course we need to re-map it to the saved property values. + /// + private MembershipUser CreateWithMembershipProvider(MemberSave contentItem, out MembershipCreateStatus status) + { + MembershipUser membershipUser; - //TODO: We are not supporting q/a - passing in empty here - membershipUser = _provider.CreateUser( - contentItem.Username, - contentItem.Password.NewPassword, - contentItem.Email, - "TEMP", //some membership provider's require something here even if q/a is disabled! - "TEMP", //some membership provider's require something here even if q/a is disabled! - contentItem.IsApproved, - contentItem.PersistedContent.Key, //custom membership provider, we'll link that based on the IMember unique id (GUID) - out status); + switch (MembershipScenario) + { + case MembershipScenario.NativeUmbraco: + //We are using the umbraco membership provider, create the member using the membership provider first. + var umbracoMembershipProvider = (UmbracoMembershipProviderBase)_provider; + //TODO: We are not supporting q/a - passing in empty here + membershipUser = umbracoMembershipProvider.CreateUser( + contentItem.ContentTypeAlias, contentItem.Username, + contentItem.Password.NewPassword, + contentItem.Email, "", "", + contentItem.IsApproved, + Guid.NewGuid(), //since it's the umbraco provider, the user key here doesn't make any difference + out status); - break; - case MembershipScenario.StandaloneCustomProvider: - // we don't have a member type to use so we will just create the basic membership user with the provider with no - // link back to the umbraco data - - var newKey = Guid.NewGuid(); - //TODO: We are not supporting q/a - passing in empty here - membershipUser = _provider.CreateUser( - contentItem.Username, - contentItem.Password.NewPassword, - contentItem.Email, - "TEMP", //some membership provider's require something here even if q/a is disabled! - "TEMP", //some membership provider's require something here even if q/a is disabled! - contentItem.IsApproved, - newKey, - out status); - - break; - default: - throw new ArgumentOutOfRangeException(); - } + break; + case MembershipScenario.CustomProviderWithUmbracoLink: + //We are using a custom membership provider, we'll create an empty IMember first to get the unique id to use + // as the provider user key. + //create it - this persisted item has already been set in the MemberBinder based on the 'Member' member type: + Services.MemberService.Save(contentItem.PersistedContent); - //TODO: Localize these! - switch (status) - { - case MembershipCreateStatus.Success: + //TODO: We are not supporting q/a - passing in empty here + membershipUser = _provider.CreateUser( + contentItem.Username, + contentItem.Password.NewPassword, + contentItem.Email, + "TEMP", //some membership provider's require something here even if q/a is disabled! + "TEMP", //some membership provider's require something here even if q/a is disabled! + contentItem.IsApproved, + contentItem.PersistedContent.Key, //custom membership provider, we'll link that based on the IMember unique id (GUID) + out status); - //map the key back - contentItem.Key = membershipUser.ProviderUserKey.TryConvertTo().Result; - contentItem.PersistedContent.Key = contentItem.Key; + break; + case MembershipScenario.StandaloneCustomProvider: + // we don't have a member type to use so we will just create the basic membership user with the provider with no + // link back to the umbraco data - //if the comments are there then we need to save them - if (contentItem.Comments.IsNullOrWhiteSpace() == false) - { - membershipUser.Comment = contentItem.Comments; - _provider.UpdateUser(membershipUser); - } + var newKey = Guid.NewGuid(); + //TODO: We are not supporting q/a - passing in empty here + membershipUser = _provider.CreateUser( + contentItem.Username, + contentItem.Password.NewPassword, + contentItem.Email, + "TEMP", //some membership provider's require something here even if q/a is disabled! + "TEMP", //some membership provider's require something here even if q/a is disabled! + contentItem.IsApproved, + newKey, + out status); - RefetchMemberData(contentItem, LookupType.ByUserName); + break; + default: + throw new ArgumentOutOfRangeException(); + } - break; - case MembershipCreateStatus.InvalidUserName: - ModelState.AddPropertyError( - new ValidationResult("Invalid user name", new[] { "value" }), - string.Format("{0}login", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); - break; - case MembershipCreateStatus.InvalidPassword: - ModelState.AddPropertyError( - new ValidationResult("Invalid password", new[] { "value" }), - string.Format("{0}password", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); - break; - case MembershipCreateStatus.InvalidQuestion: - case MembershipCreateStatus.InvalidAnswer: - throw new NotSupportedException("Currently the member editor does not support providers that have RequiresQuestionAndAnswer specified"); - case MembershipCreateStatus.InvalidEmail: - ModelState.AddPropertyError( - new ValidationResult("Invalid email", new[] { "value" }), - string.Format("{0}email", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); - break; - case MembershipCreateStatus.DuplicateUserName: - ModelState.AddPropertyError( - new ValidationResult("Username is already in use", new[] { "value" }), - string.Format("{0}login", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); - break; - case MembershipCreateStatus.DuplicateEmail: - ModelState.AddPropertyError( - new ValidationResult("Email address is already in use", new[] { "value" }), - string.Format("{0}email", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); - break; - case MembershipCreateStatus.InvalidProviderUserKey: - ModelState.AddPropertyError( - //specify 'default' just so that it shows up as a notification - is not assigned to a property - new ValidationResult("Invalid provider user key"), "default"); - break; - case MembershipCreateStatus.DuplicateProviderUserKey: - ModelState.AddPropertyError( - //specify 'default' just so that it shows up as a notification - is not assigned to a property - new ValidationResult("Duplicate provider user key"), "default"); - break; - case MembershipCreateStatus.ProviderError: - case MembershipCreateStatus.UserRejected: - ModelState.AddPropertyError( - //specify 'default' just so that it shows up as a notification - is not assigned to a property - new ValidationResult("User could not be created (rejected by provider)"), "default"); - break; - default: - throw new ArgumentOutOfRangeException(); - } + //TODO: Localize these! + switch (status) + { + case MembershipCreateStatus.Success: - return membershipUser; - } + //map the key back + contentItem.Key = membershipUser.ProviderUserKey.TryConvertTo().Result; + contentItem.PersistedContent.Key = contentItem.Key; - /// - /// Permanently deletes a member - /// - /// - /// - /// - [HttpPost] - public HttpResponseMessage DeleteByKey(Guid key) - { - IMember foundMember; - MembershipUser foundMembershipUser; - switch (MembershipScenario) - { - case MembershipScenario.NativeUmbraco: - foundMember = Services.MemberService.GetByKey(key); - if (foundMember == null) - { - return HandleContentNotFound(key, false); - } - Services.MemberService.Delete(foundMember); - break; - case MembershipScenario.CustomProviderWithUmbracoLink: - foundMember = Services.MemberService.GetByKey(key); - if (foundMember != null) - { - Services.MemberService.Delete(foundMember); - } - foundMembershipUser = _provider.GetUser(key, false); - if (foundMembershipUser != null) - { - _provider.DeleteUser(foundMembershipUser.UserName, true); - } - break; - case MembershipScenario.StandaloneCustomProvider: - foundMembershipUser = _provider.GetUser(key, false); - if (foundMembershipUser != null) - { - _provider.DeleteUser(foundMembershipUser.UserName, true); - } - break; - default: - throw new ArgumentOutOfRangeException(); - } + //if the comments are there then we need to save them + if (contentItem.Comments.IsNullOrWhiteSpace() == false) + { + membershipUser.Comment = contentItem.Comments; + _provider.UpdateUser(membershipUser); + } - return Request.CreateResponse(HttpStatusCode.OK); - } - } + RefetchMemberData(contentItem, LookupType.ByUserName); + + break; + case MembershipCreateStatus.InvalidUserName: + ModelState.AddPropertyError( + new ValidationResult("Invalid user name", new[] { "value" }), + string.Format("{0}login", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); + break; + case MembershipCreateStatus.InvalidPassword: + ModelState.AddPropertyError( + new ValidationResult("Invalid password", new[] { "value" }), + string.Format("{0}password", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); + break; + case MembershipCreateStatus.InvalidQuestion: + case MembershipCreateStatus.InvalidAnswer: + throw new NotSupportedException("Currently the member editor does not support providers that have RequiresQuestionAndAnswer specified"); + case MembershipCreateStatus.InvalidEmail: + ModelState.AddPropertyError( + new ValidationResult("Invalid email", new[] { "value" }), + string.Format("{0}email", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); + break; + case MembershipCreateStatus.DuplicateUserName: + ModelState.AddPropertyError( + new ValidationResult("Username is already in use", new[] { "value" }), + string.Format("{0}login", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); + break; + case MembershipCreateStatus.DuplicateEmail: + ModelState.AddPropertyError( + new ValidationResult("Email address is already in use", new[] { "value" }), + string.Format("{0}email", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); + break; + case MembershipCreateStatus.InvalidProviderUserKey: + ModelState.AddPropertyError( + //specify 'default' just so that it shows up as a notification - is not assigned to a property + new ValidationResult("Invalid provider user key"), "default"); + break; + case MembershipCreateStatus.DuplicateProviderUserKey: + ModelState.AddPropertyError( + //specify 'default' just so that it shows up as a notification - is not assigned to a property + new ValidationResult("Duplicate provider user key"), "default"); + break; + case MembershipCreateStatus.ProviderError: + case MembershipCreateStatus.UserRejected: + ModelState.AddPropertyError( + //specify 'default' just so that it shows up as a notification - is not assigned to a property + new ValidationResult("User could not be created (rejected by provider)"), "default"); + break; + default: + throw new ArgumentOutOfRangeException(); + } + + return membershipUser; + } + + /// + /// Permanently deletes a member + /// + /// + /// + /// + [HttpPost] + public HttpResponseMessage DeleteByKey(Guid key) + { + IMember foundMember; + MembershipUser foundMembershipUser; + switch (MembershipScenario) + { + case MembershipScenario.NativeUmbraco: + foundMember = Services.MemberService.GetByKey(key); + if (foundMember == null) + { + return HandleContentNotFound(key, false); + } + Services.MemberService.Delete(foundMember); + break; + case MembershipScenario.CustomProviderWithUmbracoLink: + foundMember = Services.MemberService.GetByKey(key); + if (foundMember != null) + { + Services.MemberService.Delete(foundMember); + } + foundMembershipUser = _provider.GetUser(key, false); + if (foundMembershipUser != null) + { + _provider.DeleteUser(foundMembershipUser.UserName, true); + } + break; + case MembershipScenario.StandaloneCustomProvider: + foundMembershipUser = _provider.GetUser(key, false); + if (foundMembershipUser != null) + { + _provider.DeleteUser(foundMembershipUser.UserName, true); + } + break; + default: + throw new ArgumentOutOfRangeException(); + } + + return Request.CreateResponse(HttpStatusCode.OK); + } + } } diff --git a/src/UmbracoExamine/UmbracoMemberIndexer.cs b/src/UmbracoExamine/UmbracoMemberIndexer.cs index 873391da50..8edc470bfa 100644 --- a/src/UmbracoExamine/UmbracoMemberIndexer.cs +++ b/src/UmbracoExamine/UmbracoMemberIndexer.cs @@ -16,207 +16,208 @@ using Lucene.Net.Analysis; namespace UmbracoExamine { - + + /// + /// Custom indexer for members + /// + public class UmbracoMemberIndexer : UmbracoContentIndexer + { + + private readonly IMemberService _memberService; + private readonly IDataTypeService _dataTypeService; + /// - /// Custom indexer for members - /// - public class UmbracoMemberIndexer : UmbracoContentIndexer - { + /// Default constructor + /// + public UmbracoMemberIndexer() : base() + { + _dataTypeService = ApplicationContext.Current.Services.DataTypeService; + _memberService = ApplicationContext.Current.Services.MemberService; + } - private readonly IMemberService _memberService; - private readonly IDataTypeService _dataTypeService; + /// + /// Constructor to allow for creating an indexer at runtime + /// + /// + /// + /// + /// + [Obsolete("Use the overload that specifies the Umbraco services")] + public UmbracoMemberIndexer(IIndexCriteria indexerData, DirectoryInfo indexPath, IDataService dataService, Analyzer analyzer, bool async) + : base(indexerData, indexPath, dataService, analyzer, async) + { + _dataTypeService = ApplicationContext.Current.Services.DataTypeService; + _memberService = ApplicationContext.Current.Services.MemberService; + } - /// - /// Default constructor - /// - public UmbracoMemberIndexer() : base() - { - _dataTypeService = ApplicationContext.Current.Services.DataTypeService; - _memberService = ApplicationContext.Current.Services.MemberService; - } + /// + /// Constructor to allow for creating an indexer at runtime + /// + /// + /// + /// + /// + /// + /// + /// + public UmbracoMemberIndexer(IIndexCriteria indexerData, DirectoryInfo indexPath, IDataService dataService, + IDataTypeService dataTypeService, + IMemberService memberService, + Analyzer analyzer, bool async) + : base(indexerData, indexPath, dataService, analyzer, async) + { + _dataTypeService = dataTypeService; + _memberService = memberService; + } - /// - /// Constructor to allow for creating an indexer at runtime - /// - /// - /// - /// - /// - [Obsolete("Use the overload that specifies the Umbraco services")] - public UmbracoMemberIndexer(IIndexCriteria indexerData, DirectoryInfo indexPath, IDataService dataService, Analyzer analyzer, bool async) - : base(indexerData, indexPath, dataService, analyzer, async) - { - _dataTypeService = ApplicationContext.Current.Services.DataTypeService; - _memberService = ApplicationContext.Current.Services.MemberService; - } - /// - /// Constructor to allow for creating an indexer at runtime - /// - /// - /// - /// - /// - /// - /// - /// - public UmbracoMemberIndexer(IIndexCriteria indexerData, DirectoryInfo indexPath, IDataService dataService, - IDataTypeService dataTypeService, - IMemberService memberService, - Analyzer analyzer, bool async) - : base(indexerData, indexPath, dataService, analyzer, async) - { - _dataTypeService = dataTypeService; - _memberService = memberService; - } - + /// + /// Ensures that the'_searchEmail' is added to the user fields so that it is indexed - without having to modify the config + /// + /// + /// + protected override IIndexCriteria GetIndexerData(IndexSet indexSet) + { + var indexerData = base.GetIndexerData(indexSet); - /// - /// Ensures that the'_searchEmail' is added to the user fields so that it is indexed - without having to modify the config - /// - /// - /// - protected override IIndexCriteria GetIndexerData(IndexSet indexSet) - { - var indexerData = base.GetIndexerData(indexSet); + if (CanInitialize()) + { + //If the fields are missing a custom _searchEmail, then add it - if (CanInitialize()) - { - //If the fields are missing a custom _searchEmail, then add it + if (indexerData.UserFields.Any(x => x.Name == "_searchEmail") == false) + { + var field = new IndexField { Name = "_searchEmail" }; + var policy = IndexFieldPolicies.FirstOrDefault(x => x.Name == "_searchEmail"); + if (policy != null) + { + field.Type = policy.Type; + field.EnableSorting = policy.EnableSorting; + } - if (indexerData.UserFields.Any(x => x.Name == "_searchEmail") == false) - { - var field = new IndexField {Name = "_searchEmail"}; - var policy = IndexFieldPolicies.FirstOrDefault(x => x.Name == "_searchEmail"); - if (policy != null) - { - field.Type = policy.Type; - field.EnableSorting = policy.EnableSorting; - } + return new IndexCriteria( + indexerData.StandardFields, + indexerData.UserFields.Concat(new[] { field }), + indexerData.IncludeNodeTypes, + indexerData.ExcludeNodeTypes, + indexerData.ParentNodeId + ); + } + } - return new IndexCriteria( - indexerData.StandardFields, - indexerData.UserFields.Concat(new[] {field}), - indexerData.IncludeNodeTypes, - indexerData.ExcludeNodeTypes, - indexerData.ParentNodeId - ); - } - } + return indexerData; + } - return indexerData; - } + /// + /// The supported types for this indexer + /// + protected override IEnumerable SupportedTypes + { + get + { + return new string[] { IndexTypes.Member }; + } + } - /// - /// The supported types for this indexer - /// - protected override IEnumerable SupportedTypes - { - get - { - return new string[] { IndexTypes.Member }; - } - } + /// + /// Reindex all members + /// + /// + protected override void PerformIndexAll(string type) + { + //This only supports members + if (SupportedTypes.Contains(type) == false) + return; - /// - /// Reindex all members - /// - /// - protected override void PerformIndexAll(string type) - { - //This only supports members - if (SupportedTypes.Contains(type) == false) - return; - - const int pageSize = 1000; - var pageIndex = 0; + const int pageSize = 1000; + var pageIndex = 0; - IMember[] members; + IMember[] members; - if (IndexerData.IncludeNodeTypes.Any()) - { - //if there are specific node types then just index those - foreach (var nodeType in IndexerData.IncludeNodeTypes) - { - do - { - long total; - members = _memberService.GetAll(pageIndex, pageSize, out total, "LoginName", Direction.Ascending, nodeType).ToArray(); + if (IndexerData.IncludeNodeTypes.Any()) + { + //if there are specific node types then just index those + foreach (var nodeType in IndexerData.IncludeNodeTypes) + { + do + { + long total; + members = _memberService.GetAll(pageIndex, pageSize, out total, "LoginName" + , Direction.Ascending, true, nodeType).ToArray(); - AddNodesToIndex(GetSerializedMembers(members), type); + AddNodesToIndex(GetSerializedMembers(members), type); - pageIndex++; - } while (members.Length == pageSize); - } - } - else - { - //no node types specified, do all members - do - { - int total; - members = _memberService.GetAll(pageIndex, pageSize, out total).ToArray(); + pageIndex++; + } while (members.Length == pageSize); + } + } + else + { + //no node types specified, do all members + do + { + int total; + members = _memberService.GetAll(pageIndex, pageSize, out total).ToArray(); - AddNodesToIndex(GetSerializedMembers(members), type); + AddNodesToIndex(GetSerializedMembers(members), type); - pageIndex++; - } while (members.Length == pageSize); - } - } + pageIndex++; + } while (members.Length == pageSize); + } + } - private IEnumerable GetSerializedMembers(IEnumerable members) - { - var serializer = new EntityXmlSerializer(); - return members.Select(member => serializer.Serialize(_dataTypeService, member)); - } + private IEnumerable GetSerializedMembers(IEnumerable members) + { + var serializer = new EntityXmlSerializer(); + return members.Select(member => serializer.Serialize(_dataTypeService, member)); + } - protected override XDocument GetXDocument(string xPath, string type) - { - throw new NotSupportedException(); - } - - protected override Dictionary GetSpecialFieldsToIndex(Dictionary allValuesForIndexing) - { - var fields = base.GetSpecialFieldsToIndex(allValuesForIndexing); + protected override XDocument GetXDocument(string xPath, string type) + { + throw new NotSupportedException(); + } - //adds the special path property to the index - fields.Add("__key", allValuesForIndexing["__key"]); - - return fields; + protected override Dictionary GetSpecialFieldsToIndex(Dictionary allValuesForIndexing) + { + var fields = base.GetSpecialFieldsToIndex(allValuesForIndexing); - } + //adds the special path property to the index + fields.Add("__key", allValuesForIndexing["__key"]); - /// - /// Add the special __key and _searchEmail fields - /// - /// - protected override void OnGatheringNodeData(IndexingNodeDataEventArgs e) - { - base.OnGatheringNodeData(e); + return fields; - if (e.Node.Attribute("key") != null) - { - if (e.Fields.ContainsKey("__key") == false) - e.Fields.Add("__key", e.Node.Attribute("key").Value); - } + } - if (e.Node.Attribute("email") != null) - { - //NOTE: the single underscore = it's not a 'special' field which means it will be indexed normally - if (e.Fields.ContainsKey("_searchEmail") == false) - e.Fields.Add("_searchEmail", e.Node.Attribute("email").Value.Replace(".", " ").Replace("@", " ")); - } - - if (e.Fields.ContainsKey(IconFieldName) == false) - e.Fields.Add(IconFieldName, (string)e.Node.Attribute("icon")); - } + /// + /// Add the special __key and _searchEmail fields + /// + /// + protected override void OnGatheringNodeData(IndexingNodeDataEventArgs e) + { + base.OnGatheringNodeData(e); - private static XElement GetMemberItem(int nodeId) - { - //TODO: Change this so that it is not using the LegacyLibrary, just serialize manually! - var nodes = LegacyLibrary.GetMember(nodeId); - return XElement.Parse(nodes.Current.OuterXml); - } - } + if (e.Node.Attribute("key") != null) + { + if (e.Fields.ContainsKey("__key") == false) + e.Fields.Add("__key", e.Node.Attribute("key").Value); + } + + if (e.Node.Attribute("email") != null) + { + //NOTE: the single underscore = it's not a 'special' field which means it will be indexed normally + if (e.Fields.ContainsKey("_searchEmail") == false) + e.Fields.Add("_searchEmail", e.Node.Attribute("email").Value.Replace(".", " ").Replace("@", " ")); + } + + if (e.Fields.ContainsKey(IconFieldName) == false) + e.Fields.Add(IconFieldName, (string)e.Node.Attribute("icon")); + } + + private static XElement GetMemberItem(int nodeId) + { + //TODO: Change this so that it is not using the LegacyLibrary, just serialize manually! + var nodes = LegacyLibrary.GetMember(nodeId); + return XElement.Parse(nodes.Current.OuterXml); + } + } } From 54394f2ce9fd106994d5257be957938d67496b2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Ferreira?= Date: Wed, 2 Mar 2016 09:47:43 +0000 Subject: [PATCH 09/46] fixes --- .../components/umbtable.directive.js | 114 ++++++++--------- .../src/views/components/umb-table.html | 120 ++++++++---------- 2 files changed, 109 insertions(+), 125 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbtable.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbtable.directive.js index fe80d915da..8c74abf590 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbtable.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbtable.directive.js @@ -1,71 +1,71 @@ -(function() { - 'use strict'; +(function () { + 'use strict'; - function TableDirective() { + function TableDirective() { - function link(scope, el, attr, ctrl) { + function link(scope, el, attr, ctrl) { - scope.clickItem = function(item, $event) { - if(scope.onClick) { - scope.onClick(item); - $event.stopPropagation(); - } - }; + scope.clickItem = function (item, $event) { + if (scope.onClick) { + scope.onClick(item); + $event.stopPropagation(); + } + }; - scope.selectItem = function(item, $index, $event) { - if(scope.onSelect) { - scope.onSelect(item, $index, $event); - $event.stopPropagation(); - } - }; + scope.selectItem = function (item, $index, $event) { + if (scope.onSelect) { + scope.onSelect(item, $index, $event); + $event.stopPropagation(); + } + }; - scope.selectAll = function($event) { - if(scope.onSelectAll) { - scope.onSelectAll($event); - } - }; + scope.selectAll = function ($event) { + if (scope.onSelectAll) { + scope.onSelectAll($event); + } + }; - scope.isSelectedAll = function() { - if(scope.onSelectedAll && scope.items && scope.items.length > 0) { - return scope.onSelectedAll(); - } - }; + scope.isSelectedAll = function () { + if (scope.onSelectedAll && scope.items && scope.items.length > 0) { + return scope.onSelectedAll(); + } + }; - scope.isSortDirection = function (col, direction) { - if (scope.onSortingDirection) { - return scope.onSortingDirection(col, direction); - } - }; + scope.isSortDirection = function (col, direction) { + if (scope.onSortingDirection) { + return scope.onSortingDirection(col, direction); + } + }; - scope.sort = function(field, allow) { - if(scope.onSort) { - scope.onSort(field, allow); - } - }; + scope.sort = function (field, allow, isSystem) { + if (scope.onSort) { + scope.onSort(field, allow, isSystem); + } + }; - } + } - var directive = { - restrict: 'E', - replace: true, - templateUrl: 'views/components/umb-table.html', - scope: { - items: '=', - itemProperties: '=', - allowSelectAll: '=', - onSelect: '=', - onClick: '=', - onSelectAll: '=', - onSelectedAll: '=', - onSortingDirection: '=', - onSort: '=' - }, - link: link - }; + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/umb-table.html', + scope: { + items: '=', + itemProperties: '=', + allowSelectAll: '=', + onSelect: '=', + onClick: '=', + onSelectAll: '=', + onSelectedAll: '=', + onSortingDirection: '=', + onSort: '=' + }, + link: link + }; - return directive; - } + return directive; + } - angular.module('umbraco.directives').directive('umbTable', TableDirective); + angular.module('umbraco.directives').directive('umbTable', TableDirective); })(); diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-table.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-table.html index fdd7dd23c9..1ab22eb406 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-table.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-table.html @@ -1,76 +1,60 @@
- -
- - -
-
- -
- -
- - - - - -
-
- - -
-
+ +
+
+
+ +
+ + +
+
+ +
+
- - -
- - -
- -
- - -
- -
- {{item[column.alias]}} -
- -
-
+ ng-click="selectItem(item, $index, $event)"> +
+ +
- - - - There are no items show in the list. - - +
+ + +
+
+ {{item[column.alias]}} +
+
+
+
+ + + There are no items show in the list. +
From d560223ecadb85b7f69741a0159b14a7849fde02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Ferreira?= Date: Tue, 29 Mar 2016 20:13:53 +0100 Subject: [PATCH 10/46] Fixes to make it work --- src/Umbraco.Web/Editors/ContentController.cs | 29 +++++---- .../Filters/DisableBrowserCacheAttribute.cs | 64 ++++++++++--------- 2 files changed, 49 insertions(+), 44 deletions(-) diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index ff811ce4b2..217d3123b8 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -184,16 +184,17 @@ namespace Umbraco.Web.Editors int pageSize = 0, string orderBy = "SortOrder", Direction orderDirection = Direction.Ascending, - bool orderBySystemField = true, + int? orderBySystemField = 2, string filter = "") { + var orderBySystemFieldBool = orderBySystemField == 1 || orderBySystemField == 2; long totalChildren; IContent[] children; if (pageNumber > 0 && pageSize > 0) { children = Services.ContentService .GetPagedChildren(id, (pageNumber - 1), pageSize, out totalChildren - , orderBy, orderDirection, orderBySystemField, filter).ToArray(); + , orderBy, orderDirection, orderBySystemFieldBool, filter).ToArray(); } else { @@ -219,18 +220,18 @@ namespace Umbraco.Web.Editors return HasPermission(permissionToCheck, nodeId); } - /// - /// Returns permissions for all nodes passed in for the current user - /// - /// - /// - [HttpPost] - public Dictionary GetPermissions(int[] nodeIds) - { - return Services.UserService - .GetPermissions(Security.CurrentUser, nodeIds) - .ToDictionary(x => x.EntityId, x => x.AssignedPermissions); - } + /// + /// Returns permissions for all nodes passed in for the current user + /// + /// + /// + [HttpPost] + public Dictionary GetPermissions(int[] nodeIds) + { + return Services.UserService + .GetPermissions(Security.CurrentUser, nodeIds) + .ToDictionary(x => x.EntityId, x => x.AssignedPermissions); + } [HttpGet] public bool HasPermission(string permissionToCheck, int nodeId) diff --git a/src/Umbraco.Web/WebApi/Filters/DisableBrowserCacheAttribute.cs b/src/Umbraco.Web/WebApi/Filters/DisableBrowserCacheAttribute.cs index e1890326fb..92709916ac 100644 --- a/src/Umbraco.Web/WebApi/Filters/DisableBrowserCacheAttribute.cs +++ b/src/Umbraco.Web/WebApi/Filters/DisableBrowserCacheAttribute.cs @@ -13,38 +13,42 @@ using Umbraco.Core; namespace Umbraco.Web.WebApi.Filters { - /// - /// Ensures that the request is not cached by the browser - /// - public class DisableBrowserCacheAttribute : ActionFilterAttribute - { - public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext) - { - //See: http://stackoverflow.com/questions/17755239/how-to-stop-chrome-from-caching-rest-response-from-webapi + /// + /// Ensures that the request is not cached by the browser + /// + public class DisableBrowserCacheAttribute : ActionFilterAttribute + { + public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext) + { + //See: http://stackoverflow.com/questions/17755239/how-to-stop-chrome-from-caching-rest-response-from-webapi - base.OnActionExecuted(actionExecutedContext); + base.OnActionExecuted(actionExecutedContext); + if (actionExecutedContext == null || actionExecutedContext.Response == null || + actionExecutedContext.Response.Headers == null) + { + return; + } + //NOTE: Until we upgraded to WebApi 2, this didn't work correctly and we had to revert to using + // HttpContext.Current responses. I've changed this back to what it should be now since it works + // and now with WebApi2, the HttpContext.Current responses dont! Anyways, all good now. + actionExecutedContext.Response.Headers.CacheControl = new CacheControlHeaderValue() + { + NoCache = true, + NoStore = true, + MaxAge = new TimeSpan(0), + MustRevalidate = true + }; - //NOTE: Until we upgraded to WebApi 2, this didn't work correctly and we had to revert to using - // HttpContext.Current responses. I've changed this back to what it should be now since it works - // and now with WebApi2, the HttpContext.Current responses dont! Anyways, all good now. - actionExecutedContext.Response.Headers.CacheControl = new CacheControlHeaderValue() - { - NoCache = true, - NoStore = true, - MaxAge = new TimeSpan(0), - MustRevalidate = true - }; + actionExecutedContext.Response.Headers.Pragma.Add(new NameValueHeaderValue("no-cache")); + if (actionExecutedContext.Response.Content != null) + { + actionExecutedContext.Response.Content.Headers.Expires = + //Mon, 01 Jan 1990 00:00:00 GMT + new DateTimeOffset(1990, 1, 1, 0, 0, 0, TimeSpan.Zero); + } - actionExecutedContext.Response.Headers.Pragma.Add(new NameValueHeaderValue("no-cache")); - if (actionExecutedContext.Response.Content != null) - { - actionExecutedContext.Response.Content.Headers.Expires = - //Mon, 01 Jan 1990 00:00:00 GMT - new DateTimeOffset(1990, 1, 1, 0, 0, 0, TimeSpan.Zero); - } - - - } - } + + } + } } From 9376731533e90fbe591df196634dd2222fc19408 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Ferreira?= Date: Fri, 1 Apr 2016 11:24:30 +0100 Subject: [PATCH 11/46] Adhering to Umbraco's Coding Standards --- .../Repositories/ContentRepository.cs | 1906 +++---- .../Interfaces/IContentRepository.cs | 144 +- .../Interfaces/IMediaRepository.cs | 58 +- .../Interfaces/IMemberRepository.cs | 116 +- .../Repositories/MediaRepository.cs | 1142 ++-- .../Repositories/MemberRepository.cs | 1488 +++--- .../Repositories/VersionableRepositoryBase.cs | 974 ++-- .../SqlSyntax/ISqlSyntaxProvider.cs | 138 +- .../SqlSyntax/MySqlSyntaxProvider.cs | 650 +-- .../SqlSyntax/SqlSyntaxProviderBase.cs | 1062 ++-- src/Umbraco.Core/Services/ContentService.cs | 4634 ++++++++--------- src/Umbraco.Core/Services/IContentService.cs | 1150 ++-- src/Umbraco.Core/Services/IMediaService.cs | 646 +-- src/Umbraco.Core/Services/IMemberService.cs | 376 +- src/Umbraco.Core/Services/MediaService.cs | 2628 +++++----- src/Umbraco.Core/Services/MemberService.cs | 2584 ++++----- .../Repositories/ContentRepositoryTest.cs | 1582 +++--- .../Repositories/MediaRepositoryTest.cs | 1086 ++-- .../UmbracoExamine/IndexInitializer.cs | 208 +- .../components/umbtable.directive.js | 142 +- .../src/common/resources/content.resource.js | 1222 ++--- .../src/common/resources/media.resource.js | 890 ++-- .../src/common/resources/member.resource.js | 442 +- .../src/views/components/umb-table.html | 120 +- .../list/list.listviewlayout.controller.js | 152 +- .../listview/listview.controller.js | 1038 ++-- src/Umbraco.Web/Editors/ContentController.cs | 1530 +++--- src/Umbraco.Web/Editors/MediaController.cs | 1342 ++--- src/Umbraco.Web/Editors/MemberController.cs | 1276 ++--- .../Filters/DisableBrowserCacheAttribute.cs | 108 +- src/UmbracoExamine/UmbracoMemberIndexer.cs | 344 +- 31 files changed, 15589 insertions(+), 15589 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs index 30887281f1..3ef1df3bc5 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs @@ -26,957 +26,957 @@ using Umbraco.Core.Persistence.UnitOfWork; namespace Umbraco.Core.Persistence.Repositories { - /// - /// Represents a repository for doing CRUD operations for - /// - internal class ContentRepository : RecycleBinRepository, IContentRepository - { - private readonly IContentTypeRepository _contentTypeRepository; - private readonly ITemplateRepository _templateRepository; - private readonly ITagRepository _tagRepository; - private readonly CacheHelper _cacheHelper; - private readonly ContentPreviewRepository _contentPreviewRepository; - private readonly ContentXmlRepository _contentXmlRepository; - - public ContentRepository(IDatabaseUnitOfWork work, CacheHelper cacheHelper, ILogger logger, ISqlSyntaxProvider syntaxProvider, IContentTypeRepository contentTypeRepository, ITemplateRepository templateRepository, ITagRepository tagRepository, IContentSection contentSection) - : base(work, cacheHelper, logger, syntaxProvider, contentSection) - { - if (contentTypeRepository == null) throw new ArgumentNullException("contentTypeRepository"); - if (templateRepository == null) throw new ArgumentNullException("templateRepository"); - if (tagRepository == null) throw new ArgumentNullException("tagRepository"); - _contentTypeRepository = contentTypeRepository; - _templateRepository = templateRepository; - _tagRepository = tagRepository; - _cacheHelper = cacheHelper; - _contentPreviewRepository = new ContentPreviewRepository(work, CacheHelper.CreateDisabledCacheHelper(), logger, syntaxProvider); - _contentXmlRepository = new ContentXmlRepository(work, CacheHelper.CreateDisabledCacheHelper(), logger, syntaxProvider); - - EnsureUniqueNaming = true; - } - - public bool EnsureUniqueNaming { get; set; } - - #region Overrides of RepositoryBase - - protected override IContent PerformGet(int id) - { - var sql = GetBaseQuery(false) - .Where(GetBaseWhereClause(), new { Id = id }) - .Where(x => x.Newest) - .OrderByDescending(x => x.VersionDate); - - var dto = Database.Fetch(sql).FirstOrDefault(); - - if (dto == null) - return null; - - var content = CreateContentFromDto(dto, dto.ContentVersionDto.VersionId, sql); - - return content; - } - - protected override IEnumerable PerformGetAll(params int[] ids) - { - var sql = GetBaseQuery(false); - if (ids.Any()) - { - sql.Where("umbracoNode.id in (@ids)", new { ids = ids }); - } - - //we only want the newest ones with this method - sql.Where(x => x.Newest); - - return ProcessQuery(sql); - } - - protected override IEnumerable PerformGetByQuery(IQuery query) - { - var sqlClause = GetBaseQuery(false); - var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate() - .Where(x => x.Newest) - .OrderByDescending(x => x.VersionDate) - .OrderBy(x => x.SortOrder); - - return ProcessQuery(sql); - } - - #endregion - - #region Overrides of PetaPocoRepositoryBase - - protected override Sql GetBaseQuery(bool isCount) - { - var sqlx = string.Format("LEFT OUTER JOIN {0} {1} ON ({1}.{2}={0}.{2} AND {1}.{3}=1)", - SqlSyntax.GetQuotedTableName("cmsDocument"), - SqlSyntax.GetQuotedTableName("cmsDocument2"), - SqlSyntax.GetQuotedColumnName("nodeId"), - SqlSyntax.GetQuotedColumnName("published")); - - var sql = new Sql(); - sql.Select(isCount ? "COUNT(*)" : "*") - .From() - .InnerJoin() - .On(left => left.VersionId, right => right.VersionId) - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - - // cannot do this because PetaPoco does not know how to alias the table - //.LeftOuterJoin() - //.On(left => left.NodeId, right => right.NodeId) - // so have to rely on writing our own SQL - .Append(sqlx/*, new { @published = true }*/) - - .Where(x => x.NodeObjectType == NodeObjectTypeId); - return sql; - } - - protected override string GetBaseWhereClause() - { - return "umbracoNode.id = @Id"; - } - - protected override IEnumerable GetDeleteClauses() - { - var list = new List - { - "DELETE FROM cmsTask WHERE nodeId = @Id", - "DELETE FROM umbracoUser2NodeNotify WHERE nodeId = @Id", - "DELETE FROM umbracoUser2NodePermission WHERE nodeId = @Id", - "DELETE FROM umbracoRelation WHERE parentId = @Id", - "DELETE FROM umbracoRelation WHERE childId = @Id", - "DELETE FROM cmsTagRelationship WHERE nodeId = @Id", - "DELETE FROM umbracoDomains WHERE domainRootStructureID = @Id", - "DELETE FROM cmsDocument WHERE nodeId = @Id", - "DELETE FROM cmsPropertyData WHERE contentNodeId = @Id", - "DELETE FROM cmsPreviewXml WHERE nodeId = @Id", - "DELETE FROM cmsContentVersion WHERE ContentId = @Id", - "DELETE FROM cmsContentXml WHERE nodeId = @Id", - "DELETE FROM cmsContent WHERE nodeId = @Id", - "DELETE FROM umbracoAccess WHERE nodeId = @Id", - "DELETE FROM umbracoNode WHERE id = @Id" - }; - return list; - } - - protected override Guid NodeObjectTypeId - { - get { return new Guid(Constants.ObjectTypes.Document); } - } - - #endregion - - #region Overrides of VersionableRepositoryBase - - public void RebuildXmlStructures(Func serializer, int groupSize = 5000, IEnumerable contentTypeIds = null) - { - - //Ok, now we need to remove the data and re-insert it, we'll do this all in one transaction too. - using (var tr = Database.GetTransaction()) - { - //Remove all the data first, if anything fails after this it's no problem the transaction will be reverted - if (contentTypeIds == null) - { - var subQuery = new Sql() - .Select("DISTINCT cmsContentXml.nodeId") - .From() - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId); - - var deleteSql = SqlSyntax.GetDeleteSubquery("cmsContentXml", "nodeId", subQuery); - Database.Execute(deleteSql); - } - else - { - foreach (var id in contentTypeIds) - { - var id1 = id; - var subQuery = new Sql() - .Select("cmsDocument.nodeId") - .From() - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .Where(dto => dto.Published) - .Where(dto => dto.ContentTypeId == id1); - - var deleteSql = SqlSyntax.GetDeleteSubquery("cmsContentXml", "nodeId", subQuery); - Database.Execute(deleteSql); - } - } - - //now insert the data, again if something fails here, the whole transaction is reversed - if (contentTypeIds == null) - { - var query = Query.Builder.Where(x => x.Published == true); - RebuildXmlStructuresProcessQuery(serializer, query, tr, groupSize); - } - else - { - foreach (var contentTypeId in contentTypeIds) - { - //copy local - var id = contentTypeId; - var query = Query.Builder.Where(x => x.Published == true && x.ContentTypeId == id && x.Trashed == false); - RebuildXmlStructuresProcessQuery(serializer, query, tr, groupSize); - } - } - - tr.Complete(); - } - } - - private void RebuildXmlStructuresProcessQuery(Func serializer, IQuery query, Transaction tr, int pageSize) - { - var pageIndex = 0; - var total = long.MinValue; - var processed = 0; - do - { - //NOTE: This is an important call, we cannot simply make a call to: - // GetPagedResultsByQuery(query, pageIndex, pageSize, out total, "Path", Direction.Ascending); - // because that method is used to query 'latest' content items where in this case we don't necessarily - // want latest content items because a pulished content item might not actually be the latest. - // see: http://issues.umbraco.org/issue/U4-6322 & http://issues.umbraco.org/issue/U4-5982 - var descendants = GetPagedResultsByQuery(query, pageIndex, pageSize, out total, - new Tuple("cmsDocument", "nodeId"), - ProcessQuery, "Path", Direction.Ascending, orderBySystemField: true); - - var xmlItems = (from descendant in descendants - let xml = serializer(descendant) - select new ContentXmlDto { NodeId = descendant.Id, Xml = xml.ToDataString() }).ToArray(); - - //bulk insert it into the database - Database.BulkInsertRecords(xmlItems, tr); - - processed += xmlItems.Length; - - pageIndex++; - } while (processed < total); - } - - public override IContent GetByVersion(Guid versionId) - { - var sql = GetBaseQuery(false); - sql.Where("cmsContentVersion.VersionId = @VersionId", new { VersionId = versionId }); - sql.OrderByDescending(x => x.VersionDate); - - var dto = Database.Fetch(sql).FirstOrDefault(); - - if (dto == null) - return null; - - var content = CreateContentFromDto(dto, versionId, sql); - - return content; - } - - public override void DeleteVersion(Guid versionId) - { - var sql = new Sql() - .Select("*") - .From() - .InnerJoin().On(left => left.VersionId, right => right.VersionId) - .Where(x => x.VersionId == versionId) - .Where(x => x.Newest != true); - var dto = Database.Fetch(sql).FirstOrDefault(); - - if (dto == null) return; - - using (var transaction = Database.GetTransaction()) - { - PerformDeleteVersion(dto.NodeId, versionId); - - transaction.Complete(); - } - } - - public override void DeleteVersions(int id, DateTime versionDate) - { - var sql = new Sql() - .Select("*") - .From() - .InnerJoin().On(left => left.VersionId, right => right.VersionId) - .Where(x => x.NodeId == id) - .Where(x => x.VersionDate < versionDate) - .Where(x => x.Newest != true); - var list = Database.Fetch(sql); - if (list.Any() == false) return; - - using (var transaction = Database.GetTransaction()) - { - foreach (var dto in list) - { - PerformDeleteVersion(id, dto.VersionId); - } - - transaction.Complete(); - } - } - - protected override void PerformDeleteVersion(int id, Guid versionId) - { - Database.Delete("WHERE nodeId = @Id AND versionId = @VersionId", new { Id = id, VersionId = versionId }); - Database.Delete("WHERE contentNodeId = @Id AND versionId = @VersionId", new { Id = id, VersionId = versionId }); - Database.Delete("WHERE ContentId = @Id AND VersionId = @VersionId", new { Id = id, VersionId = versionId }); - Database.Delete("WHERE nodeId = @Id AND versionId = @VersionId", new { Id = id, VersionId = versionId }); - } - - #endregion - - #region Unit of Work Implementation - - protected override void PersistDeletedItem(IContent entity) - { - //We need to clear out all access rules but we need to do this in a manual way since - // nothing in that table is joined to a content id - var subQuery = new Sql() - .Select("umbracoAccessRule.accessId") - .From(SqlSyntax) - .InnerJoin(SqlSyntax) - .On(SqlSyntax, left => left.AccessId, right => right.Id) - .Where(dto => dto.NodeId == entity.Id); - Database.Execute(SqlSyntax.GetDeleteSubquery("umbracoAccessRule", "accessId", subQuery)); - - //now let the normal delete clauses take care of everything else - base.PersistDeletedItem(entity); - } - - protected override void PersistNewItem(IContent entity) - { - ((Content)entity).AddingEntity(); - - //ensure the default template is assigned - if (entity.Template == null) - { - entity.Template = entity.ContentType.DefaultTemplate; - } - - //Ensure unique name on the same level - entity.Name = EnsureUniqueNodeName(entity.ParentId, entity.Name); - - //Ensure that strings don't contain characters that are invalid in XML - entity.SanitizeEntityPropertiesForXmlStorage(); - - var factory = new ContentFactory(NodeObjectTypeId, entity.Id); - var dto = factory.BuildDto(entity); - - //NOTE Should the logic below have some kind of fallback for empty parent ids ? - //Logic for setting Path, Level and SortOrder - var parent = Database.First("WHERE id = @ParentId", new { ParentId = entity.ParentId }); - var level = parent.Level + 1; - var maxSortOrder = Database.ExecuteScalar( - "SELECT coalesce(max(sortOrder),-1) FROM umbracoNode WHERE parentid = @ParentId AND nodeObjectType = @NodeObjectType", - new { /*ParentId =*/ entity.ParentId, NodeObjectType = NodeObjectTypeId }); - var sortOrder = maxSortOrder + 1; - - //Create the (base) node data - umbracoNode - var nodeDto = dto.ContentVersionDto.ContentDto.NodeDto; - nodeDto.Path = parent.Path; - nodeDto.Level = short.Parse(level.ToString(CultureInfo.InvariantCulture)); - nodeDto.SortOrder = sortOrder; - var o = Database.IsNew(nodeDto) ? Convert.ToInt32(Database.Insert(nodeDto)) : Database.Update(nodeDto); - - //Update with new correct path - nodeDto.Path = string.Concat(parent.Path, ",", nodeDto.NodeId); - Database.Update(nodeDto); - - //Update entity with correct values - entity.Id = nodeDto.NodeId; //Set Id on entity to ensure an Id is set - entity.Path = nodeDto.Path; - entity.SortOrder = sortOrder; - entity.Level = level; - - //Assign the same permissions to it as the parent node - // http://issues.umbraco.org/issue/U4-2161 - var permissionsRepo = new PermissionRepository(UnitOfWork, _cacheHelper, SqlSyntax); - var parentPermissions = permissionsRepo.GetPermissionsForEntity(entity.ParentId).ToArray(); - //if there are parent permissions then assign them, otherwise leave null and permissions will become the - // user's default permissions. - if (parentPermissions.Any()) - { - var userPermissions = ( - from perm in parentPermissions - from p in perm.AssignedPermissions - select new EntityPermissionSet.UserPermission(perm.UserId, p)).ToList(); - - permissionsRepo.ReplaceEntityPermissions(new EntityPermissionSet(entity.Id, userPermissions)); - //flag the entity's permissions changed flag so we can track those changes. - //Currently only used for the cache refreshers to detect if we should refresh all user permissions cache. - ((Content)entity).PermissionsChanged = true; - } - - //Create the Content specific data - cmsContent - var contentDto = dto.ContentVersionDto.ContentDto; - contentDto.NodeId = nodeDto.NodeId; - Database.Insert(contentDto); - - //Create the first version - cmsContentVersion - //Assumes a new Version guid and Version date (modified date) has been set - var contentVersionDto = dto.ContentVersionDto; - contentVersionDto.NodeId = nodeDto.NodeId; - Database.Insert(contentVersionDto); - - //Create the Document specific data for this version - cmsDocument - //Assumes a new Version guid has been generated - dto.NodeId = nodeDto.NodeId; - Database.Insert(dto); - - //Create the PropertyData for this version - cmsPropertyData - var propertyFactory = new PropertyFactory(entity.ContentType.CompositionPropertyTypes.ToArray(), entity.Version, entity.Id); - var propertyDataDtos = propertyFactory.BuildDto(entity.Properties); - var keyDictionary = new Dictionary(); - - //Add Properties - foreach (var propertyDataDto in propertyDataDtos) - { - var primaryKey = Convert.ToInt32(Database.Insert(propertyDataDto)); - keyDictionary.Add(propertyDataDto.PropertyTypeId, primaryKey); - } - - //Update Properties with its newly set Id - foreach (var property in entity.Properties) - { - property.Id = keyDictionary[property.PropertyTypeId]; - } - - //lastly, check if we are a creating a published version , then update the tags table - if (entity.Published) - { - UpdatePropertyTags(entity, _tagRepository); - } - - // published => update published version infos, else leave it blank - if (entity.Published) - { - dto.DocumentPublishedReadOnlyDto = new DocumentPublishedReadOnlyDto - { - VersionId = dto.VersionId, - Newest = true, - NodeId = dto.NodeId, - Published = true - }; - ((Content)entity).PublishedVersionGuid = dto.VersionId; - } - - entity.ResetDirtyProperties(); - } - - protected override void PersistUpdatedItem(IContent entity) - { - var publishedState = ((Content)entity).PublishedState; - - //check if we need to make any database changes at all - if (entity.RequiresSaving(publishedState) == false) - { - entity.ResetDirtyProperties(); - return; - } - - //check if we need to create a new version - bool shouldCreateNewVersion = entity.ShouldCreateNewVersion(publishedState); - if (shouldCreateNewVersion) - { - //Updates Modified date and Version Guid - ((Content)entity).UpdatingEntity(); - } - else - { - entity.UpdateDate = DateTime.Now; - } - - //Ensure unique name on the same level - entity.Name = EnsureUniqueNodeName(entity.ParentId, entity.Name, entity.Id); - - //Ensure that strings don't contain characters that are invalid in XML - entity.SanitizeEntityPropertiesForXmlStorage(); - - //Look up parent to get and set the correct Path and update SortOrder if ParentId has changed - if (entity.IsPropertyDirty("ParentId")) - { - var parent = Database.First("WHERE id = @ParentId", new { ParentId = entity.ParentId }); - entity.Path = string.Concat(parent.Path, ",", entity.Id); - entity.Level = parent.Level + 1; - var maxSortOrder = - Database.ExecuteScalar( - "SELECT coalesce(max(sortOrder),0) FROM umbracoNode WHERE parentid = @ParentId AND nodeObjectType = @NodeObjectType", - new { ParentId = entity.ParentId, NodeObjectType = NodeObjectTypeId }); - entity.SortOrder = maxSortOrder + 1; - - //Question: If we move a node, should we update permissions to inherit from the new parent if the parent has permissions assigned? - // if we do that, then we'd need to propogate permissions all the way downward which might not be ideal for many people. - // Gonna just leave it as is for now, and not re-propogate permissions. - } - - var factory = new ContentFactory(NodeObjectTypeId, entity.Id); - //Look up Content entry to get Primary for updating the DTO - var contentDto = Database.SingleOrDefault("WHERE nodeId = @Id", new { Id = entity.Id }); - factory.SetPrimaryKey(contentDto.PrimaryKey); - var dto = factory.BuildDto(entity); - - //Updates the (base) node data - umbracoNode - var nodeDto = dto.ContentVersionDto.ContentDto.NodeDto; - var o = Database.Update(nodeDto); - - //Only update this DTO if the contentType has actually changed - if (contentDto.ContentTypeId != entity.ContentTypeId) - { - //Create the Content specific data - cmsContent - var newContentDto = dto.ContentVersionDto.ContentDto; - Database.Update(newContentDto); - } - - //a flag that we'll use later to create the tags in the tag db table - var publishedStateChanged = false; - - //If Published state has changed then previous versions should have their publish state reset. - //If state has been changed to unpublished the previous versions publish state should also be reset. - //if (((ICanBeDirty)entity).IsPropertyDirty("Published") && (entity.Published || publishedState == PublishedState.Unpublished)) - if (entity.ShouldClearPublishedFlagForPreviousVersions(publishedState, shouldCreateNewVersion)) - { - var publishedDocs = Database.Fetch("WHERE nodeId = @Id AND published = @IsPublished", new { Id = entity.Id, IsPublished = true }); - foreach (var doc in publishedDocs) - { - var docDto = doc; - docDto.Published = false; - Database.Update(docDto); - } - - //this is a newly published version so we'll update the tags table too (end of this method) - publishedStateChanged = true; - } - - //Look up (newest) entries by id in cmsDocument table to set newest = false - var documentDtos = Database.Fetch("WHERE nodeId = @Id AND newest = @IsNewest", new { Id = entity.Id, IsNewest = true }); - foreach (var documentDto in documentDtos) - { - var docDto = documentDto; - docDto.Newest = false; - Database.Update(docDto); - } - - var contentVersionDto = dto.ContentVersionDto; - if (shouldCreateNewVersion) - { - //Create a new version - cmsContentVersion - //Assumes a new Version guid and Version date (modified date) has been set - Database.Insert(contentVersionDto); - //Create the Document specific data for this version - cmsDocument - //Assumes a new Version guid has been generated - Database.Insert(dto); - } - else - { - //In order to update the ContentVersion we need to retrieve its primary key id - var contentVerDto = Database.SingleOrDefault("WHERE VersionId = @Version", new { Version = entity.Version }); - contentVersionDto.Id = contentVerDto.Id; - - Database.Update(contentVersionDto); - Database.Update(dto); - } - - //Create the PropertyData for this version - cmsPropertyData - var propertyFactory = new PropertyFactory(entity.ContentType.CompositionPropertyTypes.ToArray(), entity.Version, entity.Id); - var propertyDataDtos = propertyFactory.BuildDto(entity.Properties); - var keyDictionary = new Dictionary(); - - //Add Properties - foreach (var propertyDataDto in propertyDataDtos) - { - if (shouldCreateNewVersion == false && propertyDataDto.Id > 0) - { - Database.Update(propertyDataDto); - } - else - { - int primaryKey = Convert.ToInt32(Database.Insert(propertyDataDto)); - keyDictionary.Add(propertyDataDto.PropertyTypeId, primaryKey); - } - } - - //Update Properties with its newly set Id - if (keyDictionary.Any()) - { - foreach (var property in entity.Properties) - { - if (keyDictionary.ContainsKey(property.PropertyTypeId) == false) continue; - - property.Id = keyDictionary[property.PropertyTypeId]; - } - } - - //lastly, check if we are a newly published version and then update the tags table - if (publishedStateChanged && entity.Published) - { - UpdatePropertyTags(entity, _tagRepository); - } - else if (publishedStateChanged && (entity.Trashed || entity.Published == false)) - { - //it's in the trash or not published remove all entity tags - ClearEntityTags(entity, _tagRepository); - } - - // published => update published version infos, - // else if unpublished then clear published version infos - if (entity.Published) - { - dto.DocumentPublishedReadOnlyDto = new DocumentPublishedReadOnlyDto - { - VersionId = dto.VersionId, - Newest = true, - NodeId = dto.NodeId, - Published = true - }; - ((Content)entity).PublishedVersionGuid = dto.VersionId; - } - else if (publishedStateChanged) - { - dto.DocumentPublishedReadOnlyDto = new DocumentPublishedReadOnlyDto - { - VersionId = default(Guid), - Newest = false, - NodeId = dto.NodeId, - Published = false - }; - ((Content)entity).PublishedVersionGuid = default(Guid); - } - - entity.ResetDirtyProperties(); - } - - - #endregion - - #region Implementation of IContentRepository - - public IEnumerable GetByPublishedVersion(IQuery query) - { - // we WANT to return contents in top-down order, ie parents should come before children - // ideal would be pure xml "document order" which can be achieved with: - // ORDER BY substring(path, 1, len(path) - charindex(',', reverse(path))), sortOrder - // but that's probably an overkill - sorting by level,sortOrder should be enough - - var sqlClause = GetBaseQuery(false); - var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate() - .Where(x => x.Published) - .OrderBy(x => x.Level, SqlSyntax) - .OrderBy(x => x.SortOrder, SqlSyntax); - - //NOTE: This doesn't allow properties to be part of the query - var dtos = Database.Fetch(sql); - - foreach (var dto in dtos) - { - //Check in the cache first. If it exists there AND it is published - // then we can use that entity. Otherwise if it is not published (which can be the case - // because we only store the 'latest' entries in the cache which might not be the published - // version) - var fromCache = RuntimeCache.GetCacheItem(GetCacheIdKey(dto.NodeId)); - //var fromCache = TryGetFromCache(dto.NodeId); - if (fromCache != null && fromCache.Published) - { - yield return fromCache; - } - else - { - yield return CreateContentFromDto(dto, dto.VersionId, sql); - } - } - } - - public int CountPublished() - { - var sql = GetBaseQuery(true).Where(x => x.Trashed == false) - .Where(x => x.Published == true); - return Database.ExecuteScalar(sql); - } - - public void ReplaceContentPermissions(EntityPermissionSet permissionSet) - { - var repo = new PermissionRepository(UnitOfWork, _cacheHelper, SqlSyntax); - repo.ReplaceEntityPermissions(permissionSet); - } - - public void ClearPublished(IContent content) - { - // race cond! - var documentDtos = Database.Fetch("WHERE nodeId=@id AND published=@published", new { id = content.Id, published = true }); - foreach (var documentDto in documentDtos) - { - documentDto.Published = false; - Database.Update(documentDto); - } - } - - /// - /// Assigns a single permission to the current content item for the specified user ids - /// - /// - /// - /// - public void AssignEntityPermission(IContent entity, char permission, IEnumerable userIds) - { - var repo = new PermissionRepository(UnitOfWork, _cacheHelper, SqlSyntax); - repo.AssignEntityPermission(entity, permission, userIds); - } - - public IEnumerable GetPermissionsForEntity(int entityId) - { - var repo = new PermissionRepository(UnitOfWork, _cacheHelper, SqlSyntax); - return repo.GetPermissionsForEntity(entityId); - } - - /// - /// Adds/updates content/published xml - /// - /// - /// - public void AddOrUpdateContentXml(IContent content, Func xml) - { - _contentXmlRepository.AddOrUpdate(new ContentXmlEntity(content, xml)); - } - - /// - /// Used to remove the content xml for a content item - /// - /// - public void DeleteContentXml(IContent content) - { - _contentXmlRepository.Delete(new ContentXmlEntity(content)); - } - - /// - /// Adds/updates preview xml - /// - /// - /// - public void AddOrUpdatePreviewXml(IContent content, Func xml) - { - _contentPreviewRepository.AddOrUpdate(new ContentPreviewEntity(content, xml)); - } - - /// - /// Gets paged content results - /// - /// Query to excute - /// Page number - /// Page size - /// Total records query would return without paging - /// Field to order by - /// Direction to order by - /// Flag to indicate when ordering by system field - /// Search text filter - /// An Enumerable list of objects - public IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, - string orderBy, Direction orderDirection, bool orderBySystemField, string filter = "") - { - - //NOTE: This uses the GetBaseQuery method but that does not take into account the required 'newest' field which is - // what we always require for a paged result, so we'll ensure it's included in the filter - - var args = new List(); - var sbWhere = new StringBuilder("AND (cmsDocument.newest = 1)"); - - if (filter.IsNullOrWhiteSpace() == false) - { - sbWhere.Append(" AND (cmsDocument." + SqlSyntax.GetQuotedColumnName("text") + " LIKE @" + args.Count + ")"); - args.Add("%" + filter + "%"); - } - - Func> filterCallback = () => new Tuple(sbWhere.ToString(), args.ToArray()); - - return GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, - new Tuple("cmsDocument", "nodeId"), - ProcessQuery, orderBy, orderDirection, orderBySystemField, - filterCallback); - - } - - #endregion - - #region IRecycleBinRepository members - - protected override int RecycleBinId - { - get { return Constants.System.RecycleBinContent; } - } - - #endregion - - protected override string GetDatabaseFieldNameForOrderBy(string orderBy) - { - //Some custom ones - switch (orderBy.ToUpperInvariant()) - { - case "NAME": - return "cmsDocument.text"; - case "UPDATER": - //TODO: This isn't going to work very nicely because it's going to order by ID, not by letter - return "cmsDocument.documentUser"; - } - - return base.GetDatabaseFieldNameForOrderBy(orderBy); - } - - private IEnumerable ProcessQuery(Sql sql) - { - //NOTE: This doesn't allow properties to be part of the query - var dtos = Database.Fetch(sql); - - //nothing found - if (dtos.Any() == false) return Enumerable.Empty(); - - //content types - //NOTE: This should be ok for an SQL 'IN' statement, there shouldn't be an insane amount of content types - var contentTypes = _contentTypeRepository.GetAll(dtos.Select(x => x.ContentVersionDto.ContentDto.ContentTypeId).ToArray()) - .ToArray(); - - - var ids = dtos - .Where(dto => dto.TemplateId.HasValue && dto.TemplateId.Value > 0) - .Select(x => x.TemplateId.Value).ToArray(); - - //NOTE: This should be ok for an SQL 'IN' statement, there shouldn't be an insane amount of content types - var templates = ids.Length == 0 ? Enumerable.Empty() : _templateRepository.GetAll(ids).ToArray(); - - var dtosWithContentTypes = dtos - //This select into and null check are required because we don't have a foreign damn key on the contentType column - // http://issues.umbraco.org/issue/U4-5503 - .Select(x => new { dto = x, contentType = contentTypes.FirstOrDefault(ct => ct.Id == x.ContentVersionDto.ContentDto.ContentTypeId) }) - .Where(x => x.contentType != null) - .ToArray(); - - //Go get the property data for each document - var docDefs = dtosWithContentTypes.Select(d => new DocumentDefinition( - d.dto.NodeId, - d.dto.VersionId, - d.dto.ContentVersionDto.VersionDate, - d.dto.ContentVersionDto.ContentDto.NodeDto.CreateDate, - d.contentType)); - - var propertyData = GetPropertyCollection(sql, docDefs); - - return dtosWithContentTypes.Select(d => CreateContentFromDto( - d.dto, - contentTypes.First(ct => ct.Id == d.dto.ContentVersionDto.ContentDto.ContentTypeId), - templates.FirstOrDefault(tem => tem.Id == (d.dto.TemplateId.HasValue ? d.dto.TemplateId.Value : -1)), - propertyData[d.dto.NodeId])); - } - - /// - /// Private method to create a content object from a DocumentDto, which is used by Get and GetByVersion. - /// - /// - /// - /// - /// - /// - private IContent CreateContentFromDto(DocumentDto dto, - IContentType contentType, - ITemplate template, - Models.PropertyCollection propCollection) - { - var factory = new ContentFactory(contentType, NodeObjectTypeId, dto.NodeId); - var content = factory.BuildEntity(dto); - - //Check if template id is set on DocumentDto, and get ITemplate if it is. - if (dto.TemplateId.HasValue && dto.TemplateId.Value > 0) - { - content.Template = template ?? _templateRepository.Get(dto.TemplateId.Value); - } - else - { - //ensure there isn't one set. - content.Template = null; - } - - content.Properties = propCollection; - - //on initial construction we don't want to have dirty properties tracked - // http://issues.umbraco.org/issue/U4-1946 - ((Entity)content).ResetDirtyProperties(false); - return content; - } - - /// - /// Private method to create a content object from a DocumentDto, which is used by Get and GetByVersion. - /// - /// - /// - /// - /// - private IContent CreateContentFromDto(DocumentDto dto, Guid versionId, Sql docSql) - { - var contentType = _contentTypeRepository.Get(dto.ContentVersionDto.ContentDto.ContentTypeId); - - var factory = new ContentFactory(contentType, NodeObjectTypeId, dto.NodeId); - var content = factory.BuildEntity(dto); - - //Check if template id is set on DocumentDto, and get ITemplate if it is. - if (dto.TemplateId.HasValue && dto.TemplateId.Value > 0) - { - content.Template = _templateRepository.Get(dto.TemplateId.Value); - } - - var docDef = new DocumentDefinition(dto.NodeId, versionId, content.UpdateDate, content.CreateDate, contentType); - - var properties = GetPropertyCollection(docSql, new[] { docDef }); - - content.Properties = properties[dto.NodeId]; - - //on initial construction we don't want to have dirty properties tracked - // http://issues.umbraco.org/issue/U4-1946 - ((Entity)content).ResetDirtyProperties(false); - return content; - } - - private string EnsureUniqueNodeName(int parentId, string nodeName, int id = 0) - { - if (EnsureUniqueNaming == false) - return nodeName; - - var sql = new Sql(); - sql.Select("*") - .From() - .Where(x => x.NodeObjectType == NodeObjectTypeId && x.ParentId == parentId && 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; - } - - /// - /// Dispose disposable properties - /// - /// - /// Ensure the unit of work is disposed - /// - protected override void DisposeResources() - { - _contentTypeRepository.Dispose(); - _templateRepository.Dispose(); - _tagRepository.Dispose(); - _contentPreviewRepository.Dispose(); - _contentXmlRepository.Dispose(); - } - } + /// + /// Represents a repository for doing CRUD operations for + /// + internal class ContentRepository : RecycleBinRepository, IContentRepository + { + private readonly IContentTypeRepository _contentTypeRepository; + private readonly ITemplateRepository _templateRepository; + private readonly ITagRepository _tagRepository; + private readonly CacheHelper _cacheHelper; + private readonly ContentPreviewRepository _contentPreviewRepository; + private readonly ContentXmlRepository _contentXmlRepository; + + public ContentRepository(IDatabaseUnitOfWork work, CacheHelper cacheHelper, ILogger logger, ISqlSyntaxProvider syntaxProvider, IContentTypeRepository contentTypeRepository, ITemplateRepository templateRepository, ITagRepository tagRepository, IContentSection contentSection) + : base(work, cacheHelper, logger, syntaxProvider, contentSection) + { + if (contentTypeRepository == null) throw new ArgumentNullException("contentTypeRepository"); + if (templateRepository == null) throw new ArgumentNullException("templateRepository"); + if (tagRepository == null) throw new ArgumentNullException("tagRepository"); + _contentTypeRepository = contentTypeRepository; + _templateRepository = templateRepository; + _tagRepository = tagRepository; + _cacheHelper = cacheHelper; + _contentPreviewRepository = new ContentPreviewRepository(work, CacheHelper.CreateDisabledCacheHelper(), logger, syntaxProvider); + _contentXmlRepository = new ContentXmlRepository(work, CacheHelper.CreateDisabledCacheHelper(), logger, syntaxProvider); + + EnsureUniqueNaming = true; + } + + public bool EnsureUniqueNaming { get; set; } + + #region Overrides of RepositoryBase + + protected override IContent PerformGet(int id) + { + var sql = GetBaseQuery(false) + .Where(GetBaseWhereClause(), new { Id = id }) + .Where(x => x.Newest) + .OrderByDescending(x => x.VersionDate); + + var dto = Database.Fetch(sql).FirstOrDefault(); + + if (dto == null) + return null; + + var content = CreateContentFromDto(dto, dto.ContentVersionDto.VersionId, sql); + + return content; + } + + protected override IEnumerable PerformGetAll(params int[] ids) + { + var sql = GetBaseQuery(false); + if (ids.Any()) + { + sql.Where("umbracoNode.id in (@ids)", new { ids = ids }); + } + + //we only want the newest ones with this method + sql.Where(x => x.Newest); + + return ProcessQuery(sql); + } + + protected override IEnumerable PerformGetByQuery(IQuery query) + { + var sqlClause = GetBaseQuery(false); + var translator = new SqlTranslator(sqlClause, query); + var sql = translator.Translate() + .Where(x => x.Newest) + .OrderByDescending(x => x.VersionDate) + .OrderBy(x => x.SortOrder); + + return ProcessQuery(sql); + } + + #endregion + + #region Overrides of PetaPocoRepositoryBase + + protected override Sql GetBaseQuery(bool isCount) + { + var sqlx = string.Format("LEFT OUTER JOIN {0} {1} ON ({1}.{2}={0}.{2} AND {1}.{3}=1)", + SqlSyntax.GetQuotedTableName("cmsDocument"), + SqlSyntax.GetQuotedTableName("cmsDocument2"), + SqlSyntax.GetQuotedColumnName("nodeId"), + SqlSyntax.GetQuotedColumnName("published")); + + var sql = new Sql(); + sql.Select(isCount ? "COUNT(*)" : "*") + .From() + .InnerJoin() + .On(left => left.VersionId, right => right.VersionId) + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + + // cannot do this because PetaPoco does not know how to alias the table + //.LeftOuterJoin() + //.On(left => left.NodeId, right => right.NodeId) + // so have to rely on writing our own SQL + .Append(sqlx/*, new { @published = true }*/) + + .Where(x => x.NodeObjectType == NodeObjectTypeId); + return sql; + } + + protected override string GetBaseWhereClause() + { + return "umbracoNode.id = @Id"; + } + + protected override IEnumerable GetDeleteClauses() + { + var list = new List + { + "DELETE FROM cmsTask WHERE nodeId = @Id", + "DELETE FROM umbracoUser2NodeNotify WHERE nodeId = @Id", + "DELETE FROM umbracoUser2NodePermission WHERE nodeId = @Id", + "DELETE FROM umbracoRelation WHERE parentId = @Id", + "DELETE FROM umbracoRelation WHERE childId = @Id", + "DELETE FROM cmsTagRelationship WHERE nodeId = @Id", + "DELETE FROM umbracoDomains WHERE domainRootStructureID = @Id", + "DELETE FROM cmsDocument WHERE nodeId = @Id", + "DELETE FROM cmsPropertyData WHERE contentNodeId = @Id", + "DELETE FROM cmsPreviewXml WHERE nodeId = @Id", + "DELETE FROM cmsContentVersion WHERE ContentId = @Id", + "DELETE FROM cmsContentXml WHERE nodeId = @Id", + "DELETE FROM cmsContent WHERE nodeId = @Id", + "DELETE FROM umbracoAccess WHERE nodeId = @Id", + "DELETE FROM umbracoNode WHERE id = @Id" + }; + return list; + } + + protected override Guid NodeObjectTypeId + { + get { return new Guid(Constants.ObjectTypes.Document); } + } + + #endregion + + #region Overrides of VersionableRepositoryBase + + public void RebuildXmlStructures(Func serializer, int groupSize = 5000, IEnumerable contentTypeIds = null) + { + + //Ok, now we need to remove the data and re-insert it, we'll do this all in one transaction too. + using (var tr = Database.GetTransaction()) + { + //Remove all the data first, if anything fails after this it's no problem the transaction will be reverted + if (contentTypeIds == null) + { + var subQuery = new Sql() + .Select("DISTINCT cmsContentXml.nodeId") + .From() + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId); + + var deleteSql = SqlSyntax.GetDeleteSubquery("cmsContentXml", "nodeId", subQuery); + Database.Execute(deleteSql); + } + else + { + foreach (var id in contentTypeIds) + { + var id1 = id; + var subQuery = new Sql() + .Select("cmsDocument.nodeId") + .From() + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .Where(dto => dto.Published) + .Where(dto => dto.ContentTypeId == id1); + + var deleteSql = SqlSyntax.GetDeleteSubquery("cmsContentXml", "nodeId", subQuery); + Database.Execute(deleteSql); + } + } + + //now insert the data, again if something fails here, the whole transaction is reversed + if (contentTypeIds == null) + { + var query = Query.Builder.Where(x => x.Published == true); + RebuildXmlStructuresProcessQuery(serializer, query, tr, groupSize); + } + else + { + foreach (var contentTypeId in contentTypeIds) + { + //copy local + var id = contentTypeId; + var query = Query.Builder.Where(x => x.Published == true && x.ContentTypeId == id && x.Trashed == false); + RebuildXmlStructuresProcessQuery(serializer, query, tr, groupSize); + } + } + + tr.Complete(); + } + } + + private void RebuildXmlStructuresProcessQuery(Func serializer, IQuery query, Transaction tr, int pageSize) + { + var pageIndex = 0; + var total = long.MinValue; + var processed = 0; + do + { + //NOTE: This is an important call, we cannot simply make a call to: + // GetPagedResultsByQuery(query, pageIndex, pageSize, out total, "Path", Direction.Ascending); + // because that method is used to query 'latest' content items where in this case we don't necessarily + // want latest content items because a pulished content item might not actually be the latest. + // see: http://issues.umbraco.org/issue/U4-6322 & http://issues.umbraco.org/issue/U4-5982 + var descendants = GetPagedResultsByQuery(query, pageIndex, pageSize, out total, + new Tuple("cmsDocument", "nodeId"), + ProcessQuery, "Path", Direction.Ascending, orderBySystemField: true); + + var xmlItems = (from descendant in descendants + let xml = serializer(descendant) + select new ContentXmlDto { NodeId = descendant.Id, Xml = xml.ToDataString() }).ToArray(); + + //bulk insert it into the database + Database.BulkInsertRecords(xmlItems, tr); + + processed += xmlItems.Length; + + pageIndex++; + } while (processed < total); + } + + public override IContent GetByVersion(Guid versionId) + { + var sql = GetBaseQuery(false); + sql.Where("cmsContentVersion.VersionId = @VersionId", new { VersionId = versionId }); + sql.OrderByDescending(x => x.VersionDate); + + var dto = Database.Fetch(sql).FirstOrDefault(); + + if (dto == null) + return null; + + var content = CreateContentFromDto(dto, versionId, sql); + + return content; + } + + public override void DeleteVersion(Guid versionId) + { + var sql = new Sql() + .Select("*") + .From() + .InnerJoin().On(left => left.VersionId, right => right.VersionId) + .Where(x => x.VersionId == versionId) + .Where(x => x.Newest != true); + var dto = Database.Fetch(sql).FirstOrDefault(); + + if (dto == null) return; + + using (var transaction = Database.GetTransaction()) + { + PerformDeleteVersion(dto.NodeId, versionId); + + transaction.Complete(); + } + } + + public override void DeleteVersions(int id, DateTime versionDate) + { + var sql = new Sql() + .Select("*") + .From() + .InnerJoin().On(left => left.VersionId, right => right.VersionId) + .Where(x => x.NodeId == id) + .Where(x => x.VersionDate < versionDate) + .Where(x => x.Newest != true); + var list = Database.Fetch(sql); + if (list.Any() == false) return; + + using (var transaction = Database.GetTransaction()) + { + foreach (var dto in list) + { + PerformDeleteVersion(id, dto.VersionId); + } + + transaction.Complete(); + } + } + + protected override void PerformDeleteVersion(int id, Guid versionId) + { + Database.Delete("WHERE nodeId = @Id AND versionId = @VersionId", new { Id = id, VersionId = versionId }); + Database.Delete("WHERE contentNodeId = @Id AND versionId = @VersionId", new { Id = id, VersionId = versionId }); + Database.Delete("WHERE ContentId = @Id AND VersionId = @VersionId", new { Id = id, VersionId = versionId }); + Database.Delete("WHERE nodeId = @Id AND versionId = @VersionId", new { Id = id, VersionId = versionId }); + } + + #endregion + + #region Unit of Work Implementation + + protected override void PersistDeletedItem(IContent entity) + { + //We need to clear out all access rules but we need to do this in a manual way since + // nothing in that table is joined to a content id + var subQuery = new Sql() + .Select("umbracoAccessRule.accessId") + .From(SqlSyntax) + .InnerJoin(SqlSyntax) + .On(SqlSyntax, left => left.AccessId, right => right.Id) + .Where(dto => dto.NodeId == entity.Id); + Database.Execute(SqlSyntax.GetDeleteSubquery("umbracoAccessRule", "accessId", subQuery)); + + //now let the normal delete clauses take care of everything else + base.PersistDeletedItem(entity); + } + + protected override void PersistNewItem(IContent entity) + { + ((Content)entity).AddingEntity(); + + //ensure the default template is assigned + if (entity.Template == null) + { + entity.Template = entity.ContentType.DefaultTemplate; + } + + //Ensure unique name on the same level + entity.Name = EnsureUniqueNodeName(entity.ParentId, entity.Name); + + //Ensure that strings don't contain characters that are invalid in XML + entity.SanitizeEntityPropertiesForXmlStorage(); + + var factory = new ContentFactory(NodeObjectTypeId, entity.Id); + var dto = factory.BuildDto(entity); + + //NOTE Should the logic below have some kind of fallback for empty parent ids ? + //Logic for setting Path, Level and SortOrder + var parent = Database.First("WHERE id = @ParentId", new { ParentId = entity.ParentId }); + var level = parent.Level + 1; + var maxSortOrder = Database.ExecuteScalar( + "SELECT coalesce(max(sortOrder),-1) FROM umbracoNode WHERE parentid = @ParentId AND nodeObjectType = @NodeObjectType", + new { /*ParentId =*/ entity.ParentId, NodeObjectType = NodeObjectTypeId }); + var sortOrder = maxSortOrder + 1; + + //Create the (base) node data - umbracoNode + var nodeDto = dto.ContentVersionDto.ContentDto.NodeDto; + nodeDto.Path = parent.Path; + nodeDto.Level = short.Parse(level.ToString(CultureInfo.InvariantCulture)); + nodeDto.SortOrder = sortOrder; + var o = Database.IsNew(nodeDto) ? Convert.ToInt32(Database.Insert(nodeDto)) : Database.Update(nodeDto); + + //Update with new correct path + nodeDto.Path = string.Concat(parent.Path, ",", nodeDto.NodeId); + Database.Update(nodeDto); + + //Update entity with correct values + entity.Id = nodeDto.NodeId; //Set Id on entity to ensure an Id is set + entity.Path = nodeDto.Path; + entity.SortOrder = sortOrder; + entity.Level = level; + + //Assign the same permissions to it as the parent node + // http://issues.umbraco.org/issue/U4-2161 + var permissionsRepo = new PermissionRepository(UnitOfWork, _cacheHelper, SqlSyntax); + var parentPermissions = permissionsRepo.GetPermissionsForEntity(entity.ParentId).ToArray(); + //if there are parent permissions then assign them, otherwise leave null and permissions will become the + // user's default permissions. + if (parentPermissions.Any()) + { + var userPermissions = ( + from perm in parentPermissions + from p in perm.AssignedPermissions + select new EntityPermissionSet.UserPermission(perm.UserId, p)).ToList(); + + permissionsRepo.ReplaceEntityPermissions(new EntityPermissionSet(entity.Id, userPermissions)); + //flag the entity's permissions changed flag so we can track those changes. + //Currently only used for the cache refreshers to detect if we should refresh all user permissions cache. + ((Content)entity).PermissionsChanged = true; + } + + //Create the Content specific data - cmsContent + var contentDto = dto.ContentVersionDto.ContentDto; + contentDto.NodeId = nodeDto.NodeId; + Database.Insert(contentDto); + + //Create the first version - cmsContentVersion + //Assumes a new Version guid and Version date (modified date) has been set + var contentVersionDto = dto.ContentVersionDto; + contentVersionDto.NodeId = nodeDto.NodeId; + Database.Insert(contentVersionDto); + + //Create the Document specific data for this version - cmsDocument + //Assumes a new Version guid has been generated + dto.NodeId = nodeDto.NodeId; + Database.Insert(dto); + + //Create the PropertyData for this version - cmsPropertyData + var propertyFactory = new PropertyFactory(entity.ContentType.CompositionPropertyTypes.ToArray(), entity.Version, entity.Id); + var propertyDataDtos = propertyFactory.BuildDto(entity.Properties); + var keyDictionary = new Dictionary(); + + //Add Properties + foreach (var propertyDataDto in propertyDataDtos) + { + var primaryKey = Convert.ToInt32(Database.Insert(propertyDataDto)); + keyDictionary.Add(propertyDataDto.PropertyTypeId, primaryKey); + } + + //Update Properties with its newly set Id + foreach (var property in entity.Properties) + { + property.Id = keyDictionary[property.PropertyTypeId]; + } + + //lastly, check if we are a creating a published version , then update the tags table + if (entity.Published) + { + UpdatePropertyTags(entity, _tagRepository); + } + + // published => update published version infos, else leave it blank + if (entity.Published) + { + dto.DocumentPublishedReadOnlyDto = new DocumentPublishedReadOnlyDto + { + VersionId = dto.VersionId, + Newest = true, + NodeId = dto.NodeId, + Published = true + }; + ((Content)entity).PublishedVersionGuid = dto.VersionId; + } + + entity.ResetDirtyProperties(); + } + + protected override void PersistUpdatedItem(IContent entity) + { + var publishedState = ((Content)entity).PublishedState; + + //check if we need to make any database changes at all + if (entity.RequiresSaving(publishedState) == false) + { + entity.ResetDirtyProperties(); + return; + } + + //check if we need to create a new version + bool shouldCreateNewVersion = entity.ShouldCreateNewVersion(publishedState); + if (shouldCreateNewVersion) + { + //Updates Modified date and Version Guid + ((Content)entity).UpdatingEntity(); + } + else + { + entity.UpdateDate = DateTime.Now; + } + + //Ensure unique name on the same level + entity.Name = EnsureUniqueNodeName(entity.ParentId, entity.Name, entity.Id); + + //Ensure that strings don't contain characters that are invalid in XML + entity.SanitizeEntityPropertiesForXmlStorage(); + + //Look up parent to get and set the correct Path and update SortOrder if ParentId has changed + if (entity.IsPropertyDirty("ParentId")) + { + var parent = Database.First("WHERE id = @ParentId", new { ParentId = entity.ParentId }); + entity.Path = string.Concat(parent.Path, ",", entity.Id); + entity.Level = parent.Level + 1; + var maxSortOrder = + Database.ExecuteScalar( + "SELECT coalesce(max(sortOrder),0) FROM umbracoNode WHERE parentid = @ParentId AND nodeObjectType = @NodeObjectType", + new { ParentId = entity.ParentId, NodeObjectType = NodeObjectTypeId }); + entity.SortOrder = maxSortOrder + 1; + + //Question: If we move a node, should we update permissions to inherit from the new parent if the parent has permissions assigned? + // if we do that, then we'd need to propogate permissions all the way downward which might not be ideal for many people. + // Gonna just leave it as is for now, and not re-propogate permissions. + } + + var factory = new ContentFactory(NodeObjectTypeId, entity.Id); + //Look up Content entry to get Primary for updating the DTO + var contentDto = Database.SingleOrDefault("WHERE nodeId = @Id", new { Id = entity.Id }); + factory.SetPrimaryKey(contentDto.PrimaryKey); + var dto = factory.BuildDto(entity); + + //Updates the (base) node data - umbracoNode + var nodeDto = dto.ContentVersionDto.ContentDto.NodeDto; + var o = Database.Update(nodeDto); + + //Only update this DTO if the contentType has actually changed + if (contentDto.ContentTypeId != entity.ContentTypeId) + { + //Create the Content specific data - cmsContent + var newContentDto = dto.ContentVersionDto.ContentDto; + Database.Update(newContentDto); + } + + //a flag that we'll use later to create the tags in the tag db table + var publishedStateChanged = false; + + //If Published state has changed then previous versions should have their publish state reset. + //If state has been changed to unpublished the previous versions publish state should also be reset. + //if (((ICanBeDirty)entity).IsPropertyDirty("Published") && (entity.Published || publishedState == PublishedState.Unpublished)) + if (entity.ShouldClearPublishedFlagForPreviousVersions(publishedState, shouldCreateNewVersion)) + { + var publishedDocs = Database.Fetch("WHERE nodeId = @Id AND published = @IsPublished", new { Id = entity.Id, IsPublished = true }); + foreach (var doc in publishedDocs) + { + var docDto = doc; + docDto.Published = false; + Database.Update(docDto); + } + + //this is a newly published version so we'll update the tags table too (end of this method) + publishedStateChanged = true; + } + + //Look up (newest) entries by id in cmsDocument table to set newest = false + var documentDtos = Database.Fetch("WHERE nodeId = @Id AND newest = @IsNewest", new { Id = entity.Id, IsNewest = true }); + foreach (var documentDto in documentDtos) + { + var docDto = documentDto; + docDto.Newest = false; + Database.Update(docDto); + } + + var contentVersionDto = dto.ContentVersionDto; + if (shouldCreateNewVersion) + { + //Create a new version - cmsContentVersion + //Assumes a new Version guid and Version date (modified date) has been set + Database.Insert(contentVersionDto); + //Create the Document specific data for this version - cmsDocument + //Assumes a new Version guid has been generated + Database.Insert(dto); + } + else + { + //In order to update the ContentVersion we need to retrieve its primary key id + var contentVerDto = Database.SingleOrDefault("WHERE VersionId = @Version", new { Version = entity.Version }); + contentVersionDto.Id = contentVerDto.Id; + + Database.Update(contentVersionDto); + Database.Update(dto); + } + + //Create the PropertyData for this version - cmsPropertyData + var propertyFactory = new PropertyFactory(entity.ContentType.CompositionPropertyTypes.ToArray(), entity.Version, entity.Id); + var propertyDataDtos = propertyFactory.BuildDto(entity.Properties); + var keyDictionary = new Dictionary(); + + //Add Properties + foreach (var propertyDataDto in propertyDataDtos) + { + if (shouldCreateNewVersion == false && propertyDataDto.Id > 0) + { + Database.Update(propertyDataDto); + } + else + { + int primaryKey = Convert.ToInt32(Database.Insert(propertyDataDto)); + keyDictionary.Add(propertyDataDto.PropertyTypeId, primaryKey); + } + } + + //Update Properties with its newly set Id + if (keyDictionary.Any()) + { + foreach (var property in entity.Properties) + { + if (keyDictionary.ContainsKey(property.PropertyTypeId) == false) continue; + + property.Id = keyDictionary[property.PropertyTypeId]; + } + } + + //lastly, check if we are a newly published version and then update the tags table + if (publishedStateChanged && entity.Published) + { + UpdatePropertyTags(entity, _tagRepository); + } + else if (publishedStateChanged && (entity.Trashed || entity.Published == false)) + { + //it's in the trash or not published remove all entity tags + ClearEntityTags(entity, _tagRepository); + } + + // published => update published version infos, + // else if unpublished then clear published version infos + if (entity.Published) + { + dto.DocumentPublishedReadOnlyDto = new DocumentPublishedReadOnlyDto + { + VersionId = dto.VersionId, + Newest = true, + NodeId = dto.NodeId, + Published = true + }; + ((Content)entity).PublishedVersionGuid = dto.VersionId; + } + else if (publishedStateChanged) + { + dto.DocumentPublishedReadOnlyDto = new DocumentPublishedReadOnlyDto + { + VersionId = default(Guid), + Newest = false, + NodeId = dto.NodeId, + Published = false + }; + ((Content)entity).PublishedVersionGuid = default(Guid); + } + + entity.ResetDirtyProperties(); + } + + + #endregion + + #region Implementation of IContentRepository + + public IEnumerable GetByPublishedVersion(IQuery query) + { + // we WANT to return contents in top-down order, ie parents should come before children + // ideal would be pure xml "document order" which can be achieved with: + // ORDER BY substring(path, 1, len(path) - charindex(',', reverse(path))), sortOrder + // but that's probably an overkill - sorting by level,sortOrder should be enough + + var sqlClause = GetBaseQuery(false); + var translator = new SqlTranslator(sqlClause, query); + var sql = translator.Translate() + .Where(x => x.Published) + .OrderBy(x => x.Level, SqlSyntax) + .OrderBy(x => x.SortOrder, SqlSyntax); + + //NOTE: This doesn't allow properties to be part of the query + var dtos = Database.Fetch(sql); + + foreach (var dto in dtos) + { + //Check in the cache first. If it exists there AND it is published + // then we can use that entity. Otherwise if it is not published (which can be the case + // because we only store the 'latest' entries in the cache which might not be the published + // version) + var fromCache = RuntimeCache.GetCacheItem(GetCacheIdKey(dto.NodeId)); + //var fromCache = TryGetFromCache(dto.NodeId); + if (fromCache != null && fromCache.Published) + { + yield return fromCache; + } + else + { + yield return CreateContentFromDto(dto, dto.VersionId, sql); + } + } + } + + public int CountPublished() + { + var sql = GetBaseQuery(true).Where(x => x.Trashed == false) + .Where(x => x.Published == true); + return Database.ExecuteScalar(sql); + } + + public void ReplaceContentPermissions(EntityPermissionSet permissionSet) + { + var repo = new PermissionRepository(UnitOfWork, _cacheHelper, SqlSyntax); + repo.ReplaceEntityPermissions(permissionSet); + } + + public void ClearPublished(IContent content) + { + // race cond! + var documentDtos = Database.Fetch("WHERE nodeId=@id AND published=@published", new { id = content.Id, published = true }); + foreach (var documentDto in documentDtos) + { + documentDto.Published = false; + Database.Update(documentDto); + } + } + + /// + /// Assigns a single permission to the current content item for the specified user ids + /// + /// + /// + /// + public void AssignEntityPermission(IContent entity, char permission, IEnumerable userIds) + { + var repo = new PermissionRepository(UnitOfWork, _cacheHelper, SqlSyntax); + repo.AssignEntityPermission(entity, permission, userIds); + } + + public IEnumerable GetPermissionsForEntity(int entityId) + { + var repo = new PermissionRepository(UnitOfWork, _cacheHelper, SqlSyntax); + return repo.GetPermissionsForEntity(entityId); + } + + /// + /// Adds/updates content/published xml + /// + /// + /// + public void AddOrUpdateContentXml(IContent content, Func xml) + { + _contentXmlRepository.AddOrUpdate(new ContentXmlEntity(content, xml)); + } + + /// + /// Used to remove the content xml for a content item + /// + /// + public void DeleteContentXml(IContent content) + { + _contentXmlRepository.Delete(new ContentXmlEntity(content)); + } + + /// + /// Adds/updates preview xml + /// + /// + /// + public void AddOrUpdatePreviewXml(IContent content, Func xml) + { + _contentPreviewRepository.AddOrUpdate(new ContentPreviewEntity(content, xml)); + } + + /// + /// Gets paged content results + /// + /// Query to excute + /// Page number + /// Page size + /// Total records query would return without paging + /// Field to order by + /// Direction to order by + /// Flag to indicate when ordering by system field + /// Search text filter + /// An Enumerable list of objects + public IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, + string orderBy, Direction orderDirection, bool orderBySystemField, string filter = "") + { + + //NOTE: This uses the GetBaseQuery method but that does not take into account the required 'newest' field which is + // what we always require for a paged result, so we'll ensure it's included in the filter + + var args = new List(); + var sbWhere = new StringBuilder("AND (cmsDocument.newest = 1)"); + + if (filter.IsNullOrWhiteSpace() == false) + { + sbWhere.Append(" AND (cmsDocument." + SqlSyntax.GetQuotedColumnName("text") + " LIKE @" + args.Count + ")"); + args.Add("%" + filter + "%"); + } + + Func> filterCallback = () => new Tuple(sbWhere.ToString(), args.ToArray()); + + return GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, + new Tuple("cmsDocument", "nodeId"), + ProcessQuery, orderBy, orderDirection, orderBySystemField, + filterCallback); + + } + + #endregion + + #region IRecycleBinRepository members + + protected override int RecycleBinId + { + get { return Constants.System.RecycleBinContent; } + } + + #endregion + + protected override string GetDatabaseFieldNameForOrderBy(string orderBy) + { + //Some custom ones + switch (orderBy.ToUpperInvariant()) + { + case "NAME": + return "cmsDocument.text"; + case "UPDATER": + //TODO: This isn't going to work very nicely because it's going to order by ID, not by letter + return "cmsDocument.documentUser"; + } + + return base.GetDatabaseFieldNameForOrderBy(orderBy); + } + + private IEnumerable ProcessQuery(Sql sql) + { + //NOTE: This doesn't allow properties to be part of the query + var dtos = Database.Fetch(sql); + + //nothing found + if (dtos.Any() == false) return Enumerable.Empty(); + + //content types + //NOTE: This should be ok for an SQL 'IN' statement, there shouldn't be an insane amount of content types + var contentTypes = _contentTypeRepository.GetAll(dtos.Select(x => x.ContentVersionDto.ContentDto.ContentTypeId).ToArray()) + .ToArray(); + + + var ids = dtos + .Where(dto => dto.TemplateId.HasValue && dto.TemplateId.Value > 0) + .Select(x => x.TemplateId.Value).ToArray(); + + //NOTE: This should be ok for an SQL 'IN' statement, there shouldn't be an insane amount of content types + var templates = ids.Length == 0 ? Enumerable.Empty() : _templateRepository.GetAll(ids).ToArray(); + + var dtosWithContentTypes = dtos + //This select into and null check are required because we don't have a foreign damn key on the contentType column + // http://issues.umbraco.org/issue/U4-5503 + .Select(x => new { dto = x, contentType = contentTypes.FirstOrDefault(ct => ct.Id == x.ContentVersionDto.ContentDto.ContentTypeId) }) + .Where(x => x.contentType != null) + .ToArray(); + + //Go get the property data for each document + var docDefs = dtosWithContentTypes.Select(d => new DocumentDefinition( + d.dto.NodeId, + d.dto.VersionId, + d.dto.ContentVersionDto.VersionDate, + d.dto.ContentVersionDto.ContentDto.NodeDto.CreateDate, + d.contentType)); + + var propertyData = GetPropertyCollection(sql, docDefs); + + return dtosWithContentTypes.Select(d => CreateContentFromDto( + d.dto, + contentTypes.First(ct => ct.Id == d.dto.ContentVersionDto.ContentDto.ContentTypeId), + templates.FirstOrDefault(tem => tem.Id == (d.dto.TemplateId.HasValue ? d.dto.TemplateId.Value : -1)), + propertyData[d.dto.NodeId])); + } + + /// + /// Private method to create a content object from a DocumentDto, which is used by Get and GetByVersion. + /// + /// + /// + /// + /// + /// + private IContent CreateContentFromDto(DocumentDto dto, + IContentType contentType, + ITemplate template, + Models.PropertyCollection propCollection) + { + var factory = new ContentFactory(contentType, NodeObjectTypeId, dto.NodeId); + var content = factory.BuildEntity(dto); + + //Check if template id is set on DocumentDto, and get ITemplate if it is. + if (dto.TemplateId.HasValue && dto.TemplateId.Value > 0) + { + content.Template = template ?? _templateRepository.Get(dto.TemplateId.Value); + } + else + { + //ensure there isn't one set. + content.Template = null; + } + + content.Properties = propCollection; + + //on initial construction we don't want to have dirty properties tracked + // http://issues.umbraco.org/issue/U4-1946 + ((Entity)content).ResetDirtyProperties(false); + return content; + } + + /// + /// Private method to create a content object from a DocumentDto, which is used by Get and GetByVersion. + /// + /// + /// + /// + /// + private IContent CreateContentFromDto(DocumentDto dto, Guid versionId, Sql docSql) + { + var contentType = _contentTypeRepository.Get(dto.ContentVersionDto.ContentDto.ContentTypeId); + + var factory = new ContentFactory(contentType, NodeObjectTypeId, dto.NodeId); + var content = factory.BuildEntity(dto); + + //Check if template id is set on DocumentDto, and get ITemplate if it is. + if (dto.TemplateId.HasValue && dto.TemplateId.Value > 0) + { + content.Template = _templateRepository.Get(dto.TemplateId.Value); + } + + var docDef = new DocumentDefinition(dto.NodeId, versionId, content.UpdateDate, content.CreateDate, contentType); + + var properties = GetPropertyCollection(docSql, new[] { docDef }); + + content.Properties = properties[dto.NodeId]; + + //on initial construction we don't want to have dirty properties tracked + // http://issues.umbraco.org/issue/U4-1946 + ((Entity)content).ResetDirtyProperties(false); + return content; + } + + private string EnsureUniqueNodeName(int parentId, string nodeName, int id = 0) + { + if (EnsureUniqueNaming == false) + return nodeName; + + var sql = new Sql(); + sql.Select("*") + .From() + .Where(x => x.NodeObjectType == NodeObjectTypeId && x.ParentId == parentId && 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; + } + + /// + /// Dispose disposable properties + /// + /// + /// Ensure the unit of work is disposed + /// + protected override void DisposeResources() + { + _contentTypeRepository.Dispose(); + _templateRepository.Dispose(); + _tagRepository.Dispose(); + _contentPreviewRepository.Dispose(); + _contentXmlRepository.Dispose(); + } + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs index 0662581b15..ab176df6d0 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs @@ -9,85 +9,85 @@ using Umbraco.Core.Persistence.Querying; namespace Umbraco.Core.Persistence.Repositories { - public interface IContentRepository : IRepositoryVersionable, IRecycleBinRepository, IDeleteMediaFilesRepository - { - /// - /// Get the count of published items - /// - /// - /// - /// We require this on the repo because the IQuery{IContent} cannot supply the 'newest' parameter - /// - int CountPublished(); + public interface IContentRepository : IRepositoryVersionable, IRecycleBinRepository, IDeleteMediaFilesRepository + { + /// + /// Get the count of published items + /// + /// + /// + /// We require this on the repo because the IQuery{IContent} cannot supply the 'newest' parameter + /// + int CountPublished(); - /// - /// Used to bulk update the permissions set for a content item. This will replace all permissions - /// assigned to an entity with a list of user id & permission pairs. - /// - /// - void ReplaceContentPermissions(EntityPermissionSet permissionSet); + /// + /// Used to bulk update the permissions set for a content item. This will replace all permissions + /// assigned to an entity with a list of user id & permission pairs. + /// + /// + void ReplaceContentPermissions(EntityPermissionSet permissionSet); - /// - /// Clears the published flag for a content. - /// - /// - void ClearPublished(IContent content); + /// + /// Clears the published flag for a content. + /// + /// + void ClearPublished(IContent content); - /// - /// Gets all published Content by the specified query - /// - /// Query to execute against published versions - /// An enumerable list of - IEnumerable GetByPublishedVersion(IQuery query); + /// + /// Gets all published Content by the specified query + /// + /// Query to execute against published versions + /// An enumerable list of + IEnumerable GetByPublishedVersion(IQuery query); - /// - /// Assigns a single permission to the current content item for the specified user ids - /// - /// - /// - /// - void AssignEntityPermission(IContent entity, char permission, IEnumerable userIds); + /// + /// Assigns a single permission to the current content item for the specified user ids + /// + /// + /// + /// + void AssignEntityPermission(IContent entity, char permission, IEnumerable userIds); - /// - /// Gets the list of permissions for the content item - /// - /// - /// - IEnumerable GetPermissionsForEntity(int entityId); + /// + /// Gets the list of permissions for the content item + /// + /// + /// + IEnumerable GetPermissionsForEntity(int entityId); - /// - /// Used to add/update published xml for the content item - /// - /// - /// - void AddOrUpdateContentXml(IContent content, Func xml); + /// + /// Used to add/update published xml for the content item + /// + /// + /// + void AddOrUpdateContentXml(IContent content, Func xml); - /// - /// Used to remove the content xml for a content item - /// - /// - void DeleteContentXml(IContent content); + /// + /// Used to remove the content xml for a content item + /// + /// + void DeleteContentXml(IContent content); - /// - /// Used to add/update preview xml for the content item - /// - /// - /// - void AddOrUpdatePreviewXml(IContent content, Func xml); + /// + /// Used to add/update preview xml for the content item + /// + /// + /// + void AddOrUpdatePreviewXml(IContent content, Func xml); - /// - /// Gets paged content results - /// - /// Query to excute - /// Page number - /// Page size - /// Total records query would return without paging - /// Field to order by - /// Direction to order by - /// Flag to indicate when ordering by system field - /// Search text filter - /// An Enumerable list of objects - IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, - string orderBy, Direction orderDirection, bool orderBySystemField, string filter = ""); - } + /// + /// Gets paged content results + /// + /// Query to excute + /// Page number + /// Page size + /// Total records query would return without paging + /// Field to order by + /// Direction to order by + /// Flag to indicate when ordering by system field + /// Search text filter + /// An Enumerable list of objects + IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, + string orderBy, Direction orderDirection, bool orderBySystemField, string filter = ""); + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMediaRepository.cs index 358a099ea0..907f9b62c5 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMediaRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMediaRepository.cs @@ -7,36 +7,36 @@ using Umbraco.Core.Persistence.Querying; namespace Umbraco.Core.Persistence.Repositories { - public interface IMediaRepository : IRepositoryVersionable, IRecycleBinRepository, IDeleteMediaFilesRepository - { + public interface IMediaRepository : IRepositoryVersionable, IRecycleBinRepository, IDeleteMediaFilesRepository + { - /// - /// Used to add/update published xml for the media item - /// - /// - /// - void AddOrUpdateContentXml(IMedia content, Func xml); + /// + /// Used to add/update published xml for the media item + /// + /// + /// + void AddOrUpdateContentXml(IMedia content, Func xml); - /// - /// Used to add/update preview xml for the content item - /// - /// - /// - void AddOrUpdatePreviewXml(IMedia content, Func xml); + /// + /// Used to add/update preview xml for the content item + /// + /// + /// + void AddOrUpdatePreviewXml(IMedia content, Func xml); - /// - /// Gets paged media results - /// - /// Query to excute - /// Page number - /// Page size - /// Total records query would return without paging - /// Field to order by - /// Direction to order by - /// Flag to indicate when ordering by system field - /// Search text filter - /// An Enumerable list of objects - IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, - string orderBy, Direction orderDirection, bool orderBySystemField, string filter = ""); - } + /// + /// Gets paged media results + /// + /// Query to excute + /// Page number + /// Page size + /// Total records query would return without paging + /// Field to order by + /// Direction to order by + /// Flag to indicate when ordering by system field + /// Search text filter + /// An Enumerable list of objects + IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, + string orderBy, Direction orderDirection, bool orderBySystemField, string filter = ""); + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMemberRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMemberRepository.cs index c59bf5d192..a24116f0e2 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMemberRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMemberRepository.cs @@ -9,70 +9,70 @@ using Umbraco.Core.Persistence.Querying; namespace Umbraco.Core.Persistence.Repositories { - public interface IMemberRepository : IRepositoryVersionable, IDeleteMediaFilesRepository - { - /// - /// Finds members in a given role - /// - /// - /// - /// - /// - IEnumerable FindMembersInRole(string roleName, string usernameToMatch, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith); + public interface IMemberRepository : IRepositoryVersionable, IDeleteMediaFilesRepository + { + /// + /// Finds members in a given role + /// + /// + /// + /// + /// + IEnumerable FindMembersInRole(string roleName, string usernameToMatch, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith); - /// - /// Get all members in a specific group - /// - /// - /// - IEnumerable GetByMemberGroup(string groupName); + /// + /// Get all members in a specific group + /// + /// + /// + IEnumerable GetByMemberGroup(string groupName); - /// - /// Checks if a member with the username exists - /// - /// - /// - bool Exists(string username); + /// + /// Checks if a member with the username exists + /// + /// + /// + bool Exists(string username); - /// - /// Gets the count of items based on a complex query - /// - /// - /// - int GetCountByQuery(IQuery query); + /// + /// Gets the count of items based on a complex query + /// + /// + /// + int GetCountByQuery(IQuery query); - /// - /// Gets paged member results - /// - /// The query. - /// Index of the page. - /// Size of the page. - /// The total records. - /// The order by column - /// The order direction. - /// Flag to indicate when ordering by system field - /// Search query - /// - IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, - string orderBy, Direction orderDirection, bool orderBySystemField, string filter = ""); + /// + /// Gets paged member results + /// + /// The query. + /// Index of the page. + /// Size of the page. + /// The total records. + /// The order by column + /// The order direction. + /// Flag to indicate when ordering by system field + /// Search query + /// + IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, + string orderBy, Direction orderDirection, bool orderBySystemField, string filter = ""); - //IEnumerable GetPagedResultsByQuery( - // Sql sql, int pageIndex, int pageSize, out int totalRecords, - // Func, int[]> resolveIds); + //IEnumerable GetPagedResultsByQuery( + // Sql sql, int pageIndex, int pageSize, out int totalRecords, + // Func, int[]> resolveIds); - /// - /// Used to add/update published xml for the media item - /// - /// - /// - void AddOrUpdateContentXml(IMember content, Func xml); + /// + /// Used to add/update published xml for the media item + /// + /// + /// + void AddOrUpdateContentXml(IMember content, Func xml); - /// - /// Used to add/update preview xml for the content item - /// - /// - /// - void AddOrUpdatePreviewXml(IMember content, Func xml); + /// + /// Used to add/update preview xml for the content item + /// + /// + /// + void AddOrUpdatePreviewXml(IMember content, Func xml); - } + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs index d425f0afda..bb54eebeec 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs @@ -22,575 +22,575 @@ using Umbraco.Core.Services; namespace Umbraco.Core.Persistence.Repositories { - /// - /// Represents a repository for doing CRUD operations for - /// - internal class MediaRepository : RecycleBinRepository, IMediaRepository - { - private readonly IMediaTypeRepository _mediaTypeRepository; - private readonly ITagRepository _tagRepository; - private readonly ContentXmlRepository _contentXmlRepository; - private readonly ContentPreviewRepository _contentPreviewRepository; - - public MediaRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax, IMediaTypeRepository mediaTypeRepository, ITagRepository tagRepository, IContentSection contentSection) - : base(work, cache, logger, sqlSyntax, contentSection) - { - if (mediaTypeRepository == null) throw new ArgumentNullException("mediaTypeRepository"); - if (tagRepository == null) throw new ArgumentNullException("tagRepository"); - _mediaTypeRepository = mediaTypeRepository; - _tagRepository = tagRepository; - _contentXmlRepository = new ContentXmlRepository(work, CacheHelper.CreateDisabledCacheHelper(), logger, sqlSyntax); - _contentPreviewRepository = new ContentPreviewRepository(work, CacheHelper.CreateDisabledCacheHelper(), logger, sqlSyntax); - EnsureUniqueNaming = contentSection.EnsureUniqueNaming; - } - - public bool EnsureUniqueNaming { get; private set; } - - #region Overrides of RepositoryBase - - protected override IMedia PerformGet(int id) - { - var sql = GetBaseQuery(false); - sql.Where(GetBaseWhereClause(), new { Id = id }); - sql.OrderByDescending(x => x.VersionDate); - - var dto = Database.Fetch(sql).FirstOrDefault(); - - if (dto == null) - return null; - - var content = CreateMediaFromDto(dto, dto.VersionId, sql); - - return content; - } - - protected override IEnumerable PerformGetAll(params int[] ids) - { - var sql = GetBaseQuery(false); - if (ids.Any()) - { - sql.Where("umbracoNode.id in (@ids)", new { ids = ids }); - } - - return ProcessQuery(sql); - } - - protected override IEnumerable PerformGetByQuery(IQuery query) - { - var sqlClause = GetBaseQuery(false); - var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate() - .OrderBy(x => x.SortOrder); - - return ProcessQuery(sql); - } - - #endregion - - #region Overrides of PetaPocoRepositoryBase - - protected override Sql GetBaseQuery(bool isCount) - { - var sql = new Sql(); - sql.Select(isCount ? "COUNT(*)" : "*") - .From() - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .Where(x => x.NodeObjectType == NodeObjectTypeId); - return sql; - } - - protected override string GetBaseWhereClause() - { - return "umbracoNode.id = @Id"; - } - - protected override IEnumerable GetDeleteClauses() - { - var list = new List - { - "DELETE FROM cmsTask WHERE nodeId = @Id", - "DELETE FROM umbracoUser2NodeNotify WHERE nodeId = @Id", - "DELETE FROM umbracoUser2NodePermission WHERE nodeId = @Id", - "DELETE FROM umbracoRelation WHERE parentId = @Id", - "DELETE FROM umbracoRelation WHERE childId = @Id", - "DELETE FROM cmsTagRelationship WHERE nodeId = @Id", - "DELETE FROM cmsDocument WHERE nodeId = @Id", - "DELETE FROM cmsPropertyData WHERE contentNodeId = @Id", - "DELETE FROM cmsPreviewXml WHERE nodeId = @Id", - "DELETE FROM cmsContentVersion WHERE ContentId = @Id", - "DELETE FROM cmsContentXml WHERE nodeId = @Id", - "DELETE FROM cmsContent WHERE nodeId = @Id", - "DELETE FROM umbracoNode WHERE id = @Id" - }; - return list; - } - - protected override Guid NodeObjectTypeId - { - get { return new Guid(Constants.ObjectTypes.Media); } - } - - #endregion - - #region Overrides of VersionableRepositoryBase - - public override IMedia GetByVersion(Guid versionId) - { - var sql = GetBaseQuery(false); - sql.Where("cmsContentVersion.VersionId = @VersionId", new { VersionId = versionId }); - sql.OrderByDescending(x => x.VersionDate); - - var dto = Database.Fetch(sql).FirstOrDefault(); - - if (dto == null) - return null; - - var mediaType = _mediaTypeRepository.Get(dto.ContentDto.ContentTypeId); - - var factory = new MediaFactory(mediaType, NodeObjectTypeId, dto.NodeId); - var media = factory.BuildEntity(dto); - - var properties = GetPropertyCollection(sql, new[] { new DocumentDefinition(dto.NodeId, dto.VersionId, media.UpdateDate, media.CreateDate, mediaType) }); - - media.Properties = properties[dto.NodeId]; - - //on initial construction we don't want to have dirty properties tracked - // http://issues.umbraco.org/issue/U4-1946 - ((Entity)media).ResetDirtyProperties(false); - return media; - } - - public void RebuildXmlStructures(Func serializer, int groupSize = 5000, IEnumerable contentTypeIds = null) - { - - //Ok, now we need to remove the data and re-insert it, we'll do this all in one transaction too. - using (var tr = Database.GetTransaction()) - { - //Remove all the data first, if anything fails after this it's no problem the transaction will be reverted - if (contentTypeIds == null) - { - var mediaObjectType = Guid.Parse(Constants.ObjectTypes.Media); - var subQuery = new Sql() - .Select("DISTINCT cmsContentXml.nodeId") - .From() - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .Where(dto => dto.NodeObjectType == mediaObjectType); - - var deleteSql = SqlSyntax.GetDeleteSubquery("cmsContentXml", "nodeId", subQuery); - Database.Execute(deleteSql); - } - else - { - foreach (var id in contentTypeIds) - { - var id1 = id; - var mediaObjectType = Guid.Parse(Constants.ObjectTypes.Media); - var subQuery = new Sql() - .Select("DISTINCT cmsContentXml.nodeId") - .From() - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .Where(dto => dto.NodeObjectType == mediaObjectType) - .Where(dto => dto.ContentTypeId == id1); - - var deleteSql = SqlSyntax.GetDeleteSubquery("cmsContentXml", "nodeId", subQuery); - Database.Execute(deleteSql); - } - } - - //now insert the data, again if something fails here, the whole transaction is reversed - if (contentTypeIds == null) - { - var query = Query.Builder; - RebuildXmlStructuresProcessQuery(serializer, query, tr, groupSize); - } - else - { - foreach (var contentTypeId in contentTypeIds) - { - //copy local - var id = contentTypeId; - var query = Query.Builder.Where(x => x.ContentTypeId == id && x.Trashed == false); - RebuildXmlStructuresProcessQuery(serializer, query, tr, groupSize); - } - } - - tr.Complete(); - } - } - - private void RebuildXmlStructuresProcessQuery(Func serializer, IQuery query, Transaction tr, int pageSize) - { - var pageIndex = 0; - var total = long.MinValue; - var processed = 0; - do - { - var descendants = GetPagedResultsByQuery(query, pageIndex, pageSize, out total, "Path", Direction.Ascending, orderBySystemField: true); - - var xmlItems = (from descendant in descendants - let xml = serializer(descendant) - select new ContentXmlDto { NodeId = descendant.Id, Xml = xml.ToDataString() }).ToArray(); - - //bulk insert it into the database - Database.BulkInsertRecords(xmlItems, tr); - - processed += xmlItems.Length; - - pageIndex++; - } while (processed < total); - } - - public void AddOrUpdateContentXml(IMedia content, Func xml) - { - _contentXmlRepository.AddOrUpdate(new ContentXmlEntity(content, xml)); - } - - public void AddOrUpdatePreviewXml(IMedia content, Func xml) - { - _contentPreviewRepository.AddOrUpdate(new ContentPreviewEntity(content, xml)); - } - - protected override void PerformDeleteVersion(int id, Guid versionId) - { - Database.Delete("WHERE nodeId = @Id AND versionId = @VersionId", new { Id = id, VersionId = versionId }); - Database.Delete("WHERE contentNodeId = @Id AND versionId = @VersionId", new { Id = id, VersionId = versionId }); - Database.Delete("WHERE ContentId = @Id AND VersionId = @VersionId", new { Id = id, VersionId = versionId }); - } - - #endregion - - #region Unit of Work Implementation - - protected override void PersistNewItem(IMedia entity) - { - ((Models.Media)entity).AddingEntity(); - - //Ensure unique name on the same level - entity.Name = EnsureUniqueNodeName(entity.ParentId, entity.Name); - - //Ensure that strings don't contain characters that are invalid in XML - entity.SanitizeEntityPropertiesForXmlStorage(); - - var factory = new MediaFactory(NodeObjectTypeId, entity.Id); - var dto = factory.BuildDto(entity); - - //NOTE Should the logic below have some kind of fallback for empty parent ids ? - //Logic for setting Path, Level and SortOrder - var parent = Database.First("WHERE id = @ParentId", new { ParentId = entity.ParentId }); - var level = parent.Level + 1; - var maxSortOrder = Database.ExecuteScalar( - "SELECT coalesce(max(sortOrder),-1) FROM umbracoNode WHERE parentid = @ParentId AND nodeObjectType = @NodeObjectType", - new { /*ParentId =*/ entity.ParentId, NodeObjectType = NodeObjectTypeId }); - var sortOrder = maxSortOrder + 1; - - //Create the (base) node data - umbracoNode - var nodeDto = dto.ContentDto.NodeDto; - nodeDto.Path = parent.Path; - nodeDto.Level = short.Parse(level.ToString(CultureInfo.InvariantCulture)); - nodeDto.SortOrder = sortOrder; - var o = Database.IsNew(nodeDto) ? Convert.ToInt32(Database.Insert(nodeDto)) : Database.Update(nodeDto); - - //Update with new correct path - nodeDto.Path = string.Concat(parent.Path, ",", nodeDto.NodeId); - Database.Update(nodeDto); - - //Update entity with correct values - entity.Id = nodeDto.NodeId; //Set Id on entity to ensure an Id is set - entity.Path = nodeDto.Path; - entity.SortOrder = sortOrder; - entity.Level = level; - - //Create the Content specific data - cmsContent - var contentDto = dto.ContentDto; - contentDto.NodeId = nodeDto.NodeId; - Database.Insert(contentDto); - - //Create the first version - cmsContentVersion - //Assumes a new Version guid and Version date (modified date) has been set - dto.NodeId = nodeDto.NodeId; - Database.Insert(dto); - - //Create the PropertyData for this version - cmsPropertyData - var propertyFactory = new PropertyFactory(entity.ContentType.CompositionPropertyTypes.ToArray(), entity.Version, entity.Id); - var propertyDataDtos = propertyFactory.BuildDto(entity.Properties); - var keyDictionary = new Dictionary(); - - //Add Properties - foreach (var propertyDataDto in propertyDataDtos) - { - var primaryKey = Convert.ToInt32(Database.Insert(propertyDataDto)); - keyDictionary.Add(propertyDataDto.PropertyTypeId, primaryKey); - } - - //Update Properties with its newly set Id - foreach (var property in entity.Properties) - { - property.Id = keyDictionary[property.PropertyTypeId]; - } - - UpdatePropertyTags(entity, _tagRepository); - - entity.ResetDirtyProperties(); - } - - protected override void PersistUpdatedItem(IMedia entity) - { - //Updates Modified date - ((Models.Media)entity).UpdatingEntity(); - - //Ensure unique name on the same level - entity.Name = EnsureUniqueNodeName(entity.ParentId, entity.Name, entity.Id); - - //Ensure that strings don't contain characters that are invalid in XML - entity.SanitizeEntityPropertiesForXmlStorage(); - - //Look up parent to get and set the correct Path and update SortOrder if ParentId has changed - if (entity.IsPropertyDirty("ParentId")) - { - var parent = Database.First("WHERE id = @ParentId", new { ParentId = entity.ParentId }); - entity.Path = string.Concat(parent.Path, ",", entity.Id); - entity.Level = parent.Level + 1; - var maxSortOrder = - Database.ExecuteScalar( - "SELECT coalesce(max(sortOrder),0) FROM umbracoNode WHERE parentid = @ParentId AND nodeObjectType = @NodeObjectType", - new { ParentId = entity.ParentId, NodeObjectType = NodeObjectTypeId }); - entity.SortOrder = maxSortOrder + 1; - } - - var factory = new MediaFactory(NodeObjectTypeId, entity.Id); - //Look up Content entry to get Primary for updating the DTO - var contentDto = Database.SingleOrDefault("WHERE nodeId = @Id", new { Id = entity.Id }); - factory.SetPrimaryKey(contentDto.PrimaryKey); - var dto = factory.BuildDto(entity); - - //Updates the (base) node data - umbracoNode - var nodeDto = dto.ContentDto.NodeDto; - var o = Database.Update(nodeDto); - - //Only update this DTO if the contentType has actually changed - if (contentDto.ContentTypeId != entity.ContentTypeId) - { - //Create the Content specific data - cmsContent - var newContentDto = dto.ContentDto; - Database.Update(newContentDto); - } - - //In order to update the ContentVersion we need to retrieve its primary key id - var contentVerDto = Database.SingleOrDefault("WHERE VersionId = @Version", new { Version = entity.Version }); - dto.Id = contentVerDto.Id; - //Updates the current version - cmsContentVersion - //Assumes a Version guid exists and Version date (modified date) has been set/updated - Database.Update(dto); - - //Create the PropertyData for this version - cmsPropertyData - var propertyFactory = new PropertyFactory(entity.ContentType.CompositionPropertyTypes.ToArray(), entity.Version, entity.Id); - var propertyDataDtos = propertyFactory.BuildDto(entity.Properties); - var keyDictionary = new Dictionary(); - - //Add Properties - foreach (var propertyDataDto in propertyDataDtos) - { - if (propertyDataDto.Id > 0) - { - Database.Update(propertyDataDto); - } - else - { - int primaryKey = Convert.ToInt32(Database.Insert(propertyDataDto)); - keyDictionary.Add(propertyDataDto.PropertyTypeId, primaryKey); - } - } - - //Update Properties with its newly set Id - if (keyDictionary.Any()) - { - foreach (var property in entity.Properties) - { - property.Id = keyDictionary[property.PropertyTypeId]; - } - } - - UpdatePropertyTags(entity, _tagRepository); - - entity.ResetDirtyProperties(); - } - - #endregion - - #region IRecycleBinRepository members - - protected override int RecycleBinId - { - get { return Constants.System.RecycleBinMedia; } - } - - #endregion - - /// - /// Gets paged media results - /// - /// Query to excute - /// Page number - /// Page size - /// Total records query would return without paging - /// Field to order by - /// Direction to order by - /// Flag to indicate when ordering by system field - /// Search text filter - /// An Enumerable list of objects - public IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, - string orderBy, Direction orderDirection, bool orderBySystemField, string filter = "") - { - var args = new List(); - var sbWhere = new StringBuilder(); - Func> filterCallback = null; - if (filter.IsNullOrWhiteSpace() == false) - { - sbWhere.Append("AND (umbracoNode." + SqlSyntax.GetQuotedColumnName("text") + " LIKE @" + args.Count + ")"); - args.Add("%" + filter + "%"); - filterCallback = () => new Tuple(sbWhere.ToString().Trim(), args.ToArray()); - } - - return GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, - new Tuple("cmsContentVersion", "contentId"), - ProcessQuery, orderBy, orderDirection, orderBySystemField, - filterCallback); - - } - - private IEnumerable ProcessQuery(Sql sql) - { - //NOTE: This doesn't allow properties to be part of the query - var dtos = Database.Fetch(sql); - - var ids = dtos.Select(x => x.ContentDto.ContentTypeId).ToArray(); - - //content types - var contentTypes = ids.Length == 0 ? Enumerable.Empty() : _mediaTypeRepository.GetAll(ids).ToArray(); - - var dtosWithContentTypes = dtos - //This select into and null check are required because we don't have a foreign damn key on the contentType column - // http://issues.umbraco.org/issue/U4-5503 - .Select(x => new { dto = x, contentType = contentTypes.FirstOrDefault(ct => ct.Id == x.ContentDto.ContentTypeId) }) - .Where(x => x.contentType != null) - .ToArray(); - - //Go get the property data for each document - var docDefs = dtosWithContentTypes.Select(d => new DocumentDefinition( - d.dto.NodeId, - d.dto.VersionId, - d.dto.VersionDate, - d.dto.ContentDto.NodeDto.CreateDate, - d.contentType)) - .ToArray(); - - var propertyData = GetPropertyCollection(sql, docDefs); - - return dtosWithContentTypes.Select(d => CreateMediaFromDto( - d.dto, - contentTypes.First(ct => ct.Id == d.dto.ContentDto.ContentTypeId), - propertyData[d.dto.NodeId])); - } - - /// - /// Private method to create a media object from a ContentDto - /// - /// - /// - /// - /// - private IMedia CreateMediaFromDto(ContentVersionDto dto, - IMediaType contentType, - PropertyCollection propCollection) - { - var factory = new MediaFactory(contentType, NodeObjectTypeId, dto.NodeId); - var media = factory.BuildEntity(dto); - - media.Properties = propCollection; - - //on initial construction we don't want to have dirty properties tracked - // http://issues.umbraco.org/issue/U4-1946 - ((Entity)media).ResetDirtyProperties(false); - return media; - } - - /// - /// Private method to create a media object from a ContentDto - /// - /// - /// - /// - /// - private IMedia CreateMediaFromDto(ContentVersionDto dto, Guid versionId, Sql docSql) - { - var contentType = _mediaTypeRepository.Get(dto.ContentDto.ContentTypeId); - - var factory = new MediaFactory(contentType, NodeObjectTypeId, dto.NodeId); - var media = factory.BuildEntity(dto); - - var docDef = new DocumentDefinition(dto.NodeId, versionId, media.UpdateDate, media.CreateDate, contentType); - - var properties = GetPropertyCollection(docSql, new[] { docDef }); - - media.Properties = properties[dto.NodeId]; - - //on initial construction we don't want to have dirty properties tracked - // http://issues.umbraco.org/issue/U4-1946 - ((Entity)media).ResetDirtyProperties(false); - return media; - } - - private string EnsureUniqueNodeName(int parentId, string nodeName, int id = 0) - { - if (EnsureUniqueNaming == false) - return nodeName; - - var sql = new Sql(); - sql.Select("*") - .From() - .Where(x => x.NodeObjectType == NodeObjectTypeId && x.ParentId == parentId && 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; - } - - /// - /// Dispose disposable properties - /// - /// - /// Ensure the unit of work is disposed - /// - protected override void DisposeResources() - { - _mediaTypeRepository.Dispose(); - _tagRepository.Dispose(); - _contentXmlRepository.Dispose(); - _contentPreviewRepository.Dispose(); - } - } + /// + /// Represents a repository for doing CRUD operations for + /// + internal class MediaRepository : RecycleBinRepository, IMediaRepository + { + private readonly IMediaTypeRepository _mediaTypeRepository; + private readonly ITagRepository _tagRepository; + private readonly ContentXmlRepository _contentXmlRepository; + private readonly ContentPreviewRepository _contentPreviewRepository; + + public MediaRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax, IMediaTypeRepository mediaTypeRepository, ITagRepository tagRepository, IContentSection contentSection) + : base(work, cache, logger, sqlSyntax, contentSection) + { + if (mediaTypeRepository == null) throw new ArgumentNullException("mediaTypeRepository"); + if (tagRepository == null) throw new ArgumentNullException("tagRepository"); + _mediaTypeRepository = mediaTypeRepository; + _tagRepository = tagRepository; + _contentXmlRepository = new ContentXmlRepository(work, CacheHelper.CreateDisabledCacheHelper(), logger, sqlSyntax); + _contentPreviewRepository = new ContentPreviewRepository(work, CacheHelper.CreateDisabledCacheHelper(), logger, sqlSyntax); + EnsureUniqueNaming = contentSection.EnsureUniqueNaming; + } + + public bool EnsureUniqueNaming { get; private set; } + + #region Overrides of RepositoryBase + + protected override IMedia PerformGet(int id) + { + var sql = GetBaseQuery(false); + sql.Where(GetBaseWhereClause(), new { Id = id }); + sql.OrderByDescending(x => x.VersionDate); + + var dto = Database.Fetch(sql).FirstOrDefault(); + + if (dto == null) + return null; + + var content = CreateMediaFromDto(dto, dto.VersionId, sql); + + return content; + } + + protected override IEnumerable PerformGetAll(params int[] ids) + { + var sql = GetBaseQuery(false); + if (ids.Any()) + { + sql.Where("umbracoNode.id in (@ids)", new { ids = ids }); + } + + return ProcessQuery(sql); + } + + protected override IEnumerable PerformGetByQuery(IQuery query) + { + var sqlClause = GetBaseQuery(false); + var translator = new SqlTranslator(sqlClause, query); + var sql = translator.Translate() + .OrderBy(x => x.SortOrder); + + return ProcessQuery(sql); + } + + #endregion + + #region Overrides of PetaPocoRepositoryBase + + protected override Sql GetBaseQuery(bool isCount) + { + var sql = new Sql(); + sql.Select(isCount ? "COUNT(*)" : "*") + .From() + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .Where(x => x.NodeObjectType == NodeObjectTypeId); + return sql; + } + + protected override string GetBaseWhereClause() + { + return "umbracoNode.id = @Id"; + } + + protected override IEnumerable GetDeleteClauses() + { + var list = new List + { + "DELETE FROM cmsTask WHERE nodeId = @Id", + "DELETE FROM umbracoUser2NodeNotify WHERE nodeId = @Id", + "DELETE FROM umbracoUser2NodePermission WHERE nodeId = @Id", + "DELETE FROM umbracoRelation WHERE parentId = @Id", + "DELETE FROM umbracoRelation WHERE childId = @Id", + "DELETE FROM cmsTagRelationship WHERE nodeId = @Id", + "DELETE FROM cmsDocument WHERE nodeId = @Id", + "DELETE FROM cmsPropertyData WHERE contentNodeId = @Id", + "DELETE FROM cmsPreviewXml WHERE nodeId = @Id", + "DELETE FROM cmsContentVersion WHERE ContentId = @Id", + "DELETE FROM cmsContentXml WHERE nodeId = @Id", + "DELETE FROM cmsContent WHERE nodeId = @Id", + "DELETE FROM umbracoNode WHERE id = @Id" + }; + return list; + } + + protected override Guid NodeObjectTypeId + { + get { return new Guid(Constants.ObjectTypes.Media); } + } + + #endregion + + #region Overrides of VersionableRepositoryBase + + public override IMedia GetByVersion(Guid versionId) + { + var sql = GetBaseQuery(false); + sql.Where("cmsContentVersion.VersionId = @VersionId", new { VersionId = versionId }); + sql.OrderByDescending(x => x.VersionDate); + + var dto = Database.Fetch(sql).FirstOrDefault(); + + if (dto == null) + return null; + + var mediaType = _mediaTypeRepository.Get(dto.ContentDto.ContentTypeId); + + var factory = new MediaFactory(mediaType, NodeObjectTypeId, dto.NodeId); + var media = factory.BuildEntity(dto); + + var properties = GetPropertyCollection(sql, new[] { new DocumentDefinition(dto.NodeId, dto.VersionId, media.UpdateDate, media.CreateDate, mediaType) }); + + media.Properties = properties[dto.NodeId]; + + //on initial construction we don't want to have dirty properties tracked + // http://issues.umbraco.org/issue/U4-1946 + ((Entity)media).ResetDirtyProperties(false); + return media; + } + + public void RebuildXmlStructures(Func serializer, int groupSize = 5000, IEnumerable contentTypeIds = null) + { + + //Ok, now we need to remove the data and re-insert it, we'll do this all in one transaction too. + using (var tr = Database.GetTransaction()) + { + //Remove all the data first, if anything fails after this it's no problem the transaction will be reverted + if (contentTypeIds == null) + { + var mediaObjectType = Guid.Parse(Constants.ObjectTypes.Media); + var subQuery = new Sql() + .Select("DISTINCT cmsContentXml.nodeId") + .From() + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .Where(dto => dto.NodeObjectType == mediaObjectType); + + var deleteSql = SqlSyntax.GetDeleteSubquery("cmsContentXml", "nodeId", subQuery); + Database.Execute(deleteSql); + } + else + { + foreach (var id in contentTypeIds) + { + var id1 = id; + var mediaObjectType = Guid.Parse(Constants.ObjectTypes.Media); + var subQuery = new Sql() + .Select("DISTINCT cmsContentXml.nodeId") + .From() + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .Where(dto => dto.NodeObjectType == mediaObjectType) + .Where(dto => dto.ContentTypeId == id1); + + var deleteSql = SqlSyntax.GetDeleteSubquery("cmsContentXml", "nodeId", subQuery); + Database.Execute(deleteSql); + } + } + + //now insert the data, again if something fails here, the whole transaction is reversed + if (contentTypeIds == null) + { + var query = Query.Builder; + RebuildXmlStructuresProcessQuery(serializer, query, tr, groupSize); + } + else + { + foreach (var contentTypeId in contentTypeIds) + { + //copy local + var id = contentTypeId; + var query = Query.Builder.Where(x => x.ContentTypeId == id && x.Trashed == false); + RebuildXmlStructuresProcessQuery(serializer, query, tr, groupSize); + } + } + + tr.Complete(); + } + } + + private void RebuildXmlStructuresProcessQuery(Func serializer, IQuery query, Transaction tr, int pageSize) + { + var pageIndex = 0; + var total = long.MinValue; + var processed = 0; + do + { + var descendants = GetPagedResultsByQuery(query, pageIndex, pageSize, out total, "Path", Direction.Ascending, orderBySystemField: true); + + var xmlItems = (from descendant in descendants + let xml = serializer(descendant) + select new ContentXmlDto { NodeId = descendant.Id, Xml = xml.ToDataString() }).ToArray(); + + //bulk insert it into the database + Database.BulkInsertRecords(xmlItems, tr); + + processed += xmlItems.Length; + + pageIndex++; + } while (processed < total); + } + + public void AddOrUpdateContentXml(IMedia content, Func xml) + { + _contentXmlRepository.AddOrUpdate(new ContentXmlEntity(content, xml)); + } + + public void AddOrUpdatePreviewXml(IMedia content, Func xml) + { + _contentPreviewRepository.AddOrUpdate(new ContentPreviewEntity(content, xml)); + } + + protected override void PerformDeleteVersion(int id, Guid versionId) + { + Database.Delete("WHERE nodeId = @Id AND versionId = @VersionId", new { Id = id, VersionId = versionId }); + Database.Delete("WHERE contentNodeId = @Id AND versionId = @VersionId", new { Id = id, VersionId = versionId }); + Database.Delete("WHERE ContentId = @Id AND VersionId = @VersionId", new { Id = id, VersionId = versionId }); + } + + #endregion + + #region Unit of Work Implementation + + protected override void PersistNewItem(IMedia entity) + { + ((Models.Media)entity).AddingEntity(); + + //Ensure unique name on the same level + entity.Name = EnsureUniqueNodeName(entity.ParentId, entity.Name); + + //Ensure that strings don't contain characters that are invalid in XML + entity.SanitizeEntityPropertiesForXmlStorage(); + + var factory = new MediaFactory(NodeObjectTypeId, entity.Id); + var dto = factory.BuildDto(entity); + + //NOTE Should the logic below have some kind of fallback for empty parent ids ? + //Logic for setting Path, Level and SortOrder + var parent = Database.First("WHERE id = @ParentId", new { ParentId = entity.ParentId }); + var level = parent.Level + 1; + var maxSortOrder = Database.ExecuteScalar( + "SELECT coalesce(max(sortOrder),-1) FROM umbracoNode WHERE parentid = @ParentId AND nodeObjectType = @NodeObjectType", + new { /*ParentId =*/ entity.ParentId, NodeObjectType = NodeObjectTypeId }); + var sortOrder = maxSortOrder + 1; + + //Create the (base) node data - umbracoNode + var nodeDto = dto.ContentDto.NodeDto; + nodeDto.Path = parent.Path; + nodeDto.Level = short.Parse(level.ToString(CultureInfo.InvariantCulture)); + nodeDto.SortOrder = sortOrder; + var o = Database.IsNew(nodeDto) ? Convert.ToInt32(Database.Insert(nodeDto)) : Database.Update(nodeDto); + + //Update with new correct path + nodeDto.Path = string.Concat(parent.Path, ",", nodeDto.NodeId); + Database.Update(nodeDto); + + //Update entity with correct values + entity.Id = nodeDto.NodeId; //Set Id on entity to ensure an Id is set + entity.Path = nodeDto.Path; + entity.SortOrder = sortOrder; + entity.Level = level; + + //Create the Content specific data - cmsContent + var contentDto = dto.ContentDto; + contentDto.NodeId = nodeDto.NodeId; + Database.Insert(contentDto); + + //Create the first version - cmsContentVersion + //Assumes a new Version guid and Version date (modified date) has been set + dto.NodeId = nodeDto.NodeId; + Database.Insert(dto); + + //Create the PropertyData for this version - cmsPropertyData + var propertyFactory = new PropertyFactory(entity.ContentType.CompositionPropertyTypes.ToArray(), entity.Version, entity.Id); + var propertyDataDtos = propertyFactory.BuildDto(entity.Properties); + var keyDictionary = new Dictionary(); + + //Add Properties + foreach (var propertyDataDto in propertyDataDtos) + { + var primaryKey = Convert.ToInt32(Database.Insert(propertyDataDto)); + keyDictionary.Add(propertyDataDto.PropertyTypeId, primaryKey); + } + + //Update Properties with its newly set Id + foreach (var property in entity.Properties) + { + property.Id = keyDictionary[property.PropertyTypeId]; + } + + UpdatePropertyTags(entity, _tagRepository); + + entity.ResetDirtyProperties(); + } + + protected override void PersistUpdatedItem(IMedia entity) + { + //Updates Modified date + ((Models.Media)entity).UpdatingEntity(); + + //Ensure unique name on the same level + entity.Name = EnsureUniqueNodeName(entity.ParentId, entity.Name, entity.Id); + + //Ensure that strings don't contain characters that are invalid in XML + entity.SanitizeEntityPropertiesForXmlStorage(); + + //Look up parent to get and set the correct Path and update SortOrder if ParentId has changed + if (entity.IsPropertyDirty("ParentId")) + { + var parent = Database.First("WHERE id = @ParentId", new { ParentId = entity.ParentId }); + entity.Path = string.Concat(parent.Path, ",", entity.Id); + entity.Level = parent.Level + 1; + var maxSortOrder = + Database.ExecuteScalar( + "SELECT coalesce(max(sortOrder),0) FROM umbracoNode WHERE parentid = @ParentId AND nodeObjectType = @NodeObjectType", + new { ParentId = entity.ParentId, NodeObjectType = NodeObjectTypeId }); + entity.SortOrder = maxSortOrder + 1; + } + + var factory = new MediaFactory(NodeObjectTypeId, entity.Id); + //Look up Content entry to get Primary for updating the DTO + var contentDto = Database.SingleOrDefault("WHERE nodeId = @Id", new { Id = entity.Id }); + factory.SetPrimaryKey(contentDto.PrimaryKey); + var dto = factory.BuildDto(entity); + + //Updates the (base) node data - umbracoNode + var nodeDto = dto.ContentDto.NodeDto; + var o = Database.Update(nodeDto); + + //Only update this DTO if the contentType has actually changed + if (contentDto.ContentTypeId != entity.ContentTypeId) + { + //Create the Content specific data - cmsContent + var newContentDto = dto.ContentDto; + Database.Update(newContentDto); + } + + //In order to update the ContentVersion we need to retrieve its primary key id + var contentVerDto = Database.SingleOrDefault("WHERE VersionId = @Version", new { Version = entity.Version }); + dto.Id = contentVerDto.Id; + //Updates the current version - cmsContentVersion + //Assumes a Version guid exists and Version date (modified date) has been set/updated + Database.Update(dto); + + //Create the PropertyData for this version - cmsPropertyData + var propertyFactory = new PropertyFactory(entity.ContentType.CompositionPropertyTypes.ToArray(), entity.Version, entity.Id); + var propertyDataDtos = propertyFactory.BuildDto(entity.Properties); + var keyDictionary = new Dictionary(); + + //Add Properties + foreach (var propertyDataDto in propertyDataDtos) + { + if (propertyDataDto.Id > 0) + { + Database.Update(propertyDataDto); + } + else + { + int primaryKey = Convert.ToInt32(Database.Insert(propertyDataDto)); + keyDictionary.Add(propertyDataDto.PropertyTypeId, primaryKey); + } + } + + //Update Properties with its newly set Id + if (keyDictionary.Any()) + { + foreach (var property in entity.Properties) + { + property.Id = keyDictionary[property.PropertyTypeId]; + } + } + + UpdatePropertyTags(entity, _tagRepository); + + entity.ResetDirtyProperties(); + } + + #endregion + + #region IRecycleBinRepository members + + protected override int RecycleBinId + { + get { return Constants.System.RecycleBinMedia; } + } + + #endregion + + /// + /// Gets paged media results + /// + /// Query to excute + /// Page number + /// Page size + /// Total records query would return without paging + /// Field to order by + /// Direction to order by + /// Flag to indicate when ordering by system field + /// Search text filter + /// An Enumerable list of objects + public IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, + string orderBy, Direction orderDirection, bool orderBySystemField, string filter = "") + { + var args = new List(); + var sbWhere = new StringBuilder(); + Func> filterCallback = null; + if (filter.IsNullOrWhiteSpace() == false) + { + sbWhere.Append("AND (umbracoNode." + SqlSyntax.GetQuotedColumnName("text") + " LIKE @" + args.Count + ")"); + args.Add("%" + filter + "%"); + filterCallback = () => new Tuple(sbWhere.ToString().Trim(), args.ToArray()); + } + + return GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, + new Tuple("cmsContentVersion", "contentId"), + ProcessQuery, orderBy, orderDirection, orderBySystemField, + filterCallback); + + } + + private IEnumerable ProcessQuery(Sql sql) + { + //NOTE: This doesn't allow properties to be part of the query + var dtos = Database.Fetch(sql); + + var ids = dtos.Select(x => x.ContentDto.ContentTypeId).ToArray(); + + //content types + var contentTypes = ids.Length == 0 ? Enumerable.Empty() : _mediaTypeRepository.GetAll(ids).ToArray(); + + var dtosWithContentTypes = dtos + //This select into and null check are required because we don't have a foreign damn key on the contentType column + // http://issues.umbraco.org/issue/U4-5503 + .Select(x => new { dto = x, contentType = contentTypes.FirstOrDefault(ct => ct.Id == x.ContentDto.ContentTypeId) }) + .Where(x => x.contentType != null) + .ToArray(); + + //Go get the property data for each document + var docDefs = dtosWithContentTypes.Select(d => new DocumentDefinition( + d.dto.NodeId, + d.dto.VersionId, + d.dto.VersionDate, + d.dto.ContentDto.NodeDto.CreateDate, + d.contentType)) + .ToArray(); + + var propertyData = GetPropertyCollection(sql, docDefs); + + return dtosWithContentTypes.Select(d => CreateMediaFromDto( + d.dto, + contentTypes.First(ct => ct.Id == d.dto.ContentDto.ContentTypeId), + propertyData[d.dto.NodeId])); + } + + /// + /// Private method to create a media object from a ContentDto + /// + /// + /// + /// + /// + private IMedia CreateMediaFromDto(ContentVersionDto dto, + IMediaType contentType, + PropertyCollection propCollection) + { + var factory = new MediaFactory(contentType, NodeObjectTypeId, dto.NodeId); + var media = factory.BuildEntity(dto); + + media.Properties = propCollection; + + //on initial construction we don't want to have dirty properties tracked + // http://issues.umbraco.org/issue/U4-1946 + ((Entity)media).ResetDirtyProperties(false); + return media; + } + + /// + /// Private method to create a media object from a ContentDto + /// + /// + /// + /// + /// + private IMedia CreateMediaFromDto(ContentVersionDto dto, Guid versionId, Sql docSql) + { + var contentType = _mediaTypeRepository.Get(dto.ContentDto.ContentTypeId); + + var factory = new MediaFactory(contentType, NodeObjectTypeId, dto.NodeId); + var media = factory.BuildEntity(dto); + + var docDef = new DocumentDefinition(dto.NodeId, versionId, media.UpdateDate, media.CreateDate, contentType); + + var properties = GetPropertyCollection(docSql, new[] { docDef }); + + media.Properties = properties[dto.NodeId]; + + //on initial construction we don't want to have dirty properties tracked + // http://issues.umbraco.org/issue/U4-1946 + ((Entity)media).ResetDirtyProperties(false); + return media; + } + + private string EnsureUniqueNodeName(int parentId, string nodeName, int id = 0) + { + if (EnsureUniqueNaming == false) + return nodeName; + + var sql = new Sql(); + sql.Select("*") + .From() + .Where(x => x.NodeObjectType == NodeObjectTypeId && x.ParentId == parentId && 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; + } + + /// + /// Dispose disposable properties + /// + /// + /// Ensure the unit of work is disposed + /// + protected override void DisposeResources() + { + _mediaTypeRepository.Dispose(); + _tagRepository.Dispose(); + _contentXmlRepository.Dispose(); + _contentPreviewRepository.Dispose(); + } + } } diff --git a/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs index c103d24b2d..e0718d0356 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs @@ -23,748 +23,748 @@ using Umbraco.Core.Dynamics; namespace Umbraco.Core.Persistence.Repositories { - /// - /// Represents a repository for doing CRUD operations for - /// - internal class MemberRepository : VersionableRepositoryBase, IMemberRepository - { - private readonly IMemberTypeRepository _memberTypeRepository; - private readonly ITagRepository _tagRepository; - private readonly IMemberGroupRepository _memberGroupRepository; - private readonly ContentXmlRepository _contentXmlRepository; - private readonly ContentPreviewRepository _contentPreviewRepository; - - public MemberRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax, IMemberTypeRepository memberTypeRepository, IMemberGroupRepository memberGroupRepository, ITagRepository tagRepository, IContentSection contentSection) - : base(work, cache, logger, sqlSyntax, contentSection) - { - if (memberTypeRepository == null) throw new ArgumentNullException("memberTypeRepository"); - if (tagRepository == null) throw new ArgumentNullException("tagRepository"); - _memberTypeRepository = memberTypeRepository; - _tagRepository = tagRepository; - _memberGroupRepository = memberGroupRepository; - _contentXmlRepository = new ContentXmlRepository(work, CacheHelper.CreateDisabledCacheHelper(), logger, sqlSyntax); - _contentPreviewRepository = new ContentPreviewRepository(work, CacheHelper.CreateDisabledCacheHelper(), logger, sqlSyntax); - } - - #region Overrides of RepositoryBase - - protected override IMember PerformGet(int id) - { - var sql = GetBaseQuery(false); - sql.Where(GetBaseWhereClause(), new { Id = id }); - sql.OrderByDescending(x => x.VersionDate); - - var dto = Database.Fetch(sql).FirstOrDefault(); - - if (dto == null) - return null; - - var content = CreateMemberFromDto(dto, dto.ContentVersionDto.VersionId, sql); - - return content; - - } - - protected override IEnumerable PerformGetAll(params int[] ids) - { - var sql = GetBaseQuery(false); - if (ids.Any()) - { - sql.Where("umbracoNode.id in (@ids)", new { ids = ids }); - } - - return ProcessQuery(sql); - - } - - protected override IEnumerable PerformGetByQuery(IQuery query) - { - var baseQuery = GetBaseQuery(false); - - //check if the query is based on properties or not - - var wheres = query.GetWhereClauses(); - //this is a pretty rudimentary check but wil work, we just need to know if this query requires property - // level queries - if (wheres.Any(x => x.Item1.Contains("cmsPropertyType"))) - { - var sqlWithProps = GetNodeIdQueryWithPropertyData(); - var translator = new SqlTranslator(sqlWithProps, query); - var sql = translator.Translate(); - - baseQuery.Append(new Sql("WHERE umbracoNode.id IN (" + sql.SQL + ")", sql.Arguments)) - .OrderBy(x => x.SortOrder); - - return ProcessQuery(baseQuery); - } - else - { - var translator = new SqlTranslator(baseQuery, query); - var sql = translator.Translate() - .OrderBy(x => x.SortOrder); - - return ProcessQuery(sql); - } - - } - - #endregion - - #region Overrides of PetaPocoRepositoryBase - - protected override Sql GetBaseQuery(bool isCount) - { - var sql = new Sql(); - sql.Select(isCount ? "COUNT(*)" : "*") - .From() - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - //We're joining the type so we can do a query against the member type - not sure if this adds much overhead or not? - // the execution plan says it doesn't so we'll go with that and in that case, it might be worth joining the content - // types by default on the document and media repo's so we can query by content type there too. - .InnerJoin().On(left => left.NodeId, right => right.ContentTypeId) - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .Where(x => x.NodeObjectType == NodeObjectTypeId); - return sql; - - } - - protected override string GetBaseWhereClause() - { - return "umbracoNode.id = @Id"; - } - - protected Sql GetNodeIdQueryWithPropertyData() - { - var sql = new Sql(); - sql.Select("DISTINCT(umbracoNode.id)") - .From() - .InnerJoin().On(left => left.NodeId, right => right.NodeId) - .InnerJoin().On(left => left.NodeId, right => right.ContentTypeId) - .InnerJoin().On(left => left.NodeId, right => right.NodeId) - .InnerJoin().On(left => left.NodeId, right => right.NodeId) - .LeftJoin().On(left => left.ContentTypeId, right => right.ContentTypeId) - .LeftJoin().On(left => left.DataTypeId, right => right.DataTypeId) - .LeftJoin().On(left => left.PropertyTypeId, right => right.Id) - .Append("AND cmsPropertyData.versionId = cmsContentVersion.VersionId") - .Where(x => x.NodeObjectType == NodeObjectTypeId); - return sql; - } - - protected override IEnumerable GetDeleteClauses() - { - var list = new List - { - "DELETE FROM cmsTask WHERE nodeId = @Id", - "DELETE FROM umbracoUser2NodeNotify WHERE nodeId = @Id", - "DELETE FROM umbracoUser2NodePermission WHERE nodeId = @Id", - "DELETE FROM umbracoRelation WHERE parentId = @Id", - "DELETE FROM umbracoRelation WHERE childId = @Id", - "DELETE FROM cmsTagRelationship WHERE nodeId = @Id", - "DELETE FROM cmsPropertyData WHERE contentNodeId = @Id", - "DELETE FROM cmsMember2MemberGroup WHERE Member = @Id", - "DELETE FROM cmsMember WHERE nodeId = @Id", - "DELETE FROM cmsContentVersion WHERE ContentId = @Id", - "DELETE FROM cmsContentXml WHERE nodeId = @Id", - "DELETE FROM cmsContent WHERE nodeId = @Id", - "DELETE FROM umbracoNode WHERE id = @Id" - }; - return list; - } - - protected override Guid NodeObjectTypeId - { - get { return new Guid(Constants.ObjectTypes.Member); } - } - - #endregion - - #region Unit of Work Implementation - - protected override void PersistNewItem(IMember entity) - { - ((Member)entity).AddingEntity(); - - //Ensure that strings don't contain characters that are invalid in XML - entity.SanitizeEntityPropertiesForXmlStorage(); - - var factory = new MemberFactory(NodeObjectTypeId, entity.Id); - var dto = factory.BuildDto(entity); - - //NOTE Should the logic below have some kind of fallback for empty parent ids ? - //Logic for setting Path, Level and SortOrder - var parent = Database.First("WHERE id = @ParentId", new { ParentId = ((IUmbracoEntity)entity).ParentId }); - int level = parent.Level + 1; - int sortOrder = - Database.ExecuteScalar("SELECT COUNT(*) FROM umbracoNode WHERE parentID = @ParentId AND nodeObjectType = @NodeObjectType", - new { ParentId = ((IUmbracoEntity)entity).ParentId, NodeObjectType = NodeObjectTypeId }); - - //Create the (base) node data - umbracoNode - var nodeDto = dto.ContentVersionDto.ContentDto.NodeDto; - nodeDto.Path = parent.Path; - nodeDto.Level = short.Parse(level.ToString(CultureInfo.InvariantCulture)); - nodeDto.SortOrder = sortOrder; - var o = Database.IsNew(nodeDto) ? Convert.ToInt32(Database.Insert(nodeDto)) : Database.Update(nodeDto); - - //Update with new correct path - nodeDto.Path = string.Concat(parent.Path, ",", nodeDto.NodeId); - Database.Update(nodeDto); - - //Update entity with correct values - entity.Id = nodeDto.NodeId; //Set Id on entity to ensure an Id is set - entity.Path = nodeDto.Path; - entity.SortOrder = sortOrder; - entity.Level = level; - - //Create the Content specific data - cmsContent - var contentDto = dto.ContentVersionDto.ContentDto; - contentDto.NodeId = nodeDto.NodeId; - Database.Insert(contentDto); - - //Create the first version - cmsContentVersion - //Assumes a new Version guid and Version date (modified date) has been set - dto.ContentVersionDto.NodeId = nodeDto.NodeId; - Database.Insert(dto.ContentVersionDto); - - //Create the first entry in cmsMember - dto.NodeId = nodeDto.NodeId; - Database.Insert(dto); - - //Create the PropertyData for this version - cmsPropertyData - var propertyFactory = new PropertyFactory(entity.ContentType.CompositionPropertyTypes.ToArray(), entity.Version, entity.Id); - //Add Properties - // - don't try to save the property if it doesn't exist (or doesn't have an ID) on the content type - // - this can occur if the member type doesn't contain the built-in properties that the - // - member object contains. - var propsToPersist = entity.Properties.Where(x => x.PropertyType.HasIdentity).ToArray(); - var propertyDataDtos = propertyFactory.BuildDto(propsToPersist); - var keyDictionary = new Dictionary(); - - //Add Properties - foreach (var propertyDataDto in propertyDataDtos) - { - var primaryKey = Convert.ToInt32(Database.Insert(propertyDataDto)); - keyDictionary.Add(propertyDataDto.PropertyTypeId, primaryKey); - } - - //Update Properties with its newly set Id - foreach (var property in propsToPersist) - { - property.Id = keyDictionary[property.PropertyTypeId]; - } - - UpdatePropertyTags(entity, _tagRepository); - - ((Member)entity).ResetDirtyProperties(); - } - - protected override void PersistUpdatedItem(IMember entity) - { - //Updates Modified date - ((Member)entity).UpdatingEntity(); - - //Ensure that strings don't contain characters that are invalid in XML - entity.SanitizeEntityPropertiesForXmlStorage(); - - var dirtyEntity = (ICanBeDirty)entity; - - //Look up parent to get and set the correct Path and update SortOrder if ParentId has changed - if (dirtyEntity.IsPropertyDirty("ParentId")) - { - var parent = Database.First("WHERE id = @ParentId", new { ParentId = ((IUmbracoEntity)entity).ParentId }); - ((IUmbracoEntity)entity).Path = string.Concat(parent.Path, ",", entity.Id); - ((IUmbracoEntity)entity).Level = parent.Level + 1; - var maxSortOrder = - Database.ExecuteScalar( - "SELECT coalesce(max(sortOrder),0) FROM umbracoNode WHERE parentid = @ParentId AND nodeObjectType = @NodeObjectType", - new { ParentId = ((IUmbracoEntity)entity).ParentId, NodeObjectType = NodeObjectTypeId }); - ((IUmbracoEntity)entity).SortOrder = maxSortOrder + 1; - } - - var factory = new MemberFactory(NodeObjectTypeId, entity.Id); - //Look up Content entry to get Primary for updating the DTO - var contentDto = Database.SingleOrDefault("WHERE nodeId = @Id", new { Id = entity.Id }); - factory.SetPrimaryKey(contentDto.PrimaryKey); - var dto = factory.BuildDto(entity); - - //Updates the (base) node data - umbracoNode - var nodeDto = dto.ContentVersionDto.ContentDto.NodeDto; - var o = Database.Update(nodeDto); - - //Only update this DTO if the contentType has actually changed - if (contentDto.ContentTypeId != ((Member)entity).ContentTypeId) - { - //Create the Content specific data - cmsContent - var newContentDto = dto.ContentVersionDto.ContentDto; - Database.Update(newContentDto); - } - - //In order to update the ContentVersion we need to retrieve its primary key id - var contentVerDto = Database.SingleOrDefault("WHERE VersionId = @Version", new { Version = entity.Version }); - dto.ContentVersionDto.Id = contentVerDto.Id; - //Updates the current version - cmsContentVersion - //Assumes a Version guid exists and Version date (modified date) has been set/updated - Database.Update(dto.ContentVersionDto); - - //Updates the cmsMember entry if it has changed - - //NOTE: these cols are the REAL column names in the db - var changedCols = new List(); - - if (dirtyEntity.IsPropertyDirty("Email")) - { - changedCols.Add("Email"); - } - if (dirtyEntity.IsPropertyDirty("Username")) - { - changedCols.Add("LoginName"); - } - // DO NOT update the password if it has not changed or if it is null or empty - if (dirtyEntity.IsPropertyDirty("RawPasswordValue") && entity.RawPasswordValue.IsNullOrWhiteSpace() == false) - { - changedCols.Add("Password"); - } - //only update the changed cols - if (changedCols.Count > 0) - { - Database.Update(dto, changedCols); - } - - //TODO ContentType for the Member entity - - //Create the PropertyData for this version - cmsPropertyData - var propertyFactory = new PropertyFactory(entity.ContentType.CompositionPropertyTypes.ToArray(), entity.Version, entity.Id); - var keyDictionary = new Dictionary(); - - //Add Properties - // - don't try to save the property if it doesn't exist (or doesn't have an ID) on the content type - // - this can occur if the member type doesn't contain the built-in properties that the - // - member object contains. - var propsToPersist = entity.Properties.Where(x => x.PropertyType.HasIdentity).ToArray(); - - var propertyDataDtos = propertyFactory.BuildDto(propsToPersist); - - foreach (var propertyDataDto in propertyDataDtos) - { - if (propertyDataDto.Id > 0) - { - Database.Update(propertyDataDto); - } - else - { - int primaryKey = Convert.ToInt32(Database.Insert(propertyDataDto)); - keyDictionary.Add(propertyDataDto.PropertyTypeId, primaryKey); - } - } - - //Update Properties with its newly set Id - if (keyDictionary.Any()) - { - foreach (var property in ((Member)entity).Properties) - { - property.Id = keyDictionary[property.PropertyTypeId]; - } - } - - UpdatePropertyTags(entity, _tagRepository); - - dirtyEntity.ResetDirtyProperties(); - } - - #endregion - - #region Overrides of VersionableRepositoryBase - - public void RebuildXmlStructures(Func serializer, int groupSize = 5000, IEnumerable contentTypeIds = null) - { - - //Ok, now we need to remove the data and re-insert it, we'll do this all in one transaction too. - using (var tr = Database.GetTransaction()) - { - //Remove all the data first, if anything fails after this it's no problem the transaction will be reverted - if (contentTypeIds == null) - { - var memberObjectType = Guid.Parse(Constants.ObjectTypes.Member); - var subQuery = new Sql() - .Select("DISTINCT cmsContentXml.nodeId") - .From() - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .Where(dto => dto.NodeObjectType == memberObjectType); - - var deleteSql = SqlSyntax.GetDeleteSubquery("cmsContentXml", "nodeId", subQuery); - Database.Execute(deleteSql); - } - else - { - foreach (var id in contentTypeIds) - { - var id1 = id; - var memberObjectType = Guid.Parse(Constants.ObjectTypes.Member); - var subQuery = new Sql() - .Select("DISTINCT cmsContentXml.nodeId") - .From() - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .Where(dto => dto.NodeObjectType == memberObjectType) - .Where(dto => dto.ContentTypeId == id1); - - var deleteSql = SqlSyntax.GetDeleteSubquery("cmsContentXml", "nodeId", subQuery); - Database.Execute(deleteSql); - } - } - - //now insert the data, again if something fails here, the whole transaction is reversed - if (contentTypeIds == null) - { - var query = Query.Builder; - RebuildXmlStructuresProcessQuery(serializer, query, tr, groupSize); - } - else - { - foreach (var contentTypeId in contentTypeIds) - { - //copy local - var id = contentTypeId; - var query = Query.Builder.Where(x => x.ContentTypeId == id && x.Trashed == false); - RebuildXmlStructuresProcessQuery(serializer, query, tr, groupSize); - } - } - - tr.Complete(); - } - } - - private void RebuildXmlStructuresProcessQuery(Func serializer, IQuery query, Transaction tr, int pageSize) - { - var pageIndex = 0; - var total = long.MinValue; - var processed = 0; - do - { - var descendants = GetPagedResultsByQuery(query, pageIndex, pageSize, out total, "Path", Direction.Ascending, orderBySystemField: true); - - var xmlItems = (from descendant in descendants - let xml = serializer(descendant) - select new ContentXmlDto { NodeId = descendant.Id, Xml = xml.ToDataString() }).ToArray(); - - //bulk insert it into the database - Database.BulkInsertRecords(xmlItems, tr); - - processed += xmlItems.Length; - - pageIndex++; - } while (processed < total); - } - - public override IMember GetByVersion(Guid versionId) - { - var sql = GetBaseQuery(false); - sql.Where("cmsContentVersion.VersionId = @VersionId", new { VersionId = versionId }); - sql.OrderByDescending(x => x.VersionDate); - - var dto = Database.Fetch(sql).FirstOrDefault(); - - if (dto == null) - return null; - - var memberType = _memberTypeRepository.Get(dto.ContentVersionDto.ContentDto.ContentTypeId); - - var factory = new MemberFactory(memberType, NodeObjectTypeId, dto.NodeId); - var media = factory.BuildEntity(dto); - - var properties = GetPropertyCollection(sql, new[] { new DocumentDefinition(dto.NodeId, dto.ContentVersionDto.VersionId, media.UpdateDate, media.CreateDate, memberType) }); - - media.Properties = properties[dto.NodeId]; - - //on initial construction we don't want to have dirty properties tracked - // http://issues.umbraco.org/issue/U4-1946 - ((Entity)media).ResetDirtyProperties(false); - return media; - - } - - protected override void PerformDeleteVersion(int id, Guid versionId) - { - Database.Delete("WHERE nodeId = @Id AND versionId = @VersionId", new { Id = id, VersionId = versionId }); - Database.Delete("WHERE contentNodeId = @Id AND versionId = @VersionId", new { Id = id, VersionId = versionId }); - Database.Delete("WHERE ContentId = @Id AND VersionId = @VersionId", new { Id = id, VersionId = versionId }); - } - - #endregion - - public IEnumerable FindMembersInRole(string roleName, string usernameToMatch, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith) - { - //get the group id - var grpQry = new Query().Where(group => group.Name.Equals(roleName)); - var memberGroup = _memberGroupRepository.GetByQuery(grpQry).FirstOrDefault(); - if (memberGroup == null) return Enumerable.Empty(); - - // get the members by username - var query = new Query(); - switch (matchType) - { - case StringPropertyMatchType.Exact: - query.Where(member => member.Username.Equals(usernameToMatch)); - break; - case StringPropertyMatchType.Contains: - query.Where(member => member.Username.Contains(usernameToMatch)); - break; - case StringPropertyMatchType.StartsWith: - query.Where(member => member.Username.StartsWith(usernameToMatch)); - break; - case StringPropertyMatchType.EndsWith: - query.Where(member => member.Username.EndsWith(usernameToMatch)); - break; - case StringPropertyMatchType.Wildcard: - query.Where(member => member.Username.SqlWildcard(usernameToMatch, TextColumnType.NVarchar)); - break; - default: - throw new ArgumentOutOfRangeException("matchType"); - } - var matchedMembers = GetByQuery(query).ToArray(); - - var membersInGroup = new List(); - //then we need to filter the matched members that are in the role - //since the max sql params are 2100 on sql server, we'll reduce that to be safe for potentially other servers and run the queries in batches - var inGroups = matchedMembers.InGroupsOf(1000); - foreach (var batch in inGroups) - { - var memberIdBatch = batch.Select(x => x.Id); - var sql = new Sql().Select("*").From() - .Where(dto => dto.MemberGroup == memberGroup.Id) - .Where("Member IN (@memberIds)", new { memberIds = memberIdBatch }); - var memberIdsInGroup = Database.Fetch(sql) - .Select(x => x.Member).ToArray(); - - membersInGroup.AddRange(matchedMembers.Where(x => memberIdsInGroup.Contains(x.Id))); - } - - return membersInGroup; - - } - - /// - /// Get all members in a specific group - /// - /// - /// - public IEnumerable GetByMemberGroup(string groupName) - { - var grpQry = new Query().Where(group => group.Name.Equals(groupName)); - var memberGroup = _memberGroupRepository.GetByQuery(grpQry).FirstOrDefault(); - if (memberGroup == null) return Enumerable.Empty(); - - var subQuery = new Sql().Select("Member").From().Where(dto => dto.MemberGroup == memberGroup.Id); - - var sql = GetBaseQuery(false) - //TODO: An inner join would be better, though I've read that the query optimizer will always turn a - // subquery with an IN clause into an inner join anyways. - .Append(new Sql("WHERE umbracoNode.id IN (" + subQuery.SQL + ")", subQuery.Arguments)) - .OrderByDescending(x => x.VersionDate) - .OrderBy(x => x.SortOrder); - - return ProcessQuery(sql); - - } - - public bool Exists(string username) - { - var sql = new Sql(); - - sql.Select("COUNT(*)") - .From() - .Where(x => x.LoginName == username); - - return Database.ExecuteScalar(sql) > 0; - } - - public int GetCountByQuery(IQuery query) - { - var sqlWithProps = GetNodeIdQueryWithPropertyData(); - var translator = new SqlTranslator(sqlWithProps, query); - var sql = translator.Translate(); - - //get the COUNT base query - var fullSql = GetBaseQuery(true) - .Append(new Sql("WHERE umbracoNode.id IN (" + sql.SQL + ")", sql.Arguments)); - - return Database.ExecuteScalar(fullSql); - } - - /// - /// Gets paged member results - /// - /// - /// The where clause, if this is null all records are queried - /// - /// Index of the page. - /// Size of the page. - /// The total records. - /// The order by column - /// The order direction. - /// Flag to indicate when ordering by system field - /// Search query - /// - /// - /// The query supplied will ONLY work with data specifically on the cmsMember table because we are using PetaPoco paging (SQL paging) - /// - public IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, - string orderBy, Direction orderDirection, bool orderBySystemField, string filter = "") - { - var args = new List(); - var sbWhere = new StringBuilder(); - Func> filterCallback = null; - if (filter.IsNullOrWhiteSpace() == false) - { - sbWhere.Append("AND ((umbracoNode. " + SqlSyntax.GetQuotedColumnName("text") + " LIKE @" + args.Count + ") " + - "OR (cmsMember.LoginName LIKE @0" + args.Count + "))"); - args.Add("%" + filter + "%"); - filterCallback = () => new Tuple(sbWhere.ToString().Trim(), args.ToArray()); - } - - return GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, - new Tuple("cmsMember", "nodeId"), - ProcessQuery, orderBy, orderDirection, orderBySystemField, - filterCallback); - } - - public void AddOrUpdateContentXml(IMember content, Func xml) - { - _contentXmlRepository.AddOrUpdate(new ContentXmlEntity(content, xml)); - } - - public void AddOrUpdatePreviewXml(IMember content, Func xml) - { - _contentPreviewRepository.AddOrUpdate(new ContentPreviewEntity(content, xml)); - } - - protected override string GetDatabaseFieldNameForOrderBy(string orderBy) - { - //Some custom ones - switch (orderBy.ToUpperInvariant()) - { - case "EMAIL": - return "cmsMember.Email"; - case "LOGINNAME": - return "cmsMember.LoginName"; - } - - return base.GetDatabaseFieldNameForOrderBy(orderBy); - } - - protected override string GetEntityPropertyNameForOrderBy(string orderBy) - { - //Some custom ones - switch (orderBy.ToUpperInvariant()) - { - case "LOGINNAME": - return "Username"; - } - - return base.GetEntityPropertyNameForOrderBy(orderBy); - } - - private IEnumerable ProcessQuery(Sql sql) - { - //NOTE: This doesn't allow properties to be part of the query - var dtos = Database.Fetch(sql); - - var ids = dtos.Select(x => x.ContentVersionDto.ContentDto.ContentTypeId).ToArray(); - - //content types - var contentTypes = ids.Length == 0 ? Enumerable.Empty() : _memberTypeRepository.GetAll(ids).ToArray(); - - var dtosWithContentTypes = dtos - //This select into and null check are required because we don't have a foreign damn key on the contentType column - // http://issues.umbraco.org/issue/U4-5503 - .Select(x => new { dto = x, contentType = contentTypes.FirstOrDefault(ct => ct.Id == x.ContentVersionDto.ContentDto.ContentTypeId) }) - .Where(x => x.contentType != null) - .ToArray(); - - //Go get the property data for each document - IEnumerable docDefs = dtosWithContentTypes.Select(d => new DocumentDefinition( - d.dto.NodeId, - d.dto.ContentVersionDto.VersionId, - d.dto.ContentVersionDto.VersionDate, - d.dto.ContentVersionDto.ContentDto.NodeDto.CreateDate, - d.contentType)); - - var propertyData = GetPropertyCollection(sql, docDefs); - - return dtosWithContentTypes.Select(d => CreateMemberFromDto( - d.dto, - contentTypes.First(ct => ct.Id == d.dto.ContentVersionDto.ContentDto.ContentTypeId), - propertyData[d.dto.NodeId])); - } - - /// - /// Private method to create a member object from a MemberDto - /// - /// - /// - /// - /// - private IMember CreateMemberFromDto(MemberDto dto, - IMemberType contentType, - PropertyCollection propCollection) - { - var factory = new MemberFactory(contentType, NodeObjectTypeId, dto.ContentVersionDto.NodeId); - var member = factory.BuildEntity(dto); - - member.Properties = propCollection; - - //on initial construction we don't want to have dirty properties tracked - // http://issues.umbraco.org/issue/U4-1946 - ((Entity)member).ResetDirtyProperties(false); - return member; - } - - /// - /// Private method to create a member object from a MemberDto - /// - /// - /// - /// - /// - private IMember CreateMemberFromDto(MemberDto dto, Guid versionId, Sql docSql) - { - var memberType = _memberTypeRepository.Get(dto.ContentVersionDto.ContentDto.ContentTypeId); - - var factory = new MemberFactory(memberType, NodeObjectTypeId, dto.ContentVersionDto.NodeId); - var member = factory.BuildEntity(dto); - - var docDef = new DocumentDefinition(dto.ContentVersionDto.NodeId, versionId, member.UpdateDate, member.CreateDate, memberType); - - var properties = GetPropertyCollection(docSql, new[] { docDef }); - - member.Properties = properties[dto.ContentVersionDto.NodeId]; - - //on initial construction we don't want to have dirty properties tracked - // http://issues.umbraco.org/issue/U4-1946 - ((Entity)member).ResetDirtyProperties(false); - return member; - } - - /// - /// Dispose disposable properties - /// - /// - /// Ensure the unit of work is disposed - /// - protected override void DisposeResources() - { - _memberTypeRepository.Dispose(); - _tagRepository.Dispose(); - _memberGroupRepository.Dispose(); - _contentXmlRepository.Dispose(); - _contentPreviewRepository.Dispose(); - } - - } + /// + /// Represents a repository for doing CRUD operations for + /// + internal class MemberRepository : VersionableRepositoryBase, IMemberRepository + { + private readonly IMemberTypeRepository _memberTypeRepository; + private readonly ITagRepository _tagRepository; + private readonly IMemberGroupRepository _memberGroupRepository; + private readonly ContentXmlRepository _contentXmlRepository; + private readonly ContentPreviewRepository _contentPreviewRepository; + + public MemberRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax, IMemberTypeRepository memberTypeRepository, IMemberGroupRepository memberGroupRepository, ITagRepository tagRepository, IContentSection contentSection) + : base(work, cache, logger, sqlSyntax, contentSection) + { + if (memberTypeRepository == null) throw new ArgumentNullException("memberTypeRepository"); + if (tagRepository == null) throw new ArgumentNullException("tagRepository"); + _memberTypeRepository = memberTypeRepository; + _tagRepository = tagRepository; + _memberGroupRepository = memberGroupRepository; + _contentXmlRepository = new ContentXmlRepository(work, CacheHelper.CreateDisabledCacheHelper(), logger, sqlSyntax); + _contentPreviewRepository = new ContentPreviewRepository(work, CacheHelper.CreateDisabledCacheHelper(), logger, sqlSyntax); + } + + #region Overrides of RepositoryBase + + protected override IMember PerformGet(int id) + { + var sql = GetBaseQuery(false); + sql.Where(GetBaseWhereClause(), new { Id = id }); + sql.OrderByDescending(x => x.VersionDate); + + var dto = Database.Fetch(sql).FirstOrDefault(); + + if (dto == null) + return null; + + var content = CreateMemberFromDto(dto, dto.ContentVersionDto.VersionId, sql); + + return content; + + } + + protected override IEnumerable PerformGetAll(params int[] ids) + { + var sql = GetBaseQuery(false); + if (ids.Any()) + { + sql.Where("umbracoNode.id in (@ids)", new { ids = ids }); + } + + return ProcessQuery(sql); + + } + + protected override IEnumerable PerformGetByQuery(IQuery query) + { + var baseQuery = GetBaseQuery(false); + + //check if the query is based on properties or not + + var wheres = query.GetWhereClauses(); + //this is a pretty rudimentary check but wil work, we just need to know if this query requires property + // level queries + if (wheres.Any(x => x.Item1.Contains("cmsPropertyType"))) + { + var sqlWithProps = GetNodeIdQueryWithPropertyData(); + var translator = new SqlTranslator(sqlWithProps, query); + var sql = translator.Translate(); + + baseQuery.Append(new Sql("WHERE umbracoNode.id IN (" + sql.SQL + ")", sql.Arguments)) + .OrderBy(x => x.SortOrder); + + return ProcessQuery(baseQuery); + } + else + { + var translator = new SqlTranslator(baseQuery, query); + var sql = translator.Translate() + .OrderBy(x => x.SortOrder); + + return ProcessQuery(sql); + } + + } + + #endregion + + #region Overrides of PetaPocoRepositoryBase + + protected override Sql GetBaseQuery(bool isCount) + { + var sql = new Sql(); + sql.Select(isCount ? "COUNT(*)" : "*") + .From() + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + //We're joining the type so we can do a query against the member type - not sure if this adds much overhead or not? + // the execution plan says it doesn't so we'll go with that and in that case, it might be worth joining the content + // types by default on the document and media repo's so we can query by content type there too. + .InnerJoin().On(left => left.NodeId, right => right.ContentTypeId) + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .Where(x => x.NodeObjectType == NodeObjectTypeId); + return sql; + + } + + protected override string GetBaseWhereClause() + { + return "umbracoNode.id = @Id"; + } + + protected Sql GetNodeIdQueryWithPropertyData() + { + var sql = new Sql(); + sql.Select("DISTINCT(umbracoNode.id)") + .From() + .InnerJoin().On(left => left.NodeId, right => right.NodeId) + .InnerJoin().On(left => left.NodeId, right => right.ContentTypeId) + .InnerJoin().On(left => left.NodeId, right => right.NodeId) + .InnerJoin().On(left => left.NodeId, right => right.NodeId) + .LeftJoin().On(left => left.ContentTypeId, right => right.ContentTypeId) + .LeftJoin().On(left => left.DataTypeId, right => right.DataTypeId) + .LeftJoin().On(left => left.PropertyTypeId, right => right.Id) + .Append("AND cmsPropertyData.versionId = cmsContentVersion.VersionId") + .Where(x => x.NodeObjectType == NodeObjectTypeId); + return sql; + } + + protected override IEnumerable GetDeleteClauses() + { + var list = new List + { + "DELETE FROM cmsTask WHERE nodeId = @Id", + "DELETE FROM umbracoUser2NodeNotify WHERE nodeId = @Id", + "DELETE FROM umbracoUser2NodePermission WHERE nodeId = @Id", + "DELETE FROM umbracoRelation WHERE parentId = @Id", + "DELETE FROM umbracoRelation WHERE childId = @Id", + "DELETE FROM cmsTagRelationship WHERE nodeId = @Id", + "DELETE FROM cmsPropertyData WHERE contentNodeId = @Id", + "DELETE FROM cmsMember2MemberGroup WHERE Member = @Id", + "DELETE FROM cmsMember WHERE nodeId = @Id", + "DELETE FROM cmsContentVersion WHERE ContentId = @Id", + "DELETE FROM cmsContentXml WHERE nodeId = @Id", + "DELETE FROM cmsContent WHERE nodeId = @Id", + "DELETE FROM umbracoNode WHERE id = @Id" + }; + return list; + } + + protected override Guid NodeObjectTypeId + { + get { return new Guid(Constants.ObjectTypes.Member); } + } + + #endregion + + #region Unit of Work Implementation + + protected override void PersistNewItem(IMember entity) + { + ((Member)entity).AddingEntity(); + + //Ensure that strings don't contain characters that are invalid in XML + entity.SanitizeEntityPropertiesForXmlStorage(); + + var factory = new MemberFactory(NodeObjectTypeId, entity.Id); + var dto = factory.BuildDto(entity); + + //NOTE Should the logic below have some kind of fallback for empty parent ids ? + //Logic for setting Path, Level and SortOrder + var parent = Database.First("WHERE id = @ParentId", new { ParentId = ((IUmbracoEntity)entity).ParentId }); + int level = parent.Level + 1; + int sortOrder = + Database.ExecuteScalar("SELECT COUNT(*) FROM umbracoNode WHERE parentID = @ParentId AND nodeObjectType = @NodeObjectType", + new { ParentId = ((IUmbracoEntity)entity).ParentId, NodeObjectType = NodeObjectTypeId }); + + //Create the (base) node data - umbracoNode + var nodeDto = dto.ContentVersionDto.ContentDto.NodeDto; + nodeDto.Path = parent.Path; + nodeDto.Level = short.Parse(level.ToString(CultureInfo.InvariantCulture)); + nodeDto.SortOrder = sortOrder; + var o = Database.IsNew(nodeDto) ? Convert.ToInt32(Database.Insert(nodeDto)) : Database.Update(nodeDto); + + //Update with new correct path + nodeDto.Path = string.Concat(parent.Path, ",", nodeDto.NodeId); + Database.Update(nodeDto); + + //Update entity with correct values + entity.Id = nodeDto.NodeId; //Set Id on entity to ensure an Id is set + entity.Path = nodeDto.Path; + entity.SortOrder = sortOrder; + entity.Level = level; + + //Create the Content specific data - cmsContent + var contentDto = dto.ContentVersionDto.ContentDto; + contentDto.NodeId = nodeDto.NodeId; + Database.Insert(contentDto); + + //Create the first version - cmsContentVersion + //Assumes a new Version guid and Version date (modified date) has been set + dto.ContentVersionDto.NodeId = nodeDto.NodeId; + Database.Insert(dto.ContentVersionDto); + + //Create the first entry in cmsMember + dto.NodeId = nodeDto.NodeId; + Database.Insert(dto); + + //Create the PropertyData for this version - cmsPropertyData + var propertyFactory = new PropertyFactory(entity.ContentType.CompositionPropertyTypes.ToArray(), entity.Version, entity.Id); + //Add Properties + // - don't try to save the property if it doesn't exist (or doesn't have an ID) on the content type + // - this can occur if the member type doesn't contain the built-in properties that the + // - member object contains. + var propsToPersist = entity.Properties.Where(x => x.PropertyType.HasIdentity).ToArray(); + var propertyDataDtos = propertyFactory.BuildDto(propsToPersist); + var keyDictionary = new Dictionary(); + + //Add Properties + foreach (var propertyDataDto in propertyDataDtos) + { + var primaryKey = Convert.ToInt32(Database.Insert(propertyDataDto)); + keyDictionary.Add(propertyDataDto.PropertyTypeId, primaryKey); + } + + //Update Properties with its newly set Id + foreach (var property in propsToPersist) + { + property.Id = keyDictionary[property.PropertyTypeId]; + } + + UpdatePropertyTags(entity, _tagRepository); + + ((Member)entity).ResetDirtyProperties(); + } + + protected override void PersistUpdatedItem(IMember entity) + { + //Updates Modified date + ((Member)entity).UpdatingEntity(); + + //Ensure that strings don't contain characters that are invalid in XML + entity.SanitizeEntityPropertiesForXmlStorage(); + + var dirtyEntity = (ICanBeDirty)entity; + + //Look up parent to get and set the correct Path and update SortOrder if ParentId has changed + if (dirtyEntity.IsPropertyDirty("ParentId")) + { + var parent = Database.First("WHERE id = @ParentId", new { ParentId = ((IUmbracoEntity)entity).ParentId }); + ((IUmbracoEntity)entity).Path = string.Concat(parent.Path, ",", entity.Id); + ((IUmbracoEntity)entity).Level = parent.Level + 1; + var maxSortOrder = + Database.ExecuteScalar( + "SELECT coalesce(max(sortOrder),0) FROM umbracoNode WHERE parentid = @ParentId AND nodeObjectType = @NodeObjectType", + new { ParentId = ((IUmbracoEntity)entity).ParentId, NodeObjectType = NodeObjectTypeId }); + ((IUmbracoEntity)entity).SortOrder = maxSortOrder + 1; + } + + var factory = new MemberFactory(NodeObjectTypeId, entity.Id); + //Look up Content entry to get Primary for updating the DTO + var contentDto = Database.SingleOrDefault("WHERE nodeId = @Id", new { Id = entity.Id }); + factory.SetPrimaryKey(contentDto.PrimaryKey); + var dto = factory.BuildDto(entity); + + //Updates the (base) node data - umbracoNode + var nodeDto = dto.ContentVersionDto.ContentDto.NodeDto; + var o = Database.Update(nodeDto); + + //Only update this DTO if the contentType has actually changed + if (contentDto.ContentTypeId != ((Member)entity).ContentTypeId) + { + //Create the Content specific data - cmsContent + var newContentDto = dto.ContentVersionDto.ContentDto; + Database.Update(newContentDto); + } + + //In order to update the ContentVersion we need to retrieve its primary key id + var contentVerDto = Database.SingleOrDefault("WHERE VersionId = @Version", new { Version = entity.Version }); + dto.ContentVersionDto.Id = contentVerDto.Id; + //Updates the current version - cmsContentVersion + //Assumes a Version guid exists and Version date (modified date) has been set/updated + Database.Update(dto.ContentVersionDto); + + //Updates the cmsMember entry if it has changed + + //NOTE: these cols are the REAL column names in the db + var changedCols = new List(); + + if (dirtyEntity.IsPropertyDirty("Email")) + { + changedCols.Add("Email"); + } + if (dirtyEntity.IsPropertyDirty("Username")) + { + changedCols.Add("LoginName"); + } + // DO NOT update the password if it has not changed or if it is null or empty + if (dirtyEntity.IsPropertyDirty("RawPasswordValue") && entity.RawPasswordValue.IsNullOrWhiteSpace() == false) + { + changedCols.Add("Password"); + } + //only update the changed cols + if (changedCols.Count > 0) + { + Database.Update(dto, changedCols); + } + + //TODO ContentType for the Member entity + + //Create the PropertyData for this version - cmsPropertyData + var propertyFactory = new PropertyFactory(entity.ContentType.CompositionPropertyTypes.ToArray(), entity.Version, entity.Id); + var keyDictionary = new Dictionary(); + + //Add Properties + // - don't try to save the property if it doesn't exist (or doesn't have an ID) on the content type + // - this can occur if the member type doesn't contain the built-in properties that the + // - member object contains. + var propsToPersist = entity.Properties.Where(x => x.PropertyType.HasIdentity).ToArray(); + + var propertyDataDtos = propertyFactory.BuildDto(propsToPersist); + + foreach (var propertyDataDto in propertyDataDtos) + { + if (propertyDataDto.Id > 0) + { + Database.Update(propertyDataDto); + } + else + { + int primaryKey = Convert.ToInt32(Database.Insert(propertyDataDto)); + keyDictionary.Add(propertyDataDto.PropertyTypeId, primaryKey); + } + } + + //Update Properties with its newly set Id + if (keyDictionary.Any()) + { + foreach (var property in ((Member)entity).Properties) + { + property.Id = keyDictionary[property.PropertyTypeId]; + } + } + + UpdatePropertyTags(entity, _tagRepository); + + dirtyEntity.ResetDirtyProperties(); + } + + #endregion + + #region Overrides of VersionableRepositoryBase + + public void RebuildXmlStructures(Func serializer, int groupSize = 5000, IEnumerable contentTypeIds = null) + { + + //Ok, now we need to remove the data and re-insert it, we'll do this all in one transaction too. + using (var tr = Database.GetTransaction()) + { + //Remove all the data first, if anything fails after this it's no problem the transaction will be reverted + if (contentTypeIds == null) + { + var memberObjectType = Guid.Parse(Constants.ObjectTypes.Member); + var subQuery = new Sql() + .Select("DISTINCT cmsContentXml.nodeId") + .From() + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .Where(dto => dto.NodeObjectType == memberObjectType); + + var deleteSql = SqlSyntax.GetDeleteSubquery("cmsContentXml", "nodeId", subQuery); + Database.Execute(deleteSql); + } + else + { + foreach (var id in contentTypeIds) + { + var id1 = id; + var memberObjectType = Guid.Parse(Constants.ObjectTypes.Member); + var subQuery = new Sql() + .Select("DISTINCT cmsContentXml.nodeId") + .From() + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .Where(dto => dto.NodeObjectType == memberObjectType) + .Where(dto => dto.ContentTypeId == id1); + + var deleteSql = SqlSyntax.GetDeleteSubquery("cmsContentXml", "nodeId", subQuery); + Database.Execute(deleteSql); + } + } + + //now insert the data, again if something fails here, the whole transaction is reversed + if (contentTypeIds == null) + { + var query = Query.Builder; + RebuildXmlStructuresProcessQuery(serializer, query, tr, groupSize); + } + else + { + foreach (var contentTypeId in contentTypeIds) + { + //copy local + var id = contentTypeId; + var query = Query.Builder.Where(x => x.ContentTypeId == id && x.Trashed == false); + RebuildXmlStructuresProcessQuery(serializer, query, tr, groupSize); + } + } + + tr.Complete(); + } + } + + private void RebuildXmlStructuresProcessQuery(Func serializer, IQuery query, Transaction tr, int pageSize) + { + var pageIndex = 0; + var total = long.MinValue; + var processed = 0; + do + { + var descendants = GetPagedResultsByQuery(query, pageIndex, pageSize, out total, "Path", Direction.Ascending, orderBySystemField: true); + + var xmlItems = (from descendant in descendants + let xml = serializer(descendant) + select new ContentXmlDto { NodeId = descendant.Id, Xml = xml.ToDataString() }).ToArray(); + + //bulk insert it into the database + Database.BulkInsertRecords(xmlItems, tr); + + processed += xmlItems.Length; + + pageIndex++; + } while (processed < total); + } + + public override IMember GetByVersion(Guid versionId) + { + var sql = GetBaseQuery(false); + sql.Where("cmsContentVersion.VersionId = @VersionId", new { VersionId = versionId }); + sql.OrderByDescending(x => x.VersionDate); + + var dto = Database.Fetch(sql).FirstOrDefault(); + + if (dto == null) + return null; + + var memberType = _memberTypeRepository.Get(dto.ContentVersionDto.ContentDto.ContentTypeId); + + var factory = new MemberFactory(memberType, NodeObjectTypeId, dto.NodeId); + var media = factory.BuildEntity(dto); + + var properties = GetPropertyCollection(sql, new[] { new DocumentDefinition(dto.NodeId, dto.ContentVersionDto.VersionId, media.UpdateDate, media.CreateDate, memberType) }); + + media.Properties = properties[dto.NodeId]; + + //on initial construction we don't want to have dirty properties tracked + // http://issues.umbraco.org/issue/U4-1946 + ((Entity)media).ResetDirtyProperties(false); + return media; + + } + + protected override void PerformDeleteVersion(int id, Guid versionId) + { + Database.Delete("WHERE nodeId = @Id AND versionId = @VersionId", new { Id = id, VersionId = versionId }); + Database.Delete("WHERE contentNodeId = @Id AND versionId = @VersionId", new { Id = id, VersionId = versionId }); + Database.Delete("WHERE ContentId = @Id AND VersionId = @VersionId", new { Id = id, VersionId = versionId }); + } + + #endregion + + public IEnumerable FindMembersInRole(string roleName, string usernameToMatch, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith) + { + //get the group id + var grpQry = new Query().Where(group => group.Name.Equals(roleName)); + var memberGroup = _memberGroupRepository.GetByQuery(grpQry).FirstOrDefault(); + if (memberGroup == null) return Enumerable.Empty(); + + // get the members by username + var query = new Query(); + switch (matchType) + { + case StringPropertyMatchType.Exact: + query.Where(member => member.Username.Equals(usernameToMatch)); + break; + case StringPropertyMatchType.Contains: + query.Where(member => member.Username.Contains(usernameToMatch)); + break; + case StringPropertyMatchType.StartsWith: + query.Where(member => member.Username.StartsWith(usernameToMatch)); + break; + case StringPropertyMatchType.EndsWith: + query.Where(member => member.Username.EndsWith(usernameToMatch)); + break; + case StringPropertyMatchType.Wildcard: + query.Where(member => member.Username.SqlWildcard(usernameToMatch, TextColumnType.NVarchar)); + break; + default: + throw new ArgumentOutOfRangeException("matchType"); + } + var matchedMembers = GetByQuery(query).ToArray(); + + var membersInGroup = new List(); + //then we need to filter the matched members that are in the role + //since the max sql params are 2100 on sql server, we'll reduce that to be safe for potentially other servers and run the queries in batches + var inGroups = matchedMembers.InGroupsOf(1000); + foreach (var batch in inGroups) + { + var memberIdBatch = batch.Select(x => x.Id); + var sql = new Sql().Select("*").From() + .Where(dto => dto.MemberGroup == memberGroup.Id) + .Where("Member IN (@memberIds)", new { memberIds = memberIdBatch }); + var memberIdsInGroup = Database.Fetch(sql) + .Select(x => x.Member).ToArray(); + + membersInGroup.AddRange(matchedMembers.Where(x => memberIdsInGroup.Contains(x.Id))); + } + + return membersInGroup; + + } + + /// + /// Get all members in a specific group + /// + /// + /// + public IEnumerable GetByMemberGroup(string groupName) + { + var grpQry = new Query().Where(group => group.Name.Equals(groupName)); + var memberGroup = _memberGroupRepository.GetByQuery(grpQry).FirstOrDefault(); + if (memberGroup == null) return Enumerable.Empty(); + + var subQuery = new Sql().Select("Member").From().Where(dto => dto.MemberGroup == memberGroup.Id); + + var sql = GetBaseQuery(false) + //TODO: An inner join would be better, though I've read that the query optimizer will always turn a + // subquery with an IN clause into an inner join anyways. + .Append(new Sql("WHERE umbracoNode.id IN (" + subQuery.SQL + ")", subQuery.Arguments)) + .OrderByDescending(x => x.VersionDate) + .OrderBy(x => x.SortOrder); + + return ProcessQuery(sql); + + } + + public bool Exists(string username) + { + var sql = new Sql(); + + sql.Select("COUNT(*)") + .From() + .Where(x => x.LoginName == username); + + return Database.ExecuteScalar(sql) > 0; + } + + public int GetCountByQuery(IQuery query) + { + var sqlWithProps = GetNodeIdQueryWithPropertyData(); + var translator = new SqlTranslator(sqlWithProps, query); + var sql = translator.Translate(); + + //get the COUNT base query + var fullSql = GetBaseQuery(true) + .Append(new Sql("WHERE umbracoNode.id IN (" + sql.SQL + ")", sql.Arguments)); + + return Database.ExecuteScalar(fullSql); + } + + /// + /// Gets paged member results + /// + /// + /// The where clause, if this is null all records are queried + /// + /// Index of the page. + /// Size of the page. + /// The total records. + /// The order by column + /// The order direction. + /// Flag to indicate when ordering by system field + /// Search query + /// + /// + /// The query supplied will ONLY work with data specifically on the cmsMember table because we are using PetaPoco paging (SQL paging) + /// + public IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, + string orderBy, Direction orderDirection, bool orderBySystemField, string filter = "") + { + var args = new List(); + var sbWhere = new StringBuilder(); + Func> filterCallback = null; + if (filter.IsNullOrWhiteSpace() == false) + { + sbWhere.Append("AND ((umbracoNode. " + SqlSyntax.GetQuotedColumnName("text") + " LIKE @" + args.Count + ") " + + "OR (cmsMember.LoginName LIKE @0" + args.Count + "))"); + args.Add("%" + filter + "%"); + filterCallback = () => new Tuple(sbWhere.ToString().Trim(), args.ToArray()); + } + + return GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, + new Tuple("cmsMember", "nodeId"), + ProcessQuery, orderBy, orderDirection, orderBySystemField, + filterCallback); + } + + public void AddOrUpdateContentXml(IMember content, Func xml) + { + _contentXmlRepository.AddOrUpdate(new ContentXmlEntity(content, xml)); + } + + public void AddOrUpdatePreviewXml(IMember content, Func xml) + { + _contentPreviewRepository.AddOrUpdate(new ContentPreviewEntity(content, xml)); + } + + protected override string GetDatabaseFieldNameForOrderBy(string orderBy) + { + //Some custom ones + switch (orderBy.ToUpperInvariant()) + { + case "EMAIL": + return "cmsMember.Email"; + case "LOGINNAME": + return "cmsMember.LoginName"; + } + + return base.GetDatabaseFieldNameForOrderBy(orderBy); + } + + protected override string GetEntityPropertyNameForOrderBy(string orderBy) + { + //Some custom ones + switch (orderBy.ToUpperInvariant()) + { + case "LOGINNAME": + return "Username"; + } + + return base.GetEntityPropertyNameForOrderBy(orderBy); + } + + private IEnumerable ProcessQuery(Sql sql) + { + //NOTE: This doesn't allow properties to be part of the query + var dtos = Database.Fetch(sql); + + var ids = dtos.Select(x => x.ContentVersionDto.ContentDto.ContentTypeId).ToArray(); + + //content types + var contentTypes = ids.Length == 0 ? Enumerable.Empty() : _memberTypeRepository.GetAll(ids).ToArray(); + + var dtosWithContentTypes = dtos + //This select into and null check are required because we don't have a foreign damn key on the contentType column + // http://issues.umbraco.org/issue/U4-5503 + .Select(x => new { dto = x, contentType = contentTypes.FirstOrDefault(ct => ct.Id == x.ContentVersionDto.ContentDto.ContentTypeId) }) + .Where(x => x.contentType != null) + .ToArray(); + + //Go get the property data for each document + IEnumerable docDefs = dtosWithContentTypes.Select(d => new DocumentDefinition( + d.dto.NodeId, + d.dto.ContentVersionDto.VersionId, + d.dto.ContentVersionDto.VersionDate, + d.dto.ContentVersionDto.ContentDto.NodeDto.CreateDate, + d.contentType)); + + var propertyData = GetPropertyCollection(sql, docDefs); + + return dtosWithContentTypes.Select(d => CreateMemberFromDto( + d.dto, + contentTypes.First(ct => ct.Id == d.dto.ContentVersionDto.ContentDto.ContentTypeId), + propertyData[d.dto.NodeId])); + } + + /// + /// Private method to create a member object from a MemberDto + /// + /// + /// + /// + /// + private IMember CreateMemberFromDto(MemberDto dto, + IMemberType contentType, + PropertyCollection propCollection) + { + var factory = new MemberFactory(contentType, NodeObjectTypeId, dto.ContentVersionDto.NodeId); + var member = factory.BuildEntity(dto); + + member.Properties = propCollection; + + //on initial construction we don't want to have dirty properties tracked + // http://issues.umbraco.org/issue/U4-1946 + ((Entity)member).ResetDirtyProperties(false); + return member; + } + + /// + /// Private method to create a member object from a MemberDto + /// + /// + /// + /// + /// + private IMember CreateMemberFromDto(MemberDto dto, Guid versionId, Sql docSql) + { + var memberType = _memberTypeRepository.Get(dto.ContentVersionDto.ContentDto.ContentTypeId); + + var factory = new MemberFactory(memberType, NodeObjectTypeId, dto.ContentVersionDto.NodeId); + var member = factory.BuildEntity(dto); + + var docDef = new DocumentDefinition(dto.ContentVersionDto.NodeId, versionId, member.UpdateDate, member.CreateDate, memberType); + + var properties = GetPropertyCollection(docSql, new[] { docDef }); + + member.Properties = properties[dto.ContentVersionDto.NodeId]; + + //on initial construction we don't want to have dirty properties tracked + // http://issues.umbraco.org/issue/U4-1946 + ((Entity)member).ResetDirtyProperties(false); + return member; + } + + /// + /// Dispose disposable properties + /// + /// + /// Ensure the unit of work is disposed + /// + protected override void DisposeResources() + { + _memberTypeRepository.Dispose(); + _tagRepository.Dispose(); + _memberGroupRepository.Dispose(); + _contentXmlRepository.Dispose(); + _contentPreviewRepository.Dispose(); + } + + } } diff --git a/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs index 4fd1c83436..acff02c892 100644 --- a/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs @@ -25,256 +25,256 @@ using Umbraco.Core.IO; namespace Umbraco.Core.Persistence.Repositories { - using SqlSyntax; - internal abstract class VersionableRepositoryBase : PetaPocoRepositoryBase - where TEntity : class, IAggregateRoot - { - private readonly IContentSection _contentSection; + using SqlSyntax; + internal abstract class VersionableRepositoryBase : PetaPocoRepositoryBase + where TEntity : class, IAggregateRoot + { + private readonly IContentSection _contentSection; - protected VersionableRepositoryBase(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax, IContentSection contentSection) - : base(work, cache, logger, sqlSyntax) - { - _contentSection = contentSection; - } + protected VersionableRepositoryBase(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax, IContentSection contentSection) + : base(work, cache, logger, sqlSyntax) + { + _contentSection = contentSection; + } - #region IRepositoryVersionable Implementation + #region IRepositoryVersionable Implementation - public virtual IEnumerable GetAllVersions(int id) - { - var sql = new Sql(); - sql.Select("*") - .From(SqlSyntax) - .InnerJoin(SqlSyntax) - .On(SqlSyntax, left => left.NodeId, right => right.NodeId) - .InnerJoin(SqlSyntax) - .On(SqlSyntax, left => left.NodeId, right => right.NodeId) - .Where(x => x.NodeObjectType == NodeObjectTypeId) - .Where(x => x.NodeId == id) - .OrderByDescending(x => x.VersionDate, SqlSyntax); + public virtual IEnumerable GetAllVersions(int id) + { + var sql = new Sql(); + sql.Select("*") + .From(SqlSyntax) + .InnerJoin(SqlSyntax) + .On(SqlSyntax, left => left.NodeId, right => right.NodeId) + .InnerJoin(SqlSyntax) + .On(SqlSyntax, left => left.NodeId, right => right.NodeId) + .Where(x => x.NodeObjectType == NodeObjectTypeId) + .Where(x => x.NodeId == id) + .OrderByDescending(x => x.VersionDate, SqlSyntax); - var dtos = Database.Fetch(sql); - foreach (var dto in dtos) - { - yield return GetByVersion(dto.VersionId); - } - } + var dtos = Database.Fetch(sql); + foreach (var dto in dtos) + { + yield return GetByVersion(dto.VersionId); + } + } - public virtual void DeleteVersion(Guid versionId) - { - var dto = Database.FirstOrDefault("WHERE versionId = @VersionId", new { VersionId = versionId }); - if (dto == null) return; + public virtual void DeleteVersion(Guid versionId) + { + var dto = Database.FirstOrDefault("WHERE versionId = @VersionId", new { VersionId = versionId }); + if (dto == null) return; - //Ensure that the lastest version is not deleted - var latestVersionDto = Database.FirstOrDefault("WHERE ContentId = @Id ORDER BY VersionDate DESC", new { Id = dto.NodeId }); - if (latestVersionDto.VersionId == dto.VersionId) - return; + //Ensure that the lastest version is not deleted + var latestVersionDto = Database.FirstOrDefault("WHERE ContentId = @Id ORDER BY VersionDate DESC", new { Id = dto.NodeId }); + if (latestVersionDto.VersionId == dto.VersionId) + return; - using (var transaction = Database.GetTransaction()) - { - PerformDeleteVersion(dto.NodeId, versionId); + using (var transaction = Database.GetTransaction()) + { + PerformDeleteVersion(dto.NodeId, versionId); - transaction.Complete(); - } - } + transaction.Complete(); + } + } - public virtual void DeleteVersions(int id, DateTime versionDate) - { - //Ensure that the latest version is not part of the versions being deleted - var latestVersionDto = Database.FirstOrDefault("WHERE ContentId = @Id ORDER BY VersionDate DESC", new { Id = id }); - var list = - Database.Fetch( - "WHERE versionId <> @VersionId AND (ContentId = @Id AND VersionDate < @VersionDate)", - new { VersionId = latestVersionDto.VersionId, Id = id, VersionDate = versionDate }); - if (list.Any() == false) return; + public virtual void DeleteVersions(int id, DateTime versionDate) + { + //Ensure that the latest version is not part of the versions being deleted + var latestVersionDto = Database.FirstOrDefault("WHERE ContentId = @Id ORDER BY VersionDate DESC", new { Id = id }); + var list = + Database.Fetch( + "WHERE versionId <> @VersionId AND (ContentId = @Id AND VersionDate < @VersionDate)", + new { VersionId = latestVersionDto.VersionId, Id = id, VersionDate = versionDate }); + if (list.Any() == false) return; - using (var transaction = Database.GetTransaction()) - { - foreach (var dto in list) - { - PerformDeleteVersion(id, dto.VersionId); - } + using (var transaction = Database.GetTransaction()) + { + foreach (var dto in list) + { + PerformDeleteVersion(id, dto.VersionId); + } - transaction.Complete(); - } - } + transaction.Complete(); + } + } - public abstract TEntity GetByVersion(Guid versionId); + public abstract TEntity GetByVersion(Guid versionId); - /// - /// Protected method to execute the delete statements for removing a single version for a TEntity item. - /// - /// Id of the to delete a version from - /// Guid id of the version to delete - protected abstract void PerformDeleteVersion(int id, Guid versionId); + /// + /// Protected method to execute the delete statements for removing a single version for a TEntity item. + /// + /// Id of the to delete a version from + /// Guid id of the version to delete + protected abstract void PerformDeleteVersion(int id, Guid versionId); - #endregion + #endregion - public int CountDescendants(int parentId, string contentTypeAlias = null) - { - var pathMatch = parentId == -1 - ? "-1," - : "," + parentId + ","; - var sql = new Sql(); - if (contentTypeAlias.IsNullOrWhiteSpace()) - { - sql.Select("COUNT(*)") - .From() - .Where(x => x.NodeObjectType == NodeObjectTypeId) - .Where(x => x.Path.Contains(pathMatch)); - } - else - { - sql.Select("COUNT(*)") - .From() - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .InnerJoin() - .On(left => left.NodeId, right => right.ContentTypeId) - .Where(x => x.NodeObjectType == NodeObjectTypeId) - .Where(x => x.Path.Contains(pathMatch)) - .Where(x => x.Alias == contentTypeAlias); - } + public int CountDescendants(int parentId, string contentTypeAlias = null) + { + var pathMatch = parentId == -1 + ? "-1," + : "," + parentId + ","; + var sql = new Sql(); + if (contentTypeAlias.IsNullOrWhiteSpace()) + { + sql.Select("COUNT(*)") + .From() + .Where(x => x.NodeObjectType == NodeObjectTypeId) + .Where(x => x.Path.Contains(pathMatch)); + } + else + { + sql.Select("COUNT(*)") + .From() + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .InnerJoin() + .On(left => left.NodeId, right => right.ContentTypeId) + .Where(x => x.NodeObjectType == NodeObjectTypeId) + .Where(x => x.Path.Contains(pathMatch)) + .Where(x => x.Alias == contentTypeAlias); + } - return Database.ExecuteScalar(sql); - } + return Database.ExecuteScalar(sql); + } - public int CountChildren(int parentId, string contentTypeAlias = null) - { - var sql = new Sql(); - if (contentTypeAlias.IsNullOrWhiteSpace()) - { - sql.Select("COUNT(*)") - .From() - .Where(x => x.NodeObjectType == NodeObjectTypeId) - .Where(x => x.ParentId == parentId); - } - else - { - sql.Select("COUNT(*)") - .From() - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .InnerJoin() - .On(left => left.NodeId, right => right.ContentTypeId) - .Where(x => x.NodeObjectType == NodeObjectTypeId) - .Where(x => x.ParentId == parentId) - .Where(x => x.Alias == contentTypeAlias); - } + public int CountChildren(int parentId, string contentTypeAlias = null) + { + var sql = new Sql(); + if (contentTypeAlias.IsNullOrWhiteSpace()) + { + sql.Select("COUNT(*)") + .From() + .Where(x => x.NodeObjectType == NodeObjectTypeId) + .Where(x => x.ParentId == parentId); + } + else + { + sql.Select("COUNT(*)") + .From() + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .InnerJoin() + .On(left => left.NodeId, right => right.ContentTypeId) + .Where(x => x.NodeObjectType == NodeObjectTypeId) + .Where(x => x.ParentId == parentId) + .Where(x => x.Alias == contentTypeAlias); + } - return Database.ExecuteScalar(sql); - } + return Database.ExecuteScalar(sql); + } - /// - /// Get the total count of entities - /// - /// - /// - public int Count(string contentTypeAlias = null) - { - var sql = new Sql(); - if (contentTypeAlias.IsNullOrWhiteSpace()) - { - sql.Select("COUNT(*)") - .From() - .Where(x => x.NodeObjectType == NodeObjectTypeId); - } - else - { - sql.Select("COUNT(*)") - .From() - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .InnerJoin() - .On(left => left.NodeId, right => right.ContentTypeId) - .Where(x => x.NodeObjectType == NodeObjectTypeId) - .Where(x => x.Alias == contentTypeAlias); - } + /// + /// Get the total count of entities + /// + /// + /// + public int Count(string contentTypeAlias = null) + { + var sql = new Sql(); + if (contentTypeAlias.IsNullOrWhiteSpace()) + { + sql.Select("COUNT(*)") + .From() + .Where(x => x.NodeObjectType == NodeObjectTypeId); + } + else + { + sql.Select("COUNT(*)") + .From() + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .InnerJoin() + .On(left => left.NodeId, right => right.ContentTypeId) + .Where(x => x.NodeObjectType == NodeObjectTypeId) + .Where(x => x.Alias == contentTypeAlias); + } - return Database.ExecuteScalar(sql); - } + return Database.ExecuteScalar(sql); + } - /// - /// This removes associated tags from the entity - used generally when an entity is recycled - /// - /// - /// - protected void ClearEntityTags(IContentBase entity, ITagRepository tagRepo) - { - tagRepo.ClearTagsFromEntity(entity.Id); - } + /// + /// This removes associated tags from the entity - used generally when an entity is recycled + /// + /// + /// + protected void ClearEntityTags(IContentBase entity, ITagRepository tagRepo) + { + tagRepo.ClearTagsFromEntity(entity.Id); + } - /// - /// Updates the tag repository with any tag enabled properties and their values - /// - /// - /// - protected void UpdatePropertyTags(IContentBase entity, ITagRepository tagRepo) - { - foreach (var tagProp in entity.Properties.Where(x => x.TagSupport.Enable)) - { - if (tagProp.TagSupport.Behavior == PropertyTagBehavior.Remove) - { - //remove the specific tags - tagRepo.RemoveTagsFromProperty( - entity.Id, - tagProp.PropertyTypeId, - tagProp.TagSupport.Tags.Select(x => new Tag { Text = x.Item1, Group = x.Item2 })); - } - else - { - //assign the tags - tagRepo.AssignTagsToProperty( - entity.Id, - tagProp.PropertyTypeId, - tagProp.TagSupport.Tags.Select(x => new Tag { Text = x.Item1, Group = x.Item2 }), - tagProp.TagSupport.Behavior == PropertyTagBehavior.Replace); - } - } - } + /// + /// Updates the tag repository with any tag enabled properties and their values + /// + /// + /// + protected void UpdatePropertyTags(IContentBase entity, ITagRepository tagRepo) + { + foreach (var tagProp in entity.Properties.Where(x => x.TagSupport.Enable)) + { + if (tagProp.TagSupport.Behavior == PropertyTagBehavior.Remove) + { + //remove the specific tags + tagRepo.RemoveTagsFromProperty( + entity.Id, + tagProp.PropertyTypeId, + tagProp.TagSupport.Tags.Select(x => new Tag { Text = x.Item1, Group = x.Item2 })); + } + else + { + //assign the tags + tagRepo.AssignTagsToProperty( + entity.Id, + tagProp.PropertyTypeId, + tagProp.TagSupport.Tags.Select(x => new Tag { Text = x.Item1, Group = x.Item2 }), + tagProp.TagSupport.Behavior == PropertyTagBehavior.Replace); + } + } + } - private Sql GetFilteredSqlForPagedResults(Sql sql, Func> defaultFilter = null) - { - //copy to var so that the original isn't changed - var filteredSql = new Sql(sql.SQL, sql.Arguments); - // Apply filter - if (defaultFilter != null) - { - var filterResult = defaultFilter(); - filteredSql.Append(filterResult.Item1, filterResult.Item2); - } - return filteredSql; - } + private Sql GetFilteredSqlForPagedResults(Sql sql, Func> defaultFilter = null) + { + //copy to var so that the original isn't changed + var filteredSql = new Sql(sql.SQL, sql.Arguments); + // Apply filter + if (defaultFilter != null) + { + var filterResult = defaultFilter(); + filteredSql.Append(filterResult.Item1, filterResult.Item2); + } + return filteredSql; + } - private Sql GetSortedSqlForPagedResults(Sql sql, Direction orderDirection, string orderBy, bool orderBySystemField) - { - //copy to var so that the original isn't changed - var sortedSql = new Sql(sql.SQL, sql.Arguments); + private Sql GetSortedSqlForPagedResults(Sql sql, Direction orderDirection, string orderBy, bool orderBySystemField) + { + //copy to var so that the original isn't changed + var sortedSql = new Sql(sql.SQL, sql.Arguments); - if (orderBySystemField) - { - // Apply order according to parameters - if (string.IsNullOrEmpty(orderBy) == false) - { - var orderByParams = new[] { GetDatabaseFieldNameForOrderBy(orderBy) }; - if (orderDirection == Direction.Ascending) - { - sortedSql.OrderBy(orderByParams); - } - else - { - sortedSql.OrderByDescending(orderByParams); - } - } - } - else - { - // Sorting by a custom field, so set-up sub-query for ORDER BY clause to pull through valie - // from most recent content version for the given order by field - var sortedInt = string.Format(SqlSyntaxContext.SqlSyntaxProvider.ConvertIntegerToOrderableString, "dataInt"); - var sortedDate = string.Format(SqlSyntaxContext.SqlSyntaxProvider.ConvertDateToOrderableString, "dataDate"); - var sortedString = string.Format(SqlSyntaxContext.SqlSyntaxProvider.IsNull, "dataNvarchar", "''"); + if (orderBySystemField) + { + // Apply order according to parameters + if (string.IsNullOrEmpty(orderBy) == false) + { + var orderByParams = new[] { GetDatabaseFieldNameForOrderBy(orderBy) }; + if (orderDirection == Direction.Ascending) + { + sortedSql.OrderBy(orderByParams); + } + else + { + sortedSql.OrderByDescending(orderByParams); + } + } + } + else + { + // Sorting by a custom field, so set-up sub-query for ORDER BY clause to pull through valie + // from most recent content version for the given order by field + var sortedInt = string.Format(SqlSyntaxContext.SqlSyntaxProvider.ConvertIntegerToOrderableString, "dataInt"); + var sortedDate = string.Format(SqlSyntaxContext.SqlSyntaxProvider.ConvertDateToOrderableString, "dataDate"); + var sortedString = string.Format(SqlSyntaxContext.SqlSyntaxProvider.IsNull, "dataNvarchar", "''"); - var orderBySql = string.Format(@"ORDER BY ( + var orderBySql = string.Format(@"ORDER BY ( SELECT CASE WHEN dataInt Is Not Null THEN {0} WHEN dataDate Is Not Null THEN {1} @@ -291,125 +291,125 @@ namespace Umbraco.Core.Persistence.Repositories INNER JOIN cmsPropertyType cpt ON cpt.Id = cpd.propertytypeId WHERE c.nodeId = umbracoNode.Id and cpt.Alias = @0)", sortedInt, sortedDate, sortedString); - sortedSql.Append(orderBySql, orderBy); - if (orderDirection == Direction.Descending) - { - sortedSql.Append(" DESC"); - } - } - return sortedSql; - } + sortedSql.Append(orderBySql, orderBy); + if (orderDirection == Direction.Descending) + { + sortedSql.Append(" DESC"); + } + } + return sortedSql; + } - /// - /// A helper method for inheritors to get the paged results by query in a way that minimizes queries - /// - /// The type of the d. - /// The 'true' entity type (i.e. Content, Member, etc...) - /// The query. - /// Index of the page. - /// Size of the page. - /// The total records. - /// The tablename + column name for the SELECT statement fragment to return the node id from the query - /// A callback to create the default filter to be applied if there is one - /// A callback to process the query result - /// The order by column - /// The order direction. - /// Flag to indicate when ordering by system field - /// - /// orderBy - protected IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, - Tuple nodeIdSelect, - Func> processQuery, - string orderBy, - Direction orderDirection, - bool orderBySystemField, - Func> defaultFilter = null) - where TContentBase : class, IAggregateRoot, TEntity - { - if (orderBy == null) throw new ArgumentNullException("orderBy"); + /// + /// A helper method for inheritors to get the paged results by query in a way that minimizes queries + /// + /// The type of the d. + /// The 'true' entity type (i.e. Content, Member, etc...) + /// The query. + /// Index of the page. + /// Size of the page. + /// The total records. + /// The tablename + column name for the SELECT statement fragment to return the node id from the query + /// A callback to create the default filter to be applied if there is one + /// A callback to process the query result + /// The order by column + /// The order direction. + /// Flag to indicate when ordering by system field + /// + /// orderBy + protected IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, + Tuple nodeIdSelect, + Func> processQuery, + string orderBy, + Direction orderDirection, + bool orderBySystemField, + Func> defaultFilter = null) + where TContentBase : class, IAggregateRoot, TEntity + { + if (orderBy == null) throw new ArgumentNullException("orderBy"); - // Get base query - var sqlBase = GetBaseQuery(false); + // Get base query + var sqlBase = GetBaseQuery(false); - if (query == null) query = new Query(); - var translator = new SqlTranslator(sqlBase, query); - var sqlQuery = translator.Translate(); + if (query == null) query = new Query(); + var translator = new SqlTranslator(sqlBase, query); + var sqlQuery = translator.Translate(); - // Note we can't do multi-page for several DTOs like we can multi-fetch and are doing in PerformGetByQuery, - // but actually given we are doing a Get on each one (again as in PerformGetByQuery), we only need the node Id. - // So we'll modify the SQL. - var sqlNodeIds = new Sql( - sqlQuery.SQL.Replace("SELECT *", string.Format("SELECT {0}.{1}", nodeIdSelect.Item1, nodeIdSelect.Item2)), - sqlQuery.Arguments); + // Note we can't do multi-page for several DTOs like we can multi-fetch and are doing in PerformGetByQuery, + // but actually given we are doing a Get on each one (again as in PerformGetByQuery), we only need the node Id. + // So we'll modify the SQL. + var sqlNodeIds = new Sql( + sqlQuery.SQL.Replace("SELECT *", string.Format("SELECT {0}.{1}", nodeIdSelect.Item1, nodeIdSelect.Item2)), + sqlQuery.Arguments); - //get sorted and filtered sql - var sqlNodeIdsWithSort = GetSortedSqlForPagedResults( - GetFilteredSqlForPagedResults(sqlNodeIds, defaultFilter), - orderDirection, orderBy, orderBySystemField); + //get sorted and filtered sql + var sqlNodeIdsWithSort = GetSortedSqlForPagedResults( + GetFilteredSqlForPagedResults(sqlNodeIds, defaultFilter), + orderDirection, orderBy, orderBySystemField); - // Get page of results and total count - IEnumerable result; - var pagedResult = Database.Page(pageIndex + 1, pageSize, sqlNodeIdsWithSort); - totalRecords = Convert.ToInt32(pagedResult.TotalItems); + // Get page of results and total count + IEnumerable result; + var pagedResult = Database.Page(pageIndex + 1, pageSize, sqlNodeIdsWithSort); + totalRecords = Convert.ToInt32(pagedResult.TotalItems); - //NOTE: We need to check the actual items returned, not the 'totalRecords', that is because if you request a page number - // that doesn't actually have any data on it, the totalRecords will still indicate there are records but there are none in - // the pageResult, then the GetAll will actually return ALL records in the db. - if (pagedResult.Items.Any()) - { - //Crete the inner paged query that was used above to get the paged result, we'll use that as the inner sub query - var args = sqlNodeIdsWithSort.Arguments; - string sqlStringCount, sqlStringPage; - Database.BuildPageQueries(pageIndex * pageSize, pageSize, sqlNodeIdsWithSort.SQL, ref args, out sqlStringCount, out sqlStringPage); + //NOTE: We need to check the actual items returned, not the 'totalRecords', that is because if you request a page number + // that doesn't actually have any data on it, the totalRecords will still indicate there are records but there are none in + // the pageResult, then the GetAll will actually return ALL records in the db. + if (pagedResult.Items.Any()) + { + //Crete the inner paged query that was used above to get the paged result, we'll use that as the inner sub query + var args = sqlNodeIdsWithSort.Arguments; + string sqlStringCount, sqlStringPage; + Database.BuildPageQueries(pageIndex * pageSize, pageSize, sqlNodeIdsWithSort.SQL, ref args, out sqlStringCount, out sqlStringPage); - //if this is for sql server, the sqlPage will start with a SELECT * but we don't want that, we only want to return the nodeId - sqlStringPage = sqlStringPage - .Replace("SELECT *", - //This ensures we only take the field name of the node id select and not the table name - since the resulting select - // will ony work with the field name. - "SELECT " + nodeIdSelect.Item2); + //if this is for sql server, the sqlPage will start with a SELECT * but we don't want that, we only want to return the nodeId + sqlStringPage = sqlStringPage + .Replace("SELECT *", + //This ensures we only take the field name of the node id select and not the table name - since the resulting select + // will ony work with the field name. + "SELECT " + nodeIdSelect.Item2); - //We need to make this an inner join on the paged query - var splitQuery = sqlQuery.SQL.Split(new[] { "WHERE " }, StringSplitOptions.None); - var withInnerJoinSql = new Sql(splitQuery[0]) - .Append("INNER JOIN (") - //join the paged query with the paged query arguments - .Append(sqlStringPage, args) - .Append(") temp ") - .Append(string.Format("ON {0}.{1} = temp.{1}", nodeIdSelect.Item1, nodeIdSelect.Item2)) - //add the original where clause back with the original arguments - .Where(splitQuery[1], sqlQuery.Arguments); + //We need to make this an inner join on the paged query + var splitQuery = sqlQuery.SQL.Split(new[] { "WHERE " }, StringSplitOptions.None); + var withInnerJoinSql = new Sql(splitQuery[0]) + .Append("INNER JOIN (") + //join the paged query with the paged query arguments + .Append(sqlStringPage, args) + .Append(") temp ") + .Append(string.Format("ON {0}.{1} = temp.{1}", nodeIdSelect.Item1, nodeIdSelect.Item2)) + //add the original where clause back with the original arguments + .Where(splitQuery[1], sqlQuery.Arguments); - //get sorted and filtered sql - var fullQuery = GetSortedSqlForPagedResults( - GetFilteredSqlForPagedResults(withInnerJoinSql, defaultFilter), - orderDirection, orderBy, orderBySystemField); - return processQuery(fullQuery); - } - else - { - result = Enumerable.Empty(); - } + //get sorted and filtered sql + var fullQuery = GetSortedSqlForPagedResults( + GetFilteredSqlForPagedResults(withInnerJoinSql, defaultFilter), + orderDirection, orderBy, orderBySystemField); + return processQuery(fullQuery); + } + else + { + result = Enumerable.Empty(); + } - return result; - } + return result; + } - protected IDictionary GetPropertyCollection( - Sql docSql, - IEnumerable documentDefs) - { - if (documentDefs.Any() == false) return new Dictionary(); + protected IDictionary GetPropertyCollection( + Sql docSql, + IEnumerable documentDefs) + { + if (documentDefs.Any() == false) return new Dictionary(); - //we need to parse the original SQL statement and reduce the columns to just cmsContent.nodeId, cmsContentVersion.VersionId so that we can use - // the statement to go get the property data for all of the items by using an inner join - var parsedOriginalSql = "SELECT {0} " + docSql.SQL.Substring(docSql.SQL.IndexOf("FROM", StringComparison.Ordinal)); - //now remove everything from an Orderby clause and beyond - if (parsedOriginalSql.InvariantContains("ORDER BY ")) - { - parsedOriginalSql = parsedOriginalSql.Substring(0, parsedOriginalSql.LastIndexOf("ORDER BY ", StringComparison.Ordinal)); - } + //we need to parse the original SQL statement and reduce the columns to just cmsContent.nodeId, cmsContentVersion.VersionId so that we can use + // the statement to go get the property data for all of the items by using an inner join + var parsedOriginalSql = "SELECT {0} " + docSql.SQL.Substring(docSql.SQL.IndexOf("FROM", StringComparison.Ordinal)); + //now remove everything from an Orderby clause and beyond + if (parsedOriginalSql.InvariantContains("ORDER BY ")) + { + parsedOriginalSql = parsedOriginalSql.Substring(0, parsedOriginalSql.LastIndexOf("ORDER BY ", StringComparison.Ordinal)); + } - var propSql = new Sql(@"SELECT cmsPropertyData.* + var propSql = new Sql(@"SELECT cmsPropertyData.* FROM cmsPropertyData INNER JOIN cmsPropertyType ON cmsPropertyData.propertytypeid = cmsPropertyType.id @@ -419,13 +419,13 @@ ON cmsPropertyData.versionId = docData.VersionId AND cmsPropertyData.contentNode LEFT OUTER JOIN cmsDataTypePreValues ON cmsPropertyType.dataTypeId = cmsDataTypePreValues.datatypeNodeId", docSql.Arguments); - var allPropertyData = Database.Fetch(propSql); + var allPropertyData = Database.Fetch(propSql); - //This is a lazy access call to get all prevalue data for the data types that make up all of these properties which we use - // below if any property requires tag support - var allPreValues = new Lazy>(() => - { - var preValsSql = new Sql(@"SELECT a.id, a.value, a.sortorder, a.alias, a.datatypeNodeId + //This is a lazy access call to get all prevalue data for the data types that make up all of these properties which we use + // below if any property requires tag support + var allPreValues = new Lazy>(() => + { + var preValsSql = new Sql(@"SELECT a.id, a.value, a.sortorder, a.alias, a.datatypeNodeId FROM cmsDataTypePreValues a WHERE EXISTS( SELECT DISTINCT b.id as preValIdInner @@ -437,185 +437,185 @@ WHERE EXISTS( ON cmsPropertyType.contentTypeId = docData.contentType WHERE a.id = b.id)", docSql.Arguments); - return Database.Fetch(preValsSql); - }); + return Database.Fetch(preValsSql); + }); - var result = new Dictionary(); + var result = new Dictionary(); - var propertiesWithTagSupport = new Dictionary(); + var propertiesWithTagSupport = new Dictionary(); - //iterate each definition grouped by it's content type - this will mean less property type iterations while building - // up the property collections - foreach (var compositionGroup in documentDefs.GroupBy(x => x.Composition)) - { - var compositionProperties = compositionGroup.Key.CompositionPropertyTypes.ToArray(); + //iterate each definition grouped by it's content type - this will mean less property type iterations while building + // up the property collections + foreach (var compositionGroup in documentDefs.GroupBy(x => x.Composition)) + { + var compositionProperties = compositionGroup.Key.CompositionPropertyTypes.ToArray(); - foreach (var def in compositionGroup) - { - var propertyDataDtos = allPropertyData.Where(x => x.NodeId == def.Id).Distinct(); + foreach (var def in compositionGroup) + { + var propertyDataDtos = allPropertyData.Where(x => x.NodeId == def.Id).Distinct(); - var propertyFactory = new PropertyFactory(compositionProperties, def.Version, def.Id, def.CreateDate, def.VersionDate); - var properties = propertyFactory.BuildEntity(propertyDataDtos.ToArray()).ToArray(); + var propertyFactory = new PropertyFactory(compositionProperties, def.Version, def.Id, def.CreateDate, def.VersionDate); + var properties = propertyFactory.BuildEntity(propertyDataDtos.ToArray()).ToArray(); - var newProperties = properties.Where(x => x.HasIdentity == false && x.PropertyType.HasIdentity); + var newProperties = properties.Where(x => x.HasIdentity == false && x.PropertyType.HasIdentity); - foreach (var property in newProperties) - { - var propertyDataDto = new PropertyDataDto { NodeId = def.Id, PropertyTypeId = property.PropertyTypeId, VersionId = def.Version }; - int primaryKey = Convert.ToInt32(Database.Insert(propertyDataDto)); + foreach (var property in newProperties) + { + var propertyDataDto = new PropertyDataDto { NodeId = def.Id, PropertyTypeId = property.PropertyTypeId, VersionId = def.Version }; + int primaryKey = Convert.ToInt32(Database.Insert(propertyDataDto)); - property.Version = def.Version; - property.Id = primaryKey; - } + property.Version = def.Version; + property.Id = primaryKey; + } - foreach (var property in properties) - { - //NOTE: The benchmarks run with and without the following code show very little change so this is not a perf bottleneck - var editor = PropertyEditorResolver.Current.GetByAlias(property.PropertyType.PropertyEditorAlias); + foreach (var property in properties) + { + //NOTE: The benchmarks run with and without the following code show very little change so this is not a perf bottleneck + var editor = PropertyEditorResolver.Current.GetByAlias(property.PropertyType.PropertyEditorAlias); - var tagSupport = propertiesWithTagSupport.ContainsKey(property.PropertyType.PropertyEditorAlias) - ? propertiesWithTagSupport[property.PropertyType.PropertyEditorAlias] - : TagExtractor.GetAttribute(editor); + var tagSupport = propertiesWithTagSupport.ContainsKey(property.PropertyType.PropertyEditorAlias) + ? propertiesWithTagSupport[property.PropertyType.PropertyEditorAlias] + : TagExtractor.GetAttribute(editor); - if (tagSupport != null) - { - //add to local cache so we don't need to reflect next time for this property editor alias - propertiesWithTagSupport[property.PropertyType.PropertyEditorAlias] = tagSupport; + if (tagSupport != null) + { + //add to local cache so we don't need to reflect next time for this property editor alias + propertiesWithTagSupport[property.PropertyType.PropertyEditorAlias] = tagSupport; - //this property has tags, so we need to extract them and for that we need the prevals which we've already looked up - var preValData = allPreValues.Value.Where(x => x.DataTypeNodeId == property.PropertyType.DataTypeDefinitionId) - .Distinct() - .ToArray(); + //this property has tags, so we need to extract them and for that we need the prevals which we've already looked up + var preValData = allPreValues.Value.Where(x => x.DataTypeNodeId == property.PropertyType.DataTypeDefinitionId) + .Distinct() + .ToArray(); - var asDictionary = preValData.ToDictionary(x => x.Alias, x => new PreValue(x.Id, x.Value, x.SortOrder)); + var asDictionary = preValData.ToDictionary(x => x.Alias, x => new PreValue(x.Id, x.Value, x.SortOrder)); - var preVals = new PreValueCollection(asDictionary); + var preVals = new PreValueCollection(asDictionary); - var contentPropData = new ContentPropertyData(property.Value, - preVals, - new Dictionary()); + var contentPropData = new ContentPropertyData(property.Value, + preVals, + new Dictionary()); - TagExtractor.SetPropertyTags(property, contentPropData, property.Value, tagSupport); - } - } + TagExtractor.SetPropertyTags(property, contentPropData, property.Value, tagSupport); + } + } - if (result.ContainsKey(def.Id)) - { - Logger.Warn>("The query returned multiple property sets for document definition " + def.Id + ", " + def.Composition.Name); - } - result[def.Id] = new PropertyCollection(properties); - } - } + if (result.ContainsKey(def.Id)) + { + Logger.Warn>("The query returned multiple property sets for document definition " + def.Id + ", " + def.Composition.Name); + } + result[def.Id] = new PropertyCollection(properties); + } + } - return result; - } + return result; + } - public class DocumentDefinition - { - /// - /// Initializes a new instance of the class. - /// - public DocumentDefinition(int id, Guid version, DateTime versionDate, DateTime createDate, IContentTypeComposition composition) - { - Id = id; - Version = version; - VersionDate = versionDate; - CreateDate = createDate; - Composition = composition; - } + public class DocumentDefinition + { + /// + /// Initializes a new instance of the class. + /// + public DocumentDefinition(int id, Guid version, DateTime versionDate, DateTime createDate, IContentTypeComposition composition) + { + Id = id; + Version = version; + VersionDate = versionDate; + CreateDate = createDate; + Composition = composition; + } - public int Id { get; set; } - public Guid Version { get; set; } - public DateTime VersionDate { get; set; } - public DateTime CreateDate { get; set; } - public IContentTypeComposition Composition { get; set; } - } + public int Id { get; set; } + public Guid Version { get; set; } + public DateTime VersionDate { get; set; } + public DateTime CreateDate { get; set; } + public IContentTypeComposition Composition { get; set; } + } - protected virtual string GetDatabaseFieldNameForOrderBy(string orderBy) - { - // Translate the passed order by field (which were originally defined for in-memory object sorting - // of ContentItemBasic instances) to the database field names. - switch (orderBy.ToUpperInvariant()) - { - case "UPDATEDATE": - return "cmsContentVersion.VersionDate"; - case "NAME": - return "umbracoNode.text"; - case "OWNER": - //TODO: This isn't going to work very nicely because it's going to order by ID, not by letter - return "umbracoNode.nodeUser"; - // Members only - case "USERNAME": - return "cmsMember.LoginName"; - default: - //ensure invalid SQL cannot be submitted - return Regex.Replace(orderBy, @"[^\w\.,`\[\]@-]", ""); - } - } + protected virtual string GetDatabaseFieldNameForOrderBy(string orderBy) + { + // Translate the passed order by field (which were originally defined for in-memory object sorting + // of ContentItemBasic instances) to the database field names. + switch (orderBy.ToUpperInvariant()) + { + case "UPDATEDATE": + return "cmsContentVersion.VersionDate"; + case "NAME": + return "umbracoNode.text"; + case "OWNER": + //TODO: This isn't going to work very nicely because it's going to order by ID, not by letter + return "umbracoNode.nodeUser"; + // Members only + case "USERNAME": + return "cmsMember.LoginName"; + default: + //ensure invalid SQL cannot be submitted + return Regex.Replace(orderBy, @"[^\w\.,`\[\]@-]", ""); + } + } - protected virtual string GetEntityPropertyNameForOrderBy(string orderBy) - { - // Translate the passed order by field (which were originally defined for in-memory object sorting - // of ContentItemBasic instances) to the IMedia property names. - switch (orderBy.ToUpperInvariant()) - { - case "OWNER": - //TODO: This isn't going to work very nicely because it's going to order by ID, not by letter - return "CreatorId"; - case "UPDATER": - //TODO: This isn't going to work very nicely because it's going to order by ID, not by letter - return "WriterId"; - case "VERSIONDATE": - return "UpdateDate"; - default: - //ensure invalid SQL cannot be submitted - return Regex.Replace(orderBy, @"[^\w\.,`\[\]@-]", ""); - } - } + protected virtual string GetEntityPropertyNameForOrderBy(string orderBy) + { + // Translate the passed order by field (which were originally defined for in-memory object sorting + // of ContentItemBasic instances) to the IMedia property names. + switch (orderBy.ToUpperInvariant()) + { + case "OWNER": + //TODO: This isn't going to work very nicely because it's going to order by ID, not by letter + return "CreatorId"; + case "UPDATER": + //TODO: This isn't going to work very nicely because it's going to order by ID, not by letter + return "WriterId"; + case "VERSIONDATE": + return "UpdateDate"; + default: + //ensure invalid SQL cannot be submitted + return Regex.Replace(orderBy, @"[^\w\.,`\[\]@-]", ""); + } + } - /// - /// Deletes all media files passed in. - /// - /// - /// - public virtual bool DeleteMediaFiles(IEnumerable files) - { - //ensure duplicates are removed - files = files.Distinct(); + /// + /// Deletes all media files passed in. + /// + /// + /// + public virtual bool DeleteMediaFiles(IEnumerable files) + { + //ensure duplicates are removed + files = files.Distinct(); - var allsuccess = true; + var allsuccess = true; - var fs = FileSystemProviderManager.Current.GetFileSystemProvider(); - Parallel.ForEach(files, file => - { - try - { - if (file.IsNullOrWhiteSpace()) return; + var fs = FileSystemProviderManager.Current.GetFileSystemProvider(); + Parallel.ForEach(files, file => + { + try + { + if (file.IsNullOrWhiteSpace()) return; - var relativeFilePath = fs.GetRelativePath(file); - if (fs.FileExists(relativeFilePath) == false) return; + var relativeFilePath = fs.GetRelativePath(file); + if (fs.FileExists(relativeFilePath) == false) return; - var parentDirectory = System.IO.Path.GetDirectoryName(relativeFilePath); + var parentDirectory = System.IO.Path.GetDirectoryName(relativeFilePath); - // don't want to delete the media folder if not using directories. - if (_contentSection.UploadAllowDirectories && parentDirectory != fs.GetRelativePath("/")) - { - //issue U4-771: if there is a parent directory the recursive parameter should be true - fs.DeleteDirectory(parentDirectory, String.IsNullOrEmpty(parentDirectory) == false); - } - else - { - fs.DeleteFile(file, true); - } - } - catch (Exception e) - { - Logger.Error>("An error occurred while deleting file attached to nodes: " + file, e); - allsuccess = false; - } - }); + // don't want to delete the media folder if not using directories. + if (_contentSection.UploadAllowDirectories && parentDirectory != fs.GetRelativePath("/")) + { + //issue U4-771: if there is a parent directory the recursive parameter should be true + fs.DeleteDirectory(parentDirectory, String.IsNullOrEmpty(parentDirectory) == false); + } + else + { + fs.DeleteFile(file, true); + } + } + catch (Exception e) + { + Logger.Error>("An error occurred while deleting file attached to nodes: " + file, e); + allsuccess = false; + } + }); - return allsuccess; - } - } + return allsuccess; + } + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/ISqlSyntaxProvider.cs b/src/Umbraco.Core/Persistence/SqlSyntax/ISqlSyntaxProvider.cs index ef4601f1e5..e663c8e64b 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/ISqlSyntaxProvider.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/ISqlSyntaxProvider.cs @@ -6,79 +6,79 @@ using Umbraco.Core.Persistence.Querying; namespace Umbraco.Core.Persistence.SqlSyntax { - /// - /// Defines an SqlSyntaxProvider - /// - public interface ISqlSyntaxProvider - { - string EscapeString(string val); + /// + /// Defines an SqlSyntaxProvider + /// + public interface ISqlSyntaxProvider + { + string EscapeString(string val); - string GetWildcardPlaceholder(); - string GetStringColumnEqualComparison(string column, int paramIndex, TextColumnType columnType); - string GetStringColumnWildcardComparison(string column, int paramIndex, TextColumnType columnType); + string GetWildcardPlaceholder(); + string GetStringColumnEqualComparison(string column, int paramIndex, TextColumnType columnType); + string GetStringColumnWildcardComparison(string column, int paramIndex, TextColumnType columnType); - [Obsolete("Use the overload with the parameter index instead")] - string GetStringColumnEqualComparison(string column, string value, TextColumnType columnType); - [Obsolete("Use the overload with the parameter index instead")] - string GetStringColumnStartsWithComparison(string column, string value, TextColumnType columnType); - [Obsolete("Use the overload with the parameter index instead")] - string GetStringColumnEndsWithComparison(string column, string value, TextColumnType columnType); - [Obsolete("Use the overload with the parameter index instead")] - string GetStringColumnContainsComparison(string column, string value, TextColumnType columnType); - [Obsolete("Use the overload with the parameter index instead")] - string GetStringColumnWildcardComparison(string column, string value, TextColumnType columnType); + [Obsolete("Use the overload with the parameter index instead")] + string GetStringColumnEqualComparison(string column, string value, TextColumnType columnType); + [Obsolete("Use the overload with the parameter index instead")] + string GetStringColumnStartsWithComparison(string column, string value, TextColumnType columnType); + [Obsolete("Use the overload with the parameter index instead")] + string GetStringColumnEndsWithComparison(string column, string value, TextColumnType columnType); + [Obsolete("Use the overload with the parameter index instead")] + string GetStringColumnContainsComparison(string column, string value, TextColumnType columnType); + [Obsolete("Use the overload with the parameter index instead")] + string GetStringColumnWildcardComparison(string column, string value, TextColumnType columnType); - string GetQuotedTableName(string tableName); - string GetQuotedColumnName(string columnName); - string GetQuotedName(string name); - bool DoesTableExist(Database db, string tableName); - string GetIndexType(IndexTypes indexTypes); - string GetSpecialDbType(SpecialDbTypes dbTypes); - string CreateTable { get; } - string DropTable { get; } - string AddColumn { get; } - string DropColumn { get; } - string AlterColumn { get; } - string RenameColumn { get; } - string RenameTable { get; } - string CreateSchema { get; } - string AlterSchema { get; } - string DropSchema { get; } - string CreateIndex { get; } - string DropIndex { get; } - string InsertData { get; } - string UpdateData { get; } - string DeleteData { get; } - string TruncateTable { get; } - string CreateConstraint { get; } - string DeleteConstraint { get; } - string CreateForeignKeyConstraint { get; } - string DeleteDefaultConstraint { get; } - string FormatDateTime(DateTime date, bool includeTime = true); - string Format(TableDefinition table); - string Format(IEnumerable columns); - List Format(IEnumerable indexes); - List Format(IEnumerable foreignKeys); - string FormatPrimaryKey(TableDefinition table); - string GetQuotedValue(string value); - string Format(ColumnDefinition column); - string Format(IndexDefinition index); - string Format(ForeignKeyDefinition foreignKey); - string FormatColumnRename(string tableName, string oldName, string newName); - string FormatTableRename(string oldName, string newName); - bool SupportsClustered(); - bool SupportsIdentityInsert(); - bool? SupportsCaseInsensitiveQueries(Database db); + string GetQuotedTableName(string tableName); + string GetQuotedColumnName(string columnName); + string GetQuotedName(string name); + bool DoesTableExist(Database db, string tableName); + string GetIndexType(IndexTypes indexTypes); + string GetSpecialDbType(SpecialDbTypes dbTypes); + string CreateTable { get; } + string DropTable { get; } + string AddColumn { get; } + string DropColumn { get; } + string AlterColumn { get; } + string RenameColumn { get; } + string RenameTable { get; } + string CreateSchema { get; } + string AlterSchema { get; } + string DropSchema { get; } + string CreateIndex { get; } + string DropIndex { get; } + string InsertData { get; } + string UpdateData { get; } + string DeleteData { get; } + string TruncateTable { get; } + string CreateConstraint { get; } + string DeleteConstraint { get; } + string CreateForeignKeyConstraint { get; } + string DeleteDefaultConstraint { get; } + string FormatDateTime(DateTime date, bool includeTime = true); + string Format(TableDefinition table); + string Format(IEnumerable columns); + List Format(IEnumerable indexes); + List Format(IEnumerable foreignKeys); + string FormatPrimaryKey(TableDefinition table); + string GetQuotedValue(string value); + string Format(ColumnDefinition column); + string Format(IndexDefinition index); + string Format(ForeignKeyDefinition foreignKey); + string FormatColumnRename(string tableName, string oldName, string newName); + string FormatTableRename(string oldName, string newName); + bool SupportsClustered(); + bool SupportsIdentityInsert(); + bool? SupportsCaseInsensitiveQueries(Database db); - string IsNull { get; } - string ConvertIntegerToOrderableString { get; } - string ConvertDateToOrderableString { get; } + string IsNull { get; } + string ConvertIntegerToOrderableString { get; } + string ConvertDateToOrderableString { get; } - IEnumerable GetTablesInSchema(Database db); - IEnumerable GetColumnsInSchema(Database db); - IEnumerable> GetConstraintsPerTable(Database db); - IEnumerable> GetConstraintsPerColumn(Database db); + IEnumerable GetTablesInSchema(Database db); + IEnumerable GetColumnsInSchema(Database db); + IEnumerable> GetConstraintsPerTable(Database db); + IEnumerable> GetConstraintsPerColumn(Database db); - IEnumerable> GetDefinedIndexes(Database db); - } + IEnumerable> GetDefinedIndexes(Database db); + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/MySqlSyntaxProvider.cs b/src/Umbraco.Core/Persistence/SqlSyntax/MySqlSyntaxProvider.cs index c29afa7d32..c45aade707 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/MySqlSyntaxProvider.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/MySqlSyntaxProvider.cs @@ -7,393 +7,393 @@ using Umbraco.Core.Persistence.DatabaseModelDefinitions; namespace Umbraco.Core.Persistence.SqlSyntax { - /// - /// Represents an SqlSyntaxProvider for MySql - /// - [SqlSyntaxProviderAttribute("MySql.Data.MySqlClient")] - public class MySqlSyntaxProvider : SqlSyntaxProviderBase - { - private readonly ILogger _logger; + /// + /// Represents an SqlSyntaxProvider for MySql + /// + [SqlSyntaxProviderAttribute("MySql.Data.MySqlClient")] + public class MySqlSyntaxProvider : SqlSyntaxProviderBase + { + private readonly ILogger _logger; - public MySqlSyntaxProvider(ILogger logger) - { - _logger = logger; + public MySqlSyntaxProvider(ILogger logger) + { + _logger = logger; - AutoIncrementDefinition = "AUTO_INCREMENT"; - IntColumnDefinition = "int(11)"; - BoolColumnDefinition = "tinyint(1)"; - DateTimeColumnDefinition = "TIMESTAMP"; - TimeColumnDefinition = "time"; - DecimalColumnDefinition = "decimal(38,6)"; - GuidColumnDefinition = "char(36)"; + AutoIncrementDefinition = "AUTO_INCREMENT"; + IntColumnDefinition = "int(11)"; + BoolColumnDefinition = "tinyint(1)"; + DateTimeColumnDefinition = "TIMESTAMP"; + TimeColumnDefinition = "time"; + DecimalColumnDefinition = "decimal(38,6)"; + GuidColumnDefinition = "char(36)"; - DefaultValueFormat = "DEFAULT {0}"; + DefaultValueFormat = "DEFAULT {0}"; InitColumnTypeMap(); - } + } - public override IEnumerable GetTablesInSchema(Database db) - { - List list; - try - { - //needs to be open to read the schema name - db.OpenSharedConnection(); + public override IEnumerable GetTablesInSchema(Database db) + { + List list; + try + { + //needs to be open to read the schema name + db.OpenSharedConnection(); - var items = - db.Fetch( - "SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = @TableSchema", - new { TableSchema = db.Connection.Database }); - list = items.Select(x => x.TABLE_NAME).Cast().ToList(); - } - finally - { - db.CloseSharedConnection(); - } - return list; - } + var items = + db.Fetch( + "SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = @TableSchema", + new { TableSchema = db.Connection.Database }); + list = items.Select(x => x.TABLE_NAME).Cast().ToList(); + } + finally + { + db.CloseSharedConnection(); + } + return list; + } - public override IEnumerable GetColumnsInSchema(Database db) - { - List list; - try - { - //needs to be open to read the schema name - db.OpenSharedConnection(); + public override IEnumerable GetColumnsInSchema(Database db) + { + List list; + try + { + //needs to be open to read the schema name + db.OpenSharedConnection(); - var items = - db.Fetch( - "SELECT TABLE_NAME, COLUMN_NAME, ORDINAL_POSITION, COLUMN_DEFAULT, IS_NULLABLE, DATA_TYPE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = @TableSchema", - new { TableSchema = db.Connection.Database }); - list = - items.Select( - item => - new ColumnInfo(item.TABLE_NAME, item.COLUMN_NAME, int.Parse(item.ORDINAL_POSITION.ToString()), item.COLUMN_DEFAULT ?? "", - item.IS_NULLABLE, item.DATA_TYPE)).ToList(); - } - finally - { - db.CloseSharedConnection(); - } - return list; - } + var items = + db.Fetch( + "SELECT TABLE_NAME, COLUMN_NAME, ORDINAL_POSITION, COLUMN_DEFAULT, IS_NULLABLE, DATA_TYPE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = @TableSchema", + new { TableSchema = db.Connection.Database }); + list = + items.Select( + item => + new ColumnInfo(item.TABLE_NAME, item.COLUMN_NAME, int.Parse(item.ORDINAL_POSITION.ToString()), item.COLUMN_DEFAULT ?? "", + item.IS_NULLABLE, item.DATA_TYPE)).ToList(); + } + finally + { + db.CloseSharedConnection(); + } + return list; + } - public override IEnumerable> GetConstraintsPerTable(Database db) - { - List> list; - try - { - //needs to be open to read the schema name - db.OpenSharedConnection(); + public override IEnumerable> GetConstraintsPerTable(Database db) + { + List> list; + try + { + //needs to be open to read the schema name + db.OpenSharedConnection(); - //Does not include indexes and constraints are named differently - var items = - db.Fetch( - "SELECT TABLE_NAME, CONSTRAINT_NAME FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE TABLE_SCHEMA = @TableSchema", - new { TableSchema = db.Connection.Database }); - list = items.Select(item => new Tuple(item.TABLE_NAME, item.CONSTRAINT_NAME)).ToList(); - } - finally - { - db.CloseSharedConnection(); - } - return list; - } + //Does not include indexes and constraints are named differently + var items = + db.Fetch( + "SELECT TABLE_NAME, CONSTRAINT_NAME FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE TABLE_SCHEMA = @TableSchema", + new { TableSchema = db.Connection.Database }); + list = items.Select(item => new Tuple(item.TABLE_NAME, item.CONSTRAINT_NAME)).ToList(); + } + finally + { + db.CloseSharedConnection(); + } + return list; + } - public override IEnumerable> GetConstraintsPerColumn(Database db) - { - List> list; - try - { - //needs to be open to read the schema name - db.OpenSharedConnection(); + public override IEnumerable> GetConstraintsPerColumn(Database db) + { + List> list; + try + { + //needs to be open to read the schema name + db.OpenSharedConnection(); - //Does not include indexes and constraints are named differently - var items = - db.Fetch( - "SELECT TABLE_NAME, COLUMN_NAME, CONSTRAINT_NAME FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE WHERE TABLE_SCHEMA = @TableSchema", - new { TableSchema = db.Connection.Database }); - list = - items.Select( - item => - new Tuple(item.TABLE_NAME, item.COLUMN_NAME, item.CONSTRAINT_NAME)) - .ToList(); - } - finally - { - db.CloseSharedConnection(); - } - return list; - } + //Does not include indexes and constraints are named differently + var items = + db.Fetch( + "SELECT TABLE_NAME, COLUMN_NAME, CONSTRAINT_NAME FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE WHERE TABLE_SCHEMA = @TableSchema", + new { TableSchema = db.Connection.Database }); + list = + items.Select( + item => + new Tuple(item.TABLE_NAME, item.COLUMN_NAME, item.CONSTRAINT_NAME)) + .ToList(); + } + finally + { + db.CloseSharedConnection(); + } + return list; + } - public override IEnumerable> GetDefinedIndexes(Database db) - { - List> list; - try - { - //needs to be open to read the schema name - db.OpenSharedConnection(); + public override IEnumerable> GetDefinedIndexes(Database db) + { + List> list; + try + { + //needs to be open to read the schema name + db.OpenSharedConnection(); - var indexes = - db.Fetch(@"SELECT DISTINCT + var indexes = + db.Fetch(@"SELECT DISTINCT TABLE_NAME, INDEX_NAME, COLUMN_NAME, CASE NON_UNIQUE WHEN 1 THEN 0 ELSE 1 END AS `UNIQUE` FROM INFORMATION_SCHEMA.STATISTICS WHERE TABLE_SCHEMA = @TableSchema AND INDEX_NAME <> COLUMN_NAME AND INDEX_NAME <> 'PRIMARY' ORDER BY TABLE_NAME, INDEX_NAME", - new { TableSchema = db.Connection.Database }); - list = - indexes.Select( - item => - new Tuple(item.TABLE_NAME, item.INDEX_NAME, item.COLUMN_NAME, item.UNIQUE == 1)) - .ToList(); - } - finally - { - db.CloseSharedConnection(); - } - return list; - } + new { TableSchema = db.Connection.Database }); + list = + indexes.Select( + item => + new Tuple(item.TABLE_NAME, item.INDEX_NAME, item.COLUMN_NAME, item.UNIQUE == 1)) + .ToList(); + } + finally + { + db.CloseSharedConnection(); + } + return list; + } - public override bool DoesTableExist(Database db, string tableName) - { - long result; - try - { - //needs to be open to read the schema name - db.OpenSharedConnection(); + public override bool DoesTableExist(Database db, string tableName) + { + long result; + try + { + //needs to be open to read the schema name + db.OpenSharedConnection(); - result = - db.ExecuteScalar("SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES " + - "WHERE TABLE_NAME = @TableName AND " + - "TABLE_SCHEMA = @TableSchema", - new { TableName = tableName, TableSchema = db.Connection.Database }); + result = + db.ExecuteScalar("SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES " + + "WHERE TABLE_NAME = @TableName AND " + + "TABLE_SCHEMA = @TableSchema", + new { TableName = tableName, TableSchema = db.Connection.Database }); - } - finally - { - db.CloseSharedConnection(); - } + } + finally + { + db.CloseSharedConnection(); + } - return result > 0; - } + return result > 0; + } - public override bool SupportsClustered() - { - return true; - } + public override bool SupportsClustered() + { + return true; + } - public override bool SupportsIdentityInsert() - { - return false; - } + public override bool SupportsIdentityInsert() + { + return false; + } - /// - /// This is used ONLY if we need to format datetime without using SQL parameters (i.e. during migrations) - /// - /// - /// - /// - /// - /// MySQL has a DateTime standard that is unambiguous and works on all servers: - /// YYYYMMDDHHMMSS - /// - public override string FormatDateTime(DateTime date, bool includeTime = true) - { - return includeTime ? date.ToString("yyyyMMddHHmmss") : date.ToString("yyyyMMdd"); - } + /// + /// This is used ONLY if we need to format datetime without using SQL parameters (i.e. during migrations) + /// + /// + /// + /// + /// + /// MySQL has a DateTime standard that is unambiguous and works on all servers: + /// YYYYMMDDHHMMSS + /// + public override string FormatDateTime(DateTime date, bool includeTime = true) + { + return includeTime ? date.ToString("yyyyMMddHHmmss") : date.ToString("yyyyMMdd"); + } - public override string GetQuotedTableName(string tableName) - { - return string.Format("`{0}`", tableName); - } + public override string GetQuotedTableName(string tableName) + { + return string.Format("`{0}`", tableName); + } - public override string GetQuotedColumnName(string columnName) - { - return string.Format("`{0}`", columnName); - } + public override string GetQuotedColumnName(string columnName) + { + return string.Format("`{0}`", columnName); + } - public override string GetQuotedName(string name) - { - return string.Format("`{0}`", name); - } + public override string GetQuotedName(string name) + { + return string.Format("`{0}`", name); + } - public override string GetSpecialDbType(SpecialDbTypes dbTypes) - { - if (dbTypes == SpecialDbTypes.NCHAR) - { - return "CHAR"; - } - else if (dbTypes == SpecialDbTypes.NTEXT) - return "LONGTEXT"; + public override string GetSpecialDbType(SpecialDbTypes dbTypes) + { + if (dbTypes == SpecialDbTypes.NCHAR) + { + return "CHAR"; + } + else if (dbTypes == SpecialDbTypes.NTEXT) + return "LONGTEXT"; - return "NVARCHAR"; - } + return "NVARCHAR"; + } - public override string Format(TableDefinition table) - { - string primaryKey = string.Empty; - var columnDefinition = table.Columns.FirstOrDefault(x => x.IsPrimaryKey); - if (columnDefinition != null) - { - string columns = string.IsNullOrEmpty(columnDefinition.PrimaryKeyColumns) - ? GetQuotedColumnName(columnDefinition.Name) - : string.Join(", ", columnDefinition.PrimaryKeyColumns - .Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries) - .Select(GetQuotedColumnName)); + public override string Format(TableDefinition table) + { + string primaryKey = string.Empty; + var columnDefinition = table.Columns.FirstOrDefault(x => x.IsPrimaryKey); + if (columnDefinition != null) + { + string columns = string.IsNullOrEmpty(columnDefinition.PrimaryKeyColumns) + ? GetQuotedColumnName(columnDefinition.Name) + : string.Join(", ", columnDefinition.PrimaryKeyColumns + .Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries) + .Select(GetQuotedColumnName)); - primaryKey = string.Format(", \nPRIMARY KEY {0} ({1})", columnDefinition.IsIndexed ? "CLUSTERED" : "NONCLUSTERED", columns); - } + primaryKey = string.Format(", \nPRIMARY KEY {0} ({1})", columnDefinition.IsIndexed ? "CLUSTERED" : "NONCLUSTERED", columns); + } - var statement = string.Format(CreateTable, GetQuotedTableName(table.Name), Format(table.Columns), primaryKey); + var statement = string.Format(CreateTable, GetQuotedTableName(table.Name), Format(table.Columns), primaryKey); - return statement; - } + return statement; + } - public override string Format(IndexDefinition index) - { - string name = string.IsNullOrEmpty(index.Name) - ? string.Format("IX_{0}_{1}", index.TableName, index.ColumnName) - : index.Name; + public override string Format(IndexDefinition index) + { + string name = string.IsNullOrEmpty(index.Name) + ? string.Format("IX_{0}_{1}", index.TableName, index.ColumnName) + : index.Name; - string columns = index.Columns.Any() - ? string.Join(",", index.Columns.Select(x => GetQuotedColumnName(x.Name))) - : GetQuotedColumnName(index.ColumnName); + string columns = index.Columns.Any() + ? string.Join(",", index.Columns.Select(x => GetQuotedColumnName(x.Name))) + : GetQuotedColumnName(index.ColumnName); - return string.Format(CreateIndex, - GetQuotedName(name), - GetQuotedTableName(index.TableName), - columns); - } + return string.Format(CreateIndex, + GetQuotedName(name), + GetQuotedTableName(index.TableName), + columns); + } - public override string Format(ForeignKeyDefinition foreignKey) - { - return string.Format(CreateForeignKeyConstraint, - GetQuotedTableName(foreignKey.ForeignTable), - GetQuotedColumnName(foreignKey.ForeignColumns.First()), - GetQuotedTableName(foreignKey.PrimaryTable), - GetQuotedColumnName(foreignKey.PrimaryColumns.First()), - FormatCascade("DELETE", foreignKey.OnDelete), - FormatCascade("UPDATE", foreignKey.OnUpdate)); - } + public override string Format(ForeignKeyDefinition foreignKey) + { + return string.Format(CreateForeignKeyConstraint, + GetQuotedTableName(foreignKey.ForeignTable), + GetQuotedColumnName(foreignKey.ForeignColumns.First()), + GetQuotedTableName(foreignKey.PrimaryTable), + GetQuotedColumnName(foreignKey.PrimaryColumns.First()), + FormatCascade("DELETE", foreignKey.OnDelete), + FormatCascade("UPDATE", foreignKey.OnUpdate)); + } - public override string FormatPrimaryKey(TableDefinition table) - { - return string.Empty; - } + public override string FormatPrimaryKey(TableDefinition table) + { + return string.Empty; + } - protected override string FormatConstraint(ColumnDefinition column) - { - return string.Empty; - } + protected override string FormatConstraint(ColumnDefinition column) + { + return string.Empty; + } - protected override string FormatIdentity(ColumnDefinition column) - { - return column.IsIdentity ? AutoIncrementDefinition : string.Empty; - } + protected override string FormatIdentity(ColumnDefinition column) + { + return column.IsIdentity ? AutoIncrementDefinition : string.Empty; + } - protected override string FormatDefaultValue(ColumnDefinition column) - { - if (column.DefaultValue == null) - return string.Empty; + protected override string FormatDefaultValue(ColumnDefinition column) + { + if (column.DefaultValue == null) + return string.Empty; - //hack - probably not needed with latest changes - if (column.DefaultValue.ToString().ToLower().Equals("getdate()".ToLower())) - column.DefaultValue = SystemMethods.CurrentDateTime; + //hack - probably not needed with latest changes + if (column.DefaultValue.ToString().ToLower().Equals("getdate()".ToLower())) + column.DefaultValue = SystemMethods.CurrentDateTime; - // see if this is for a system method - if (column.DefaultValue is SystemMethods) - { - string method = FormatSystemMethods((SystemMethods)column.DefaultValue); - if (string.IsNullOrEmpty(method)) - return string.Empty; + // see if this is for a system method + if (column.DefaultValue is SystemMethods) + { + string method = FormatSystemMethods((SystemMethods)column.DefaultValue); + if (string.IsNullOrEmpty(method)) + return string.Empty; - return string.Format(DefaultValueFormat, method); - } + return string.Format(DefaultValueFormat, method); + } - //needs quote - return string.Format(DefaultValueFormat, string.Format("'{0}'", column.DefaultValue)); - } + //needs quote + return string.Format(DefaultValueFormat, string.Format("'{0}'", column.DefaultValue)); + } - protected override string FormatPrimaryKey(ColumnDefinition column) - { - return string.Empty; - } + protected override string FormatPrimaryKey(ColumnDefinition column) + { + return string.Empty; + } - protected override string FormatSystemMethods(SystemMethods systemMethod) - { - switch (systemMethod) - { - case SystemMethods.NewGuid: - return null; // NOT SUPPORTED! - //return "NEWID()"; - case SystemMethods.CurrentDateTime: - return "CURRENT_TIMESTAMP"; - //case SystemMethods.NewSequentialId: - // return "NEWSEQUENTIALID()"; - //case SystemMethods.CurrentUTCDateTime: - // return "GETUTCDATE()"; - } + protected override string FormatSystemMethods(SystemMethods systemMethod) + { + switch (systemMethod) + { + case SystemMethods.NewGuid: + return null; // NOT SUPPORTED! + //return "NEWID()"; + case SystemMethods.CurrentDateTime: + return "CURRENT_TIMESTAMP"; + //case SystemMethods.NewSequentialId: + // return "NEWSEQUENTIALID()"; + //case SystemMethods.CurrentUTCDateTime: + // return "GETUTCDATE()"; + } - return null; - } + return null; + } - public override string DeleteDefaultConstraint - { - get - { - return "ALTER TABLE {0} ALTER COLUMN {1} DROP DEFAULT"; - } - } + public override string DeleteDefaultConstraint + { + get + { + return "ALTER TABLE {0} ALTER COLUMN {1} DROP DEFAULT"; + } + } - public override string AlterColumn { get { return "ALTER TABLE {0} MODIFY COLUMN {1}"; } } + public override string AlterColumn { get { return "ALTER TABLE {0} MODIFY COLUMN {1}"; } } - //CREATE TABLE {0} ({1}) ENGINE = INNODB versus CREATE TABLE {0} ({1}) ENGINE = MYISAM ? - public override string CreateTable { get { return "CREATE TABLE {0} ({1}{2})"; } } + //CREATE TABLE {0} ({1}) ENGINE = INNODB versus CREATE TABLE {0} ({1}) ENGINE = MYISAM ? + public override string CreateTable { get { return "CREATE TABLE {0} ({1}{2})"; } } - public override string CreateIndex { get { return "CREATE INDEX {0} ON {1} ({2})"; } } + public override string CreateIndex { get { return "CREATE INDEX {0} ON {1} ({2})"; } } - public override string CreateForeignKeyConstraint { get { return "ALTER TABLE {0} ADD FOREIGN KEY ({1}) REFERENCES {2} ({3}){4}{5}"; } } + public override string CreateForeignKeyConstraint { get { return "ALTER TABLE {0} ADD FOREIGN KEY ({1}) REFERENCES {2} ({3}){4}{5}"; } } - public override string DeleteConstraint { get { return "ALTER TABLE {0} DROP {1} {2}"; } } + public override string DeleteConstraint { get { return "ALTER TABLE {0} DROP {1} {2}"; } } - public override string DropIndex { get { return "DROP INDEX {0} ON {1}"; } } + public override string DropIndex { get { return "DROP INDEX {0} ON {1}"; } } - public override string RenameColumn { get { return "ALTER TABLE {0} CHANGE {1} {2}"; } } - public override string IsNull { get { return "IFNULL({0},{1})"; } } - public override string ConvertIntegerToOrderableString { get { return "LPAD({0}, 8, '0')"; } } - public override string ConvertDateToOrderableString { get { return "DATE_FORMAT({0}, '%Y%m%d')"; } } + public override string RenameColumn { get { return "ALTER TABLE {0} CHANGE {1} {2}"; } } + public override string IsNull { get { return "IFNULL({0},{1})"; } } + public override string ConvertIntegerToOrderableString { get { return "LPAD({0}, 8, '0')"; } } + public override string ConvertDateToOrderableString { get { return "DATE_FORMAT({0}, '%Y%m%d')"; } } - public override bool? SupportsCaseInsensitiveQueries(Database db) - { - bool? supportsCaseInsensitiveQueries = null; + public override bool? SupportsCaseInsensitiveQueries(Database db) + { + bool? supportsCaseInsensitiveQueries = null; - try - { - db.OpenSharedConnection(); - // Need 4 @ signs as it is regarded as a parameter, @@ escapes it once, @@@@ escapes it twice - var lowerCaseTableNames = db.Fetch("SELECT @@@@Global.lower_case_table_names"); + try + { + db.OpenSharedConnection(); + // Need 4 @ signs as it is regarded as a parameter, @@ escapes it once, @@@@ escapes it twice + var lowerCaseTableNames = db.Fetch("SELECT @@@@Global.lower_case_table_names"); - if (lowerCaseTableNames.Any()) - supportsCaseInsensitiveQueries = lowerCaseTableNames.First() == 1; - } - catch (Exception ex) - { - _logger.Error("Error querying for lower_case support", ex); - } - finally - { - db.CloseSharedConnection(); - } + if (lowerCaseTableNames.Any()) + supportsCaseInsensitiveQueries = lowerCaseTableNames.First() == 1; + } + catch (Exception ex) + { + _logger.Error("Error querying for lower_case support", ex); + } + finally + { + db.CloseSharedConnection(); + } - // Could return null, which means testing failed, - // add message to check with their hosting provider - return supportsCaseInsensitiveQueries; - } + // Could return null, which means testing failed, + // add message to check with their hosting provider + return supportsCaseInsensitiveQueries; + } - public override string EscapeString(string val) - { - return PetaPocoExtensions.EscapeAtSymbols(MySql.Data.MySqlClient.MySqlHelper.EscapeString(val)); - } - } + public override string EscapeString(string val) + { + return PetaPocoExtensions.EscapeAtSymbols(MySql.Data.MySqlClient.MySqlHelper.EscapeString(val)); + } + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs b/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs index 3b3fe103d0..35c133ce6e 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs @@ -10,535 +10,535 @@ using Umbraco.Core.Persistence.Querying; namespace Umbraco.Core.Persistence.SqlSyntax { - /// - /// Represents the Base Sql Syntax provider implementation. - /// - /// - /// All Sql Syntax provider implementations should derive from this abstract class. - /// - /// - public abstract class SqlSyntaxProviderBase : ISqlSyntaxProvider - where TSyntax : ISqlSyntaxProvider - { - protected SqlSyntaxProviderBase() - { - ClauseOrder = new List> - { - FormatString, - FormatType, - FormatNullable, - FormatConstraint, - FormatDefaultValue, - FormatPrimaryKey, - FormatIdentity - }; - - //defaults for all providers - StringLengthColumnDefinitionFormat = StringLengthUnicodeColumnDefinitionFormat; - StringColumnDefinition = string.Format(StringLengthColumnDefinitionFormat, DefaultStringLength); - DecimalColumnDefinition = string.Format(DecimalColumnDefinitionFormat, DefaultDecimalPrecision, DefaultDecimalScale); - - InitColumnTypeMap(); - } - - public string GetWildcardPlaceholder() - { - return "%"; - } - - public string StringLengthNonUnicodeColumnDefinitionFormat = "VARCHAR({0})"; - public string StringLengthUnicodeColumnDefinitionFormat = "NVARCHAR({0})"; - public string DecimalColumnDefinitionFormat = "DECIMAL({0},{1})"; - - public string DefaultValueFormat = "DEFAULT ({0})"; - public int DefaultStringLength = 255; - public int DefaultDecimalPrecision = 20; - public int DefaultDecimalScale = 9; - - //Set by Constructor - public string StringColumnDefinition; - public string StringLengthColumnDefinitionFormat; - - public string AutoIncrementDefinition = "AUTOINCREMENT"; - public string IntColumnDefinition = "INTEGER"; - public string LongColumnDefinition = "BIGINT"; - public string GuidColumnDefinition = "GUID"; - public string BoolColumnDefinition = "BOOL"; - public string RealColumnDefinition = "DOUBLE"; - public string DecimalColumnDefinition; - public string BlobColumnDefinition = "BLOB"; - public string DateTimeColumnDefinition = "DATETIME"; - public string TimeColumnDefinition = "DATETIME"; - - protected IList> ClauseOrder { get; set; } - - protected DbTypes DbTypeMap = new DbTypes(); - protected void InitColumnTypeMap() - { - DbTypeMap.Set(DbType.String, StringColumnDefinition); - DbTypeMap.Set(DbType.StringFixedLength, StringColumnDefinition); - DbTypeMap.Set(DbType.StringFixedLength, StringColumnDefinition); - DbTypeMap.Set(DbType.String, StringColumnDefinition); - DbTypeMap.Set(DbType.Boolean, BoolColumnDefinition); - DbTypeMap.Set(DbType.Boolean, BoolColumnDefinition); - DbTypeMap.Set(DbType.Guid, GuidColumnDefinition); - DbTypeMap.Set(DbType.Guid, GuidColumnDefinition); - DbTypeMap.Set(DbType.DateTime, DateTimeColumnDefinition); - DbTypeMap.Set(DbType.DateTime, DateTimeColumnDefinition); - DbTypeMap.Set(DbType.Time, TimeColumnDefinition); - DbTypeMap.Set(DbType.Time, TimeColumnDefinition); - DbTypeMap.Set(DbType.Time, TimeColumnDefinition); - DbTypeMap.Set(DbType.Time, TimeColumnDefinition); - - DbTypeMap.Set(DbType.Byte, IntColumnDefinition); - DbTypeMap.Set(DbType.Byte, IntColumnDefinition); - DbTypeMap.Set(DbType.SByte, IntColumnDefinition); - DbTypeMap.Set(DbType.SByte, IntColumnDefinition); - DbTypeMap.Set(DbType.Int16, IntColumnDefinition); - DbTypeMap.Set(DbType.Int16, IntColumnDefinition); - DbTypeMap.Set(DbType.UInt16, IntColumnDefinition); - DbTypeMap.Set(DbType.UInt16, IntColumnDefinition); - DbTypeMap.Set(DbType.Int32, IntColumnDefinition); - DbTypeMap.Set(DbType.Int32, IntColumnDefinition); - DbTypeMap.Set(DbType.UInt32, IntColumnDefinition); - DbTypeMap.Set(DbType.UInt32, IntColumnDefinition); - - DbTypeMap.Set(DbType.Int64, LongColumnDefinition); - DbTypeMap.Set(DbType.Int64, LongColumnDefinition); - DbTypeMap.Set(DbType.UInt64, LongColumnDefinition); - DbTypeMap.Set(DbType.UInt64, LongColumnDefinition); - - DbTypeMap.Set(DbType.Single, RealColumnDefinition); - DbTypeMap.Set(DbType.Single, RealColumnDefinition); - DbTypeMap.Set(DbType.Double, RealColumnDefinition); - DbTypeMap.Set(DbType.Double, RealColumnDefinition); - - DbTypeMap.Set(DbType.Decimal, DecimalColumnDefinition); - DbTypeMap.Set(DbType.Decimal, DecimalColumnDefinition); - - DbTypeMap.Set(DbType.Binary, BlobColumnDefinition); - } - - public virtual string EscapeString(string val) - { - return PetaPocoExtensions.EscapeAtSymbols(val.Replace("'", "''")); - } - - public virtual string GetStringColumnEqualComparison(string column, int paramIndex, TextColumnType columnType) - { - //use the 'upper' method to always ensure strings are matched without case sensitivity no matter what the db setting. - return string.Format("upper({0}) = upper(@{1})", column, paramIndex); - } - - public virtual string GetStringColumnWildcardComparison(string column, int paramIndex, TextColumnType columnType) - { - //use the 'upper' method to always ensure strings are matched without case sensitivity no matter what the db setting. - return string.Format("upper({0}) LIKE upper(@{1})", column, paramIndex); - } - - [Obsolete("Use the overload with the parameter index instead")] - public virtual string GetStringColumnEqualComparison(string column, string value, TextColumnType columnType) - { - //use the 'upper' method to always ensure strings are matched without case sensitivity no matter what the db setting. - return string.Format("upper({0}) = '{1}'", column, value.ToUpper()); - } - - [Obsolete("Use the overload with the parameter index instead")] - public virtual string GetStringColumnStartsWithComparison(string column, string value, TextColumnType columnType) - { - //use the 'upper' method to always ensure strings are matched without case sensitivity no matter what the db setting. - return string.Format("upper({0}) LIKE '{1}%'", column, value.ToUpper()); - } - - [Obsolete("Use the overload with the parameter index instead")] - public virtual string GetStringColumnEndsWithComparison(string column, string value, TextColumnType columnType) - { - //use the 'upper' method to always ensure strings are matched without case sensitivity no matter what the db setting. - return string.Format("upper({0}) LIKE '%{1}'", column, value.ToUpper()); - } - - [Obsolete("Use the overload with the parameter index instead")] - public virtual string GetStringColumnContainsComparison(string column, string value, TextColumnType columnType) - { - //use the 'upper' method to always ensure strings are matched without case sensitivity no matter what the db setting. - return string.Format("upper({0}) LIKE '%{1}%'", column, value.ToUpper()); - } - - [Obsolete("Use the overload with the parameter index instead")] - public virtual string GetStringColumnWildcardComparison(string column, string value, TextColumnType columnType) - { - //use the 'upper' method to always ensure strings are matched without case sensitivity no matter what the db setting. - return string.Format("upper({0}) LIKE '{1}'", column, value.ToUpper()); - } - - public virtual string GetQuotedTableName(string tableName) - { - return string.Format("\"{0}\"", tableName); - } - - public virtual string GetQuotedColumnName(string columnName) - { - return string.Format("\"{0}\"", columnName); - } - - public virtual string GetQuotedName(string name) - { - return string.Format("\"{0}\"", name); - } - - public virtual string GetQuotedValue(string value) - { - return string.Format("'{0}'", value); - } - - public virtual string GetIndexType(IndexTypes indexTypes) - { - string indexType; - - if (indexTypes == IndexTypes.Clustered) - { - indexType = "CLUSTERED"; - } - else - { - indexType = indexTypes == IndexTypes.NonClustered - ? "NONCLUSTERED" - : "UNIQUE NONCLUSTERED"; - } - return indexType; - } - - public virtual string GetSpecialDbType(SpecialDbTypes dbTypes) - { - if (dbTypes == SpecialDbTypes.NCHAR) - { - return "NCHAR"; - } - else if (dbTypes == SpecialDbTypes.NTEXT) - return "NTEXT"; - - return "NVARCHAR"; - } - - public virtual bool? SupportsCaseInsensitiveQueries(Database db) - { - return true; - } - - public virtual IEnumerable GetTablesInSchema(Database db) - { - return new List(); - } - - public virtual IEnumerable GetColumnsInSchema(Database db) - { - return new List(); - } - - public virtual IEnumerable> GetConstraintsPerTable(Database db) - { - return new List>(); - } - - public virtual IEnumerable> GetConstraintsPerColumn(Database db) - { - return new List>(); - } - - public abstract IEnumerable> GetDefinedIndexes(Database db); - - public virtual bool DoesTableExist(Database db, string tableName) - { - return false; - } - - public virtual bool SupportsClustered() - { - return true; - } - - public virtual bool SupportsIdentityInsert() - { - return true; - } - - /// - /// This is used ONLY if we need to format datetime without using SQL parameters (i.e. during migrations) - /// - /// - /// - /// - /// - /// MSSQL has a DateTime standard that is unambiguous and works on all servers: - /// YYYYMMDD HH:mm:ss - /// - public virtual string FormatDateTime(DateTime date, bool includeTime = true) - { - // need CultureInfo.InvariantCulture because ":" here is the "time separator" and - // may be converted to something else in different cultures (eg "." in DK). - return date.ToString(includeTime ? "yyyyMMdd HH:mm:ss" : "yyyyMMdd", CultureInfo.InvariantCulture); - } - - public virtual string Format(TableDefinition table) - { - var statement = string.Format(CreateTable, GetQuotedTableName(table.Name), Format(table.Columns)); - - return statement; - } - - public virtual List Format(IEnumerable indexes) - { - return indexes.Select(Format).ToList(); - } - - public virtual string Format(IndexDefinition index) - { - string name = string.IsNullOrEmpty(index.Name) - ? string.Format("IX_{0}_{1}", index.TableName, index.ColumnName) - : index.Name; - - string columns = index.Columns.Any() - ? string.Join(",", index.Columns.Select(x => GetQuotedColumnName(x.Name))) - : GetQuotedColumnName(index.ColumnName); - - return string.Format(CreateIndex, GetIndexType(index.IndexType), " ", GetQuotedName(name), - GetQuotedTableName(index.TableName), columns); - } - - public virtual List Format(IEnumerable foreignKeys) - { - return foreignKeys.Select(Format).ToList(); - } - - public virtual string Format(ForeignKeyDefinition foreignKey) - { - string constraintName = string.IsNullOrEmpty(foreignKey.Name) - ? string.Format("FK_{0}_{1}_{2}", foreignKey.ForeignTable, foreignKey.PrimaryTable, foreignKey.PrimaryColumns.First()) - : foreignKey.Name; - - return string.Format(CreateForeignKeyConstraint, - GetQuotedTableName(foreignKey.ForeignTable), - GetQuotedName(constraintName), - GetQuotedColumnName(foreignKey.ForeignColumns.First()), - GetQuotedTableName(foreignKey.PrimaryTable), - GetQuotedColumnName(foreignKey.PrimaryColumns.First()), - FormatCascade("DELETE", foreignKey.OnDelete), - FormatCascade("UPDATE", foreignKey.OnUpdate)); - } - - public virtual string Format(IEnumerable columns) - { - var sb = new StringBuilder(); - foreach (var column in columns) - { - sb.Append(Format(column) + ",\n"); - } - return sb.ToString().TrimEnd(",\n"); - } - - public virtual string Format(ColumnDefinition column) - { - var clauses = new List(); - - foreach (var action in ClauseOrder) - { - string clause = action(column); - if (!string.IsNullOrEmpty(clause)) - clauses.Add(clause); - } - - return string.Join(" ", clauses.ToArray()); - } - - public virtual string FormatPrimaryKey(TableDefinition table) - { - var columnDefinition = table.Columns.FirstOrDefault(x => x.IsPrimaryKey); - if (columnDefinition == null) - return string.Empty; - - string constraintName = string.IsNullOrEmpty(columnDefinition.PrimaryKeyName) - ? string.Format("PK_{0}", table.Name) - : columnDefinition.PrimaryKeyName; - - string columns = string.IsNullOrEmpty(columnDefinition.PrimaryKeyColumns) - ? GetQuotedColumnName(columnDefinition.Name) - : string.Join(", ", columnDefinition.PrimaryKeyColumns - .Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries) - .Select(GetQuotedColumnName)); - - string primaryKeyPart = string.Concat("PRIMARY KEY", columnDefinition.IsIndexed ? " CLUSTERED" : " NONCLUSTERED"); - - return string.Format(CreateConstraint, - GetQuotedTableName(table.Name), - GetQuotedName(constraintName), - primaryKeyPart, - columns); - } - - public virtual string FormatColumnRename(string tableName, string oldName, string newName) - { - return string.Format(RenameColumn, - GetQuotedTableName(tableName), - GetQuotedColumnName(oldName), - GetQuotedColumnName(newName)); - } - - public virtual string FormatTableRename(string oldName, string newName) - { - return string.Format(RenameTable, GetQuotedTableName(oldName), GetQuotedTableName(newName)); - } - - protected virtual string FormatCascade(string onWhat, Rule rule) - { - string action = "NO ACTION"; - switch (rule) - { - case Rule.None: - return ""; - case Rule.Cascade: - action = "CASCADE"; - break; - case Rule.SetNull: - action = "SET NULL"; - break; - case Rule.SetDefault: - action = "SET DEFAULT"; - break; - } - - return string.Format(" ON {0} {1}", onWhat, action); - } - - protected virtual string FormatString(ColumnDefinition column) - { - return GetQuotedColumnName(column.Name); - } - - protected virtual string FormatType(ColumnDefinition column) - { - if (column.Type.HasValue == false && string.IsNullOrEmpty(column.CustomType) == false) - return column.CustomType; - - if (column.HasSpecialDbType) - { - if (column.Size != default(int)) - { - return string.Format("{0}({1})", - GetSpecialDbType(column.DbType), - column.Size); - } - - return GetSpecialDbType(column.DbType); - } - - Type type = column.Type.HasValue - ? DbTypeMap.ColumnDbTypeMap.First(x => x.Value == column.Type.Value).Key - : column.PropertyType; - - if (type == typeof(string)) - { - var valueOrDefault = column.Size != default(int) ? column.Size : DefaultStringLength; - return string.Format(StringLengthColumnDefinitionFormat, valueOrDefault); - } - - if (type == typeof(decimal)) - { - var precision = column.Size != default(int) ? column.Size : DefaultDecimalPrecision; - var scale = column.Precision != default(int) ? column.Precision : DefaultDecimalScale; - return string.Format(DecimalColumnDefinitionFormat, precision, scale); - } - - string definition = DbTypeMap.ColumnTypeMap.First(x => x.Key == type).Value; - string dbTypeDefinition = column.Size != default(int) - ? string.Format("{0}({1})", definition, column.Size) - : definition; - //NOTE Percision is left out - return dbTypeDefinition; - } - - protected virtual string FormatNullable(ColumnDefinition column) - { - return column.IsNullable ? "NULL" : "NOT NULL"; - } - - protected virtual string FormatConstraint(ColumnDefinition column) - { - if (string.IsNullOrEmpty(column.ConstraintName) && column.DefaultValue == null) - return string.Empty; - - return string.Format("CONSTRAINT {0}", - string.IsNullOrEmpty(column.ConstraintName) - ? GetQuotedName(string.Format("DF_{0}_{1}", column.TableName, column.Name)) - : column.ConstraintName); - } - - protected virtual string FormatDefaultValue(ColumnDefinition column) - { - if (column.DefaultValue == null) - return string.Empty; - - //hack - probably not needed with latest changes - if (column.DefaultValue.ToString().ToLower().Equals("getdate()".ToLower())) - column.DefaultValue = SystemMethods.CurrentDateTime; - - // see if this is for a system method - if (column.DefaultValue is SystemMethods) - { - string method = FormatSystemMethods((SystemMethods)column.DefaultValue); - if (string.IsNullOrEmpty(method)) - return string.Empty; - - return string.Format(DefaultValueFormat, method); - } - - return string.Format(DefaultValueFormat, GetQuotedValue(column.DefaultValue.ToString())); - } - - protected virtual string FormatPrimaryKey(ColumnDefinition column) - { - return string.Empty; - } - - protected abstract string FormatSystemMethods(SystemMethods systemMethod); - - protected abstract string FormatIdentity(ColumnDefinition column); - - public virtual string DeleteDefaultConstraint - { - get - { - throw new NotSupportedException("Default constraints are not supported"); - } - } - - public virtual string CreateTable { get { return "CREATE TABLE {0} ({1})"; } } - public virtual string DropTable { get { return "DROP TABLE {0}"; } } - - public virtual string AddColumn { get { return "ALTER TABLE {0} ADD COLUMN {1}"; } } - public virtual string DropColumn { get { return "ALTER TABLE {0} DROP COLUMN {1}"; } } - public virtual string AlterColumn { get { return "ALTER TABLE {0} ALTER COLUMN {1}"; } } - public virtual string RenameColumn { get { return "ALTER TABLE {0} RENAME COLUMN {1} TO {2}"; } } - - public virtual string RenameTable { get { return "RENAME TABLE {0} TO {1}"; } } - - public virtual string CreateSchema { get { return "CREATE SCHEMA {0}"; } } - public virtual string AlterSchema { get { return "ALTER SCHEMA {0} TRANSFER {1}.{2}"; } } - public virtual string DropSchema { get { return "DROP SCHEMA {0}"; } } - - public virtual string CreateIndex { get { return "CREATE {0}{1}INDEX {2} ON {3} ({4})"; } } - public virtual string DropIndex { get { return "DROP INDEX {0}"; } } - - public virtual string InsertData { get { return "INSERT INTO {0} ({1}) VALUES ({2})"; } } - public virtual string UpdateData { get { return "UPDATE {0} SET {1} WHERE {2}"; } } - public virtual string DeleteData { get { return "DELETE FROM {0} WHERE {1}"; } } - public virtual string TruncateTable { get { return "TRUNCATE TABLE {0}"; } } - - public virtual string CreateConstraint { get { return "ALTER TABLE {0} ADD CONSTRAINT {1} {2} ({3})"; } } - public virtual string DeleteConstraint { get { return "ALTER TABLE {0} DROP CONSTRAINT {1}"; } } - public virtual string CreateForeignKeyConstraint { get { return "ALTER TABLE {0} ADD CONSTRAINT {1} FOREIGN KEY ({2}) REFERENCES {3} ({4}){5}{6}"; } } - - public virtual string IsNull { get { return "ISNULL({0},{1})"; } } - public virtual string ConvertIntegerToOrderableString { get { return "RIGHT('00000000' + CAST({0} AS varchar(8)),8)"; } } - public virtual string ConvertDateToOrderableString { get { return "CONVERT(varchar, {0}, 102)"; } } - } + /// + /// Represents the Base Sql Syntax provider implementation. + /// + /// + /// All Sql Syntax provider implementations should derive from this abstract class. + /// + /// + public abstract class SqlSyntaxProviderBase : ISqlSyntaxProvider + where TSyntax : ISqlSyntaxProvider + { + protected SqlSyntaxProviderBase() + { + ClauseOrder = new List> + { + FormatString, + FormatType, + FormatNullable, + FormatConstraint, + FormatDefaultValue, + FormatPrimaryKey, + FormatIdentity + }; + + //defaults for all providers + StringLengthColumnDefinitionFormat = StringLengthUnicodeColumnDefinitionFormat; + StringColumnDefinition = string.Format(StringLengthColumnDefinitionFormat, DefaultStringLength); + DecimalColumnDefinition = string.Format(DecimalColumnDefinitionFormat, DefaultDecimalPrecision, DefaultDecimalScale); + + InitColumnTypeMap(); + } + + public string GetWildcardPlaceholder() + { + return "%"; + } + + public string StringLengthNonUnicodeColumnDefinitionFormat = "VARCHAR({0})"; + public string StringLengthUnicodeColumnDefinitionFormat = "NVARCHAR({0})"; + public string DecimalColumnDefinitionFormat = "DECIMAL({0},{1})"; + + public string DefaultValueFormat = "DEFAULT ({0})"; + public int DefaultStringLength = 255; + public int DefaultDecimalPrecision = 20; + public int DefaultDecimalScale = 9; + + //Set by Constructor + public string StringColumnDefinition; + public string StringLengthColumnDefinitionFormat; + + public string AutoIncrementDefinition = "AUTOINCREMENT"; + public string IntColumnDefinition = "INTEGER"; + public string LongColumnDefinition = "BIGINT"; + public string GuidColumnDefinition = "GUID"; + public string BoolColumnDefinition = "BOOL"; + public string RealColumnDefinition = "DOUBLE"; + public string DecimalColumnDefinition; + public string BlobColumnDefinition = "BLOB"; + public string DateTimeColumnDefinition = "DATETIME"; + public string TimeColumnDefinition = "DATETIME"; + + protected IList> ClauseOrder { get; set; } + + protected DbTypes DbTypeMap = new DbTypes(); + protected void InitColumnTypeMap() + { + DbTypeMap.Set(DbType.String, StringColumnDefinition); + DbTypeMap.Set(DbType.StringFixedLength, StringColumnDefinition); + DbTypeMap.Set(DbType.StringFixedLength, StringColumnDefinition); + DbTypeMap.Set(DbType.String, StringColumnDefinition); + DbTypeMap.Set(DbType.Boolean, BoolColumnDefinition); + DbTypeMap.Set(DbType.Boolean, BoolColumnDefinition); + DbTypeMap.Set(DbType.Guid, GuidColumnDefinition); + DbTypeMap.Set(DbType.Guid, GuidColumnDefinition); + DbTypeMap.Set(DbType.DateTime, DateTimeColumnDefinition); + DbTypeMap.Set(DbType.DateTime, DateTimeColumnDefinition); + DbTypeMap.Set(DbType.Time, TimeColumnDefinition); + DbTypeMap.Set(DbType.Time, TimeColumnDefinition); + DbTypeMap.Set(DbType.Time, TimeColumnDefinition); + DbTypeMap.Set(DbType.Time, TimeColumnDefinition); + + DbTypeMap.Set(DbType.Byte, IntColumnDefinition); + DbTypeMap.Set(DbType.Byte, IntColumnDefinition); + DbTypeMap.Set(DbType.SByte, IntColumnDefinition); + DbTypeMap.Set(DbType.SByte, IntColumnDefinition); + DbTypeMap.Set(DbType.Int16, IntColumnDefinition); + DbTypeMap.Set(DbType.Int16, IntColumnDefinition); + DbTypeMap.Set(DbType.UInt16, IntColumnDefinition); + DbTypeMap.Set(DbType.UInt16, IntColumnDefinition); + DbTypeMap.Set(DbType.Int32, IntColumnDefinition); + DbTypeMap.Set(DbType.Int32, IntColumnDefinition); + DbTypeMap.Set(DbType.UInt32, IntColumnDefinition); + DbTypeMap.Set(DbType.UInt32, IntColumnDefinition); + + DbTypeMap.Set(DbType.Int64, LongColumnDefinition); + DbTypeMap.Set(DbType.Int64, LongColumnDefinition); + DbTypeMap.Set(DbType.UInt64, LongColumnDefinition); + DbTypeMap.Set(DbType.UInt64, LongColumnDefinition); + + DbTypeMap.Set(DbType.Single, RealColumnDefinition); + DbTypeMap.Set(DbType.Single, RealColumnDefinition); + DbTypeMap.Set(DbType.Double, RealColumnDefinition); + DbTypeMap.Set(DbType.Double, RealColumnDefinition); + + DbTypeMap.Set(DbType.Decimal, DecimalColumnDefinition); + DbTypeMap.Set(DbType.Decimal, DecimalColumnDefinition); + + DbTypeMap.Set(DbType.Binary, BlobColumnDefinition); + } + + public virtual string EscapeString(string val) + { + return PetaPocoExtensions.EscapeAtSymbols(val.Replace("'", "''")); + } + + public virtual string GetStringColumnEqualComparison(string column, int paramIndex, TextColumnType columnType) + { + //use the 'upper' method to always ensure strings are matched without case sensitivity no matter what the db setting. + return string.Format("upper({0}) = upper(@{1})", column, paramIndex); + } + + public virtual string GetStringColumnWildcardComparison(string column, int paramIndex, TextColumnType columnType) + { + //use the 'upper' method to always ensure strings are matched without case sensitivity no matter what the db setting. + return string.Format("upper({0}) LIKE upper(@{1})", column, paramIndex); + } + + [Obsolete("Use the overload with the parameter index instead")] + public virtual string GetStringColumnEqualComparison(string column, string value, TextColumnType columnType) + { + //use the 'upper' method to always ensure strings are matched without case sensitivity no matter what the db setting. + return string.Format("upper({0}) = '{1}'", column, value.ToUpper()); + } + + [Obsolete("Use the overload with the parameter index instead")] + public virtual string GetStringColumnStartsWithComparison(string column, string value, TextColumnType columnType) + { + //use the 'upper' method to always ensure strings are matched without case sensitivity no matter what the db setting. + return string.Format("upper({0}) LIKE '{1}%'", column, value.ToUpper()); + } + + [Obsolete("Use the overload with the parameter index instead")] + public virtual string GetStringColumnEndsWithComparison(string column, string value, TextColumnType columnType) + { + //use the 'upper' method to always ensure strings are matched without case sensitivity no matter what the db setting. + return string.Format("upper({0}) LIKE '%{1}'", column, value.ToUpper()); + } + + [Obsolete("Use the overload with the parameter index instead")] + public virtual string GetStringColumnContainsComparison(string column, string value, TextColumnType columnType) + { + //use the 'upper' method to always ensure strings are matched without case sensitivity no matter what the db setting. + return string.Format("upper({0}) LIKE '%{1}%'", column, value.ToUpper()); + } + + [Obsolete("Use the overload with the parameter index instead")] + public virtual string GetStringColumnWildcardComparison(string column, string value, TextColumnType columnType) + { + //use the 'upper' method to always ensure strings are matched without case sensitivity no matter what the db setting. + return string.Format("upper({0}) LIKE '{1}'", column, value.ToUpper()); + } + + public virtual string GetQuotedTableName(string tableName) + { + return string.Format("\"{0}\"", tableName); + } + + public virtual string GetQuotedColumnName(string columnName) + { + return string.Format("\"{0}\"", columnName); + } + + public virtual string GetQuotedName(string name) + { + return string.Format("\"{0}\"", name); + } + + public virtual string GetQuotedValue(string value) + { + return string.Format("'{0}'", value); + } + + public virtual string GetIndexType(IndexTypes indexTypes) + { + string indexType; + + if (indexTypes == IndexTypes.Clustered) + { + indexType = "CLUSTERED"; + } + else + { + indexType = indexTypes == IndexTypes.NonClustered + ? "NONCLUSTERED" + : "UNIQUE NONCLUSTERED"; + } + return indexType; + } + + public virtual string GetSpecialDbType(SpecialDbTypes dbTypes) + { + if (dbTypes == SpecialDbTypes.NCHAR) + { + return "NCHAR"; + } + else if (dbTypes == SpecialDbTypes.NTEXT) + return "NTEXT"; + + return "NVARCHAR"; + } + + public virtual bool? SupportsCaseInsensitiveQueries(Database db) + { + return true; + } + + public virtual IEnumerable GetTablesInSchema(Database db) + { + return new List(); + } + + public virtual IEnumerable GetColumnsInSchema(Database db) + { + return new List(); + } + + public virtual IEnumerable> GetConstraintsPerTable(Database db) + { + return new List>(); + } + + public virtual IEnumerable> GetConstraintsPerColumn(Database db) + { + return new List>(); + } + + public abstract IEnumerable> GetDefinedIndexes(Database db); + + public virtual bool DoesTableExist(Database db, string tableName) + { + return false; + } + + public virtual bool SupportsClustered() + { + return true; + } + + public virtual bool SupportsIdentityInsert() + { + return true; + } + + /// + /// This is used ONLY if we need to format datetime without using SQL parameters (i.e. during migrations) + /// + /// + /// + /// + /// + /// MSSQL has a DateTime standard that is unambiguous and works on all servers: + /// YYYYMMDD HH:mm:ss + /// + public virtual string FormatDateTime(DateTime date, bool includeTime = true) + { + // need CultureInfo.InvariantCulture because ":" here is the "time separator" and + // may be converted to something else in different cultures (eg "." in DK). + return date.ToString(includeTime ? "yyyyMMdd HH:mm:ss" : "yyyyMMdd", CultureInfo.InvariantCulture); + } + + public virtual string Format(TableDefinition table) + { + var statement = string.Format(CreateTable, GetQuotedTableName(table.Name), Format(table.Columns)); + + return statement; + } + + public virtual List Format(IEnumerable indexes) + { + return indexes.Select(Format).ToList(); + } + + public virtual string Format(IndexDefinition index) + { + string name = string.IsNullOrEmpty(index.Name) + ? string.Format("IX_{0}_{1}", index.TableName, index.ColumnName) + : index.Name; + + string columns = index.Columns.Any() + ? string.Join(",", index.Columns.Select(x => GetQuotedColumnName(x.Name))) + : GetQuotedColumnName(index.ColumnName); + + return string.Format(CreateIndex, GetIndexType(index.IndexType), " ", GetQuotedName(name), + GetQuotedTableName(index.TableName), columns); + } + + public virtual List Format(IEnumerable foreignKeys) + { + return foreignKeys.Select(Format).ToList(); + } + + public virtual string Format(ForeignKeyDefinition foreignKey) + { + string constraintName = string.IsNullOrEmpty(foreignKey.Name) + ? string.Format("FK_{0}_{1}_{2}", foreignKey.ForeignTable, foreignKey.PrimaryTable, foreignKey.PrimaryColumns.First()) + : foreignKey.Name; + + return string.Format(CreateForeignKeyConstraint, + GetQuotedTableName(foreignKey.ForeignTable), + GetQuotedName(constraintName), + GetQuotedColumnName(foreignKey.ForeignColumns.First()), + GetQuotedTableName(foreignKey.PrimaryTable), + GetQuotedColumnName(foreignKey.PrimaryColumns.First()), + FormatCascade("DELETE", foreignKey.OnDelete), + FormatCascade("UPDATE", foreignKey.OnUpdate)); + } + + public virtual string Format(IEnumerable columns) + { + var sb = new StringBuilder(); + foreach (var column in columns) + { + sb.Append(Format(column) + ",\n"); + } + return sb.ToString().TrimEnd(",\n"); + } + + public virtual string Format(ColumnDefinition column) + { + var clauses = new List(); + + foreach (var action in ClauseOrder) + { + string clause = action(column); + if (!string.IsNullOrEmpty(clause)) + clauses.Add(clause); + } + + return string.Join(" ", clauses.ToArray()); + } + + public virtual string FormatPrimaryKey(TableDefinition table) + { + var columnDefinition = table.Columns.FirstOrDefault(x => x.IsPrimaryKey); + if (columnDefinition == null) + return string.Empty; + + string constraintName = string.IsNullOrEmpty(columnDefinition.PrimaryKeyName) + ? string.Format("PK_{0}", table.Name) + : columnDefinition.PrimaryKeyName; + + string columns = string.IsNullOrEmpty(columnDefinition.PrimaryKeyColumns) + ? GetQuotedColumnName(columnDefinition.Name) + : string.Join(", ", columnDefinition.PrimaryKeyColumns + .Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries) + .Select(GetQuotedColumnName)); + + string primaryKeyPart = string.Concat("PRIMARY KEY", columnDefinition.IsIndexed ? " CLUSTERED" : " NONCLUSTERED"); + + return string.Format(CreateConstraint, + GetQuotedTableName(table.Name), + GetQuotedName(constraintName), + primaryKeyPart, + columns); + } + + public virtual string FormatColumnRename(string tableName, string oldName, string newName) + { + return string.Format(RenameColumn, + GetQuotedTableName(tableName), + GetQuotedColumnName(oldName), + GetQuotedColumnName(newName)); + } + + public virtual string FormatTableRename(string oldName, string newName) + { + return string.Format(RenameTable, GetQuotedTableName(oldName), GetQuotedTableName(newName)); + } + + protected virtual string FormatCascade(string onWhat, Rule rule) + { + string action = "NO ACTION"; + switch (rule) + { + case Rule.None: + return ""; + case Rule.Cascade: + action = "CASCADE"; + break; + case Rule.SetNull: + action = "SET NULL"; + break; + case Rule.SetDefault: + action = "SET DEFAULT"; + break; + } + + return string.Format(" ON {0} {1}", onWhat, action); + } + + protected virtual string FormatString(ColumnDefinition column) + { + return GetQuotedColumnName(column.Name); + } + + protected virtual string FormatType(ColumnDefinition column) + { + if (column.Type.HasValue == false && string.IsNullOrEmpty(column.CustomType) == false) + return column.CustomType; + + if (column.HasSpecialDbType) + { + if (column.Size != default(int)) + { + return string.Format("{0}({1})", + GetSpecialDbType(column.DbType), + column.Size); + } + + return GetSpecialDbType(column.DbType); + } + + Type type = column.Type.HasValue + ? DbTypeMap.ColumnDbTypeMap.First(x => x.Value == column.Type.Value).Key + : column.PropertyType; + + if (type == typeof(string)) + { + var valueOrDefault = column.Size != default(int) ? column.Size : DefaultStringLength; + return string.Format(StringLengthColumnDefinitionFormat, valueOrDefault); + } + + if (type == typeof(decimal)) + { + var precision = column.Size != default(int) ? column.Size : DefaultDecimalPrecision; + var scale = column.Precision != default(int) ? column.Precision : DefaultDecimalScale; + return string.Format(DecimalColumnDefinitionFormat, precision, scale); + } + + string definition = DbTypeMap.ColumnTypeMap.First(x => x.Key == type).Value; + string dbTypeDefinition = column.Size != default(int) + ? string.Format("{0}({1})", definition, column.Size) + : definition; + //NOTE Percision is left out + return dbTypeDefinition; + } + + protected virtual string FormatNullable(ColumnDefinition column) + { + return column.IsNullable ? "NULL" : "NOT NULL"; + } + + protected virtual string FormatConstraint(ColumnDefinition column) + { + if (string.IsNullOrEmpty(column.ConstraintName) && column.DefaultValue == null) + return string.Empty; + + return string.Format("CONSTRAINT {0}", + string.IsNullOrEmpty(column.ConstraintName) + ? GetQuotedName(string.Format("DF_{0}_{1}", column.TableName, column.Name)) + : column.ConstraintName); + } + + protected virtual string FormatDefaultValue(ColumnDefinition column) + { + if (column.DefaultValue == null) + return string.Empty; + + //hack - probably not needed with latest changes + if (column.DefaultValue.ToString().ToLower().Equals("getdate()".ToLower())) + column.DefaultValue = SystemMethods.CurrentDateTime; + + // see if this is for a system method + if (column.DefaultValue is SystemMethods) + { + string method = FormatSystemMethods((SystemMethods)column.DefaultValue); + if (string.IsNullOrEmpty(method)) + return string.Empty; + + return string.Format(DefaultValueFormat, method); + } + + return string.Format(DefaultValueFormat, GetQuotedValue(column.DefaultValue.ToString())); + } + + protected virtual string FormatPrimaryKey(ColumnDefinition column) + { + return string.Empty; + } + + protected abstract string FormatSystemMethods(SystemMethods systemMethod); + + protected abstract string FormatIdentity(ColumnDefinition column); + + public virtual string DeleteDefaultConstraint + { + get + { + throw new NotSupportedException("Default constraints are not supported"); + } + } + + public virtual string CreateTable { get { return "CREATE TABLE {0} ({1})"; } } + public virtual string DropTable { get { return "DROP TABLE {0}"; } } + + public virtual string AddColumn { get { return "ALTER TABLE {0} ADD COLUMN {1}"; } } + public virtual string DropColumn { get { return "ALTER TABLE {0} DROP COLUMN {1}"; } } + public virtual string AlterColumn { get { return "ALTER TABLE {0} ALTER COLUMN {1}"; } } + public virtual string RenameColumn { get { return "ALTER TABLE {0} RENAME COLUMN {1} TO {2}"; } } + + public virtual string RenameTable { get { return "RENAME TABLE {0} TO {1}"; } } + + public virtual string CreateSchema { get { return "CREATE SCHEMA {0}"; } } + public virtual string AlterSchema { get { return "ALTER SCHEMA {0} TRANSFER {1}.{2}"; } } + public virtual string DropSchema { get { return "DROP SCHEMA {0}"; } } + + public virtual string CreateIndex { get { return "CREATE {0}{1}INDEX {2} ON {3} ({4})"; } } + public virtual string DropIndex { get { return "DROP INDEX {0}"; } } + + public virtual string InsertData { get { return "INSERT INTO {0} ({1}) VALUES ({2})"; } } + public virtual string UpdateData { get { return "UPDATE {0} SET {1} WHERE {2}"; } } + public virtual string DeleteData { get { return "DELETE FROM {0} WHERE {1}"; } } + public virtual string TruncateTable { get { return "TRUNCATE TABLE {0}"; } } + + public virtual string CreateConstraint { get { return "ALTER TABLE {0} ADD CONSTRAINT {1} {2} ({3})"; } } + public virtual string DeleteConstraint { get { return "ALTER TABLE {0} DROP CONSTRAINT {1}"; } } + public virtual string CreateForeignKeyConstraint { get { return "ALTER TABLE {0} ADD CONSTRAINT {1} FOREIGN KEY ({2}) REFERENCES {3} ({4}){5}{6}"; } } + + public virtual string IsNull { get { return "ISNULL({0},{1})"; } } + public virtual string ConvertIntegerToOrderableString { get { return "RIGHT('00000000' + CAST({0} AS varchar(8)),8)"; } } + public virtual string ConvertDateToOrderableString { get { return "CONVERT(varchar, {0}, 102)"; } } + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index 0e5b55866f..773d26a455 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -23,2321 +23,2321 @@ using Umbraco.Core.Publishing; namespace Umbraco.Core.Services { - /// - /// Represents the Content Service, which is an easy access to operations involving - /// - public class ContentService : RepositoryService, IContentService, IContentServiceOperations - { - private readonly IPublishingStrategy _publishingStrategy; - private readonly EntityXmlSerializer _entitySerializer = new EntityXmlSerializer(); - private readonly IDataTypeService _dataTypeService; - private readonly IUserService _userService; - - //Support recursive locks because some of the methods that require locking call other methods that require locking. - //for example, the Move method needs to be locked but this calls the Save method which also needs to be locked. - private static readonly ReaderWriterLockSlim Locker = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion); - - public ContentService( - IDatabaseUnitOfWorkProvider provider, - RepositoryFactory repositoryFactory, - ILogger logger, - IEventMessagesFactory eventMessagesFactory, - IPublishingStrategy publishingStrategy, - IDataTypeService dataTypeService, - IUserService userService) - : base(provider, repositoryFactory, logger, eventMessagesFactory) - { - if (publishingStrategy == null) throw new ArgumentNullException("publishingStrategy"); - if (dataTypeService == null) throw new ArgumentNullException("dataTypeService"); - if (userService == null) throw new ArgumentNullException("userService"); - _publishingStrategy = publishingStrategy; - _dataTypeService = dataTypeService; - _userService = userService; - } - - public int CountPublished(string contentTypeAlias = null) - { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateContentRepository(uow)) - { - return repository.CountPublished(); - } - } - - public int Count(string contentTypeAlias = null) - { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateContentRepository(uow)) - { - return repository.Count(contentTypeAlias); - } - } - - public int CountChildren(int parentId, string contentTypeAlias = null) - { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateContentRepository(uow)) - { - return repository.CountChildren(parentId, contentTypeAlias); - } - } - - public int CountDescendants(int parentId, string contentTypeAlias = null) - { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateContentRepository(uow)) - { - return repository.CountDescendants(parentId, contentTypeAlias); - } - } - - /// - /// Used to bulk update the permissions set for a content item. This will replace all permissions - /// assigned to an entity with a list of user id & permission pairs. - /// - /// - public void ReplaceContentPermissions(EntityPermissionSet permissionSet) - { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateContentRepository(uow)) - { - repository.ReplaceContentPermissions(permissionSet); - } - } - - /// - /// Assigns a single permission to the current content item for the specified user ids - /// - /// - /// - /// - public void AssignContentPermission(IContent entity, char permission, IEnumerable userIds) - { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateContentRepository(uow)) - { - repository.AssignEntityPermission(entity, permission, userIds); - } - } - - /// - /// Gets the list of permissions for the content item - /// - /// - /// - public IEnumerable GetPermissionsForEntity(IContent content) - { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateContentRepository(uow)) - { - return repository.GetPermissionsForEntity(content.Id); - } - } - - /// - /// 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, int parentId, string contentTypeAlias, int userId = 0) - { - var contentType = FindContentTypeByAlias(contentTypeAlias); - var content = new Content(name, parentId, contentType); - var parent = GetById(content.ParentId); - content.Path = string.Concat(parent.IfNotNull(x => x.Path, content.ParentId.ToString()), ",", content.Id); - - - if (Creating.IsRaisedEventCancelled(new NewEventArgs(content, contentTypeAlias, parentId), this)) - { - content.WasCancelled = true; - return content; - } - - content.CreatorId = userId; - content.WriterId = userId; - - Created.RaiseEvent(new NewEventArgs(content, false, contentTypeAlias, parentId), this); - - var uow = UowProvider.GetUnitOfWork(); - using (var auditRepo = RepositoryFactory.CreateAuditRepository(uow)) - { - auditRepo.AddOrUpdate(new AuditItem(content.Id, string.Format("Content '{0}' was created", name), AuditType.New, content.CreatorId)); - uow.Commit(); - } - - return content; - } - - /// - /// 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 - /// Parent object for the new Content - /// Alias of the - /// Optional id of the user creating the content - /// - public IContent CreateContent(string name, IContent parent, string contentTypeAlias, int userId = 0) - { - if (parent == null) throw new ArgumentNullException("parent"); - - var contentType = FindContentTypeByAlias(contentTypeAlias); - var content = new Content(name, parent, contentType); - content.Path = string.Concat(parent.Path, ",", content.Id); - - if (Creating.IsRaisedEventCancelled(new NewEventArgs(content, contentTypeAlias, parent), this)) - { - content.WasCancelled = true; - return content; - } - - content.CreatorId = userId; - content.WriterId = userId; - - Created.RaiseEvent(new NewEventArgs(content, false, contentTypeAlias, parent), this); - - Audit(AuditType.New, string.Format("Content '{0}' was created", name), content.CreatorId, content.Id); - - return content; - } - - /// - /// Creates and saves an object using the alias of the - /// that this Content should based on. - /// - /// - /// This method returns an object that has been persisted to the database - /// and therefor has an identity. - /// - /// 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 CreateContentWithIdentity(string name, int parentId, string contentTypeAlias, int userId = 0) - { - var contentType = FindContentTypeByAlias(contentTypeAlias); - var content = new Content(name, parentId, contentType); - - //NOTE: I really hate the notion of these Creating/Created events - they are so inconsistent, I've only just found - // out that in these 'WithIdentity' methods, the Saving/Saved events were not fired, wtf. Anyways, they're added now. - if (Creating.IsRaisedEventCancelled(new NewEventArgs(content, contentTypeAlias, parentId), this)) - { - content.WasCancelled = true; - return content; - } - - if (Saving.IsRaisedEventCancelled(new SaveEventArgs(content), this)) - { - content.WasCancelled = true; - return content; - } - - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateContentRepository(uow)) - { - content.CreatorId = userId; - content.WriterId = userId; - repository.AddOrUpdate(content); - //Generate a new preview - repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); - uow.Commit(); - } - - Saved.RaiseEvent(new SaveEventArgs(content, false), this); - - Created.RaiseEvent(new NewEventArgs(content, false, contentTypeAlias, parentId), this); - - Audit(AuditType.New, string.Format("Content '{0}' was created with Id {1}", name, content.Id), content.CreatorId, content.Id); - - return content; - } - - /// - /// Creates and saves an object using the alias of the - /// that this Content should based on. - /// - /// - /// This method returns an object that has been persisted to the database - /// and therefor has an identity. - /// - /// Name of the Content object - /// Parent object for the new Content - /// Alias of the - /// Optional id of the user creating the content - /// - public IContent CreateContentWithIdentity(string name, IContent parent, string contentTypeAlias, int userId = 0) - { - if (parent == null) throw new ArgumentNullException("parent"); - - var contentType = FindContentTypeByAlias(contentTypeAlias); - var content = new Content(name, parent, contentType); - - //NOTE: I really hate the notion of these Creating/Created events - they are so inconsistent, I've only just found - // out that in these 'WithIdentity' methods, the Saving/Saved events were not fired, wtf. Anyways, they're added now. - if (Creating.IsRaisedEventCancelled(new NewEventArgs(content, contentTypeAlias, parent), this)) - { - content.WasCancelled = true; - return content; - } - - if (Saving.IsRaisedEventCancelled(new SaveEventArgs(content), this)) - { - content.WasCancelled = true; - return content; - } - - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateContentRepository(uow)) - { - content.CreatorId = userId; - content.WriterId = userId; - repository.AddOrUpdate(content); - //Generate a new preview - repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); - uow.Commit(); - } - - Saved.RaiseEvent(new SaveEventArgs(content, false), this); - - Created.RaiseEvent(new NewEventArgs(content, false, contentTypeAlias, parent), this); - - Audit(AuditType.New, string.Format("Content '{0}' was created with Id {1}", name, content.Id), content.CreatorId, content.Id); - - return content; - } - - /// - /// Gets an object by Id - /// - /// Id of the Content to retrieve - /// - public IContent GetById(int id) - { - using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) - { - return repository.Get(id); - } - } - - /// - /// Gets an object by Id - /// - /// Ids of the Content to retrieve - /// - public IEnumerable GetByIds(IEnumerable ids) - { - if (ids.Any() == false) return Enumerable.Empty(); - - using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) - { - return repository.GetAll(ids.ToArray()); - } - } - - /// - /// Gets an object by its 'UniqueId' - /// - /// Guid key of the Content to retrieve - /// - public IContent GetById(Guid key) - { - using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) - { - var query = Query.Builder.Where(x => x.Key == key); - var contents = repository.GetByQuery(query); - return contents.SingleOrDefault(); - } - } - - /// - /// Gets a collection of objects by the Id of the - /// - /// Id of the - /// An Enumerable list of objects - public IEnumerable GetContentOfContentType(int id) - { - using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) - { - var query = Query.Builder.Where(x => x.ContentTypeId == id); - var contents = repository.GetByQuery(query); - - return contents; - } - } - - internal IEnumerable GetPublishedContentOfContentType(int id) - { - using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) - { - var query = Query.Builder.Where(x => x.ContentTypeId == id); - var contents = repository.GetByPublishedVersion(query); - - return contents; - } - } - - /// - /// Gets a collection of objects by Level - /// - /// The level to retrieve Content from - /// An Enumerable list of objects - public IEnumerable GetByLevel(int level) - { - using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) - { - var query = Query.Builder.Where(x => x.Level == level && !x.Path.StartsWith(Constants.System.RecycleBinContent.ToInvariantString())); - var contents = repository.GetByQuery(query); - - return contents; - } - } - - /// - /// Gets a specific version of an item. - /// - /// Id of the version to retrieve - /// An item - public IContent GetByVersion(Guid versionId) - { - using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) - { - return repository.GetByVersion(versionId); - } - } - - - /// - /// Gets a collection of an objects versions by Id - /// - /// - /// An Enumerable list of objects - public IEnumerable GetVersions(int id) - { - using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) - { - var versions = repository.GetAllVersions(id); - return versions; - } - } - - /// - /// Gets a collection of objects, which are ancestors of the current content. - /// - /// Id of the to retrieve ancestors for - /// An Enumerable list of objects - public IEnumerable GetAncestors(int id) - { - var content = GetById(id); - return GetAncestors(content); - } - - /// - /// Gets a collection of objects, which are ancestors of the current content. - /// - /// to retrieve ancestors for - /// An Enumerable list of objects - public IEnumerable GetAncestors(IContent content) - { - //null check otherwise we get exceptions - if (content.Path.IsNullOrWhiteSpace()) return Enumerable.Empty(); - - var ids = content.Path.Split(',').Where(x => x != Constants.System.Root.ToInvariantString() && x != content.Id.ToString(CultureInfo.InvariantCulture)).Select(int.Parse).ToArray(); - if (ids.Any() == false) - return new List(); - - using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) - { - return repository.GetAll(ids); - } - } - - /// - /// Gets a collection of objects by Parent Id - /// - /// Id of the Parent to retrieve Children from - /// An Enumerable list of objects - public IEnumerable GetChildren(int id) - { - using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) - { - var query = Query.Builder.Where(x => x.ParentId == id); - var contents = repository.GetByQuery(query).OrderBy(x => x.SortOrder); - - return contents; - } - } - - [Obsolete("Use the overload with 'long' parameter types instead")] - [EditorBrowsable(EditorBrowsableState.Never)] - public IEnumerable GetPagedChildren(int id, int pageIndex, int pageSize, out int totalChildren, - string orderBy, Direction orderDirection, bool orderBySystemField = true, string filter = "") - { - Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); - Mandate.ParameterCondition(pageSize > 0, "pageSize"); - using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) - { - - var query = Query.Builder; - //if the id is System Root, then just get all - if (id != Constants.System.Root) - { - query.Where(x => x.ParentId == id); - } - long total; - var contents = repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out total, orderBy, orderDirection, orderBySystemField, filter); - totalChildren = Convert.ToInt32(total); - return contents; - } - } - - /// - /// Gets a collection of objects by Parent Id - /// - /// Id of the Parent to retrieve Children from - /// Page index (zero based) - /// Page size - /// Total records query would return without paging - /// Field to order by - /// Direction to order by - /// Flag to indicate when ordering by system field - /// Search text filter - /// An Enumerable list of objects - public IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize, out long totalChildren, - string orderBy, Direction orderDirection, bool orderBySystemField = true, string filter = "") - { - Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); - Mandate.ParameterCondition(pageSize > 0, "pageSize"); - using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) - { - - var query = Query.Builder; - //if the id is System Root, then just get all - if (id != Constants.System.Root) - { - query.Where(x => x.ParentId == id); - } - var contents = repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, orderBySystemField, filter); - - return contents; - } - } - - [Obsolete("Use the overload with 'long' parameter types instead")] - [EditorBrowsable(EditorBrowsableState.Never)] - public IEnumerable GetPagedDescendants(int id, int pageIndex, int pageSize, out int totalChildren, string orderBy = "Path", Direction orderDirection = Direction.Ascending, bool orderBySystemField = true, string filter = "") - { - Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); - Mandate.ParameterCondition(pageSize > 0, "pageSize"); - using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) - { - - var query = Query.Builder; - //if the id is System Root, then just get all - if (id != Constants.System.Root) - { - query.Where(x => x.Path.SqlContains(string.Format(",{0},", id), TextColumnType.NVarchar)); - } - long total; - var contents = repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out total, orderBy, orderDirection, orderBySystemField, filter); - totalChildren = Convert.ToInt32(total); - return contents; - } - } - - /// - /// Gets a collection of objects by Parent Id - /// - /// Id of the Parent to retrieve Descendants from - /// Page number - /// Page size - /// Total records query would return without paging - /// Field to order by - /// Direction to order by - /// Flag to indicate when ordering by system field - /// Search text filter - /// An Enumerable list of objects - public IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalChildren, string orderBy = "Path", Direction orderDirection = Direction.Ascending, bool orderBySystemField = true, string filter = "") - { - Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); - Mandate.ParameterCondition(pageSize > 0, "pageSize"); - using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) - { - - var query = Query.Builder; - //if the id is System Root, then just get all - if (id != Constants.System.Root) - { - query.Where(x => x.Path.SqlContains(string.Format(",{0},", id), TextColumnType.NVarchar)); - } - var contents = repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, orderBySystemField, filter); - - return contents; - } - } - - /// - /// Gets a collection of objects by its name or partial name - /// - /// Id of the Parent to retrieve Children from - /// Full or partial name of the children - /// An Enumerable list of objects - public IEnumerable GetChildrenByName(int parentId, string name) - { - using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) - { - var query = Query.Builder.Where(x => x.ParentId == parentId && x.Name.Contains(name)); - var contents = repository.GetByQuery(query); - - return contents; - } - } - - /// - /// Gets a collection of objects by Parent Id - /// - /// Id of the Parent to retrieve Descendants from - /// An Enumerable list of objects - public IEnumerable GetDescendants(int id) - { - var content = GetById(id); - if (content == null) - { - return Enumerable.Empty(); - } - return GetDescendants(content); - } - - /// - /// Gets a collection of objects by Parent Id - /// - /// item to retrieve Descendants from - /// An Enumerable list of objects - public IEnumerable GetDescendants(IContent content) - { - using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) - { - var pathMatch = content.Path + ","; - var query = Query.Builder.Where(x => x.Path.StartsWith(pathMatch) && x.Id != content.Id); - var contents = repository.GetByQuery(query); - - return contents; - } - } - - /// - /// Gets the parent of the current content as an item. - /// - /// Id of the to retrieve the parent from - /// Parent object - public IContent GetParent(int id) - { - var content = GetById(id); - return GetParent(content); - } - - /// - /// Gets the parent of the current content as an item. - /// - /// to retrieve the parent from - /// Parent object - public IContent GetParent(IContent content) - { - if (content.ParentId == Constants.System.Root || content.ParentId == Constants.System.RecycleBinContent) - return null; - - return GetById(content.ParentId); - } - - /// - /// Gets the published version of an item - /// - /// Id of the to retrieve version from - /// An item - public IContent GetPublishedVersion(int id) - { - var version = GetVersions(id); - return version.FirstOrDefault(x => x.Published == true); - } - - /// - /// Gets the published version of a item. - /// - /// The content item. - /// The published version, if any; otherwise, null. - public IContent GetPublishedVersion(IContent content) - { - if (content.Published) return content; - return content.HasPublishedVersion - ? GetByVersion(content.PublishedVersionGuid) - : null; - } - - /// - /// Gets a collection of objects, which reside at the first level / root - /// - /// An Enumerable list of objects - public IEnumerable GetRootContent() - { - using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) - { - var query = Query.Builder.Where(x => x.ParentId == Constants.System.Root); - var contents = repository.GetByQuery(query); - - return contents; - } - } - - /// - /// Gets all published content items - /// - /// - internal IEnumerable GetAllPublished() - { - using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) - { - var query = Query.Builder.Where(x => x.Trashed == false); - return repository.GetByPublishedVersion(query); - } - } - - /// - /// Gets a collection of objects, which has an expiration date less than or equal to today. - /// - /// An Enumerable list of objects - public IEnumerable GetContentForExpiration() - { - using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) - { - var query = Query.Builder.Where(x => x.Published == true && x.ExpireDate <= DateTime.Now); - var contents = repository.GetByQuery(query); - - return contents; - } - } - - /// - /// Gets a collection of objects, which has a release date less than or equal to today. - /// - /// An Enumerable list of objects - public IEnumerable GetContentForRelease() - { - using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) - { - var query = Query.Builder.Where(x => x.Published == false && x.ReleaseDate <= DateTime.Now); - var contents = repository.GetByQuery(query); - - return contents; - } - } - - /// - /// Gets a collection of an objects, which resides in the Recycle Bin - /// - /// An Enumerable list of objects - public IEnumerable GetContentInRecycleBin() - { - using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) - { - var query = Query.Builder.Where(x => x.Path.Contains(Constants.System.RecycleBinContent.ToInvariantString())); - var contents = repository.GetByQuery(query); - - return contents; - } - } - - - - /// - /// Checks whether an item has any children - /// - /// Id of the - /// True if the content has any children otherwise False - public bool HasChildren(int id) - { - return CountChildren(id) > 0; - } - - internal int CountChildren(int id) - { - using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) - { - var query = Query.Builder.Where(x => x.ParentId == id); - var count = repository.Count(query); - return count; - } - } - - /// - /// Checks whether an item has any published versions - /// - /// Id of the - /// True if the content has any published version otherwise False - public bool HasPublishedVersion(int id) - { - using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) - { - var query = Query.Builder.Where(x => x.Published == true && x.Id == id && x.Trashed == false); - int count = repository.Count(query); - return count > 0; - } - } - - /// - /// Checks if the passed in can be published based on the anscestors publish state. - /// - /// to check if anscestors are published - /// True if the Content can be published, otherwise False - public bool IsPublishable(IContent content) - { - //If the passed in content has yet to be saved we "fallback" to checking the Parent - //because if the Parent is publishable then the current content can be Saved and Published - if (content.HasIdentity == false) - { - IContent parent = GetById(content.ParentId); - return IsPublishable(parent, true); - } - - return IsPublishable(content, false); - } - - /// - /// This will rebuild the xml structures for content in the database. - /// - /// This is not used for anything - /// True if publishing succeeded, otherwise False - /// - /// This is used for when a document type alias or a document type property is changed, the xml will need to - /// be regenerated. - /// - public bool RePublishAll(int userId = 0) - { - try - { - RebuildXmlStructures(); - return true; - } - catch (Exception ex) - { - Logger.Error("An error occurred executing RePublishAll", ex); - return false; - } - } - - /// - /// This will rebuild the xml structures for content in the database. - /// - /// - /// If specified will only rebuild the xml for the content type's specified, otherwise will update the structure - /// for all published content. - /// - internal void RePublishAll(params int[] contentTypeIds) - { - try - { - RebuildXmlStructures(contentTypeIds); - } - catch (Exception ex) - { - Logger.Error("An error occurred executing RePublishAll", ex); - } - } - - /// - /// Publishes a single object - /// - /// The to publish - /// Optional Id of the User issueing the publishing - /// True if publishing succeeded, otherwise False - public bool Publish(IContent content, int userId = 0) - { - var result = SaveAndPublishDo(content, userId); - Logger.Info("Call was made to ContentService.Publish, use PublishWithStatus instead since that method will provide more detailed information on the outcome"); - return result.Success; - } - - /// - /// Publishes a object and all its children - /// - /// The to publish along with its children - /// Optional Id of the User issueing the publishing - /// - /// The list of statuses for all published items - IEnumerable> IContentServiceOperations.PublishWithChildren(IContent content, int userId, bool includeUnpublished) - { - return PublishWithChildrenDo(content, userId, includeUnpublished); - } - - /// - /// Saves and Publishes a single object - /// - /// The to save and publish - /// Optional Id of the User issueing the publishing - /// Optional boolean indicating whether or not to raise save events. - /// True if publishing succeeded, otherwise False - Attempt IContentServiceOperations.SaveAndPublish(IContent content, int userId, bool raiseEvents) - { - return SaveAndPublishDo(content, userId, raiseEvents); - } - - /// - /// Deletes an object by moving it to the Recycle Bin - /// - /// Move an item to the Recycle Bin will result in the item being unpublished - /// The to delete - /// Optional Id of the User deleting the Content - Attempt IContentServiceOperations.MoveToRecycleBin(IContent content, int userId) - { - var evtMsgs = EventMessagesFactory.Get(); - - using (new WriteLock(Locker)) - { - var originalPath = content.Path; - - if (Trashing.IsRaisedEventCancelled( - new MoveEventArgs(evtMsgs, new MoveEventInfo(content, originalPath, Constants.System.RecycleBinContent)), - this)) - { - return OperationStatus.Cancelled(evtMsgs); - } - - var moveInfo = new List> - { - new MoveEventInfo(content, originalPath, Constants.System.RecycleBinContent) - }; - - //Make sure that published content is unpublished before being moved to the Recycle Bin - if (HasPublishedVersion(content.Id)) - { - //TODO: this shouldn't be a 'sub operation', and if it needs to be it cannot raise events and cannot be cancelled! - UnPublish(content, userId); - } - - //Unpublish descendents of the content item that is being moved to trash - var descendants = GetDescendants(content).OrderBy(x => x.Level).ToList(); - foreach (var descendant in descendants) - { - //TODO: this shouldn't be a 'sub operation', and if it needs to be it cannot raise events and cannot be cancelled! - UnPublish(descendant, userId); - } - - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateContentRepository(uow)) - { - content.WriterId = userId; - content.ChangeTrashedState(true); - repository.AddOrUpdate(content); - - //Loop through descendants to update their trash state, but ensuring structure by keeping the ParentId - foreach (var descendant in descendants) - { - moveInfo.Add(new MoveEventInfo(descendant, descendant.Path, descendant.ParentId)); - - descendant.WriterId = userId; - descendant.ChangeTrashedState(true, descendant.ParentId); - repository.AddOrUpdate(descendant); - } - - uow.Commit(); - } - - Trashed.RaiseEvent(new MoveEventArgs(false, evtMsgs, moveInfo.ToArray()), this); - - Audit(AuditType.Move, "Move Content to Recycle Bin performed by user", userId, content.Id); - - return OperationStatus.Success(evtMsgs); - } - } - - /// - /// UnPublishes a single object - /// - /// The to publish - /// Optional Id of the User issueing the publishing - /// True if unpublishing succeeded, otherwise False - Attempt IContentServiceOperations.UnPublish(IContent content, int userId) - { - return UnPublishDo(content, false, userId); - } - - /// - /// Publishes a single object - /// - /// The to publish - /// Optional Id of the User issueing the publishing - /// True if publishing succeeded, otherwise False - public Attempt PublishWithStatus(IContent content, int userId = 0) - { - return ((IContentServiceOperations)this).Publish(content, userId); - } - - /// - /// Publishes a object and all its children - /// - /// The to publish along with its children - /// Optional Id of the User issueing the publishing - /// True if publishing succeeded, otherwise False - [Obsolete("Use PublishWithChildrenWithStatus instead, that method will provide more detailed information on the outcome and also allows the includeUnpublished flag")] - public bool PublishWithChildren(IContent content, int userId = 0) - { - var result = PublishWithChildrenDo(content, userId, true); - - //This used to just return false only when the parent content failed, otherwise would always return true so we'll - // do the same thing for the moment - if (result.All(x => x.Result.ContentItem.Id != content.Id)) - return false; - - return result.Single(x => x.Result.ContentItem.Id == content.Id).Success; - } - - /// - /// Publishes a object and all its children - /// - /// The to publish along with its children - /// Optional Id of the User issueing the publishing - /// set to true if you want to also publish children that are currently unpublished - /// True if publishing succeeded, otherwise False - public IEnumerable> PublishWithChildrenWithStatus(IContent content, int userId = 0, bool includeUnpublished = false) - { - return ((IContentServiceOperations)this).PublishWithChildren(content, userId, includeUnpublished); - } - - /// - /// UnPublishes a single object - /// - /// The to publish - /// Optional Id of the User issueing the publishing - /// True if unpublishing succeeded, otherwise False - public bool UnPublish(IContent content, int userId = 0) - { - return ((IContentServiceOperations)this).UnPublish(content, userId).Success; - } - - /// - /// Saves and Publishes a single object - /// - /// The to save and publish - /// Optional Id of the User issueing the publishing - /// Optional boolean indicating whether or not to raise save events. - /// True if publishing succeeded, otherwise False - [Obsolete("Use SaveAndPublishWithStatus instead, that method will provide more detailed information on the outcome")] - public bool SaveAndPublish(IContent content, int userId = 0, bool raiseEvents = true) - { - var result = SaveAndPublishDo(content, userId, raiseEvents); - return result.Success; - } - - /// - /// Saves and Publishes a single object - /// - /// The to save and publish - /// Optional Id of the User issueing the publishing - /// Optional boolean indicating whether or not to raise save events. - /// True if publishing succeeded, otherwise False - public Attempt SaveAndPublishWithStatus(IContent content, int userId = 0, bool raiseEvents = true) - { - return ((IContentServiceOperations)this).SaveAndPublish(content, userId, raiseEvents); - } - - /// - /// Saves a single object - /// - /// The to save - /// Optional Id of the User saving the Content - /// Optional boolean indicating whether or not to raise events. - public void Save(IContent content, int userId = 0, bool raiseEvents = true) - { - ((IContentServiceOperations)this).Save(content, userId, raiseEvents); - } - - /// - /// Saves a collection of objects. - /// - /// Collection of to save - /// Optional Id of the User saving the Content - /// Optional boolean indicating whether or not to raise events. - Attempt IContentServiceOperations.Save(IEnumerable contents, int userId, bool raiseEvents) - { - var asArray = contents.ToArray(); - - var evtMsgs = EventMessagesFactory.Get(); - - if (raiseEvents) - { - if (Saving.IsRaisedEventCancelled( - new SaveEventArgs(asArray, evtMsgs), - this)) - { - return OperationStatus.Cancelled(evtMsgs); - } - } - using (new WriteLock(Locker)) - { - var containsNew = asArray.Any(x => x.HasIdentity == false); - - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateContentRepository(uow)) - { - if (containsNew) - { - foreach (var content in asArray) - { - content.WriterId = userId; - - //Only change the publish state if the "previous" version was actually published - if (content.Published) - content.ChangePublishedState(PublishedState.Saved); - - repository.AddOrUpdate(content); - //add or update preview - repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); - } - } - else - { - foreach (var content in asArray) - { - content.WriterId = userId; - repository.AddOrUpdate(content); - //add or update preview - repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); - } - } - - uow.Commit(); - } - - if (raiseEvents) - Saved.RaiseEvent(new SaveEventArgs(asArray, false, evtMsgs), this); - - Audit(AuditType.Save, "Bulk Save content performed by user", userId == -1 ? 0 : userId, Constants.System.Root); - - return OperationStatus.Success(evtMsgs); - } - } - - /// - /// Permanently deletes an object. - /// - /// - /// This method will also delete associated media files, child content and possibly associated domains. - /// - /// Please note that this method will completely remove the Content from the database - /// The to delete - /// Optional Id of the User deleting the Content - Attempt IContentServiceOperations.Delete(IContent content, int userId) - { - var evtMsgs = EventMessagesFactory.Get(); - - using (new WriteLock(Locker)) - { - if (Deleting.IsRaisedEventCancelled( - new DeleteEventArgs(content, evtMsgs), - this)) - { - return OperationStatus.Cancelled(evtMsgs); - } - - //Make sure that published content is unpublished before being deleted - if (HasPublishedVersion(content.Id)) - { - UnPublish(content, userId); - } - - //Delete children before deleting the 'possible parent' - var children = GetChildren(content.Id); - foreach (var child in children) - { - Delete(child, userId); - } - - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateContentRepository(uow)) - { - repository.Delete(content); - uow.Commit(); - - var args = new DeleteEventArgs(content, false, evtMsgs); - Deleted.RaiseEvent(args, this); - - //remove any flagged media files - repository.DeleteMediaFiles(args.MediaFilesToDelete); - } - - Audit(AuditType.Delete, "Delete Content performed by user", userId, content.Id); - - return OperationStatus.Success(evtMsgs); - } - } - - /// - /// Publishes a single object - /// - /// The to publish - /// Optional Id of the User issueing the publishing - /// The published status attempt - Attempt IContentServiceOperations.Publish(IContent content, int userId) - { - return SaveAndPublishDo(content, userId); - } - - /// - /// Saves a single object - /// - /// The to save - /// Optional Id of the User saving the Content - /// Optional boolean indicating whether or not to raise events. - Attempt IContentServiceOperations.Save(IContent content, int userId, bool raiseEvents) - { - return Save(content, true, userId, raiseEvents); - } - - /// - /// Saves a collection of objects. - /// - /// - /// If the collection of content contains new objects that references eachother by Id or ParentId, - /// then use the overload Save method with a collection of Lazy . - /// - /// Collection of to save - /// Optional Id of the User saving the Content - /// Optional boolean indicating whether or not to raise events. - public void Save(IEnumerable contents, int userId = 0, bool raiseEvents = true) - { - ((IContentServiceOperations)this).Save(contents, userId, raiseEvents); - } - - /// - /// Deletes all content of specified type. All children of deleted content is moved to Recycle Bin. - /// - /// This needs extra care and attention as its potentially a dangerous and extensive operation - /// Id of the - /// Optional Id of the user issueing the delete operation - public void DeleteContentOfType(int contentTypeId, int userId = 0) - { - //TODO: This currently this is called from the ContentTypeService but that needs to change, - // if we are deleting a content type, we should just delete the data and do this operation slightly differently. - // This method will recursively go lookup every content item, check if any of it's descendants are - // of a different type, move them to the recycle bin, then permanently delete the content items. - // The main problem with this is that for every content item being deleted, events are raised... - // which we need for many things like keeping caches in sync, but we can surely do this MUCH better. - - using (new WriteLock(Locker)) - { - using (var uow = UowProvider.GetUnitOfWork()) - { - var repository = RepositoryFactory.CreateContentRepository(uow); - //NOTE What about content that has the contenttype as part of its composition? - var query = Query.Builder.Where(x => x.ContentTypeId == contentTypeId); - var contents = repository.GetByQuery(query).ToArray(); - - if (Deleting.IsRaisedEventCancelled(new DeleteEventArgs(contents), this)) - return; - - foreach (var content in contents.OrderByDescending(x => x.ParentId)) - { - //Look for children of current content and move that to trash before the current content is deleted - var c = content; - var childQuery = Query.Builder.Where(x => x.Path.StartsWith(c.Path)); - var children = repository.GetByQuery(childQuery); - - foreach (var child in children) - { - if (child.ContentType.Id != contentTypeId) - MoveToRecycleBin(child, userId); - } - - //Permantly delete the content - Delete(content, userId); - } - } - - Audit(AuditType.Delete, - string.Format("Delete Content of Type {0} performed by user", contentTypeId), - userId, Constants.System.Root); - } - } - - /// - /// Permanently deletes an object as well as all of its Children. - /// - /// - /// This method will also delete associated media files, child content and possibly associated domains. - /// - /// Please note that this method will completely remove the Content from the database - /// The to delete - /// Optional Id of the User deleting the Content - public void Delete(IContent content, int userId = 0) - { - ((IContentServiceOperations)this).Delete(content, userId); - } - - /// - /// Permanently deletes versions from an object prior to a specific date. - /// This method will never delete the latest version of a content item. - /// - /// Id of the object to delete versions from - /// Latest version date - /// Optional Id of the User deleting versions of a Content object - public void DeleteVersions(int id, DateTime versionDate, int userId = 0) - { - if (DeletingVersions.IsRaisedEventCancelled(new DeleteRevisionsEventArgs(id, dateToRetain: versionDate), this)) - return; - - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateContentRepository(uow)) - { - repository.DeleteVersions(id, versionDate); - uow.Commit(); - } - - DeletedVersions.RaiseEvent(new DeleteRevisionsEventArgs(id, false, dateToRetain: versionDate), this); - - Audit(AuditType.Delete, "Delete Content by version date performed by user", userId, Constants.System.Root); - } - - /// - /// Permanently deletes specific version(s) from an object. - /// This method will never delete the latest version of a content item. - /// - /// Id of the object to delete a version from - /// Id of the version to delete - /// Boolean indicating whether to delete versions prior to the versionId - /// Optional Id of the User deleting versions of a Content object - public void DeleteVersion(int id, Guid versionId, bool deletePriorVersions, int userId = 0) - { - using (new WriteLock(Locker)) - { - if (DeletingVersions.IsRaisedEventCancelled(new DeleteRevisionsEventArgs(id, specificVersion: versionId), this)) - return; - - if (deletePriorVersions) - { - var content = GetByVersion(versionId); - DeleteVersions(id, content.UpdateDate, userId); - } - - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateContentRepository(uow)) - { - repository.DeleteVersion(versionId); - uow.Commit(); - } - - DeletedVersions.RaiseEvent(new DeleteRevisionsEventArgs(id, false, specificVersion: versionId), this); - - Audit(AuditType.Delete, "Delete Content by version performed by user", userId, Constants.System.Root); - } - } - - /// - /// Deletes an object by moving it to the Recycle Bin - /// - /// Move an item to the Recycle Bin will result in the item being unpublished - /// The to delete - /// Optional Id of the User deleting the Content - public void MoveToRecycleBin(IContent content, int userId = 0) - { - ((IContentServiceOperations)this).MoveToRecycleBin(content, userId); - } - - /// - /// Moves an object to a new location by changing its parent id. - /// - /// - /// If the object is already published it will be - /// published after being moved to its new location. Otherwise it'll just - /// be saved with a new parent id. - /// - /// The to move - /// Id of the Content's new Parent - /// Optional Id of the User moving the Content - public void Move(IContent content, int parentId, int userId = 0) - { - using (new WriteLock(Locker)) - { - //This ensures that the correct method is called if this method is used to Move to recycle bin. - if (parentId == Constants.System.RecycleBinContent) - { - MoveToRecycleBin(content, userId); - return; - } - - if (Moving.IsRaisedEventCancelled( - new MoveEventArgs( - new MoveEventInfo(content, content.Path, parentId)), this)) - { - return; - } - - //used to track all the moved entities to be given to the event - var moveInfo = new List>(); - - //call private method that does the recursive moving - PerformMove(content, parentId, userId, moveInfo); - - Moved.RaiseEvent(new MoveEventArgs(false, moveInfo.ToArray()), this); - - Audit(AuditType.Move, "Move Content performed by user", userId, content.Id); - } - } - - /// - /// Empties the Recycle Bin by deleting all that resides in the bin - /// - public void EmptyRecycleBin() - { - using (new WriteLock(Locker)) - { - Dictionary> entities; - List files; - bool success; - var nodeObjectType = new Guid(Constants.ObjectTypes.Document); - - using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) - { - //Create a dictionary of ids -> dictionary of property aliases + values - entities = repository.GetEntitiesInRecycleBin() - .ToDictionary( - key => key.Id, - val => (IEnumerable)val.Properties); - - files = ((ContentRepository)repository).GetFilesInRecycleBinForUploadField(); - - if (EmptyingRecycleBin.IsRaisedEventCancelled(new RecycleBinEventArgs(nodeObjectType, entities, files), this)) - return; - - success = repository.EmptyRecycleBin(); - - EmptiedRecycleBin.RaiseEvent(new RecycleBinEventArgs(nodeObjectType, entities, files, success), this); - - if (success) - repository.DeleteMediaFiles(files); - } - } - Audit(AuditType.Delete, "Empty Content Recycle Bin performed by user", 0, Constants.System.RecycleBinContent); - } - - /// - /// Copies an object by creating a new Content object of the same type and copies all data from the current - /// to the new copy which is returned. Recursively copies all children. - /// - /// The to copy - /// Id of the Content's new Parent - /// Boolean indicating whether the copy should be related to the original - /// Optional Id of the User copying the Content - /// The newly created object - public IContent Copy(IContent content, int parentId, bool relateToOriginal, int userId = 0) - { - return Copy(content, parentId, relateToOriginal, true, userId); - } - - /// - /// Copies an object by creating a new Content object of the same type and copies all data from the current - /// to the new copy which is returned. - /// - /// The to copy - /// Id of the Content's new Parent - /// Boolean indicating whether the copy should be related to the original - /// A value indicating whether to recursively copy children. - /// Optional Id of the User copying the Content - /// The newly created object - public IContent Copy(IContent content, int parentId, bool relateToOriginal, bool recursive, int userId = 0) - { - //TODO: This all needs to be managed correctly so that the logic is submitted in one - // transaction, the CRUD needs to be moved to the repo - - using (new WriteLock(Locker)) - { - var copy = content.DeepCloneWithResetIdentities(); - copy.ParentId = parentId; - - // A copy should never be set to published automatically even if the original was. - copy.ChangePublishedState(PublishedState.Unpublished); - - if (Copying.IsRaisedEventCancelled(new CopyEventArgs(content, copy, parentId), this)) - return null; - - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateContentRepository(uow)) - { - // Update the create author and last edit author - copy.CreatorId = userId; - copy.WriterId = userId; - - repository.AddOrUpdate(copy); - //add or update a preview - repository.AddOrUpdatePreviewXml(copy, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); - uow.Commit(); - - - //Special case for the associated tags - //TODO: Move this to the repository layer in a single transaction! - //don't copy tags data in tags table if the item is in the recycle bin - if (parentId != Constants.System.RecycleBinContent) - { - - var tags = uow.Database.Fetch("WHERE nodeId = @Id", new { Id = content.Id }); - foreach (var tag in tags) - { - uow.Database.Insert(new TagRelationshipDto { NodeId = copy.Id, TagId = tag.TagId, PropertyTypeId = tag.PropertyTypeId }); - } - } - } - - if (recursive) - { - //Look for children and copy those as well - var children = GetChildren(content.Id); - foreach (var child in children) - { - //TODO: This shouldn't recurse back to this method, it should be done in a private method - // that doesn't have a nested lock and so we can perform the entire operation in one commit. - Copy(child, copy.Id, relateToOriginal, true, userId); - } - } - - Copied.RaiseEvent(new CopyEventArgs(content, copy, false, parentId, relateToOriginal), this); - - Audit(AuditType.Copy, "Copy Content performed by user", content.WriterId, content.Id); - return copy; - } - } - - - /// - /// Sends an to Publication, which executes handlers and events for the 'Send to Publication' action. - /// - /// The to send to publication - /// Optional Id of the User issueing the send to publication - /// True if sending publication was succesfull otherwise false - public bool SendToPublication(IContent content, int userId = 0) - { - if (SendingToPublish.IsRaisedEventCancelled(new SendToPublishEventArgs(content), this)) - return false; - - //Save before raising event - Save(content, userId); - - SentToPublish.RaiseEvent(new SendToPublishEventArgs(content, false), this); - - Audit(AuditType.SendToPublish, "Send to Publish performed by user", content.WriterId, content.Id); - - return true; - } - - /// - /// Rollback an object to a previous version. - /// This will create a new version, which is a copy of all the old data. - /// - /// - /// The way data is stored actually only allows us to rollback on properties - /// and not data like Name and Alias of the Content. - /// - /// Id of the being rolled back - /// Id of the version to rollback to - /// Optional Id of the User issueing the rollback of the Content - /// The newly created object - public IContent Rollback(int id, Guid versionId, int userId = 0) - { - var content = GetByVersion(versionId); - - if (RollingBack.IsRaisedEventCancelled(new RollbackEventArgs(content), this)) - return content; - - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateContentRepository(uow)) - { - content.WriterId = userId; - content.CreatorId = userId; - content.ChangePublishedState(PublishedState.Unpublished); - - repository.AddOrUpdate(content); - //add or update a preview - repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); - uow.Commit(); - } - - RolledBack.RaiseEvent(new RollbackEventArgs(content, false), this); - - Audit(AuditType.RollBack, "Content rollback performed by user", content.WriterId, content.Id); - - return content; - } - - /// - /// Sorts a collection of objects by updating the SortOrder according - /// to the ordering of items in the passed in . - /// - /// - /// Using this method will ensure that the Published-state is maintained upon sorting - /// so the cache is updated accordingly - as needed. - /// - /// - /// - /// - /// True if sorting succeeded, otherwise False - public bool Sort(IEnumerable items, int userId = 0, bool raiseEvents = true) - { - if (raiseEvents) - { - if (Saving.IsRaisedEventCancelled(new SaveEventArgs(items), this)) - return false; - } - - var shouldBePublished = new List(); - var shouldBeSaved = new List(); - - var asArray = items.ToArray(); - using (new WriteLock(Locker)) - { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateContentRepository(uow)) - { - int i = 0; - foreach (var content in asArray) - { - //If the current sort order equals that of the content - //we don't need to update it, so just increment the sort order - //and continue. - if (content.SortOrder == i) - { - i++; - continue; - } - - content.SortOrder = i; - content.WriterId = userId; - i++; - - if (content.Published) - { - //TODO: This should not be an inner operation, but if we do this, it cannot raise events and cannot be cancellable! - var published = _publishingStrategy.Publish(content, userId); - shouldBePublished.Add(content); - } - else - shouldBeSaved.Add(content); - - repository.AddOrUpdate(content); - //add or update a preview - repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); - } - - foreach (var content in shouldBePublished) - { - //Create and Save ContentXml DTO - repository.AddOrUpdateContentXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); - } - - uow.Commit(); - } - } - - if (raiseEvents) - Saved.RaiseEvent(new SaveEventArgs(asArray, false), this); - - if (shouldBePublished.Any()) - { - //TODO: This should not be an inner operation, but if we do this, it cannot raise events and cannot be cancellable! - _publishingStrategy.PublishingFinalized(shouldBePublished, false); - } - - - Audit(AuditType.Sort, "Sorting content performed by user", userId, 0); - - return true; - } - - /// - /// Rebuilds all xml content in the cmsContentXml table for all documents - /// - /// - /// Only rebuild the xml structures for the content type ids passed in, if none then rebuilds the structures - /// for all content - /// - public void RebuildXmlStructures(params int[] contentTypeIds) - { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateContentRepository(uow)) - { - repository.RebuildXmlStructures( - content => _entitySerializer.Serialize(this, _dataTypeService, _userService, content), - contentTypeIds: contentTypeIds.Length == 0 ? null : contentTypeIds); - - uow.Commit(); - } - - Audit(AuditType.Publish, "ContentService.RebuildXmlStructures completed, the xml has been regenerated in the database", 0, Constants.System.Root); - - } - - #region Internal Methods - - /// - /// Gets a collection of descendants by the first Parent. - /// - /// item to retrieve Descendants from - /// An Enumerable list of objects - internal IEnumerable GetPublishedDescendants(IContent content) - { - using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) - { - var query = Query.Builder.Where(x => x.Id != content.Id && x.Path.StartsWith(content.Path) && x.Trashed == false); - var contents = repository.GetByPublishedVersion(query); - - return contents; - } - } - - #endregion - - #region Private Methods - - private void Audit(AuditType type, string message, int userId, int objectId) - { - var uow = UowProvider.GetUnitOfWork(); - using (var auditRepo = RepositoryFactory.CreateAuditRepository(uow)) - { - auditRepo.AddOrUpdate(new AuditItem(objectId, message, type, userId)); - uow.Commit(); - } - } - - //TODO: All of this needs to be moved to the repository - private void PerformMove(IContent content, int parentId, int userId, ICollection> moveInfo) - { - //add a tracking item to use in the Moved event - moveInfo.Add(new MoveEventInfo(content, content.Path, parentId)); - - content.WriterId = userId; - if (parentId == Constants.System.Root) - { - content.Path = string.Concat(Constants.System.Root, ",", content.Id); - content.Level = 1; - } - else - { - var parent = GetById(parentId); - content.Path = string.Concat(parent.Path, ",", content.Id); - content.Level = parent.Level + 1; - } - - //If Content is being moved away from Recycle Bin, its state should be un-trashed - if (content.Trashed && parentId != Constants.System.RecycleBinContent) - { - content.ChangeTrashedState(false, parentId); - } - else - { - content.ParentId = parentId; - } - - //If Content is published, it should be (re)published from its new location - if (content.Published) - { - //If Content is Publishable its saved and published - //otherwise we save the content without changing the publish state, and generate new xml because the Path, Level and Parent has changed. - if (IsPublishable(content)) - { - //TODO: This is raising events, probably not desirable as this costs performance for event listeners like Examine - SaveAndPublish(content, userId); - } - else - { - //TODO: This is raising events, probably not desirable as this costs performance for event listeners like Examine - Save(content, false, userId); - - //TODO: This shouldn't be here! This needs to be part of the repository logic but in order to fix this we need to - // change how this method calls "Save" as it needs to save using an internal method - using (var uow = UowProvider.GetUnitOfWork()) - { - var xml = _entitySerializer.Serialize(this, _dataTypeService, _userService, content); - - var poco = new ContentXmlDto { NodeId = content.Id, Xml = xml.ToDataString() }; - var exists = - uow.Database.FirstOrDefault("WHERE nodeId = @Id", new { Id = content.Id }) != - null; - int result = exists - ? uow.Database.Update(poco) - : Convert.ToInt32(uow.Database.Insert(poco)); - } - } - } - else - { - //TODO: This is raising events, probably not desirable as this costs performance for event listeners like Examine - Save(content, userId); - } - - //Ensure that Path and Level is updated on children - var children = GetChildren(content.Id).ToArray(); - if (children.Any()) - { - foreach (var child in children) - { - PerformMove(child, content.Id, userId, moveInfo); - } - } - } - - /// - /// Publishes a object and all its children - /// - /// The to publish along with its children - /// Optional Id of the User issueing the publishing - /// If set to true, this will also publish descendants that are completely unpublished, normally this will only publish children that have previously been published - /// - /// A list of publish statues. If the parent document is not valid or cannot be published because it's parent(s) is not published - /// then the list will only contain one status item, otherwise it will contain status items for it and all of it's descendants that - /// are to be published. - /// - private IEnumerable> PublishWithChildrenDo( - IContent content, int userId = 0, bool includeUnpublished = false) - { - if (content == null) throw new ArgumentNullException("content"); - - var evtMsgs = EventMessagesFactory.Get(); - - using (new WriteLock(Locker)) - { - var result = new List>(); - - //Check if parent is published (although not if its a root node) - if parent isn't published this Content cannot be published - if (content.ParentId != Constants.System.Root && content.ParentId != Constants.System.RecycleBinContent && IsPublishable(content) == false) - { - Logger.Info( - string.Format( - "Content '{0}' with Id '{1}' could not be published because its parent or one of its ancestors is not published.", - content.Name, content.Id)); - result.Add(Attempt.Fail(new PublishStatus(content, PublishStatusType.FailedPathNotPublished, evtMsgs))); - return result; - } - - //Content contains invalid property values and can therefore not be published - fire event? - if (!content.IsValid()) - { - Logger.Info( - string.Format("Content '{0}' with Id '{1}' could not be published because of invalid properties.", - content.Name, content.Id)); - result.Add( - Attempt.Fail( - new PublishStatus(content, PublishStatusType.FailedContentInvalid, evtMsgs) - { - InvalidProperties = ((ContentBase)content).LastInvalidProperties - })); - return result; - } - - //Consider creating a Path query instead of recursive method: - //var query = Query.Builder.Where(x => x.Path.StartsWith(content.Path)); - - var updated = new List(); - var list = new List(); - list.Add(content); //include parent item - list.AddRange(GetDescendants(content)); - - var internalStrategy = (PublishingStrategy)_publishingStrategy; - - //Publish and then update the database with new status - var publishedOutcome = internalStrategy.PublishWithChildrenInternal(list, userId, includeUnpublished).ToArray(); - var published = publishedOutcome - .Where(x => x.Success || x.Result.StatusType == PublishStatusType.SuccessAlreadyPublished) - // ensure proper order (for events) - cannot publish a child before its parent! - .OrderBy(x => x.Result.ContentItem.Level) - .ThenBy(x => x.Result.ContentItem.SortOrder); - - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateContentRepository(uow)) - { - //NOTE The Publish with subpages-dialog was used more as a republish-type-thing, so we'll have to include PublishStatusType.SuccessAlreadyPublished - //in the updated-list, so the Published event is triggered with the expected set of pages and the xml is updated. - foreach (var item in published) - { - item.Result.ContentItem.WriterId = userId; - repository.AddOrUpdate(item.Result.ContentItem); - //add or update a preview - repository.AddOrUpdatePreviewXml(item.Result.ContentItem, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); - //add or update the published xml - repository.AddOrUpdateContentXml(item.Result.ContentItem, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); - updated.Add(item.Result.ContentItem); - } - - uow.Commit(); - - } - //Save xml to db and call following method to fire event: - _publishingStrategy.PublishingFinalized(updated, false); - - Audit(AuditType.Publish, "Publish with Children performed by user", userId, content.Id); - - - return publishedOutcome; - } - } - - /// - /// UnPublishes a single object - /// - /// The to publish - /// Optional boolean to avoid having the cache refreshed when calling this Unpublish method. By default this method will update the cache. - /// Optional Id of the User issueing the publishing - /// True if unpublishing succeeded, otherwise False - private Attempt UnPublishDo(IContent content, bool omitCacheRefresh = false, int userId = 0) - { - var newest = GetById(content.Id); // ensure we have the newest version - if (content.Version != newest.Version) // but use the original object if it's already the newest version - content = newest; - - var evtMsgs = EventMessagesFactory.Get(); - - var published = content.Published ? content : GetPublishedVersion(content.Id); // get the published version - if (published == null) - { - return Attempt.Succeed(new UnPublishStatus(content, UnPublishedStatusType.SuccessAlreadyUnPublished, evtMsgs)); // already unpublished - } - - var unpublished = _publishingStrategy.UnPublish(content, userId); - if (unpublished == false) return Attempt.Fail(new UnPublishStatus(content, UnPublishedStatusType.FailedCancelledByEvent, evtMsgs)); - - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateContentRepository(uow)) - { - content.WriterId = userId; - repository.AddOrUpdate(content); - // is published is not newest, reset the published flag on published version - if (published.Version != content.Version) - repository.ClearPublished(published); - repository.DeleteContentXml(content); - - uow.Commit(); - } - //Delete xml from db? and call following method to fire event through PublishingStrategy to update cache - if (omitCacheRefresh == false) - _publishingStrategy.UnPublishingFinalized(content); - - Audit(AuditType.UnPublish, "UnPublish performed by user", userId, content.Id); - - return Attempt.Succeed(new UnPublishStatus(content, UnPublishedStatusType.Success, evtMsgs)); - } - - /// - /// Saves and Publishes a single object - /// - /// The to save and publish - /// Optional Id of the User issueing the publishing - /// Optional boolean indicating whether or not to raise save events. - /// True if publishing succeeded, otherwise False - private Attempt SaveAndPublishDo(IContent content, int userId = 0, bool raiseEvents = true) - { - var evtMsgs = EventMessagesFactory.Get(); - - if (raiseEvents) - { - if (Saving.IsRaisedEventCancelled( - new SaveEventArgs(content, evtMsgs), this)) - { - return Attempt.Fail(new PublishStatus(content, PublishStatusType.FailedCancelledByEvent, evtMsgs)); - } - } - - using (new WriteLock(Locker)) - { - //Has this content item previously been published? If so, we don't need to refresh the children - var previouslyPublished = content.HasIdentity && HasPublishedVersion(content.Id); //content might not have an id - var publishStatus = new PublishStatus(content, PublishStatusType.Success, evtMsgs); //initially set to success - - //Check if parent is published (although not if its a root node) - if parent isn't published this Content cannot be published - publishStatus.StatusType = CheckAndLogIsPublishable(content); - //if it is not successful, then check if the props are valid - if ((int)publishStatus.StatusType < 10) - { - //Content contains invalid property values and can therefore not be published - fire event? - publishStatus.StatusType = CheckAndLogIsValid(content); - //set the invalid properties (if there are any) - publishStatus.InvalidProperties = ((ContentBase)content).LastInvalidProperties; - } - //if we're still successful, then publish using the strategy - if (publishStatus.StatusType == PublishStatusType.Success) - { - var internalStrategy = (PublishingStrategy)_publishingStrategy; - //Publish and then update the database with new status - var publishResult = internalStrategy.PublishInternal(content, userId); - //set the status type to the publish result - publishStatus.StatusType = publishResult.Result.StatusType; - } - - //we are successfully published if our publishStatus is still Successful - bool published = publishStatus.StatusType == PublishStatusType.Success; - - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateContentRepository(uow)) - { - if (published == false) - { - content.ChangePublishedState(PublishedState.Saved); - } - //Since this is the Save and Publish method, the content should be saved even though the publish fails or isn't allowed - if (content.HasIdentity == false) - { - content.CreatorId = userId; - } - content.WriterId = userId; - - repository.AddOrUpdate(content); - - //Generate a new preview - repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); - - if (published) - { - //Content Xml - repository.AddOrUpdateContentXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); - } - - uow.Commit(); - } - - if (raiseEvents) - Saved.RaiseEvent(new SaveEventArgs(content, false, evtMsgs), this); - - //Save xml to db and call following method to fire event through PublishingStrategy to update cache - if (published) - { - _publishingStrategy.PublishingFinalized(content); - } - - //We need to check if children and their publish state to ensure that we 'republish' content that was previously published - if (published && previouslyPublished == false && HasChildren(content.Id)) - { - var descendants = GetPublishedDescendants(content); - - _publishingStrategy.PublishingFinalized(descendants, false); - } - - Audit(AuditType.Publish, "Save and Publish performed by user", userId, content.Id); - - return Attempt.If(publishStatus.StatusType == PublishStatusType.Success, publishStatus); - } - } - - /// - /// Saves a single object - /// - /// The to save - /// Boolean indicating whether or not to change the Published state upon saving - /// Optional Id of the User saving the Content - /// Optional boolean indicating whether or not to raise events. - private Attempt Save(IContent content, bool changeState, int userId = 0, bool raiseEvents = true) - { - var evtMsgs = EventMessagesFactory.Get(); - - if (raiseEvents) - { - if (Saving.IsRaisedEventCancelled( - new SaveEventArgs(content, evtMsgs), - this)) - { - return OperationStatus.Cancelled(evtMsgs); - } - } - - using (new WriteLock(Locker)) - { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateContentRepository(uow)) - { - if (content.HasIdentity == false) - { - content.CreatorId = userId; - } - content.WriterId = userId; - - //Only change the publish state if the "previous" version was actually published or marked as unpublished - if (changeState && (content.Published || ((Content)content).PublishedState == PublishedState.Unpublished)) - content.ChangePublishedState(PublishedState.Saved); - - repository.AddOrUpdate(content); - - //Generate a new preview - repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); - - uow.Commit(); - } - - if (raiseEvents) - Saved.RaiseEvent(new SaveEventArgs(content, false, evtMsgs), this); - - Audit(AuditType.Save, "Save Content performed by user", userId, content.Id); - - return OperationStatus.Success(evtMsgs); - } - } - - /// - /// Checks if the passed in can be published based on the anscestors publish state. - /// - /// - /// Check current is only used when falling back to checking the Parent of non-saved content, as - /// non-saved content doesn't have a valid path yet. - /// - /// to check if anscestors are published - /// Boolean indicating whether the passed in content should also be checked for published versions - /// True if the Content can be published, otherwise False - private bool IsPublishable(IContent content, bool checkCurrent) - { - var ids = content.Path.Split(',').Select(int.Parse).ToList(); - foreach (var id in ids) - { - //If Id equals that of the recycle bin we return false because nothing in the bin can be published - if (id == Constants.System.RecycleBinContent) - return false; - - //We don't check the System Root, so just continue - if (id == Constants.System.Root) continue; - - //If the current id equals that of the passed in content and if current shouldn't be checked we skip it. - if (checkCurrent == false && id == content.Id) continue; - - //Check if the content for the current id is published - escape the loop if we encounter content that isn't published - var hasPublishedVersion = HasPublishedVersion(id); - if (hasPublishedVersion == false) - return false; - } - - return true; - } - - private PublishStatusType CheckAndLogIsPublishable(IContent content) - { - //Check if parent is published (although not if its a root node) - if parent isn't published this Content cannot be published - if (content.ParentId != Constants.System.Root && content.ParentId != Constants.System.RecycleBinContent && IsPublishable(content) == false) - { - Logger.Info( - string.Format( - "Content '{0}' with Id '{1}' could not be published because its parent is not published.", - content.Name, content.Id)); - return PublishStatusType.FailedPathNotPublished; - } - else if (content.ExpireDate.HasValue && content.ExpireDate.Value > DateTime.MinValue && DateTime.Now > content.ExpireDate.Value) - { - Logger.Info( - string.Format( - "Content '{0}' with Id '{1}' has expired and could not be published.", - content.Name, content.Id)); - return PublishStatusType.FailedHasExpired; - } - else if (content.ReleaseDate.HasValue && content.ReleaseDate.Value > DateTime.MinValue && content.ReleaseDate.Value > DateTime.Now) - { - Logger.Info( - string.Format( - "Content '{0}' with Id '{1}' is awaiting release and could not be published.", - content.Name, content.Id)); - return PublishStatusType.FailedAwaitingRelease; - } - - return PublishStatusType.Success; - } - - private PublishStatusType CheckAndLogIsValid(IContent content) - { - //Content contains invalid property values and can therefore not be published - fire event? - if (content.IsValid() == false) - { - Logger.Info( - string.Format( - "Content '{0}' with Id '{1}' could not be published because of invalid properties.", - content.Name, content.Id)); - return PublishStatusType.FailedContentInvalid; - } - - return PublishStatusType.Success; - } - - private IContentType FindContentTypeByAlias(string contentTypeAlias) - { - using (var repository = RepositoryFactory.CreateContentTypeRepository(UowProvider.GetUnitOfWork())) - { - var query = Query.Builder.Where(x => x.Alias == contentTypeAlias); - var types = repository.GetByQuery(query); - - if (types.Any() == false) - throw new Exception( - string.Format("No ContentType matching the passed in Alias: '{0}' was found", - contentTypeAlias)); - - var contentType = types.First(); - - if (contentType == null) - throw new Exception(string.Format("ContentType matching the passed in Alias: '{0}' was null", - contentTypeAlias)); - - return contentType; - } - } - - #endregion - - #region Proxy Event Handlers - /// - /// Occurs before publish. - /// - /// Proxy to the real event on the - public static event TypedEventHandler> Publishing - { - add { PublishingStrategy.Publishing += value; } - remove { PublishingStrategy.Publishing -= value; } - } - - /// - /// Occurs after publish. - /// - /// Proxy to the real event on the - public static event TypedEventHandler> Published - { - add { PublishingStrategy.Published += value; } - remove { PublishingStrategy.Published -= value; } - } - /// - /// Occurs before unpublish. - /// - /// Proxy to the real event on the - public static event TypedEventHandler> UnPublishing - { - add { PublishingStrategy.UnPublishing += value; } - remove { PublishingStrategy.UnPublishing -= value; } - } - - /// - /// Occurs after unpublish. - /// - /// Proxy to the real event on the - public static event TypedEventHandler> UnPublished - { - add { PublishingStrategy.UnPublished += value; } - remove { PublishingStrategy.UnPublished -= value; } - } - #endregion - - #region Event Handlers - /// - /// Occurs before Delete - /// - public static event TypedEventHandler> Deleting; - - /// - /// Occurs after Delete - /// - public static event TypedEventHandler> Deleted; - - /// - /// Occurs before Delete Versions - /// - public static event TypedEventHandler DeletingVersions; - - /// - /// Occurs after Delete Versions - /// - public static event TypedEventHandler DeletedVersions; - - /// - /// Occurs before Save - /// - public static event TypedEventHandler> Saving; - - /// - /// Occurs after Save - /// - public static event TypedEventHandler> Saved; - - /// - /// Occurs before Create - /// - [Obsolete("Use the Created event instead, the Creating and Created events both offer the same functionality, Creating event has been deprecated.")] - public static event TypedEventHandler> Creating; - - /// - /// Occurs after Create - /// - /// - /// Please note that the Content object has been created, but might not have been saved - /// so it does not have an identity yet (meaning no Id has been set). - /// - public static event TypedEventHandler> Created; - - /// - /// Occurs before Copy - /// - public static event TypedEventHandler> Copying; - - /// - /// Occurs after Copy - /// - public static event TypedEventHandler> Copied; - - /// - /// Occurs before Content is moved to Recycle Bin - /// - public static event TypedEventHandler> Trashing; - - /// - /// Occurs after Content is moved to Recycle Bin - /// - public static event TypedEventHandler> Trashed; - - /// - /// Occurs before Move - /// - public static event TypedEventHandler> Moving; - - /// - /// Occurs after Move - /// - public static event TypedEventHandler> Moved; - - /// - /// Occurs before Rollback - /// - public static event TypedEventHandler> RollingBack; - - /// - /// Occurs after Rollback - /// - public static event TypedEventHandler> RolledBack; - - /// - /// Occurs before Send to Publish - /// - public static event TypedEventHandler> SendingToPublish; - - /// - /// Occurs after Send to Publish - /// - public static event TypedEventHandler> SentToPublish; - - /// - /// Occurs before the Recycle Bin is emptied - /// - public static event TypedEventHandler EmptyingRecycleBin; - - /// - /// Occurs after the Recycle Bin has been Emptied - /// - public static event TypedEventHandler EmptiedRecycleBin; - #endregion - } + /// + /// Represents the Content Service, which is an easy access to operations involving + /// + public class ContentService : RepositoryService, IContentService, IContentServiceOperations + { + private readonly IPublishingStrategy _publishingStrategy; + private readonly EntityXmlSerializer _entitySerializer = new EntityXmlSerializer(); + private readonly IDataTypeService _dataTypeService; + private readonly IUserService _userService; + + //Support recursive locks because some of the methods that require locking call other methods that require locking. + //for example, the Move method needs to be locked but this calls the Save method which also needs to be locked. + private static readonly ReaderWriterLockSlim Locker = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion); + + public ContentService( + IDatabaseUnitOfWorkProvider provider, + RepositoryFactory repositoryFactory, + ILogger logger, + IEventMessagesFactory eventMessagesFactory, + IPublishingStrategy publishingStrategy, + IDataTypeService dataTypeService, + IUserService userService) + : base(provider, repositoryFactory, logger, eventMessagesFactory) + { + if (publishingStrategy == null) throw new ArgumentNullException("publishingStrategy"); + if (dataTypeService == null) throw new ArgumentNullException("dataTypeService"); + if (userService == null) throw new ArgumentNullException("userService"); + _publishingStrategy = publishingStrategy; + _dataTypeService = dataTypeService; + _userService = userService; + } + + public int CountPublished(string contentTypeAlias = null) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateContentRepository(uow)) + { + return repository.CountPublished(); + } + } + + public int Count(string contentTypeAlias = null) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateContentRepository(uow)) + { + return repository.Count(contentTypeAlias); + } + } + + public int CountChildren(int parentId, string contentTypeAlias = null) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateContentRepository(uow)) + { + return repository.CountChildren(parentId, contentTypeAlias); + } + } + + public int CountDescendants(int parentId, string contentTypeAlias = null) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateContentRepository(uow)) + { + return repository.CountDescendants(parentId, contentTypeAlias); + } + } + + /// + /// Used to bulk update the permissions set for a content item. This will replace all permissions + /// assigned to an entity with a list of user id & permission pairs. + /// + /// + public void ReplaceContentPermissions(EntityPermissionSet permissionSet) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateContentRepository(uow)) + { + repository.ReplaceContentPermissions(permissionSet); + } + } + + /// + /// Assigns a single permission to the current content item for the specified user ids + /// + /// + /// + /// + public void AssignContentPermission(IContent entity, char permission, IEnumerable userIds) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateContentRepository(uow)) + { + repository.AssignEntityPermission(entity, permission, userIds); + } + } + + /// + /// Gets the list of permissions for the content item + /// + /// + /// + public IEnumerable GetPermissionsForEntity(IContent content) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateContentRepository(uow)) + { + return repository.GetPermissionsForEntity(content.Id); + } + } + + /// + /// 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, int parentId, string contentTypeAlias, int userId = 0) + { + var contentType = FindContentTypeByAlias(contentTypeAlias); + var content = new Content(name, parentId, contentType); + var parent = GetById(content.ParentId); + content.Path = string.Concat(parent.IfNotNull(x => x.Path, content.ParentId.ToString()), ",", content.Id); + + + if (Creating.IsRaisedEventCancelled(new NewEventArgs(content, contentTypeAlias, parentId), this)) + { + content.WasCancelled = true; + return content; + } + + content.CreatorId = userId; + content.WriterId = userId; + + Created.RaiseEvent(new NewEventArgs(content, false, contentTypeAlias, parentId), this); + + var uow = UowProvider.GetUnitOfWork(); + using (var auditRepo = RepositoryFactory.CreateAuditRepository(uow)) + { + auditRepo.AddOrUpdate(new AuditItem(content.Id, string.Format("Content '{0}' was created", name), AuditType.New, content.CreatorId)); + uow.Commit(); + } + + return content; + } + + /// + /// 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 + /// Parent object for the new Content + /// Alias of the + /// Optional id of the user creating the content + /// + public IContent CreateContent(string name, IContent parent, string contentTypeAlias, int userId = 0) + { + if (parent == null) throw new ArgumentNullException("parent"); + + var contentType = FindContentTypeByAlias(contentTypeAlias); + var content = new Content(name, parent, contentType); + content.Path = string.Concat(parent.Path, ",", content.Id); + + if (Creating.IsRaisedEventCancelled(new NewEventArgs(content, contentTypeAlias, parent), this)) + { + content.WasCancelled = true; + return content; + } + + content.CreatorId = userId; + content.WriterId = userId; + + Created.RaiseEvent(new NewEventArgs(content, false, contentTypeAlias, parent), this); + + Audit(AuditType.New, string.Format("Content '{0}' was created", name), content.CreatorId, content.Id); + + return content; + } + + /// + /// Creates and saves an object using the alias of the + /// that this Content should based on. + /// + /// + /// This method returns an object that has been persisted to the database + /// and therefor has an identity. + /// + /// 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 CreateContentWithIdentity(string name, int parentId, string contentTypeAlias, int userId = 0) + { + var contentType = FindContentTypeByAlias(contentTypeAlias); + var content = new Content(name, parentId, contentType); + + //NOTE: I really hate the notion of these Creating/Created events - they are so inconsistent, I've only just found + // out that in these 'WithIdentity' methods, the Saving/Saved events were not fired, wtf. Anyways, they're added now. + if (Creating.IsRaisedEventCancelled(new NewEventArgs(content, contentTypeAlias, parentId), this)) + { + content.WasCancelled = true; + return content; + } + + if (Saving.IsRaisedEventCancelled(new SaveEventArgs(content), this)) + { + content.WasCancelled = true; + return content; + } + + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateContentRepository(uow)) + { + content.CreatorId = userId; + content.WriterId = userId; + repository.AddOrUpdate(content); + //Generate a new preview + repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); + uow.Commit(); + } + + Saved.RaiseEvent(new SaveEventArgs(content, false), this); + + Created.RaiseEvent(new NewEventArgs(content, false, contentTypeAlias, parentId), this); + + Audit(AuditType.New, string.Format("Content '{0}' was created with Id {1}", name, content.Id), content.CreatorId, content.Id); + + return content; + } + + /// + /// Creates and saves an object using the alias of the + /// that this Content should based on. + /// + /// + /// This method returns an object that has been persisted to the database + /// and therefor has an identity. + /// + /// Name of the Content object + /// Parent object for the new Content + /// Alias of the + /// Optional id of the user creating the content + /// + public IContent CreateContentWithIdentity(string name, IContent parent, string contentTypeAlias, int userId = 0) + { + if (parent == null) throw new ArgumentNullException("parent"); + + var contentType = FindContentTypeByAlias(contentTypeAlias); + var content = new Content(name, parent, contentType); + + //NOTE: I really hate the notion of these Creating/Created events - they are so inconsistent, I've only just found + // out that in these 'WithIdentity' methods, the Saving/Saved events were not fired, wtf. Anyways, they're added now. + if (Creating.IsRaisedEventCancelled(new NewEventArgs(content, contentTypeAlias, parent), this)) + { + content.WasCancelled = true; + return content; + } + + if (Saving.IsRaisedEventCancelled(new SaveEventArgs(content), this)) + { + content.WasCancelled = true; + return content; + } + + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateContentRepository(uow)) + { + content.CreatorId = userId; + content.WriterId = userId; + repository.AddOrUpdate(content); + //Generate a new preview + repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); + uow.Commit(); + } + + Saved.RaiseEvent(new SaveEventArgs(content, false), this); + + Created.RaiseEvent(new NewEventArgs(content, false, contentTypeAlias, parent), this); + + Audit(AuditType.New, string.Format("Content '{0}' was created with Id {1}", name, content.Id), content.CreatorId, content.Id); + + return content; + } + + /// + /// Gets an object by Id + /// + /// Id of the Content to retrieve + /// + public IContent GetById(int id) + { + using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) + { + return repository.Get(id); + } + } + + /// + /// Gets an object by Id + /// + /// Ids of the Content to retrieve + /// + public IEnumerable GetByIds(IEnumerable ids) + { + if (ids.Any() == false) return Enumerable.Empty(); + + using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) + { + return repository.GetAll(ids.ToArray()); + } + } + + /// + /// Gets an object by its 'UniqueId' + /// + /// Guid key of the Content to retrieve + /// + public IContent GetById(Guid key) + { + using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) + { + var query = Query.Builder.Where(x => x.Key == key); + var contents = repository.GetByQuery(query); + return contents.SingleOrDefault(); + } + } + + /// + /// Gets a collection of objects by the Id of the + /// + /// Id of the + /// An Enumerable list of objects + public IEnumerable GetContentOfContentType(int id) + { + using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) + { + var query = Query.Builder.Where(x => x.ContentTypeId == id); + var contents = repository.GetByQuery(query); + + return contents; + } + } + + internal IEnumerable GetPublishedContentOfContentType(int id) + { + using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) + { + var query = Query.Builder.Where(x => x.ContentTypeId == id); + var contents = repository.GetByPublishedVersion(query); + + return contents; + } + } + + /// + /// Gets a collection of objects by Level + /// + /// The level to retrieve Content from + /// An Enumerable list of objects + public IEnumerable GetByLevel(int level) + { + using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) + { + var query = Query.Builder.Where(x => x.Level == level && !x.Path.StartsWith(Constants.System.RecycleBinContent.ToInvariantString())); + var contents = repository.GetByQuery(query); + + return contents; + } + } + + /// + /// Gets a specific version of an item. + /// + /// Id of the version to retrieve + /// An item + public IContent GetByVersion(Guid versionId) + { + using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) + { + return repository.GetByVersion(versionId); + } + } + + + /// + /// Gets a collection of an objects versions by Id + /// + /// + /// An Enumerable list of objects + public IEnumerable GetVersions(int id) + { + using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) + { + var versions = repository.GetAllVersions(id); + return versions; + } + } + + /// + /// Gets a collection of objects, which are ancestors of the current content. + /// + /// Id of the to retrieve ancestors for + /// An Enumerable list of objects + public IEnumerable GetAncestors(int id) + { + var content = GetById(id); + return GetAncestors(content); + } + + /// + /// Gets a collection of objects, which are ancestors of the current content. + /// + /// to retrieve ancestors for + /// An Enumerable list of objects + public IEnumerable GetAncestors(IContent content) + { + //null check otherwise we get exceptions + if (content.Path.IsNullOrWhiteSpace()) return Enumerable.Empty(); + + var ids = content.Path.Split(',').Where(x => x != Constants.System.Root.ToInvariantString() && x != content.Id.ToString(CultureInfo.InvariantCulture)).Select(int.Parse).ToArray(); + if (ids.Any() == false) + return new List(); + + using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) + { + return repository.GetAll(ids); + } + } + + /// + /// Gets a collection of objects by Parent Id + /// + /// Id of the Parent to retrieve Children from + /// An Enumerable list of objects + public IEnumerable GetChildren(int id) + { + using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) + { + var query = Query.Builder.Where(x => x.ParentId == id); + var contents = repository.GetByQuery(query).OrderBy(x => x.SortOrder); + + return contents; + } + } + + [Obsolete("Use the overload with 'long' parameter types instead")] + [EditorBrowsable(EditorBrowsableState.Never)] + public IEnumerable GetPagedChildren(int id, int pageIndex, int pageSize, out int totalChildren, + string orderBy, Direction orderDirection, bool orderBySystemField = true, string filter = "") + { + Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); + Mandate.ParameterCondition(pageSize > 0, "pageSize"); + using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) + { + + var query = Query.Builder; + //if the id is System Root, then just get all + if (id != Constants.System.Root) + { + query.Where(x => x.ParentId == id); + } + long total; + var contents = repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out total, orderBy, orderDirection, orderBySystemField, filter); + totalChildren = Convert.ToInt32(total); + return contents; + } + } + + /// + /// Gets a collection of objects by Parent Id + /// + /// Id of the Parent to retrieve Children from + /// Page index (zero based) + /// Page size + /// Total records query would return without paging + /// Field to order by + /// Direction to order by + /// Flag to indicate when ordering by system field + /// Search text filter + /// An Enumerable list of objects + public IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize, out long totalChildren, + string orderBy, Direction orderDirection, bool orderBySystemField = true, string filter = "") + { + Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); + Mandate.ParameterCondition(pageSize > 0, "pageSize"); + using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) + { + + var query = Query.Builder; + //if the id is System Root, then just get all + if (id != Constants.System.Root) + { + query.Where(x => x.ParentId == id); + } + var contents = repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, orderBySystemField, filter); + + return contents; + } + } + + [Obsolete("Use the overload with 'long' parameter types instead")] + [EditorBrowsable(EditorBrowsableState.Never)] + public IEnumerable GetPagedDescendants(int id, int pageIndex, int pageSize, out int totalChildren, string orderBy = "Path", Direction orderDirection = Direction.Ascending, bool orderBySystemField = true, string filter = "") + { + Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); + Mandate.ParameterCondition(pageSize > 0, "pageSize"); + using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) + { + + var query = Query.Builder; + //if the id is System Root, then just get all + if (id != Constants.System.Root) + { + query.Where(x => x.Path.SqlContains(string.Format(",{0},", id), TextColumnType.NVarchar)); + } + long total; + var contents = repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out total, orderBy, orderDirection, orderBySystemField, filter); + totalChildren = Convert.ToInt32(total); + return contents; + } + } + + /// + /// Gets a collection of objects by Parent Id + /// + /// Id of the Parent to retrieve Descendants from + /// Page number + /// Page size + /// Total records query would return without paging + /// Field to order by + /// Direction to order by + /// Flag to indicate when ordering by system field + /// Search text filter + /// An Enumerable list of objects + public IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalChildren, string orderBy = "Path", Direction orderDirection = Direction.Ascending, bool orderBySystemField = true, string filter = "") + { + Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); + Mandate.ParameterCondition(pageSize > 0, "pageSize"); + using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) + { + + var query = Query.Builder; + //if the id is System Root, then just get all + if (id != Constants.System.Root) + { + query.Where(x => x.Path.SqlContains(string.Format(",{0},", id), TextColumnType.NVarchar)); + } + var contents = repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, orderBySystemField, filter); + + return contents; + } + } + + /// + /// Gets a collection of objects by its name or partial name + /// + /// Id of the Parent to retrieve Children from + /// Full or partial name of the children + /// An Enumerable list of objects + public IEnumerable GetChildrenByName(int parentId, string name) + { + using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) + { + var query = Query.Builder.Where(x => x.ParentId == parentId && x.Name.Contains(name)); + var contents = repository.GetByQuery(query); + + return contents; + } + } + + /// + /// Gets a collection of objects by Parent Id + /// + /// Id of the Parent to retrieve Descendants from + /// An Enumerable list of objects + public IEnumerable GetDescendants(int id) + { + var content = GetById(id); + if (content == null) + { + return Enumerable.Empty(); + } + return GetDescendants(content); + } + + /// + /// Gets a collection of objects by Parent Id + /// + /// item to retrieve Descendants from + /// An Enumerable list of objects + public IEnumerable GetDescendants(IContent content) + { + using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) + { + var pathMatch = content.Path + ","; + var query = Query.Builder.Where(x => x.Path.StartsWith(pathMatch) && x.Id != content.Id); + var contents = repository.GetByQuery(query); + + return contents; + } + } + + /// + /// Gets the parent of the current content as an item. + /// + /// Id of the to retrieve the parent from + /// Parent object + public IContent GetParent(int id) + { + var content = GetById(id); + return GetParent(content); + } + + /// + /// Gets the parent of the current content as an item. + /// + /// to retrieve the parent from + /// Parent object + public IContent GetParent(IContent content) + { + if (content.ParentId == Constants.System.Root || content.ParentId == Constants.System.RecycleBinContent) + return null; + + return GetById(content.ParentId); + } + + /// + /// Gets the published version of an item + /// + /// Id of the to retrieve version from + /// An item + public IContent GetPublishedVersion(int id) + { + var version = GetVersions(id); + return version.FirstOrDefault(x => x.Published == true); + } + + /// + /// Gets the published version of a item. + /// + /// The content item. + /// The published version, if any; otherwise, null. + public IContent GetPublishedVersion(IContent content) + { + if (content.Published) return content; + return content.HasPublishedVersion + ? GetByVersion(content.PublishedVersionGuid) + : null; + } + + /// + /// Gets a collection of objects, which reside at the first level / root + /// + /// An Enumerable list of objects + public IEnumerable GetRootContent() + { + using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) + { + var query = Query.Builder.Where(x => x.ParentId == Constants.System.Root); + var contents = repository.GetByQuery(query); + + return contents; + } + } + + /// + /// Gets all published content items + /// + /// + internal IEnumerable GetAllPublished() + { + using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) + { + var query = Query.Builder.Where(x => x.Trashed == false); + return repository.GetByPublishedVersion(query); + } + } + + /// + /// Gets a collection of objects, which has an expiration date less than or equal to today. + /// + /// An Enumerable list of objects + public IEnumerable GetContentForExpiration() + { + using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) + { + var query = Query.Builder.Where(x => x.Published == true && x.ExpireDate <= DateTime.Now); + var contents = repository.GetByQuery(query); + + return contents; + } + } + + /// + /// Gets a collection of objects, which has a release date less than or equal to today. + /// + /// An Enumerable list of objects + public IEnumerable GetContentForRelease() + { + using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) + { + var query = Query.Builder.Where(x => x.Published == false && x.ReleaseDate <= DateTime.Now); + var contents = repository.GetByQuery(query); + + return contents; + } + } + + /// + /// Gets a collection of an objects, which resides in the Recycle Bin + /// + /// An Enumerable list of objects + public IEnumerable GetContentInRecycleBin() + { + using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) + { + var query = Query.Builder.Where(x => x.Path.Contains(Constants.System.RecycleBinContent.ToInvariantString())); + var contents = repository.GetByQuery(query); + + return contents; + } + } + + + + /// + /// Checks whether an item has any children + /// + /// Id of the + /// True if the content has any children otherwise False + public bool HasChildren(int id) + { + return CountChildren(id) > 0; + } + + internal int CountChildren(int id) + { + using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) + { + var query = Query.Builder.Where(x => x.ParentId == id); + var count = repository.Count(query); + return count; + } + } + + /// + /// Checks whether an item has any published versions + /// + /// Id of the + /// True if the content has any published version otherwise False + public bool HasPublishedVersion(int id) + { + using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) + { + var query = Query.Builder.Where(x => x.Published == true && x.Id == id && x.Trashed == false); + int count = repository.Count(query); + return count > 0; + } + } + + /// + /// Checks if the passed in can be published based on the anscestors publish state. + /// + /// to check if anscestors are published + /// True if the Content can be published, otherwise False + public bool IsPublishable(IContent content) + { + //If the passed in content has yet to be saved we "fallback" to checking the Parent + //because if the Parent is publishable then the current content can be Saved and Published + if (content.HasIdentity == false) + { + IContent parent = GetById(content.ParentId); + return IsPublishable(parent, true); + } + + return IsPublishable(content, false); + } + + /// + /// This will rebuild the xml structures for content in the database. + /// + /// This is not used for anything + /// True if publishing succeeded, otherwise False + /// + /// This is used for when a document type alias or a document type property is changed, the xml will need to + /// be regenerated. + /// + public bool RePublishAll(int userId = 0) + { + try + { + RebuildXmlStructures(); + return true; + } + catch (Exception ex) + { + Logger.Error("An error occurred executing RePublishAll", ex); + return false; + } + } + + /// + /// This will rebuild the xml structures for content in the database. + /// + /// + /// If specified will only rebuild the xml for the content type's specified, otherwise will update the structure + /// for all published content. + /// + internal void RePublishAll(params int[] contentTypeIds) + { + try + { + RebuildXmlStructures(contentTypeIds); + } + catch (Exception ex) + { + Logger.Error("An error occurred executing RePublishAll", ex); + } + } + + /// + /// Publishes a single object + /// + /// The to publish + /// Optional Id of the User issueing the publishing + /// True if publishing succeeded, otherwise False + public bool Publish(IContent content, int userId = 0) + { + var result = SaveAndPublishDo(content, userId); + Logger.Info("Call was made to ContentService.Publish, use PublishWithStatus instead since that method will provide more detailed information on the outcome"); + return result.Success; + } + + /// + /// Publishes a object and all its children + /// + /// The to publish along with its children + /// Optional Id of the User issueing the publishing + /// + /// The list of statuses for all published items + IEnumerable> IContentServiceOperations.PublishWithChildren(IContent content, int userId, bool includeUnpublished) + { + return PublishWithChildrenDo(content, userId, includeUnpublished); + } + + /// + /// Saves and Publishes a single object + /// + /// The to save and publish + /// Optional Id of the User issueing the publishing + /// Optional boolean indicating whether or not to raise save events. + /// True if publishing succeeded, otherwise False + Attempt IContentServiceOperations.SaveAndPublish(IContent content, int userId, bool raiseEvents) + { + return SaveAndPublishDo(content, userId, raiseEvents); + } + + /// + /// Deletes an object by moving it to the Recycle Bin + /// + /// Move an item to the Recycle Bin will result in the item being unpublished + /// The to delete + /// Optional Id of the User deleting the Content + Attempt IContentServiceOperations.MoveToRecycleBin(IContent content, int userId) + { + var evtMsgs = EventMessagesFactory.Get(); + + using (new WriteLock(Locker)) + { + var originalPath = content.Path; + + if (Trashing.IsRaisedEventCancelled( + new MoveEventArgs(evtMsgs, new MoveEventInfo(content, originalPath, Constants.System.RecycleBinContent)), + this)) + { + return OperationStatus.Cancelled(evtMsgs); + } + + var moveInfo = new List> + { + new MoveEventInfo(content, originalPath, Constants.System.RecycleBinContent) + }; + + //Make sure that published content is unpublished before being moved to the Recycle Bin + if (HasPublishedVersion(content.Id)) + { + //TODO: this shouldn't be a 'sub operation', and if it needs to be it cannot raise events and cannot be cancelled! + UnPublish(content, userId); + } + + //Unpublish descendents of the content item that is being moved to trash + var descendants = GetDescendants(content).OrderBy(x => x.Level).ToList(); + foreach (var descendant in descendants) + { + //TODO: this shouldn't be a 'sub operation', and if it needs to be it cannot raise events and cannot be cancelled! + UnPublish(descendant, userId); + } + + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateContentRepository(uow)) + { + content.WriterId = userId; + content.ChangeTrashedState(true); + repository.AddOrUpdate(content); + + //Loop through descendants to update their trash state, but ensuring structure by keeping the ParentId + foreach (var descendant in descendants) + { + moveInfo.Add(new MoveEventInfo(descendant, descendant.Path, descendant.ParentId)); + + descendant.WriterId = userId; + descendant.ChangeTrashedState(true, descendant.ParentId); + repository.AddOrUpdate(descendant); + } + + uow.Commit(); + } + + Trashed.RaiseEvent(new MoveEventArgs(false, evtMsgs, moveInfo.ToArray()), this); + + Audit(AuditType.Move, "Move Content to Recycle Bin performed by user", userId, content.Id); + + return OperationStatus.Success(evtMsgs); + } + } + + /// + /// UnPublishes a single object + /// + /// The to publish + /// Optional Id of the User issueing the publishing + /// True if unpublishing succeeded, otherwise False + Attempt IContentServiceOperations.UnPublish(IContent content, int userId) + { + return UnPublishDo(content, false, userId); + } + + /// + /// Publishes a single object + /// + /// The to publish + /// Optional Id of the User issueing the publishing + /// True if publishing succeeded, otherwise False + public Attempt PublishWithStatus(IContent content, int userId = 0) + { + return ((IContentServiceOperations)this).Publish(content, userId); + } + + /// + /// Publishes a object and all its children + /// + /// The to publish along with its children + /// Optional Id of the User issueing the publishing + /// True if publishing succeeded, otherwise False + [Obsolete("Use PublishWithChildrenWithStatus instead, that method will provide more detailed information on the outcome and also allows the includeUnpublished flag")] + public bool PublishWithChildren(IContent content, int userId = 0) + { + var result = PublishWithChildrenDo(content, userId, true); + + //This used to just return false only when the parent content failed, otherwise would always return true so we'll + // do the same thing for the moment + if (result.All(x => x.Result.ContentItem.Id != content.Id)) + return false; + + return result.Single(x => x.Result.ContentItem.Id == content.Id).Success; + } + + /// + /// Publishes a object and all its children + /// + /// The to publish along with its children + /// Optional Id of the User issueing the publishing + /// set to true if you want to also publish children that are currently unpublished + /// True if publishing succeeded, otherwise False + public IEnumerable> PublishWithChildrenWithStatus(IContent content, int userId = 0, bool includeUnpublished = false) + { + return ((IContentServiceOperations)this).PublishWithChildren(content, userId, includeUnpublished); + } + + /// + /// UnPublishes a single object + /// + /// The to publish + /// Optional Id of the User issueing the publishing + /// True if unpublishing succeeded, otherwise False + public bool UnPublish(IContent content, int userId = 0) + { + return ((IContentServiceOperations)this).UnPublish(content, userId).Success; + } + + /// + /// Saves and Publishes a single object + /// + /// The to save and publish + /// Optional Id of the User issueing the publishing + /// Optional boolean indicating whether or not to raise save events. + /// True if publishing succeeded, otherwise False + [Obsolete("Use SaveAndPublishWithStatus instead, that method will provide more detailed information on the outcome")] + public bool SaveAndPublish(IContent content, int userId = 0, bool raiseEvents = true) + { + var result = SaveAndPublishDo(content, userId, raiseEvents); + return result.Success; + } + + /// + /// Saves and Publishes a single object + /// + /// The to save and publish + /// Optional Id of the User issueing the publishing + /// Optional boolean indicating whether or not to raise save events. + /// True if publishing succeeded, otherwise False + public Attempt SaveAndPublishWithStatus(IContent content, int userId = 0, bool raiseEvents = true) + { + return ((IContentServiceOperations)this).SaveAndPublish(content, userId, raiseEvents); + } + + /// + /// Saves a single object + /// + /// The to save + /// Optional Id of the User saving the Content + /// Optional boolean indicating whether or not to raise events. + public void Save(IContent content, int userId = 0, bool raiseEvents = true) + { + ((IContentServiceOperations)this).Save(content, userId, raiseEvents); + } + + /// + /// Saves a collection of objects. + /// + /// Collection of to save + /// Optional Id of the User saving the Content + /// Optional boolean indicating whether or not to raise events. + Attempt IContentServiceOperations.Save(IEnumerable contents, int userId, bool raiseEvents) + { + var asArray = contents.ToArray(); + + var evtMsgs = EventMessagesFactory.Get(); + + if (raiseEvents) + { + if (Saving.IsRaisedEventCancelled( + new SaveEventArgs(asArray, evtMsgs), + this)) + { + return OperationStatus.Cancelled(evtMsgs); + } + } + using (new WriteLock(Locker)) + { + var containsNew = asArray.Any(x => x.HasIdentity == false); + + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateContentRepository(uow)) + { + if (containsNew) + { + foreach (var content in asArray) + { + content.WriterId = userId; + + //Only change the publish state if the "previous" version was actually published + if (content.Published) + content.ChangePublishedState(PublishedState.Saved); + + repository.AddOrUpdate(content); + //add or update preview + repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); + } + } + else + { + foreach (var content in asArray) + { + content.WriterId = userId; + repository.AddOrUpdate(content); + //add or update preview + repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); + } + } + + uow.Commit(); + } + + if (raiseEvents) + Saved.RaiseEvent(new SaveEventArgs(asArray, false, evtMsgs), this); + + Audit(AuditType.Save, "Bulk Save content performed by user", userId == -1 ? 0 : userId, Constants.System.Root); + + return OperationStatus.Success(evtMsgs); + } + } + + /// + /// Permanently deletes an object. + /// + /// + /// This method will also delete associated media files, child content and possibly associated domains. + /// + /// Please note that this method will completely remove the Content from the database + /// The to delete + /// Optional Id of the User deleting the Content + Attempt IContentServiceOperations.Delete(IContent content, int userId) + { + var evtMsgs = EventMessagesFactory.Get(); + + using (new WriteLock(Locker)) + { + if (Deleting.IsRaisedEventCancelled( + new DeleteEventArgs(content, evtMsgs), + this)) + { + return OperationStatus.Cancelled(evtMsgs); + } + + //Make sure that published content is unpublished before being deleted + if (HasPublishedVersion(content.Id)) + { + UnPublish(content, userId); + } + + //Delete children before deleting the 'possible parent' + var children = GetChildren(content.Id); + foreach (var child in children) + { + Delete(child, userId); + } + + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateContentRepository(uow)) + { + repository.Delete(content); + uow.Commit(); + + var args = new DeleteEventArgs(content, false, evtMsgs); + Deleted.RaiseEvent(args, this); + + //remove any flagged media files + repository.DeleteMediaFiles(args.MediaFilesToDelete); + } + + Audit(AuditType.Delete, "Delete Content performed by user", userId, content.Id); + + return OperationStatus.Success(evtMsgs); + } + } + + /// + /// Publishes a single object + /// + /// The to publish + /// Optional Id of the User issueing the publishing + /// The published status attempt + Attempt IContentServiceOperations.Publish(IContent content, int userId) + { + return SaveAndPublishDo(content, userId); + } + + /// + /// Saves a single object + /// + /// The to save + /// Optional Id of the User saving the Content + /// Optional boolean indicating whether or not to raise events. + Attempt IContentServiceOperations.Save(IContent content, int userId, bool raiseEvents) + { + return Save(content, true, userId, raiseEvents); + } + + /// + /// Saves a collection of objects. + /// + /// + /// If the collection of content contains new objects that references eachother by Id or ParentId, + /// then use the overload Save method with a collection of Lazy . + /// + /// Collection of to save + /// Optional Id of the User saving the Content + /// Optional boolean indicating whether or not to raise events. + public void Save(IEnumerable contents, int userId = 0, bool raiseEvents = true) + { + ((IContentServiceOperations)this).Save(contents, userId, raiseEvents); + } + + /// + /// Deletes all content of specified type. All children of deleted content is moved to Recycle Bin. + /// + /// This needs extra care and attention as its potentially a dangerous and extensive operation + /// Id of the + /// Optional Id of the user issueing the delete operation + public void DeleteContentOfType(int contentTypeId, int userId = 0) + { + //TODO: This currently this is called from the ContentTypeService but that needs to change, + // if we are deleting a content type, we should just delete the data and do this operation slightly differently. + // This method will recursively go lookup every content item, check if any of it's descendants are + // of a different type, move them to the recycle bin, then permanently delete the content items. + // The main problem with this is that for every content item being deleted, events are raised... + // which we need for many things like keeping caches in sync, but we can surely do this MUCH better. + + using (new WriteLock(Locker)) + { + using (var uow = UowProvider.GetUnitOfWork()) + { + var repository = RepositoryFactory.CreateContentRepository(uow); + //NOTE What about content that has the contenttype as part of its composition? + var query = Query.Builder.Where(x => x.ContentTypeId == contentTypeId); + var contents = repository.GetByQuery(query).ToArray(); + + if (Deleting.IsRaisedEventCancelled(new DeleteEventArgs(contents), this)) + return; + + foreach (var content in contents.OrderByDescending(x => x.ParentId)) + { + //Look for children of current content and move that to trash before the current content is deleted + var c = content; + var childQuery = Query.Builder.Where(x => x.Path.StartsWith(c.Path)); + var children = repository.GetByQuery(childQuery); + + foreach (var child in children) + { + if (child.ContentType.Id != contentTypeId) + MoveToRecycleBin(child, userId); + } + + //Permantly delete the content + Delete(content, userId); + } + } + + Audit(AuditType.Delete, + string.Format("Delete Content of Type {0} performed by user", contentTypeId), + userId, Constants.System.Root); + } + } + + /// + /// Permanently deletes an object as well as all of its Children. + /// + /// + /// This method will also delete associated media files, child content and possibly associated domains. + /// + /// Please note that this method will completely remove the Content from the database + /// The to delete + /// Optional Id of the User deleting the Content + public void Delete(IContent content, int userId = 0) + { + ((IContentServiceOperations)this).Delete(content, userId); + } + + /// + /// Permanently deletes versions from an object prior to a specific date. + /// This method will never delete the latest version of a content item. + /// + /// Id of the object to delete versions from + /// Latest version date + /// Optional Id of the User deleting versions of a Content object + public void DeleteVersions(int id, DateTime versionDate, int userId = 0) + { + if (DeletingVersions.IsRaisedEventCancelled(new DeleteRevisionsEventArgs(id, dateToRetain: versionDate), this)) + return; + + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateContentRepository(uow)) + { + repository.DeleteVersions(id, versionDate); + uow.Commit(); + } + + DeletedVersions.RaiseEvent(new DeleteRevisionsEventArgs(id, false, dateToRetain: versionDate), this); + + Audit(AuditType.Delete, "Delete Content by version date performed by user", userId, Constants.System.Root); + } + + /// + /// Permanently deletes specific version(s) from an object. + /// This method will never delete the latest version of a content item. + /// + /// Id of the object to delete a version from + /// Id of the version to delete + /// Boolean indicating whether to delete versions prior to the versionId + /// Optional Id of the User deleting versions of a Content object + public void DeleteVersion(int id, Guid versionId, bool deletePriorVersions, int userId = 0) + { + using (new WriteLock(Locker)) + { + if (DeletingVersions.IsRaisedEventCancelled(new DeleteRevisionsEventArgs(id, specificVersion: versionId), this)) + return; + + if (deletePriorVersions) + { + var content = GetByVersion(versionId); + DeleteVersions(id, content.UpdateDate, userId); + } + + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateContentRepository(uow)) + { + repository.DeleteVersion(versionId); + uow.Commit(); + } + + DeletedVersions.RaiseEvent(new DeleteRevisionsEventArgs(id, false, specificVersion: versionId), this); + + Audit(AuditType.Delete, "Delete Content by version performed by user", userId, Constants.System.Root); + } + } + + /// + /// Deletes an object by moving it to the Recycle Bin + /// + /// Move an item to the Recycle Bin will result in the item being unpublished + /// The to delete + /// Optional Id of the User deleting the Content + public void MoveToRecycleBin(IContent content, int userId = 0) + { + ((IContentServiceOperations)this).MoveToRecycleBin(content, userId); + } + + /// + /// Moves an object to a new location by changing its parent id. + /// + /// + /// If the object is already published it will be + /// published after being moved to its new location. Otherwise it'll just + /// be saved with a new parent id. + /// + /// The to move + /// Id of the Content's new Parent + /// Optional Id of the User moving the Content + public void Move(IContent content, int parentId, int userId = 0) + { + using (new WriteLock(Locker)) + { + //This ensures that the correct method is called if this method is used to Move to recycle bin. + if (parentId == Constants.System.RecycleBinContent) + { + MoveToRecycleBin(content, userId); + return; + } + + if (Moving.IsRaisedEventCancelled( + new MoveEventArgs( + new MoveEventInfo(content, content.Path, parentId)), this)) + { + return; + } + + //used to track all the moved entities to be given to the event + var moveInfo = new List>(); + + //call private method that does the recursive moving + PerformMove(content, parentId, userId, moveInfo); + + Moved.RaiseEvent(new MoveEventArgs(false, moveInfo.ToArray()), this); + + Audit(AuditType.Move, "Move Content performed by user", userId, content.Id); + } + } + + /// + /// Empties the Recycle Bin by deleting all that resides in the bin + /// + public void EmptyRecycleBin() + { + using (new WriteLock(Locker)) + { + Dictionary> entities; + List files; + bool success; + var nodeObjectType = new Guid(Constants.ObjectTypes.Document); + + using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) + { + //Create a dictionary of ids -> dictionary of property aliases + values + entities = repository.GetEntitiesInRecycleBin() + .ToDictionary( + key => key.Id, + val => (IEnumerable)val.Properties); + + files = ((ContentRepository)repository).GetFilesInRecycleBinForUploadField(); + + if (EmptyingRecycleBin.IsRaisedEventCancelled(new RecycleBinEventArgs(nodeObjectType, entities, files), this)) + return; + + success = repository.EmptyRecycleBin(); + + EmptiedRecycleBin.RaiseEvent(new RecycleBinEventArgs(nodeObjectType, entities, files, success), this); + + if (success) + repository.DeleteMediaFiles(files); + } + } + Audit(AuditType.Delete, "Empty Content Recycle Bin performed by user", 0, Constants.System.RecycleBinContent); + } + + /// + /// Copies an object by creating a new Content object of the same type and copies all data from the current + /// to the new copy which is returned. Recursively copies all children. + /// + /// The to copy + /// Id of the Content's new Parent + /// Boolean indicating whether the copy should be related to the original + /// Optional Id of the User copying the Content + /// The newly created object + public IContent Copy(IContent content, int parentId, bool relateToOriginal, int userId = 0) + { + return Copy(content, parentId, relateToOriginal, true, userId); + } + + /// + /// Copies an object by creating a new Content object of the same type and copies all data from the current + /// to the new copy which is returned. + /// + /// The to copy + /// Id of the Content's new Parent + /// Boolean indicating whether the copy should be related to the original + /// A value indicating whether to recursively copy children. + /// Optional Id of the User copying the Content + /// The newly created object + public IContent Copy(IContent content, int parentId, bool relateToOriginal, bool recursive, int userId = 0) + { + //TODO: This all needs to be managed correctly so that the logic is submitted in one + // transaction, the CRUD needs to be moved to the repo + + using (new WriteLock(Locker)) + { + var copy = content.DeepCloneWithResetIdentities(); + copy.ParentId = parentId; + + // A copy should never be set to published automatically even if the original was. + copy.ChangePublishedState(PublishedState.Unpublished); + + if (Copying.IsRaisedEventCancelled(new CopyEventArgs(content, copy, parentId), this)) + return null; + + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateContentRepository(uow)) + { + // Update the create author and last edit author + copy.CreatorId = userId; + copy.WriterId = userId; + + repository.AddOrUpdate(copy); + //add or update a preview + repository.AddOrUpdatePreviewXml(copy, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); + uow.Commit(); + + + //Special case for the associated tags + //TODO: Move this to the repository layer in a single transaction! + //don't copy tags data in tags table if the item is in the recycle bin + if (parentId != Constants.System.RecycleBinContent) + { + + var tags = uow.Database.Fetch("WHERE nodeId = @Id", new { Id = content.Id }); + foreach (var tag in tags) + { + uow.Database.Insert(new TagRelationshipDto { NodeId = copy.Id, TagId = tag.TagId, PropertyTypeId = tag.PropertyTypeId }); + } + } + } + + if (recursive) + { + //Look for children and copy those as well + var children = GetChildren(content.Id); + foreach (var child in children) + { + //TODO: This shouldn't recurse back to this method, it should be done in a private method + // that doesn't have a nested lock and so we can perform the entire operation in one commit. + Copy(child, copy.Id, relateToOriginal, true, userId); + } + } + + Copied.RaiseEvent(new CopyEventArgs(content, copy, false, parentId, relateToOriginal), this); + + Audit(AuditType.Copy, "Copy Content performed by user", content.WriterId, content.Id); + return copy; + } + } + + + /// + /// Sends an to Publication, which executes handlers and events for the 'Send to Publication' action. + /// + /// The to send to publication + /// Optional Id of the User issueing the send to publication + /// True if sending publication was succesfull otherwise false + public bool SendToPublication(IContent content, int userId = 0) + { + if (SendingToPublish.IsRaisedEventCancelled(new SendToPublishEventArgs(content), this)) + return false; + + //Save before raising event + Save(content, userId); + + SentToPublish.RaiseEvent(new SendToPublishEventArgs(content, false), this); + + Audit(AuditType.SendToPublish, "Send to Publish performed by user", content.WriterId, content.Id); + + return true; + } + + /// + /// Rollback an object to a previous version. + /// This will create a new version, which is a copy of all the old data. + /// + /// + /// The way data is stored actually only allows us to rollback on properties + /// and not data like Name and Alias of the Content. + /// + /// Id of the being rolled back + /// Id of the version to rollback to + /// Optional Id of the User issueing the rollback of the Content + /// The newly created object + public IContent Rollback(int id, Guid versionId, int userId = 0) + { + var content = GetByVersion(versionId); + + if (RollingBack.IsRaisedEventCancelled(new RollbackEventArgs(content), this)) + return content; + + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateContentRepository(uow)) + { + content.WriterId = userId; + content.CreatorId = userId; + content.ChangePublishedState(PublishedState.Unpublished); + + repository.AddOrUpdate(content); + //add or update a preview + repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); + uow.Commit(); + } + + RolledBack.RaiseEvent(new RollbackEventArgs(content, false), this); + + Audit(AuditType.RollBack, "Content rollback performed by user", content.WriterId, content.Id); + + return content; + } + + /// + /// Sorts a collection of objects by updating the SortOrder according + /// to the ordering of items in the passed in . + /// + /// + /// Using this method will ensure that the Published-state is maintained upon sorting + /// so the cache is updated accordingly - as needed. + /// + /// + /// + /// + /// True if sorting succeeded, otherwise False + public bool Sort(IEnumerable items, int userId = 0, bool raiseEvents = true) + { + if (raiseEvents) + { + if (Saving.IsRaisedEventCancelled(new SaveEventArgs(items), this)) + return false; + } + + var shouldBePublished = new List(); + var shouldBeSaved = new List(); + + var asArray = items.ToArray(); + using (new WriteLock(Locker)) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateContentRepository(uow)) + { + int i = 0; + foreach (var content in asArray) + { + //If the current sort order equals that of the content + //we don't need to update it, so just increment the sort order + //and continue. + if (content.SortOrder == i) + { + i++; + continue; + } + + content.SortOrder = i; + content.WriterId = userId; + i++; + + if (content.Published) + { + //TODO: This should not be an inner operation, but if we do this, it cannot raise events and cannot be cancellable! + var published = _publishingStrategy.Publish(content, userId); + shouldBePublished.Add(content); + } + else + shouldBeSaved.Add(content); + + repository.AddOrUpdate(content); + //add or update a preview + repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); + } + + foreach (var content in shouldBePublished) + { + //Create and Save ContentXml DTO + repository.AddOrUpdateContentXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); + } + + uow.Commit(); + } + } + + if (raiseEvents) + Saved.RaiseEvent(new SaveEventArgs(asArray, false), this); + + if (shouldBePublished.Any()) + { + //TODO: This should not be an inner operation, but if we do this, it cannot raise events and cannot be cancellable! + _publishingStrategy.PublishingFinalized(shouldBePublished, false); + } + + + Audit(AuditType.Sort, "Sorting content performed by user", userId, 0); + + return true; + } + + /// + /// Rebuilds all xml content in the cmsContentXml table for all documents + /// + /// + /// Only rebuild the xml structures for the content type ids passed in, if none then rebuilds the structures + /// for all content + /// + public void RebuildXmlStructures(params int[] contentTypeIds) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateContentRepository(uow)) + { + repository.RebuildXmlStructures( + content => _entitySerializer.Serialize(this, _dataTypeService, _userService, content), + contentTypeIds: contentTypeIds.Length == 0 ? null : contentTypeIds); + + uow.Commit(); + } + + Audit(AuditType.Publish, "ContentService.RebuildXmlStructures completed, the xml has been regenerated in the database", 0, Constants.System.Root); + + } + + #region Internal Methods + + /// + /// Gets a collection of descendants by the first Parent. + /// + /// item to retrieve Descendants from + /// An Enumerable list of objects + internal IEnumerable GetPublishedDescendants(IContent content) + { + using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) + { + var query = Query.Builder.Where(x => x.Id != content.Id && x.Path.StartsWith(content.Path) && x.Trashed == false); + var contents = repository.GetByPublishedVersion(query); + + return contents; + } + } + + #endregion + + #region Private Methods + + private void Audit(AuditType type, string message, int userId, int objectId) + { + var uow = UowProvider.GetUnitOfWork(); + using (var auditRepo = RepositoryFactory.CreateAuditRepository(uow)) + { + auditRepo.AddOrUpdate(new AuditItem(objectId, message, type, userId)); + uow.Commit(); + } + } + + //TODO: All of this needs to be moved to the repository + private void PerformMove(IContent content, int parentId, int userId, ICollection> moveInfo) + { + //add a tracking item to use in the Moved event + moveInfo.Add(new MoveEventInfo(content, content.Path, parentId)); + + content.WriterId = userId; + if (parentId == Constants.System.Root) + { + content.Path = string.Concat(Constants.System.Root, ",", content.Id); + content.Level = 1; + } + else + { + var parent = GetById(parentId); + content.Path = string.Concat(parent.Path, ",", content.Id); + content.Level = parent.Level + 1; + } + + //If Content is being moved away from Recycle Bin, its state should be un-trashed + if (content.Trashed && parentId != Constants.System.RecycleBinContent) + { + content.ChangeTrashedState(false, parentId); + } + else + { + content.ParentId = parentId; + } + + //If Content is published, it should be (re)published from its new location + if (content.Published) + { + //If Content is Publishable its saved and published + //otherwise we save the content without changing the publish state, and generate new xml because the Path, Level and Parent has changed. + if (IsPublishable(content)) + { + //TODO: This is raising events, probably not desirable as this costs performance for event listeners like Examine + SaveAndPublish(content, userId); + } + else + { + //TODO: This is raising events, probably not desirable as this costs performance for event listeners like Examine + Save(content, false, userId); + + //TODO: This shouldn't be here! This needs to be part of the repository logic but in order to fix this we need to + // change how this method calls "Save" as it needs to save using an internal method + using (var uow = UowProvider.GetUnitOfWork()) + { + var xml = _entitySerializer.Serialize(this, _dataTypeService, _userService, content); + + var poco = new ContentXmlDto { NodeId = content.Id, Xml = xml.ToDataString() }; + var exists = + uow.Database.FirstOrDefault("WHERE nodeId = @Id", new { Id = content.Id }) != + null; + int result = exists + ? uow.Database.Update(poco) + : Convert.ToInt32(uow.Database.Insert(poco)); + } + } + } + else + { + //TODO: This is raising events, probably not desirable as this costs performance for event listeners like Examine + Save(content, userId); + } + + //Ensure that Path and Level is updated on children + var children = GetChildren(content.Id).ToArray(); + if (children.Any()) + { + foreach (var child in children) + { + PerformMove(child, content.Id, userId, moveInfo); + } + } + } + + /// + /// Publishes a object and all its children + /// + /// The to publish along with its children + /// Optional Id of the User issueing the publishing + /// If set to true, this will also publish descendants that are completely unpublished, normally this will only publish children that have previously been published + /// + /// A list of publish statues. If the parent document is not valid or cannot be published because it's parent(s) is not published + /// then the list will only contain one status item, otherwise it will contain status items for it and all of it's descendants that + /// are to be published. + /// + private IEnumerable> PublishWithChildrenDo( + IContent content, int userId = 0, bool includeUnpublished = false) + { + if (content == null) throw new ArgumentNullException("content"); + + var evtMsgs = EventMessagesFactory.Get(); + + using (new WriteLock(Locker)) + { + var result = new List>(); + + //Check if parent is published (although not if its a root node) - if parent isn't published this Content cannot be published + if (content.ParentId != Constants.System.Root && content.ParentId != Constants.System.RecycleBinContent && IsPublishable(content) == false) + { + Logger.Info( + string.Format( + "Content '{0}' with Id '{1}' could not be published because its parent or one of its ancestors is not published.", + content.Name, content.Id)); + result.Add(Attempt.Fail(new PublishStatus(content, PublishStatusType.FailedPathNotPublished, evtMsgs))); + return result; + } + + //Content contains invalid property values and can therefore not be published - fire event? + if (!content.IsValid()) + { + Logger.Info( + string.Format("Content '{0}' with Id '{1}' could not be published because of invalid properties.", + content.Name, content.Id)); + result.Add( + Attempt.Fail( + new PublishStatus(content, PublishStatusType.FailedContentInvalid, evtMsgs) + { + InvalidProperties = ((ContentBase)content).LastInvalidProperties + })); + return result; + } + + //Consider creating a Path query instead of recursive method: + //var query = Query.Builder.Where(x => x.Path.StartsWith(content.Path)); + + var updated = new List(); + var list = new List(); + list.Add(content); //include parent item + list.AddRange(GetDescendants(content)); + + var internalStrategy = (PublishingStrategy)_publishingStrategy; + + //Publish and then update the database with new status + var publishedOutcome = internalStrategy.PublishWithChildrenInternal(list, userId, includeUnpublished).ToArray(); + var published = publishedOutcome + .Where(x => x.Success || x.Result.StatusType == PublishStatusType.SuccessAlreadyPublished) + // ensure proper order (for events) - cannot publish a child before its parent! + .OrderBy(x => x.Result.ContentItem.Level) + .ThenBy(x => x.Result.ContentItem.SortOrder); + + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateContentRepository(uow)) + { + //NOTE The Publish with subpages-dialog was used more as a republish-type-thing, so we'll have to include PublishStatusType.SuccessAlreadyPublished + //in the updated-list, so the Published event is triggered with the expected set of pages and the xml is updated. + foreach (var item in published) + { + item.Result.ContentItem.WriterId = userId; + repository.AddOrUpdate(item.Result.ContentItem); + //add or update a preview + repository.AddOrUpdatePreviewXml(item.Result.ContentItem, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); + //add or update the published xml + repository.AddOrUpdateContentXml(item.Result.ContentItem, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); + updated.Add(item.Result.ContentItem); + } + + uow.Commit(); + + } + //Save xml to db and call following method to fire event: + _publishingStrategy.PublishingFinalized(updated, false); + + Audit(AuditType.Publish, "Publish with Children performed by user", userId, content.Id); + + + return publishedOutcome; + } + } + + /// + /// UnPublishes a single object + /// + /// The to publish + /// Optional boolean to avoid having the cache refreshed when calling this Unpublish method. By default this method will update the cache. + /// Optional Id of the User issueing the publishing + /// True if unpublishing succeeded, otherwise False + private Attempt UnPublishDo(IContent content, bool omitCacheRefresh = false, int userId = 0) + { + var newest = GetById(content.Id); // ensure we have the newest version + if (content.Version != newest.Version) // but use the original object if it's already the newest version + content = newest; + + var evtMsgs = EventMessagesFactory.Get(); + + var published = content.Published ? content : GetPublishedVersion(content.Id); // get the published version + if (published == null) + { + return Attempt.Succeed(new UnPublishStatus(content, UnPublishedStatusType.SuccessAlreadyUnPublished, evtMsgs)); // already unpublished + } + + var unpublished = _publishingStrategy.UnPublish(content, userId); + if (unpublished == false) return Attempt.Fail(new UnPublishStatus(content, UnPublishedStatusType.FailedCancelledByEvent, evtMsgs)); + + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateContentRepository(uow)) + { + content.WriterId = userId; + repository.AddOrUpdate(content); + // is published is not newest, reset the published flag on published version + if (published.Version != content.Version) + repository.ClearPublished(published); + repository.DeleteContentXml(content); + + uow.Commit(); + } + //Delete xml from db? and call following method to fire event through PublishingStrategy to update cache + if (omitCacheRefresh == false) + _publishingStrategy.UnPublishingFinalized(content); + + Audit(AuditType.UnPublish, "UnPublish performed by user", userId, content.Id); + + return Attempt.Succeed(new UnPublishStatus(content, UnPublishedStatusType.Success, evtMsgs)); + } + + /// + /// Saves and Publishes a single object + /// + /// The to save and publish + /// Optional Id of the User issueing the publishing + /// Optional boolean indicating whether or not to raise save events. + /// True if publishing succeeded, otherwise False + private Attempt SaveAndPublishDo(IContent content, int userId = 0, bool raiseEvents = true) + { + var evtMsgs = EventMessagesFactory.Get(); + + if (raiseEvents) + { + if (Saving.IsRaisedEventCancelled( + new SaveEventArgs(content, evtMsgs), this)) + { + return Attempt.Fail(new PublishStatus(content, PublishStatusType.FailedCancelledByEvent, evtMsgs)); + } + } + + using (new WriteLock(Locker)) + { + //Has this content item previously been published? If so, we don't need to refresh the children + var previouslyPublished = content.HasIdentity && HasPublishedVersion(content.Id); //content might not have an id + var publishStatus = new PublishStatus(content, PublishStatusType.Success, evtMsgs); //initially set to success + + //Check if parent is published (although not if its a root node) - if parent isn't published this Content cannot be published + publishStatus.StatusType = CheckAndLogIsPublishable(content); + //if it is not successful, then check if the props are valid + if ((int)publishStatus.StatusType < 10) + { + //Content contains invalid property values and can therefore not be published - fire event? + publishStatus.StatusType = CheckAndLogIsValid(content); + //set the invalid properties (if there are any) + publishStatus.InvalidProperties = ((ContentBase)content).LastInvalidProperties; + } + //if we're still successful, then publish using the strategy + if (publishStatus.StatusType == PublishStatusType.Success) + { + var internalStrategy = (PublishingStrategy)_publishingStrategy; + //Publish and then update the database with new status + var publishResult = internalStrategy.PublishInternal(content, userId); + //set the status type to the publish result + publishStatus.StatusType = publishResult.Result.StatusType; + } + + //we are successfully published if our publishStatus is still Successful + bool published = publishStatus.StatusType == PublishStatusType.Success; + + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateContentRepository(uow)) + { + if (published == false) + { + content.ChangePublishedState(PublishedState.Saved); + } + //Since this is the Save and Publish method, the content should be saved even though the publish fails or isn't allowed + if (content.HasIdentity == false) + { + content.CreatorId = userId; + } + content.WriterId = userId; + + repository.AddOrUpdate(content); + + //Generate a new preview + repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); + + if (published) + { + //Content Xml + repository.AddOrUpdateContentXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); + } + + uow.Commit(); + } + + if (raiseEvents) + Saved.RaiseEvent(new SaveEventArgs(content, false, evtMsgs), this); + + //Save xml to db and call following method to fire event through PublishingStrategy to update cache + if (published) + { + _publishingStrategy.PublishingFinalized(content); + } + + //We need to check if children and their publish state to ensure that we 'republish' content that was previously published + if (published && previouslyPublished == false && HasChildren(content.Id)) + { + var descendants = GetPublishedDescendants(content); + + _publishingStrategy.PublishingFinalized(descendants, false); + } + + Audit(AuditType.Publish, "Save and Publish performed by user", userId, content.Id); + + return Attempt.If(publishStatus.StatusType == PublishStatusType.Success, publishStatus); + } + } + + /// + /// Saves a single object + /// + /// The to save + /// Boolean indicating whether or not to change the Published state upon saving + /// Optional Id of the User saving the Content + /// Optional boolean indicating whether or not to raise events. + private Attempt Save(IContent content, bool changeState, int userId = 0, bool raiseEvents = true) + { + var evtMsgs = EventMessagesFactory.Get(); + + if (raiseEvents) + { + if (Saving.IsRaisedEventCancelled( + new SaveEventArgs(content, evtMsgs), + this)) + { + return OperationStatus.Cancelled(evtMsgs); + } + } + + using (new WriteLock(Locker)) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateContentRepository(uow)) + { + if (content.HasIdentity == false) + { + content.CreatorId = userId; + } + content.WriterId = userId; + + //Only change the publish state if the "previous" version was actually published or marked as unpublished + if (changeState && (content.Published || ((Content)content).PublishedState == PublishedState.Unpublished)) + content.ChangePublishedState(PublishedState.Saved); + + repository.AddOrUpdate(content); + + //Generate a new preview + repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, c)); + + uow.Commit(); + } + + if (raiseEvents) + Saved.RaiseEvent(new SaveEventArgs(content, false, evtMsgs), this); + + Audit(AuditType.Save, "Save Content performed by user", userId, content.Id); + + return OperationStatus.Success(evtMsgs); + } + } + + /// + /// Checks if the passed in can be published based on the anscestors publish state. + /// + /// + /// Check current is only used when falling back to checking the Parent of non-saved content, as + /// non-saved content doesn't have a valid path yet. + /// + /// to check if anscestors are published + /// Boolean indicating whether the passed in content should also be checked for published versions + /// True if the Content can be published, otherwise False + private bool IsPublishable(IContent content, bool checkCurrent) + { + var ids = content.Path.Split(',').Select(int.Parse).ToList(); + foreach (var id in ids) + { + //If Id equals that of the recycle bin we return false because nothing in the bin can be published + if (id == Constants.System.RecycleBinContent) + return false; + + //We don't check the System Root, so just continue + if (id == Constants.System.Root) continue; + + //If the current id equals that of the passed in content and if current shouldn't be checked we skip it. + if (checkCurrent == false && id == content.Id) continue; + + //Check if the content for the current id is published - escape the loop if we encounter content that isn't published + var hasPublishedVersion = HasPublishedVersion(id); + if (hasPublishedVersion == false) + return false; + } + + return true; + } + + private PublishStatusType CheckAndLogIsPublishable(IContent content) + { + //Check if parent is published (although not if its a root node) - if parent isn't published this Content cannot be published + if (content.ParentId != Constants.System.Root && content.ParentId != Constants.System.RecycleBinContent && IsPublishable(content) == false) + { + Logger.Info( + string.Format( + "Content '{0}' with Id '{1}' could not be published because its parent is not published.", + content.Name, content.Id)); + return PublishStatusType.FailedPathNotPublished; + } + else if (content.ExpireDate.HasValue && content.ExpireDate.Value > DateTime.MinValue && DateTime.Now > content.ExpireDate.Value) + { + Logger.Info( + string.Format( + "Content '{0}' with Id '{1}' has expired and could not be published.", + content.Name, content.Id)); + return PublishStatusType.FailedHasExpired; + } + else if (content.ReleaseDate.HasValue && content.ReleaseDate.Value > DateTime.MinValue && content.ReleaseDate.Value > DateTime.Now) + { + Logger.Info( + string.Format( + "Content '{0}' with Id '{1}' is awaiting release and could not be published.", + content.Name, content.Id)); + return PublishStatusType.FailedAwaitingRelease; + } + + return PublishStatusType.Success; + } + + private PublishStatusType CheckAndLogIsValid(IContent content) + { + //Content contains invalid property values and can therefore not be published - fire event? + if (content.IsValid() == false) + { + Logger.Info( + string.Format( + "Content '{0}' with Id '{1}' could not be published because of invalid properties.", + content.Name, content.Id)); + return PublishStatusType.FailedContentInvalid; + } + + return PublishStatusType.Success; + } + + private IContentType FindContentTypeByAlias(string contentTypeAlias) + { + using (var repository = RepositoryFactory.CreateContentTypeRepository(UowProvider.GetUnitOfWork())) + { + var query = Query.Builder.Where(x => x.Alias == contentTypeAlias); + var types = repository.GetByQuery(query); + + if (types.Any() == false) + throw new Exception( + string.Format("No ContentType matching the passed in Alias: '{0}' was found", + contentTypeAlias)); + + var contentType = types.First(); + + if (contentType == null) + throw new Exception(string.Format("ContentType matching the passed in Alias: '{0}' was null", + contentTypeAlias)); + + return contentType; + } + } + + #endregion + + #region Proxy Event Handlers + /// + /// Occurs before publish. + /// + /// Proxy to the real event on the + public static event TypedEventHandler> Publishing + { + add { PublishingStrategy.Publishing += value; } + remove { PublishingStrategy.Publishing -= value; } + } + + /// + /// Occurs after publish. + /// + /// Proxy to the real event on the + public static event TypedEventHandler> Published + { + add { PublishingStrategy.Published += value; } + remove { PublishingStrategy.Published -= value; } + } + /// + /// Occurs before unpublish. + /// + /// Proxy to the real event on the + public static event TypedEventHandler> UnPublishing + { + add { PublishingStrategy.UnPublishing += value; } + remove { PublishingStrategy.UnPublishing -= value; } + } + + /// + /// Occurs after unpublish. + /// + /// Proxy to the real event on the + public static event TypedEventHandler> UnPublished + { + add { PublishingStrategy.UnPublished += value; } + remove { PublishingStrategy.UnPublished -= value; } + } + #endregion + + #region Event Handlers + /// + /// Occurs before Delete + /// + public static event TypedEventHandler> Deleting; + + /// + /// Occurs after Delete + /// + public static event TypedEventHandler> Deleted; + + /// + /// Occurs before Delete Versions + /// + public static event TypedEventHandler DeletingVersions; + + /// + /// Occurs after Delete Versions + /// + public static event TypedEventHandler DeletedVersions; + + /// + /// Occurs before Save + /// + public static event TypedEventHandler> Saving; + + /// + /// Occurs after Save + /// + public static event TypedEventHandler> Saved; + + /// + /// Occurs before Create + /// + [Obsolete("Use the Created event instead, the Creating and Created events both offer the same functionality, Creating event has been deprecated.")] + public static event TypedEventHandler> Creating; + + /// + /// Occurs after Create + /// + /// + /// Please note that the Content object has been created, but might not have been saved + /// so it does not have an identity yet (meaning no Id has been set). + /// + public static event TypedEventHandler> Created; + + /// + /// Occurs before Copy + /// + public static event TypedEventHandler> Copying; + + /// + /// Occurs after Copy + /// + public static event TypedEventHandler> Copied; + + /// + /// Occurs before Content is moved to Recycle Bin + /// + public static event TypedEventHandler> Trashing; + + /// + /// Occurs after Content is moved to Recycle Bin + /// + public static event TypedEventHandler> Trashed; + + /// + /// Occurs before Move + /// + public static event TypedEventHandler> Moving; + + /// + /// Occurs after Move + /// + public static event TypedEventHandler> Moved; + + /// + /// Occurs before Rollback + /// + public static event TypedEventHandler> RollingBack; + + /// + /// Occurs after Rollback + /// + public static event TypedEventHandler> RolledBack; + + /// + /// Occurs before Send to Publish + /// + public static event TypedEventHandler> SendingToPublish; + + /// + /// Occurs after Send to Publish + /// + public static event TypedEventHandler> SentToPublish; + + /// + /// Occurs before the Recycle Bin is emptied + /// + public static event TypedEventHandler EmptyingRecycleBin; + + /// + /// Occurs after the Recycle Bin has been Emptied + /// + public static event TypedEventHandler EmptiedRecycleBin; + #endregion + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Services/IContentService.cs b/src/Umbraco.Core/Services/IContentService.cs index fa688eae39..95333bee85 100644 --- a/src/Umbraco.Core/Services/IContentService.cs +++ b/src/Umbraco.Core/Services/IContentService.cs @@ -8,579 +8,579 @@ using Umbraco.Core.Publishing; namespace Umbraco.Core.Services { - /// - /// A temporary interface until we are in v8, this is used to return a different result for the same method and this interface gets implemented - /// explicitly. These methods will replace the normal ones in IContentService in v8 and this will be removed. - /// - public interface IContentServiceOperations - { - //TODO: Remove this class in v8 - - //TODO: There's probably more that needs to be added like the EmptyRecycleBin, etc... - - /// - /// Saves a single object - /// - /// The to save - /// Optional Id of the User saving the Content - /// Optional boolean indicating whether or not to raise events. - Attempt Save(IContent content, int userId = 0, bool raiseEvents = true); - - /// - /// Saves a collection of objects. - /// - /// Collection of to save - /// Optional Id of the User saving the Content - /// Optional boolean indicating whether or not to raise events. - Attempt Save(IEnumerable contents, int userId = 0, bool raiseEvents = true); - - /// - /// Permanently deletes an object. - /// - /// - /// This method will also delete associated media files, child content and possibly associated domains. - /// - /// Please note that this method will completely remove the Content from the database - /// The to delete - /// Optional Id of the User deleting the Content - Attempt Delete(IContent content, int userId = 0); - - /// - /// Publishes a single object - /// - /// The to publish - /// Optional Id of the User issueing the publishing - /// The published status attempt - Attempt Publish(IContent content, int userId = 0); - - /// - /// Publishes a object and all its children - /// - /// The to publish along with its children - /// Optional Id of the User issueing the publishing - /// - /// The list of statuses for all published items - IEnumerable> PublishWithChildren(IContent content, int userId = 0, bool includeUnpublished = false); - - /// - /// Saves and Publishes a single object - /// - /// The to save and publish - /// Optional Id of the User issueing the publishing - /// Optional boolean indicating whether or not to raise save events. - /// True if publishing succeeded, otherwise False - Attempt SaveAndPublish(IContent content, int userId = 0, bool raiseEvents = true); - - /// - /// Deletes an object by moving it to the Recycle Bin - /// - /// Move an item to the Recycle Bin will result in the item being unpublished - /// The to delete - /// Optional Id of the User deleting the Content - Attempt MoveToRecycleBin(IContent content, int userId = 0); - - /// - /// UnPublishes a single object - /// - /// The to publish - /// Optional Id of the User issueing the publishing - /// True if unpublishing succeeded, otherwise False - Attempt UnPublish(IContent content, int userId = 0); - } - - /// - /// Defines the ContentService, which is an easy access to operations involving - /// - public interface IContentService : IService - { - /// - /// Rebuilds all xml content in the cmsContentXml table for all documents - /// - /// - /// Only rebuild the xml structures for the content type ids passed in, if none then rebuilds the structures - /// for all content - /// - void RebuildXmlStructures(params int[] contentTypeIds); - - int CountPublished(string contentTypeAlias = null); - int Count(string contentTypeAlias = null); - int CountChildren(int parentId, string contentTypeAlias = null); - int CountDescendants(int parentId, string contentTypeAlias = null); - - /// - /// Used to bulk update the permissions set for a content item. This will replace all permissions - /// assigned to an entity with a list of user id & permission pairs. - /// - /// - void ReplaceContentPermissions(EntityPermissionSet permissionSet); - - /// - /// Assigns a single permission to the current content item for the specified user ids - /// - /// - /// - /// - void AssignContentPermission(IContent entity, char permission, IEnumerable userIds); - - /// - /// Gets the list of permissions for the content item - /// - /// - /// - IEnumerable GetPermissionsForEntity(IContent content); - - bool SendToPublication(IContent content, int userId = 0); - - IEnumerable GetByIds(IEnumerable ids); - - /// - /// 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 - /// - IContent CreateContent(string name, int parentId, string contentTypeAlias, int userId = 0); - - /// - /// 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 - /// Parent object for the new Content - /// Alias of the - /// Optional id of the user creating the content - /// - IContent CreateContent(string name, IContent parent, string contentTypeAlias, int userId = 0); - - /// - /// Gets an object by Id - /// - /// Id of the Content to retrieve - /// - IContent GetById(int id); - - /// - /// Gets an object by its 'UniqueId' - /// - /// Guid key of the Content to retrieve - /// - IContent GetById(Guid key); - - /// - /// Gets a collection of objects by the Id of the - /// - /// Id of the - /// An Enumerable list of objects - IEnumerable GetContentOfContentType(int id); - - /// - /// Gets a collection of objects by Level - /// - /// The level to retrieve Content from - /// An Enumerable list of objects - IEnumerable GetByLevel(int level); - - /// - /// Gets a collection of objects by Parent Id - /// - /// Id of the Parent to retrieve Children from - /// An Enumerable list of objects - IEnumerable GetChildren(int id); - - [Obsolete("Use the overload with 'long' parameter types instead")] - [EditorBrowsable(EditorBrowsableState.Never)] - IEnumerable GetPagedChildren(int id, int pageIndex, int pageSize, out int totalRecords, - string orderBy = "SortOrder", Direction orderDirection = Direction.Ascending, bool orderBySystemField = true, string filter = ""); - - /// - /// Gets a collection of objects by Parent Id - /// - /// Id of the Parent to retrieve Children from - /// Page number - /// Page size - /// Total records query would return without paging - /// Field to order by - /// Direction to order by - /// Flag to indicate when ordering by system field - /// Search text filter - /// An Enumerable list of objects - IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize, out long totalRecords, - string orderBy = "SortOrder", Direction orderDirection = Direction.Ascending, bool orderBySystemField = true, string filter = ""); - - [Obsolete("Use the overload with 'long' parameter types instead")] - [EditorBrowsable(EditorBrowsableState.Never)] - IEnumerable GetPagedDescendants(int id, int pageIndex, int pageSize, out int totalRecords, - string orderBy = "Path", Direction orderDirection = Direction.Ascending, bool orderBySystemField = true, string filter = ""); - - /// - /// Gets a collection of objects by Parent Id - /// - /// Id of the Parent to retrieve Descendants from - /// Page number - /// Page size - /// Total records query would return without paging - /// Field to order by - /// Direction to order by - /// Flag to indicate when ordering by system field - /// Search text filter - /// An Enumerable list of objects - IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalRecords, - string orderBy = "Path", Direction orderDirection = Direction.Ascending, bool orderBySystemField = true, string filter = ""); - - /// - /// Gets a collection of an objects versions by its Id - /// - /// - /// An Enumerable list of objects - IEnumerable GetVersions(int id); - - /// - /// Gets a collection of objects, which reside at the first level / root - /// - /// An Enumerable list of objects - IEnumerable GetRootContent(); - - /// - /// Gets a collection of objects, which has an expiration date greater then today - /// - /// An Enumerable list of objects - IEnumerable GetContentForExpiration(); - - /// - /// Gets a collection of objects, which has a release date greater then today - /// - /// An Enumerable list of objects - IEnumerable GetContentForRelease(); - - /// - /// Gets a collection of an objects, which resides in the Recycle Bin - /// - /// An Enumerable list of objects - IEnumerable GetContentInRecycleBin(); - - /// - /// Saves a single object - /// - /// The to save - /// Optional Id of the User saving the Content - /// Optional boolean indicating whether or not to raise events. - void Save(IContent content, int userId = 0, bool raiseEvents = true); - - /// - /// Saves a collection of objects. - /// - /// Collection of to save - /// Optional Id of the User saving the Content - /// Optional boolean indicating whether or not to raise events. - void Save(IEnumerable contents, int userId = 0, bool raiseEvents = true); - - /// - /// Deletes all content of specified type. All children of deleted content is moved to Recycle Bin. - /// - /// This needs extra care and attention as its potentially a dangerous and extensive operation - /// Id of the - /// Optional Id of the user issueing the delete operation - void DeleteContentOfType(int contentTypeId, int userId = 0); - - /// - /// Permanently deletes versions from an object prior to a specific date. - /// - /// Id of the object to delete versions from - /// Latest version date - /// Optional Id of the User deleting versions of a Content object - void DeleteVersions(int id, DateTime versionDate, int userId = 0); - - /// - /// Permanently deletes a specific version from an object. - /// - /// Id of the object to delete a version from - /// Id of the version to delete - /// Boolean indicating whether to delete versions prior to the versionId - /// Optional Id of the User deleting versions of a Content object - void DeleteVersion(int id, Guid versionId, bool deletePriorVersions, int userId = 0); - - /// - /// Deletes an object by moving it to the Recycle Bin - /// - /// Move an item to the Recycle Bin will result in the item being unpublished - /// The to delete - /// Optional Id of the User deleting the Content - void MoveToRecycleBin(IContent content, int userId = 0); - - /// - /// Moves an object to a new location - /// - /// The to move - /// Id of the Content's new Parent - /// Optional Id of the User moving the Content - void Move(IContent content, int parentId, int userId = 0); - - /// - /// Empties the Recycle Bin by deleting all that resides in the bin - /// - void EmptyRecycleBin(); - - /// - /// Rollback an object to a previous version. - /// This will create a new version, which is a copy of all the old data. - /// - /// Id of the being rolled back - /// Id of the version to rollback to - /// Optional Id of the User issueing the rollback of the Content - /// The newly created object - IContent Rollback(int id, Guid versionId, int userId = 0); - - /// - /// Gets a collection of objects by its name or partial name - /// - /// Id of the Parent to retrieve Children from - /// Full or partial name of the children - /// An Enumerable list of objects - IEnumerable GetChildrenByName(int parentId, string name); - - /// - /// Gets a collection of objects by Parent Id - /// - /// Id of the Parent to retrieve Descendants from - /// An Enumerable list of objects - IEnumerable GetDescendants(int id); - - /// - /// Gets a collection of objects by Parent Id - /// - /// item to retrieve Descendants from - /// An Enumerable list of objects - IEnumerable GetDescendants(IContent content); - - /// - /// Gets a specific version of an item. - /// - /// Id of the version to retrieve - /// An item - IContent GetByVersion(Guid versionId); - - /// - /// Gets the published version of an item - /// - /// Id of the to retrieve version from - /// An item - IContent GetPublishedVersion(int id); - - /// - /// Gets the published version of a item. - /// - /// The content item. - /// The published version, if any; otherwise, null. - IContent GetPublishedVersion(IContent content); - - /// - /// Checks whether an item has any children - /// - /// Id of the - /// True if the content has any children otherwise False - bool HasChildren(int id); - - /// - /// Checks whether an item has any published versions - /// - /// Id of the - /// True if the content has any published version otherwise False - bool HasPublishedVersion(int id); - - /// - /// Re-Publishes all Content - /// - /// Optional Id of the User issueing the publishing - /// True if publishing succeeded, otherwise False - bool RePublishAll(int userId = 0); - - /// - /// Publishes a single object - /// - /// The to publish - /// Optional Id of the User issueing the publishing - /// True if publishing succeeded, otherwise False - bool Publish(IContent content, int userId = 0); - - /// - /// Publishes a single object - /// - /// The to publish - /// Optional Id of the User issueing the publishing - /// The published status attempt - Attempt PublishWithStatus(IContent content, int userId = 0); - - /// - /// Publishes a object and all its children - /// - /// The to publish along with its children - /// Optional Id of the User issueing the publishing - /// True if publishing succeeded, otherwise False - [EditorBrowsable(EditorBrowsableState.Never)] - [Obsolete("Use PublishWithChildrenWithStatus instead, that method will provide more detailed information on the outcome and also allows the includeUnpublished flag")] - bool PublishWithChildren(IContent content, int userId = 0); - - /// - /// Publishes a object and all its children - /// - /// The to publish along with its children - /// Optional Id of the User issueing the publishing - /// - /// The list of statuses for all published items - IEnumerable> PublishWithChildrenWithStatus(IContent content, int userId = 0, bool includeUnpublished = false); - - /// - /// UnPublishes a single object - /// - /// The to publish - /// Optional Id of the User issueing the publishing - /// True if unpublishing succeeded, otherwise False - bool UnPublish(IContent content, int userId = 0); - - /// - /// Saves and Publishes a single object - /// - /// The to save and publish - /// Optional Id of the User issueing the publishing - /// Optional boolean indicating whether or not to raise save events. - /// True if publishing succeeded, otherwise False - [Obsolete("Use SaveAndPublishWithStatus instead, that method will provide more detailed information on the outcome")] - [EditorBrowsable(EditorBrowsableState.Never)] - bool SaveAndPublish(IContent content, int userId = 0, bool raiseEvents = true); - - /// - /// Saves and Publishes a single object - /// - /// The to save and publish - /// Optional Id of the User issueing the publishing - /// Optional boolean indicating whether or not to raise save events. - /// True if publishing succeeded, otherwise False - Attempt SaveAndPublishWithStatus(IContent content, int userId = 0, bool raiseEvents = true); - - /// - /// Permanently deletes an object. - /// - /// - /// This method will also delete associated media files, child content and possibly associated domains. - /// - /// Please note that this method will completely remove the Content from the database - /// The to delete - /// Optional Id of the User deleting the Content - void Delete(IContent content, int userId = 0); - - /// - /// Copies an object by creating a new Content object of the same type and copies all data from the current - /// to the new copy, which is returned. Recursively copies all children. - /// - /// The to copy - /// Id of the Content's new Parent - /// Boolean indicating whether the copy should be related to the original - /// Optional Id of the User copying the Content - /// The newly created object - IContent Copy(IContent content, int parentId, bool relateToOriginal, int userId = 0); - - /// - /// Copies an object by creating a new Content object of the same type and copies all data from the current - /// to the new copy which is returned. - /// - /// The to copy - /// Id of the Content's new Parent - /// Boolean indicating whether the copy should be related to the original - /// A value indicating whether to recursively copy children. - /// Optional Id of the User copying the Content - /// The newly created object - IContent Copy(IContent content, int parentId, bool relateToOriginal, bool recursive, int userId = 0); - - /// - /// Checks if the passed in can be published based on the anscestors publish state. - /// - /// to check if anscestors are published - /// True if the Content can be published, otherwise False - bool IsPublishable(IContent content); - - /// - /// Gets a collection of objects, which are ancestors of the current content. - /// - /// Id of the to retrieve ancestors for - /// An Enumerable list of objects - IEnumerable GetAncestors(int id); - - /// - /// Gets a collection of objects, which are ancestors of the current content. - /// - /// to retrieve ancestors for - /// An Enumerable list of objects - IEnumerable GetAncestors(IContent content); - - /// - /// Sorts a collection of objects by updating the SortOrder according - /// to the ordering of items in the passed in . - /// - /// - /// Using this method will ensure that the Published-state is maintained upon sorting - /// so the cache is updated accordingly - as needed. - /// - /// - /// - /// - /// True if sorting succeeded, otherwise False - bool Sort(IEnumerable items, int userId = 0, bool raiseEvents = true); - - /// - /// Gets the parent of the current content as an item. - /// - /// Id of the to retrieve the parent from - /// Parent object - IContent GetParent(int id); - - /// - /// Gets the parent of the current content as an item. - /// - /// to retrieve the parent from - /// Parent object - IContent GetParent(IContent content); - - /// - /// Creates and saves an object using the alias of the - /// that this Content should based on. - /// - /// - /// This method returns an object that has been persisted to the database - /// and therefor has an identity. - /// - /// Name of the Content object - /// Parent object for the new Content - /// Alias of the - /// Optional id of the user creating the content - /// - IContent CreateContentWithIdentity(string name, IContent parent, string contentTypeAlias, int userId = 0); - - /// - /// Creates and saves an object using the alias of the - /// that this Content should based on. - /// - /// - /// This method returns an object that has been persisted to the database - /// and therefor has an identity. - /// - /// Name of the Content object - /// Id of Parent for the new Content - /// Alias of the - /// Optional id of the user creating the content - /// - IContent CreateContentWithIdentity(string name, int parentId, string contentTypeAlias, int userId = 0); - } + /// + /// A temporary interface until we are in v8, this is used to return a different result for the same method and this interface gets implemented + /// explicitly. These methods will replace the normal ones in IContentService in v8 and this will be removed. + /// + public interface IContentServiceOperations + { + //TODO: Remove this class in v8 + + //TODO: There's probably more that needs to be added like the EmptyRecycleBin, etc... + + /// + /// Saves a single object + /// + /// The to save + /// Optional Id of the User saving the Content + /// Optional boolean indicating whether or not to raise events. + Attempt Save(IContent content, int userId = 0, bool raiseEvents = true); + + /// + /// Saves a collection of objects. + /// + /// Collection of to save + /// Optional Id of the User saving the Content + /// Optional boolean indicating whether or not to raise events. + Attempt Save(IEnumerable contents, int userId = 0, bool raiseEvents = true); + + /// + /// Permanently deletes an object. + /// + /// + /// This method will also delete associated media files, child content and possibly associated domains. + /// + /// Please note that this method will completely remove the Content from the database + /// The to delete + /// Optional Id of the User deleting the Content + Attempt Delete(IContent content, int userId = 0); + + /// + /// Publishes a single object + /// + /// The to publish + /// Optional Id of the User issueing the publishing + /// The published status attempt + Attempt Publish(IContent content, int userId = 0); + + /// + /// Publishes a object and all its children + /// + /// The to publish along with its children + /// Optional Id of the User issueing the publishing + /// + /// The list of statuses for all published items + IEnumerable> PublishWithChildren(IContent content, int userId = 0, bool includeUnpublished = false); + + /// + /// Saves and Publishes a single object + /// + /// The to save and publish + /// Optional Id of the User issueing the publishing + /// Optional boolean indicating whether or not to raise save events. + /// True if publishing succeeded, otherwise False + Attempt SaveAndPublish(IContent content, int userId = 0, bool raiseEvents = true); + + /// + /// Deletes an object by moving it to the Recycle Bin + /// + /// Move an item to the Recycle Bin will result in the item being unpublished + /// The to delete + /// Optional Id of the User deleting the Content + Attempt MoveToRecycleBin(IContent content, int userId = 0); + + /// + /// UnPublishes a single object + /// + /// The to publish + /// Optional Id of the User issueing the publishing + /// True if unpublishing succeeded, otherwise False + Attempt UnPublish(IContent content, int userId = 0); + } + + /// + /// Defines the ContentService, which is an easy access to operations involving + /// + public interface IContentService : IService + { + /// + /// Rebuilds all xml content in the cmsContentXml table for all documents + /// + /// + /// Only rebuild the xml structures for the content type ids passed in, if none then rebuilds the structures + /// for all content + /// + void RebuildXmlStructures(params int[] contentTypeIds); + + int CountPublished(string contentTypeAlias = null); + int Count(string contentTypeAlias = null); + int CountChildren(int parentId, string contentTypeAlias = null); + int CountDescendants(int parentId, string contentTypeAlias = null); + + /// + /// Used to bulk update the permissions set for a content item. This will replace all permissions + /// assigned to an entity with a list of user id & permission pairs. + /// + /// + void ReplaceContentPermissions(EntityPermissionSet permissionSet); + + /// + /// Assigns a single permission to the current content item for the specified user ids + /// + /// + /// + /// + void AssignContentPermission(IContent entity, char permission, IEnumerable userIds); + + /// + /// Gets the list of permissions for the content item + /// + /// + /// + IEnumerable GetPermissionsForEntity(IContent content); + + bool SendToPublication(IContent content, int userId = 0); + + IEnumerable GetByIds(IEnumerable ids); + + /// + /// 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 + /// + IContent CreateContent(string name, int parentId, string contentTypeAlias, int userId = 0); + + /// + /// 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 + /// Parent object for the new Content + /// Alias of the + /// Optional id of the user creating the content + /// + IContent CreateContent(string name, IContent parent, string contentTypeAlias, int userId = 0); + + /// + /// Gets an object by Id + /// + /// Id of the Content to retrieve + /// + IContent GetById(int id); + + /// + /// Gets an object by its 'UniqueId' + /// + /// Guid key of the Content to retrieve + /// + IContent GetById(Guid key); + + /// + /// Gets a collection of objects by the Id of the + /// + /// Id of the + /// An Enumerable list of objects + IEnumerable GetContentOfContentType(int id); + + /// + /// Gets a collection of objects by Level + /// + /// The level to retrieve Content from + /// An Enumerable list of objects + IEnumerable GetByLevel(int level); + + /// + /// Gets a collection of objects by Parent Id + /// + /// Id of the Parent to retrieve Children from + /// An Enumerable list of objects + IEnumerable GetChildren(int id); + + [Obsolete("Use the overload with 'long' parameter types instead")] + [EditorBrowsable(EditorBrowsableState.Never)] + IEnumerable GetPagedChildren(int id, int pageIndex, int pageSize, out int totalRecords, + string orderBy = "SortOrder", Direction orderDirection = Direction.Ascending, bool orderBySystemField = true, string filter = ""); + + /// + /// Gets a collection of objects by Parent Id + /// + /// Id of the Parent to retrieve Children from + /// Page number + /// Page size + /// Total records query would return without paging + /// Field to order by + /// Direction to order by + /// Flag to indicate when ordering by system field + /// Search text filter + /// An Enumerable list of objects + IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize, out long totalRecords, + string orderBy = "SortOrder", Direction orderDirection = Direction.Ascending, bool orderBySystemField = true, string filter = ""); + + [Obsolete("Use the overload with 'long' parameter types instead")] + [EditorBrowsable(EditorBrowsableState.Never)] + IEnumerable GetPagedDescendants(int id, int pageIndex, int pageSize, out int totalRecords, + string orderBy = "Path", Direction orderDirection = Direction.Ascending, bool orderBySystemField = true, string filter = ""); + + /// + /// Gets a collection of objects by Parent Id + /// + /// Id of the Parent to retrieve Descendants from + /// Page number + /// Page size + /// Total records query would return without paging + /// Field to order by + /// Direction to order by + /// Flag to indicate when ordering by system field + /// Search text filter + /// An Enumerable list of objects + IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalRecords, + string orderBy = "Path", Direction orderDirection = Direction.Ascending, bool orderBySystemField = true, string filter = ""); + + /// + /// Gets a collection of an objects versions by its Id + /// + /// + /// An Enumerable list of objects + IEnumerable GetVersions(int id); + + /// + /// Gets a collection of objects, which reside at the first level / root + /// + /// An Enumerable list of objects + IEnumerable GetRootContent(); + + /// + /// Gets a collection of objects, which has an expiration date greater then today + /// + /// An Enumerable list of objects + IEnumerable GetContentForExpiration(); + + /// + /// Gets a collection of objects, which has a release date greater then today + /// + /// An Enumerable list of objects + IEnumerable GetContentForRelease(); + + /// + /// Gets a collection of an objects, which resides in the Recycle Bin + /// + /// An Enumerable list of objects + IEnumerable GetContentInRecycleBin(); + + /// + /// Saves a single object + /// + /// The to save + /// Optional Id of the User saving the Content + /// Optional boolean indicating whether or not to raise events. + void Save(IContent content, int userId = 0, bool raiseEvents = true); + + /// + /// Saves a collection of objects. + /// + /// Collection of to save + /// Optional Id of the User saving the Content + /// Optional boolean indicating whether or not to raise events. + void Save(IEnumerable contents, int userId = 0, bool raiseEvents = true); + + /// + /// Deletes all content of specified type. All children of deleted content is moved to Recycle Bin. + /// + /// This needs extra care and attention as its potentially a dangerous and extensive operation + /// Id of the + /// Optional Id of the user issueing the delete operation + void DeleteContentOfType(int contentTypeId, int userId = 0); + + /// + /// Permanently deletes versions from an object prior to a specific date. + /// + /// Id of the object to delete versions from + /// Latest version date + /// Optional Id of the User deleting versions of a Content object + void DeleteVersions(int id, DateTime versionDate, int userId = 0); + + /// + /// Permanently deletes a specific version from an object. + /// + /// Id of the object to delete a version from + /// Id of the version to delete + /// Boolean indicating whether to delete versions prior to the versionId + /// Optional Id of the User deleting versions of a Content object + void DeleteVersion(int id, Guid versionId, bool deletePriorVersions, int userId = 0); + + /// + /// Deletes an object by moving it to the Recycle Bin + /// + /// Move an item to the Recycle Bin will result in the item being unpublished + /// The to delete + /// Optional Id of the User deleting the Content + void MoveToRecycleBin(IContent content, int userId = 0); + + /// + /// Moves an object to a new location + /// + /// The to move + /// Id of the Content's new Parent + /// Optional Id of the User moving the Content + void Move(IContent content, int parentId, int userId = 0); + + /// + /// Empties the Recycle Bin by deleting all that resides in the bin + /// + void EmptyRecycleBin(); + + /// + /// Rollback an object to a previous version. + /// This will create a new version, which is a copy of all the old data. + /// + /// Id of the being rolled back + /// Id of the version to rollback to + /// Optional Id of the User issueing the rollback of the Content + /// The newly created object + IContent Rollback(int id, Guid versionId, int userId = 0); + + /// + /// Gets a collection of objects by its name or partial name + /// + /// Id of the Parent to retrieve Children from + /// Full or partial name of the children + /// An Enumerable list of objects + IEnumerable GetChildrenByName(int parentId, string name); + + /// + /// Gets a collection of objects by Parent Id + /// + /// Id of the Parent to retrieve Descendants from + /// An Enumerable list of objects + IEnumerable GetDescendants(int id); + + /// + /// Gets a collection of objects by Parent Id + /// + /// item to retrieve Descendants from + /// An Enumerable list of objects + IEnumerable GetDescendants(IContent content); + + /// + /// Gets a specific version of an item. + /// + /// Id of the version to retrieve + /// An item + IContent GetByVersion(Guid versionId); + + /// + /// Gets the published version of an item + /// + /// Id of the to retrieve version from + /// An item + IContent GetPublishedVersion(int id); + + /// + /// Gets the published version of a item. + /// + /// The content item. + /// The published version, if any; otherwise, null. + IContent GetPublishedVersion(IContent content); + + /// + /// Checks whether an item has any children + /// + /// Id of the + /// True if the content has any children otherwise False + bool HasChildren(int id); + + /// + /// Checks whether an item has any published versions + /// + /// Id of the + /// True if the content has any published version otherwise False + bool HasPublishedVersion(int id); + + /// + /// Re-Publishes all Content + /// + /// Optional Id of the User issueing the publishing + /// True if publishing succeeded, otherwise False + bool RePublishAll(int userId = 0); + + /// + /// Publishes a single object + /// + /// The to publish + /// Optional Id of the User issueing the publishing + /// True if publishing succeeded, otherwise False + bool Publish(IContent content, int userId = 0); + + /// + /// Publishes a single object + /// + /// The to publish + /// Optional Id of the User issueing the publishing + /// The published status attempt + Attempt PublishWithStatus(IContent content, int userId = 0); + + /// + /// Publishes a object and all its children + /// + /// The to publish along with its children + /// Optional Id of the User issueing the publishing + /// True if publishing succeeded, otherwise False + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Use PublishWithChildrenWithStatus instead, that method will provide more detailed information on the outcome and also allows the includeUnpublished flag")] + bool PublishWithChildren(IContent content, int userId = 0); + + /// + /// Publishes a object and all its children + /// + /// The to publish along with its children + /// Optional Id of the User issueing the publishing + /// + /// The list of statuses for all published items + IEnumerable> PublishWithChildrenWithStatus(IContent content, int userId = 0, bool includeUnpublished = false); + + /// + /// UnPublishes a single object + /// + /// The to publish + /// Optional Id of the User issueing the publishing + /// True if unpublishing succeeded, otherwise False + bool UnPublish(IContent content, int userId = 0); + + /// + /// Saves and Publishes a single object + /// + /// The to save and publish + /// Optional Id of the User issueing the publishing + /// Optional boolean indicating whether or not to raise save events. + /// True if publishing succeeded, otherwise False + [Obsolete("Use SaveAndPublishWithStatus instead, that method will provide more detailed information on the outcome")] + [EditorBrowsable(EditorBrowsableState.Never)] + bool SaveAndPublish(IContent content, int userId = 0, bool raiseEvents = true); + + /// + /// Saves and Publishes a single object + /// + /// The to save and publish + /// Optional Id of the User issueing the publishing + /// Optional boolean indicating whether or not to raise save events. + /// True if publishing succeeded, otherwise False + Attempt SaveAndPublishWithStatus(IContent content, int userId = 0, bool raiseEvents = true); + + /// + /// Permanently deletes an object. + /// + /// + /// This method will also delete associated media files, child content and possibly associated domains. + /// + /// Please note that this method will completely remove the Content from the database + /// The to delete + /// Optional Id of the User deleting the Content + void Delete(IContent content, int userId = 0); + + /// + /// Copies an object by creating a new Content object of the same type and copies all data from the current + /// to the new copy, which is returned. Recursively copies all children. + /// + /// The to copy + /// Id of the Content's new Parent + /// Boolean indicating whether the copy should be related to the original + /// Optional Id of the User copying the Content + /// The newly created object + IContent Copy(IContent content, int parentId, bool relateToOriginal, int userId = 0); + + /// + /// Copies an object by creating a new Content object of the same type and copies all data from the current + /// to the new copy which is returned. + /// + /// The to copy + /// Id of the Content's new Parent + /// Boolean indicating whether the copy should be related to the original + /// A value indicating whether to recursively copy children. + /// Optional Id of the User copying the Content + /// The newly created object + IContent Copy(IContent content, int parentId, bool relateToOriginal, bool recursive, int userId = 0); + + /// + /// Checks if the passed in can be published based on the anscestors publish state. + /// + /// to check if anscestors are published + /// True if the Content can be published, otherwise False + bool IsPublishable(IContent content); + + /// + /// Gets a collection of objects, which are ancestors of the current content. + /// + /// Id of the to retrieve ancestors for + /// An Enumerable list of objects + IEnumerable GetAncestors(int id); + + /// + /// Gets a collection of objects, which are ancestors of the current content. + /// + /// to retrieve ancestors for + /// An Enumerable list of objects + IEnumerable GetAncestors(IContent content); + + /// + /// Sorts a collection of objects by updating the SortOrder according + /// to the ordering of items in the passed in . + /// + /// + /// Using this method will ensure that the Published-state is maintained upon sorting + /// so the cache is updated accordingly - as needed. + /// + /// + /// + /// + /// True if sorting succeeded, otherwise False + bool Sort(IEnumerable items, int userId = 0, bool raiseEvents = true); + + /// + /// Gets the parent of the current content as an item. + /// + /// Id of the to retrieve the parent from + /// Parent object + IContent GetParent(int id); + + /// + /// Gets the parent of the current content as an item. + /// + /// to retrieve the parent from + /// Parent object + IContent GetParent(IContent content); + + /// + /// Creates and saves an object using the alias of the + /// that this Content should based on. + /// + /// + /// This method returns an object that has been persisted to the database + /// and therefor has an identity. + /// + /// Name of the Content object + /// Parent object for the new Content + /// Alias of the + /// Optional id of the user creating the content + /// + IContent CreateContentWithIdentity(string name, IContent parent, string contentTypeAlias, int userId = 0); + + /// + /// Creates and saves an object using the alias of the + /// that this Content should based on. + /// + /// + /// This method returns an object that has been persisted to the database + /// and therefor has an identity. + /// + /// Name of the Content object + /// Id of Parent for the new Content + /// Alias of the + /// Optional id of the user creating the content + /// + IContent CreateContentWithIdentity(string name, int parentId, string contentTypeAlias, int userId = 0); + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Services/IMediaService.cs b/src/Umbraco.Core/Services/IMediaService.cs index 0bb56bb44b..51c01703d5 100644 --- a/src/Umbraco.Core/Services/IMediaService.cs +++ b/src/Umbraco.Core/Services/IMediaService.cs @@ -7,370 +7,370 @@ using Umbraco.Core.Persistence.DatabaseModelDefinitions; namespace Umbraco.Core.Services { - /// - /// A temporary interface until we are in v8, this is used to return a different result for the same method and this interface gets implemented - /// explicitly. These methods will replace the normal ones in IContentService in v8 and this will be removed. - /// - public interface IMediaServiceOperations - { - //TODO: Remove this class in v8 + /// + /// A temporary interface until we are in v8, this is used to return a different result for the same method and this interface gets implemented + /// explicitly. These methods will replace the normal ones in IContentService in v8 and this will be removed. + /// + public interface IMediaServiceOperations + { + //TODO: Remove this class in v8 - //TODO: There's probably more that needs to be added like the EmptyRecycleBin, etc... + //TODO: There's probably more that needs to be added like the EmptyRecycleBin, etc... - /// - /// Deletes an object by moving it to the Recycle Bin - /// - /// The to delete - /// Id of the User deleting the Media - Attempt MoveToRecycleBin(IMedia media, int userId = 0); + /// + /// Deletes an object by moving it to the Recycle Bin + /// + /// The to delete + /// Id of the User deleting the Media + Attempt MoveToRecycleBin(IMedia media, int userId = 0); - /// - /// Permanently deletes an object - /// - /// - /// Please note that this method will completely remove the Media from the database, - /// but current not from the file system. - /// - /// The to delete - /// Id of the User deleting the Media - Attempt Delete(IMedia media, int userId = 0); + /// + /// Permanently deletes an object + /// + /// + /// Please note that this method will completely remove the Media from the database, + /// but current not from the file system. + /// + /// The to delete + /// Id of the User deleting the Media + Attempt Delete(IMedia media, int userId = 0); - /// - /// Saves a single object - /// - /// The to save - /// Id of the User saving the Media - /// Optional boolean indicating whether or not to raise events. - Attempt Save(IMedia media, int userId = 0, bool raiseEvents = true); + /// + /// Saves a single object + /// + /// The to save + /// Id of the User saving the Media + /// Optional boolean indicating whether or not to raise events. + Attempt Save(IMedia media, int userId = 0, bool raiseEvents = true); - /// - /// Saves a collection of objects - /// - /// Collection of to save - /// Id of the User saving the Media - /// Optional boolean indicating whether or not to raise events. - Attempt Save(IEnumerable medias, int userId = 0, bool raiseEvents = true); - } + /// + /// Saves a collection of objects + /// + /// Collection of to save + /// Id of the User saving the Media + /// Optional boolean indicating whether or not to raise events. + Attempt Save(IEnumerable medias, int userId = 0, bool raiseEvents = true); + } - /// - /// Defines the Media Service, which is an easy access to operations involving - /// - public interface IMediaService : IService - { - /// - /// Rebuilds all xml content in the cmsContentXml table for all media - /// - /// - /// Only rebuild the xml structures for the content type ids passed in, if none then rebuilds the structures - /// for all media - /// - void RebuildXmlStructures(params int[] contentTypeIds); + /// + /// Defines the Media Service, which is an easy access to operations involving + /// + public interface IMediaService : IService + { + /// + /// Rebuilds all xml content in the cmsContentXml table for all media + /// + /// + /// Only rebuild the xml structures for the content type ids passed in, if none then rebuilds the structures + /// for all media + /// + void RebuildXmlStructures(params int[] contentTypeIds); - int Count(string contentTypeAlias = null); - int CountChildren(int parentId, string contentTypeAlias = null); - int CountDescendants(int parentId, string contentTypeAlias = null); + int Count(string contentTypeAlias = null); + int CountChildren(int parentId, string contentTypeAlias = null); + int CountDescendants(int parentId, string contentTypeAlias = null); - IEnumerable GetByIds(IEnumerable ids); + IEnumerable GetByIds(IEnumerable ids); - /// - /// Creates an object using the alias of the - /// that this Media should based on. - /// - /// - /// Note that using this method will simply return a new IMedia without any identity - /// as it has not yet been persisted. It is intended as a shortcut to creating new media objects - /// that does not invoke a save operation against the database. - /// - /// Name of the Media object - /// Id of Parent for the new Media item - /// Alias of the - /// Optional id of the user creating the media item - /// - IMedia CreateMedia(string name, int parentId, string mediaTypeAlias, int userId = 0); + /// + /// Creates an object using the alias of the + /// that this Media should based on. + /// + /// + /// Note that using this method will simply return a new IMedia without any identity + /// as it has not yet been persisted. It is intended as a shortcut to creating new media objects + /// that does not invoke a save operation against the database. + /// + /// Name of the Media object + /// Id of Parent for the new Media item + /// Alias of the + /// Optional id of the user creating the media item + /// + IMedia CreateMedia(string name, int parentId, string mediaTypeAlias, int userId = 0); - /// - /// Creates an object using the alias of the - /// that this Media should based on. - /// - /// - /// Note that using this method will simply return a new IMedia without any identity - /// as it has not yet been persisted. It is intended as a shortcut to creating new media objects - /// that does not invoke a save operation against the database. - /// - /// Name of the Media object - /// Parent for the new Media item - /// Alias of the - /// Optional id of the user creating the media item - /// - IMedia CreateMedia(string name, IMedia parent, string mediaTypeAlias, int userId = 0); + /// + /// Creates an object using the alias of the + /// that this Media should based on. + /// + /// + /// Note that using this method will simply return a new IMedia without any identity + /// as it has not yet been persisted. It is intended as a shortcut to creating new media objects + /// that does not invoke a save operation against the database. + /// + /// Name of the Media object + /// Parent for the new Media item + /// Alias of the + /// Optional id of the user creating the media item + /// + IMedia CreateMedia(string name, IMedia parent, string mediaTypeAlias, int userId = 0); - /// - /// Gets an object by Id - /// - /// Id of the Content to retrieve - /// - IMedia GetById(int id); + /// + /// Gets an object by Id + /// + /// Id of the Content to retrieve + /// + IMedia GetById(int id); - /// - /// Gets a collection of objects by Parent Id - /// - /// Id of the Parent to retrieve Children from - /// An Enumerable list of objects - IEnumerable GetChildren(int id); + /// + /// Gets a collection of objects by Parent Id + /// + /// Id of the Parent to retrieve Children from + /// An Enumerable list of objects + IEnumerable GetChildren(int id); - [Obsolete("Use the overload with 'long' parameter types instead")] - [EditorBrowsable(EditorBrowsableState.Never)] - IEnumerable GetPagedChildren(int id, int pageIndex, int pageSize, out int totalRecords, - string orderBy = "SortOrder", Direction orderDirection = Direction.Ascending, bool orderBySystemField = true, string filter = ""); + [Obsolete("Use the overload with 'long' parameter types instead")] + [EditorBrowsable(EditorBrowsableState.Never)] + IEnumerable GetPagedChildren(int id, int pageIndex, int pageSize, out int totalRecords, + string orderBy = "SortOrder", Direction orderDirection = Direction.Ascending, bool orderBySystemField = true, string filter = ""); - /// - /// Gets a collection of objects by Parent Id - /// - /// Id of the Parent to retrieve Children from - /// Page number - /// Page size - /// Total records query would return without paging - /// Field to order by - /// Direction to order by - /// Flag to indicate when ordering by system field - /// Search text filter - /// An Enumerable list of objects - IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize, out long totalRecords, - string orderBy = "SortOrder", Direction orderDirection = Direction.Ascending, bool orderBySystemField = true, string filter = ""); + /// + /// Gets a collection of objects by Parent Id + /// + /// Id of the Parent to retrieve Children from + /// Page number + /// Page size + /// Total records query would return without paging + /// Field to order by + /// Direction to order by + /// Flag to indicate when ordering by system field + /// Search text filter + /// An Enumerable list of objects + IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize, out long totalRecords, + string orderBy = "SortOrder", Direction orderDirection = Direction.Ascending, bool orderBySystemField = true, string filter = ""); - [Obsolete("Use the overload with 'long' parameter types instead")] - [EditorBrowsable(EditorBrowsableState.Never)] - IEnumerable GetPagedDescendants(int id, int pageIndex, int pageSize, out int totalRecords, - string orderBy = "Path", Direction orderDirection = Direction.Ascending, bool orderBySystemField = true, string filter = ""); + [Obsolete("Use the overload with 'long' parameter types instead")] + [EditorBrowsable(EditorBrowsableState.Never)] + IEnumerable GetPagedDescendants(int id, int pageIndex, int pageSize, out int totalRecords, + string orderBy = "Path", Direction orderDirection = Direction.Ascending, bool orderBySystemField = true, string filter = ""); - /// - /// Gets a collection of objects by Parent Id - /// - /// Id of the Parent to retrieve Descendants from - /// Page number - /// Page size - /// Total records query would return without paging - /// Field to order by - /// Direction to order by - /// Flag to indicate when ordering by system field - /// Search text filter - /// An Enumerable list of objects - IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalRecords, - string orderBy = "Path", Direction orderDirection = Direction.Ascending, bool orderBySystemField = true, string filter = ""); + /// + /// Gets a collection of objects by Parent Id + /// + /// Id of the Parent to retrieve Descendants from + /// Page number + /// Page size + /// Total records query would return without paging + /// Field to order by + /// Direction to order by + /// Flag to indicate when ordering by system field + /// Search text filter + /// An Enumerable list of objects + IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalRecords, + string orderBy = "Path", Direction orderDirection = Direction.Ascending, bool orderBySystemField = true, string filter = ""); - /// - /// Gets descendants of a object by its Id - /// - /// Id of the Parent to retrieve descendants from - /// An Enumerable flat list of objects - IEnumerable GetDescendants(int id); + /// + /// Gets descendants of a object by its Id + /// + /// Id of the Parent to retrieve descendants from + /// An Enumerable flat list of objects + IEnumerable GetDescendants(int id); - /// - /// Gets a collection of objects by the Id of the - /// - /// Id of the - /// An Enumerable list of objects - IEnumerable GetMediaOfMediaType(int id); + /// + /// Gets a collection of objects by the Id of the + /// + /// Id of the + /// An Enumerable list of objects + IEnumerable GetMediaOfMediaType(int id); - /// - /// Gets a collection of objects, which reside at the first level / root - /// - /// An Enumerable list of objects - IEnumerable GetRootMedia(); + /// + /// Gets a collection of objects, which reside at the first level / root + /// + /// An Enumerable list of objects + IEnumerable GetRootMedia(); - /// - /// Gets a collection of an objects, which resides in the Recycle Bin - /// - /// An Enumerable list of objects - IEnumerable GetMediaInRecycleBin(); + /// + /// Gets a collection of an objects, which resides in the Recycle Bin + /// + /// An Enumerable list of objects + IEnumerable GetMediaInRecycleBin(); - /// - /// Moves an object to a new location - /// - /// The to move - /// Id of the Media's new Parent - /// Id of the User moving the Media - void Move(IMedia media, int parentId, int userId = 0); + /// + /// Moves an object to a new location + /// + /// The to move + /// Id of the Media's new Parent + /// Id of the User moving the Media + void Move(IMedia media, int parentId, int userId = 0); - /// - /// Deletes an object by moving it to the Recycle Bin - /// - /// The to delete - /// Id of the User deleting the Media - void MoveToRecycleBin(IMedia media, int userId = 0); + /// + /// Deletes an object by moving it to the Recycle Bin + /// + /// The to delete + /// Id of the User deleting the Media + void MoveToRecycleBin(IMedia media, int userId = 0); - /// - /// Empties the Recycle Bin by deleting all that resides in the bin - /// - void EmptyRecycleBin(); + /// + /// Empties the Recycle Bin by deleting all that resides in the bin + /// + void EmptyRecycleBin(); - /// - /// Deletes all media of specified type. All children of deleted media is moved to Recycle Bin. - /// - /// This needs extra care and attention as its potentially a dangerous and extensive operation - /// Id of the - /// Optional Id of the user deleting Media - void DeleteMediaOfType(int mediaTypeId, int userId = 0); + /// + /// Deletes all media of specified type. All children of deleted media is moved to Recycle Bin. + /// + /// This needs extra care and attention as its potentially a dangerous and extensive operation + /// Id of the + /// Optional Id of the user deleting Media + void DeleteMediaOfType(int mediaTypeId, int userId = 0); - /// - /// Permanently deletes an object - /// - /// - /// Please note that this method will completely remove the Media from the database, - /// but current not from the file system. - /// - /// The to delete - /// Id of the User deleting the Media - void Delete(IMedia media, int userId = 0); + /// + /// Permanently deletes an object + /// + /// + /// Please note that this method will completely remove the Media from the database, + /// but current not from the file system. + /// + /// The to delete + /// Id of the User deleting the Media + void Delete(IMedia media, int userId = 0); - /// - /// Saves a single object - /// - /// The to save - /// Id of the User saving the Media - /// Optional boolean indicating whether or not to raise events. - void Save(IMedia media, int userId = 0, bool raiseEvents = true); + /// + /// Saves a single object + /// + /// The to save + /// Id of the User saving the Media + /// Optional boolean indicating whether or not to raise events. + void Save(IMedia media, int userId = 0, bool raiseEvents = true); - /// - /// Saves a collection of objects - /// - /// Collection of to save - /// Id of the User saving the Media - /// Optional boolean indicating whether or not to raise events. - void Save(IEnumerable medias, int userId = 0, bool raiseEvents = true); + /// + /// Saves a collection of objects + /// + /// Collection of to save + /// Id of the User saving the Media + /// Optional boolean indicating whether or not to raise events. + void Save(IEnumerable medias, int userId = 0, bool raiseEvents = true); - /// - /// Gets an object by its 'UniqueId' - /// - /// Guid key of the Media to retrieve - /// - IMedia GetById(Guid key); + /// + /// Gets an object by its 'UniqueId' + /// + /// Guid key of the Media to retrieve + /// + IMedia GetById(Guid key); - /// - /// Gets a collection of objects by Level - /// - /// The level to retrieve Media from - /// An Enumerable list of objects - IEnumerable GetByLevel(int level); + /// + /// Gets a collection of objects by Level + /// + /// The level to retrieve Media from + /// An Enumerable list of objects + IEnumerable GetByLevel(int level); - /// - /// Gets a specific version of an item. - /// - /// Id of the version to retrieve - /// An item - IMedia GetByVersion(Guid versionId); + /// + /// Gets a specific version of an item. + /// + /// Id of the version to retrieve + /// An item + IMedia GetByVersion(Guid versionId); - /// - /// Gets a collection of an objects versions by Id - /// - /// - /// An Enumerable list of objects - IEnumerable GetVersions(int id); + /// + /// Gets a collection of an objects versions by Id + /// + /// + /// An Enumerable list of objects + IEnumerable GetVersions(int id); - /// - /// Checks whether an item has any children - /// - /// Id of the - /// True if the media has any children otherwise False - bool HasChildren(int id); + /// + /// Checks whether an item has any children + /// + /// Id of the + /// True if the media has any children otherwise False + bool HasChildren(int id); - /// - /// Permanently deletes versions from an object prior to a specific date. - /// - /// Id of the object to delete versions from - /// Latest version date - /// Optional Id of the User deleting versions of a Content object - void DeleteVersions(int id, DateTime versionDate, int userId = 0); + /// + /// Permanently deletes versions from an object prior to a specific date. + /// + /// Id of the object to delete versions from + /// Latest version date + /// Optional Id of the User deleting versions of a Content object + void DeleteVersions(int id, DateTime versionDate, int userId = 0); - /// - /// Permanently deletes specific version(s) from an object. - /// - /// Id of the object to delete a version from - /// Id of the version to delete - /// Boolean indicating whether to delete versions prior to the versionId - /// Optional Id of the User deleting versions of a Content object - void DeleteVersion(int id, Guid versionId, bool deletePriorVersions, int userId = 0); + /// + /// Permanently deletes specific version(s) from an object. + /// + /// Id of the object to delete a version from + /// Id of the version to delete + /// Boolean indicating whether to delete versions prior to the versionId + /// Optional Id of the User deleting versions of a Content object + void DeleteVersion(int id, Guid versionId, bool deletePriorVersions, int userId = 0); - /// - /// Gets an object from the path stored in the 'umbracoFile' property. - /// - /// Path of the media item to retrieve (for example: /media/1024/koala_403x328.jpg) - /// - IMedia GetMediaByPath(string mediaPath); + /// + /// Gets an object from the path stored in the 'umbracoFile' property. + /// + /// Path of the media item to retrieve (for example: /media/1024/koala_403x328.jpg) + /// + IMedia GetMediaByPath(string mediaPath); - /// - /// Gets a collection of objects, which are ancestors of the current media. - /// - /// Id of the to retrieve ancestors for - /// An Enumerable list of objects - IEnumerable GetAncestors(int id); + /// + /// Gets a collection of objects, which are ancestors of the current media. + /// + /// Id of the to retrieve ancestors for + /// An Enumerable list of objects + IEnumerable GetAncestors(int id); - /// - /// Gets a collection of objects, which are ancestors of the current media. - /// - /// to retrieve ancestors for - /// An Enumerable list of objects - IEnumerable GetAncestors(IMedia media); + /// + /// Gets a collection of objects, which are ancestors of the current media. + /// + /// to retrieve ancestors for + /// An Enumerable list of objects + IEnumerable GetAncestors(IMedia media); - /// - /// Gets descendants of a object by its Id - /// - /// The Parent object to retrieve descendants from - /// An Enumerable flat list of objects - IEnumerable GetDescendants(IMedia media); + /// + /// Gets descendants of a object by its Id + /// + /// The Parent object to retrieve descendants from + /// An Enumerable flat list of objects + IEnumerable GetDescendants(IMedia media); - /// - /// Gets the parent of the current media as an item. - /// - /// Id of the to retrieve the parent from - /// Parent object - IMedia GetParent(int id); + /// + /// Gets the parent of the current media as an item. + /// + /// Id of the to retrieve the parent from + /// Parent object + IMedia GetParent(int id); - /// - /// Gets the parent of the current media as an item. - /// - /// to retrieve the parent from - /// Parent object - IMedia GetParent(IMedia media); + /// + /// Gets the parent of the current media as an item. + /// + /// to retrieve the parent from + /// Parent object + IMedia GetParent(IMedia media); - /// - /// Sorts a collection of objects by updating the SortOrder according - /// to the ordering of items in the passed in . - /// - /// - /// - /// - /// True if sorting succeeded, otherwise False - bool Sort(IEnumerable items, int userId = 0, bool raiseEvents = true); + /// + /// Sorts a collection of objects by updating the SortOrder according + /// to the ordering of items in the passed in . + /// + /// + /// + /// + /// True if sorting succeeded, otherwise False + bool Sort(IEnumerable items, int userId = 0, bool raiseEvents = true); - /// - /// Creates an object using the alias of the - /// that this Media should based on. - /// - /// - /// This method returns an object that has been persisted to the database - /// and therefor has an identity. - /// - /// Name of the Media object - /// Parent for the new Media item - /// Alias of the - /// Optional id of the user creating the media item - /// - IMedia CreateMediaWithIdentity(string name, IMedia parent, string mediaTypeAlias, int userId = 0); + /// + /// Creates an object using the alias of the + /// that this Media should based on. + /// + /// + /// This method returns an object that has been persisted to the database + /// and therefor has an identity. + /// + /// Name of the Media object + /// Parent for the new Media item + /// Alias of the + /// Optional id of the user creating the media item + /// + IMedia CreateMediaWithIdentity(string name, IMedia parent, string mediaTypeAlias, int userId = 0); - /// - /// Creates an object using the alias of the - /// that this Media should based on. - /// - /// - /// This method returns an object that has been persisted to the database - /// and therefor has an identity. - /// - /// Name of the Media object - /// Id of Parent for the new Media item - /// Alias of the - /// Optional id of the user creating the media item - /// - IMedia CreateMediaWithIdentity(string name, int parentId, string mediaTypeAlias, int userId = 0); - } + /// + /// Creates an object using the alias of the + /// that this Media should based on. + /// + /// + /// This method returns an object that has been persisted to the database + /// and therefor has an identity. + /// + /// Name of the Media object + /// Id of Parent for the new Media item + /// Alias of the + /// Optional id of the user creating the media item + /// + IMedia CreateMediaWithIdentity(string name, int parentId, string mediaTypeAlias, int userId = 0); + } } diff --git a/src/Umbraco.Core/Services/IMemberService.cs b/src/Umbraco.Core/Services/IMemberService.cs index 2177d3b14e..3756ca246d 100644 --- a/src/Umbraco.Core/Services/IMemberService.cs +++ b/src/Umbraco.Core/Services/IMemberService.cs @@ -8,214 +8,214 @@ using Umbraco.Core.Persistence.Querying; namespace Umbraco.Core.Services { - /// - /// Defines the MemberService, which is an easy access to operations involving (umbraco) members. - /// - public interface IMemberService : IMembershipMemberService - { - /// - /// Rebuilds all xml content in the cmsContentXml table for all documents - /// - /// - /// Only rebuild the xml structures for the content type ids passed in, if none then rebuilds the structures - /// for all content - /// - void RebuildXmlStructures(params int[] contentTypeIds); + /// + /// Defines the MemberService, which is an easy access to operations involving (umbraco) members. + /// + public interface IMemberService : IMembershipMemberService + { + /// + /// Rebuilds all xml content in the cmsContentXml table for all documents + /// + /// + /// Only rebuild the xml structures for the content type ids passed in, if none then rebuilds the structures + /// for all content + /// + void RebuildXmlStructures(params int[] contentTypeIds); - [Obsolete("Use the overload with 'long' parameter types instead")] - [EditorBrowsable(EditorBrowsableState.Never)] - IEnumerable GetAll(int pageIndex, int pageSize, out int totalRecords, - string orderBy, Direction orderDirection, bool orderBySystemField = true, string memberTypeAlias = null, string filter = ""); + [Obsolete("Use the overload with 'long' parameter types instead")] + [EditorBrowsable(EditorBrowsableState.Never)] + IEnumerable GetAll(int pageIndex, int pageSize, out int totalRecords, + string orderBy, Direction orderDirection, bool orderBySystemField = true, string memberTypeAlias = null, string filter = ""); - /// - /// Gets a list of paged objects - /// - /// An can be of type - /// Current page index - /// Size of the page - /// Total number of records found (out) - /// Field to order by - /// Direction to order by - /// Flag to indicate when ordering by system field - /// - /// Search text filter - /// - IEnumerable GetAll(long pageIndex, int pageSize, out long totalRecords, - string orderBy, Direction orderDirection, bool orderBySystemField = true, string memberTypeAlias = null, string filter = ""); + /// + /// Gets a list of paged objects + /// + /// An can be of type + /// Current page index + /// Size of the page + /// Total number of records found (out) + /// Field to order by + /// Direction to order by + /// Flag to indicate when ordering by system field + /// + /// Search text filter + /// + IEnumerable GetAll(long pageIndex, int pageSize, out long totalRecords, + string orderBy, Direction orderDirection, bool orderBySystemField = true, string memberTypeAlias = null, string filter = ""); - /// - /// Creates an object without persisting it - /// - /// This method is convenient for when you need to add properties to a new Member - /// before persisting it in order to limit the amount of times its saved. - /// Also note that the returned will not have an Id until its saved. - /// Username of the Member to create - /// Email of the Member to create - /// Name of the Member to create - /// Alias of the MemberType the Member should be based on - /// - IMember CreateMember(string username, string email, string name, string memberTypeAlias); + /// + /// Creates an object without persisting it + /// + /// This method is convenient for when you need to add properties to a new Member + /// before persisting it in order to limit the amount of times its saved. + /// Also note that the returned will not have an Id until its saved. + /// Username of the Member to create + /// Email of the Member to create + /// Name of the Member to create + /// Alias of the MemberType the Member should be based on + /// + IMember CreateMember(string username, string email, string name, string memberTypeAlias); - /// - /// Creates an object without persisting it - /// - /// This method is convenient for when you need to add properties to a new Member - /// before persisting it in order to limit the amount of times its saved. - /// Also note that the returned will not have an Id until its saved. - /// Username of the Member to create - /// Email of the Member to create - /// Name of the Member to create - /// MemberType the Member should be based on - /// - IMember CreateMember(string username, string email, string name, IMemberType memberType); + /// + /// Creates an object without persisting it + /// + /// This method is convenient for when you need to add properties to a new Member + /// before persisting it in order to limit the amount of times its saved. + /// Also note that the returned will not have an Id until its saved. + /// Username of the Member to create + /// Email of the Member to create + /// Name of the Member to create + /// MemberType the Member should be based on + /// + IMember CreateMember(string username, string email, string name, IMemberType memberType); - /// - /// Creates and persists a Member - /// - /// Using this method will persist the Member object before its returned - /// meaning that it will have an Id available (unlike the CreateMember method) - /// Username of the Member to create - /// Email of the Member to create - /// Name of the Member to create - /// Alias of the MemberType the Member should be based on - /// - IMember CreateMemberWithIdentity(string username, string email, string name, string memberTypeAlias); + /// + /// Creates and persists a Member + /// + /// Using this method will persist the Member object before its returned + /// meaning that it will have an Id available (unlike the CreateMember method) + /// Username of the Member to create + /// Email of the Member to create + /// Name of the Member to create + /// Alias of the MemberType the Member should be based on + /// + IMember CreateMemberWithIdentity(string username, string email, string name, string memberTypeAlias); - /// - /// Creates and persists a Member - /// - /// Using this method will persist the Member object before its returned - /// meaning that it will have an Id available (unlike the CreateMember method) - /// Username of the Member to create - /// Email of the Member to create - /// Name of the Member to create - /// MemberType the Member should be based on - /// - IMember CreateMemberWithIdentity(string username, string email, string name, IMemberType memberType); + /// + /// Creates and persists a Member + /// + /// Using this method will persist the Member object before its returned + /// meaning that it will have an Id available (unlike the CreateMember method) + /// Username of the Member to create + /// Email of the Member to create + /// Name of the Member to create + /// MemberType the Member should be based on + /// + IMember CreateMemberWithIdentity(string username, string email, string name, IMemberType memberType); - /// - /// This is simply a helper method which essentially just wraps the MembershipProvider's ChangePassword method - /// - /// This method exists so that Umbraco developers can use one entry point to create/update - /// Members if they choose to. - /// The Member to save the password for - /// The password to encrypt and save - void SavePassword(IMember member, string password); + /// + /// This is simply a helper method which essentially just wraps the MembershipProvider's ChangePassword method + /// + /// This method exists so that Umbraco developers can use one entry point to create/update + /// Members if they choose to. + /// The Member to save the password for + /// The password to encrypt and save + void SavePassword(IMember member, string password); - /// - /// Gets the count of Members by an optional MemberType alias - /// - /// If no alias is supplied then the count for all Member will be returned - /// Optional alias for the MemberType when counting number of Members - /// with number of Members - int Count(string memberTypeAlias = null); + /// + /// Gets the count of Members by an optional MemberType alias + /// + /// If no alias is supplied then the count for all Member will be returned + /// Optional alias for the MemberType when counting number of Members + /// with number of Members + int Count(string memberTypeAlias = null); - /// - /// Checks if a Member with the id exists - /// - /// Id of the Member - /// True if the Member exists otherwise False - bool Exists(int id); + /// + /// Checks if a Member with the id exists + /// + /// Id of the Member + /// True if the Member exists otherwise False + bool Exists(int id); - /// - /// Gets a Member by the unique key - /// - /// The guid key corresponds to the unique id in the database - /// and the user id in the membership provider. - /// Id - /// - IMember GetByKey(Guid id); + /// + /// Gets a Member by the unique key + /// + /// The guid key corresponds to the unique id in the database + /// and the user id in the membership provider. + /// Id + /// + IMember GetByKey(Guid id); - /// - /// Gets a Member by its integer id - /// - /// Id - /// - IMember GetById(int id); + /// + /// Gets a Member by its integer id + /// + /// Id + /// + IMember GetById(int id); - /// - /// Gets all Members for the specified MemberType alias - /// - /// Alias of the MemberType - /// - IEnumerable GetMembersByMemberType(string memberTypeAlias); + /// + /// Gets all Members for the specified MemberType alias + /// + /// Alias of the MemberType + /// + IEnumerable GetMembersByMemberType(string memberTypeAlias); - /// - /// Gets all Members for the MemberType id - /// - /// Id of the MemberType - /// - IEnumerable GetMembersByMemberType(int memberTypeId); + /// + /// Gets all Members for the MemberType id + /// + /// Id of the MemberType + /// + IEnumerable GetMembersByMemberType(int memberTypeId); - /// - /// Gets all Members within the specified MemberGroup name - /// - /// Name of the MemberGroup - /// - IEnumerable GetMembersByGroup(string memberGroupName); + /// + /// Gets all Members within the specified MemberGroup name + /// + /// Name of the MemberGroup + /// + IEnumerable GetMembersByGroup(string memberGroupName); - /// - /// Gets all Members with the ids specified - /// - /// If no Ids are specified all Members will be retrieved - /// Optional list of Member Ids - /// - IEnumerable GetAllMembers(params int[] ids); + /// + /// Gets all Members with the ids specified + /// + /// If no Ids are specified all Members will be retrieved + /// Optional list of Member Ids + /// + IEnumerable GetAllMembers(params int[] ids); - /// - /// Delete Members of the specified MemberType id - /// - /// Id of the MemberType - void DeleteMembersOfType(int memberTypeId); + /// + /// Delete Members of the specified MemberType id + /// + /// Id of the MemberType + void DeleteMembersOfType(int memberTypeId); - [Obsolete("Use the overload with 'long' parameter types instead")] - [EditorBrowsable(EditorBrowsableState.Never)] - IEnumerable FindMembersByDisplayName(string displayNameToMatch, int pageIndex, int pageSize, out int totalRecords, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith); + [Obsolete("Use the overload with 'long' parameter types instead")] + [EditorBrowsable(EditorBrowsableState.Never)] + IEnumerable FindMembersByDisplayName(string displayNameToMatch, int pageIndex, int pageSize, out int totalRecords, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith); - /// - /// Finds Members based on their display name - /// - /// Display name to match - /// Current page index - /// Size of the page - /// Total number of records found (out) - /// The type of match to make as . Default is - /// - IEnumerable FindMembersByDisplayName(string displayNameToMatch, long pageIndex, int pageSize, out long totalRecords, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith); + /// + /// Finds Members based on their display name + /// + /// Display name to match + /// Current page index + /// Size of the page + /// Total number of records found (out) + /// The type of match to make as . Default is + /// + IEnumerable FindMembersByDisplayName(string displayNameToMatch, long pageIndex, int pageSize, out long totalRecords, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith); - /// - /// Gets a list of Members based on a property search - /// - /// Alias of the PropertyType to search for - /// Value to match - /// The type of match to make as . Default is - /// - IEnumerable GetMembersByPropertyValue(string propertyTypeAlias, string value, StringPropertyMatchType matchType = StringPropertyMatchType.Exact); + /// + /// Gets a list of Members based on a property search + /// + /// Alias of the PropertyType to search for + /// Value to match + /// The type of match to make as . Default is + /// + IEnumerable GetMembersByPropertyValue(string propertyTypeAlias, string value, StringPropertyMatchType matchType = StringPropertyMatchType.Exact); - /// - /// Gets a list of Members based on a property search - /// - /// Alias of the PropertyType to search for - /// Value to match - /// The type of match to make as . Default is - /// - IEnumerable GetMembersByPropertyValue(string propertyTypeAlias, int value, ValuePropertyMatchType matchType = ValuePropertyMatchType.Exact); + /// + /// Gets a list of Members based on a property search + /// + /// Alias of the PropertyType to search for + /// Value to match + /// The type of match to make as . Default is + /// + IEnumerable GetMembersByPropertyValue(string propertyTypeAlias, int value, ValuePropertyMatchType matchType = ValuePropertyMatchType.Exact); - /// - /// Gets a list of Members based on a property search - /// - /// Alias of the PropertyType to search for - /// Value to match - /// - IEnumerable GetMembersByPropertyValue(string propertyTypeAlias, bool value); + /// + /// Gets a list of Members based on a property search + /// + /// Alias of the PropertyType to search for + /// Value to match + /// + IEnumerable GetMembersByPropertyValue(string propertyTypeAlias, bool value); - /// - /// Gets a list of Members based on a property search - /// - /// Alias of the PropertyType to search for - /// Value to match - /// The type of match to make as . Default is - /// - IEnumerable GetMembersByPropertyValue(string propertyTypeAlias, DateTime value, ValuePropertyMatchType matchType = ValuePropertyMatchType.Exact); - } + /// + /// Gets a list of Members based on a property search + /// + /// Alias of the PropertyType to search for + /// Value to match + /// The type of match to make as . Default is + /// + IEnumerable GetMembersByPropertyValue(string propertyTypeAlias, DateTime value, ValuePropertyMatchType matchType = ValuePropertyMatchType.Exact); + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Services/MediaService.cs b/src/Umbraco.Core/Services/MediaService.cs index 5af8487b3f..3bbf8860ea 100644 --- a/src/Umbraco.Core/Services/MediaService.cs +++ b/src/Umbraco.Core/Services/MediaService.cs @@ -22,1318 +22,1318 @@ using Umbraco.Core.Publishing; namespace Umbraco.Core.Services { - /// - /// Represents the Media Service, which is an easy access to operations involving - /// - public class MediaService : RepositoryService, IMediaService, IMediaServiceOperations - { - - //Support recursive locks because some of the methods that require locking call other methods that require locking. - //for example, the Move method needs to be locked but this calls the Save method which also needs to be locked. - private static readonly ReaderWriterLockSlim Locker = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion); - - private readonly EntityXmlSerializer _entitySerializer = new EntityXmlSerializer(); - private readonly IDataTypeService _dataTypeService; - private readonly IUserService _userService; - - public MediaService(IDatabaseUnitOfWorkProvider provider, RepositoryFactory repositoryFactory, ILogger logger, IEventMessagesFactory eventMessagesFactory, IDataTypeService dataTypeService, IUserService userService) - : base(provider, repositoryFactory, logger, eventMessagesFactory) - { - if (dataTypeService == null) throw new ArgumentNullException("dataTypeService"); - if (userService == null) throw new ArgumentNullException("userService"); - _dataTypeService = dataTypeService; - _userService = userService; - } - - /// - /// Creates an object using the alias of the - /// that this Media should based on. - /// - /// - /// Note that using this method will simply return a new IMedia without any identity - /// as it has not yet been persisted. It is intended as a shortcut to creating new media objects - /// that does not invoke a save operation against the database. - /// - /// Name of the Media object - /// Id of Parent for the new Media item - /// Alias of the - /// Optional id of the user creating the media item - /// - public IMedia CreateMedia(string name, int parentId, string mediaTypeAlias, int userId = 0) - { - var mediaType = FindMediaTypeByAlias(mediaTypeAlias); - var media = new Models.Media(name, parentId, mediaType); - var parent = GetById(media.ParentId); - media.Path = string.Concat(parent.IfNotNull(x => x.Path, media.ParentId.ToString()), ",", media.Id); - - if (Creating.IsRaisedEventCancelled(new NewEventArgs(media, mediaTypeAlias, parentId), this)) - { - media.WasCancelled = true; - return media; - } - - media.CreatorId = userId; - - Created.RaiseEvent(new NewEventArgs(media, false, mediaTypeAlias, parentId), this); - - Audit(AuditType.New, string.Format("Media '{0}' was created", name), media.CreatorId, media.Id); - - return media; - } - - /// - /// Creates an object using the alias of the - /// that this Media should based on. - /// - /// - /// Note that using this method will simply return a new IMedia without any identity - /// as it has not yet been persisted. It is intended as a shortcut to creating new media objects - /// that does not invoke a save operation against the database. - /// - /// Name of the Media object - /// Parent for the new Media item - /// Alias of the - /// Optional id of the user creating the media item - /// - public IMedia CreateMedia(string name, IMedia parent, string mediaTypeAlias, int userId = 0) - { - if (parent == null) throw new ArgumentNullException("parent"); - - var mediaType = FindMediaTypeByAlias(mediaTypeAlias); - var media = new Models.Media(name, parent, mediaType); - media.Path = string.Concat(parent.Path, ",", media.Id); - - if (Creating.IsRaisedEventCancelled(new NewEventArgs(media, mediaTypeAlias, parent), this)) - { - media.WasCancelled = true; - return media; - } - - media.CreatorId = userId; - - Created.RaiseEvent(new NewEventArgs(media, false, mediaTypeAlias, parent), this); - - Audit(AuditType.New, string.Format("Media '{0}' was created", name), media.CreatorId, media.Id); - - return media; - } - - /// - /// Creates an object using the alias of the - /// that this Media should based on. - /// - /// - /// This method returns an object that has been persisted to the database - /// and therefor has an identity. - /// - /// Name of the Media object - /// Id of Parent for the new Media item - /// Alias of the - /// Optional id of the user creating the media item - /// - public IMedia CreateMediaWithIdentity(string name, int parentId, string mediaTypeAlias, int userId = 0) - { - var mediaType = FindMediaTypeByAlias(mediaTypeAlias); - var media = new Models.Media(name, parentId, mediaType); - - //NOTE: I really hate the notion of these Creating/Created events - they are so inconsistent, I've only just found - // out that in these 'WithIdentity' methods, the Saving/Saved events were not fired, wtf. Anyways, they're added now. - if (Creating.IsRaisedEventCancelled(new NewEventArgs(media, mediaTypeAlias, parentId), this)) - { - media.WasCancelled = true; - return media; - } - - if (Saving.IsRaisedEventCancelled(new SaveEventArgs(media), this)) - { - media.WasCancelled = true; - return media; - } - - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMediaRepository(uow)) - { - media.CreatorId = userId; - repository.AddOrUpdate(media); - - repository.AddOrUpdateContentXml(media, m => _entitySerializer.Serialize(this, _dataTypeService, _userService, m)); - // generate preview for blame history? - if (UmbracoConfig.For.UmbracoSettings().Content.GlobalPreviewStorageEnabled) - { - repository.AddOrUpdatePreviewXml(media, m => _entitySerializer.Serialize(this, _dataTypeService, _userService, m)); - } - - uow.Commit(); - } - - Saved.RaiseEvent(new SaveEventArgs(media, false), this); - - Created.RaiseEvent(new NewEventArgs(media, false, mediaTypeAlias, parentId), this); - - Audit(AuditType.New, string.Format("Media '{0}' was created with Id {1}", name, media.Id), media.CreatorId, media.Id); - - return media; - } - - /// - /// Creates an object using the alias of the - /// that this Media should based on. - /// - /// - /// This method returns an object that has been persisted to the database - /// and therefor has an identity. - /// - /// Name of the Media object - /// Parent for the new Media item - /// Alias of the - /// Optional id of the user creating the media item - /// - public IMedia CreateMediaWithIdentity(string name, IMedia parent, string mediaTypeAlias, int userId = 0) - { - if (parent == null) throw new ArgumentNullException("parent"); - - var mediaType = FindMediaTypeByAlias(mediaTypeAlias); - var media = new Models.Media(name, parent, mediaType); - - //NOTE: I really hate the notion of these Creating/Created events - they are so inconsistent, I've only just found - // out that in these 'WithIdentity' methods, the Saving/Saved events were not fired, wtf. Anyways, they're added now. - if (Creating.IsRaisedEventCancelled(new NewEventArgs(media, mediaTypeAlias, parent), this)) - { - media.WasCancelled = true; - return media; - } - - if (Saving.IsRaisedEventCancelled(new SaveEventArgs(media), this)) - { - media.WasCancelled = true; - return media; - } - - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMediaRepository(uow)) - { - media.CreatorId = userId; - repository.AddOrUpdate(media); - repository.AddOrUpdateContentXml(media, m => _entitySerializer.Serialize(this, _dataTypeService, _userService, m)); - // generate preview for blame history? - if (UmbracoConfig.For.UmbracoSettings().Content.GlobalPreviewStorageEnabled) - { - repository.AddOrUpdatePreviewXml(media, m => _entitySerializer.Serialize(this, _dataTypeService, _userService, m)); - } - - uow.Commit(); - } - - Saved.RaiseEvent(new SaveEventArgs(media, false), this); - - Created.RaiseEvent(new NewEventArgs(media, false, mediaTypeAlias, parent), this); - - Audit(AuditType.New, string.Format("Media '{0}' was created with Id {1}", name, media.Id), media.CreatorId, media.Id); - - return media; - } - - /// - /// Gets an object by Id - /// - /// Id of the Content to retrieve - /// - public IMedia GetById(int id) - { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMediaRepository(uow)) - { - return repository.Get(id); - } - } - - public int Count(string contentTypeAlias = null) - { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMediaRepository(uow)) - { - return repository.Count(contentTypeAlias); - } - } - - public int CountChildren(int parentId, string contentTypeAlias = null) - { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMediaRepository(uow)) - { - return repository.CountChildren(parentId, contentTypeAlias); - } - } - - public int CountDescendants(int parentId, string contentTypeAlias = null) - { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMediaRepository(uow)) - { - return repository.CountDescendants(parentId, contentTypeAlias); - } - } - - /// - /// Gets an object by Id - /// - /// Ids of the Media to retrieve - /// - public IEnumerable GetByIds(IEnumerable ids) - { - if (ids.Any() == false) return Enumerable.Empty(); - - using (var repository = RepositoryFactory.CreateMediaRepository(UowProvider.GetUnitOfWork())) - { - return repository.GetAll(ids.ToArray()); - } - } - - /// - /// Gets an object by its 'UniqueId' - /// - /// Guid key of the Media to retrieve - /// - public IMedia GetById(Guid key) - { - using (var repository = RepositoryFactory.CreateMediaRepository(UowProvider.GetUnitOfWork())) - { - var query = Query.Builder.Where(x => x.Key == key); - var contents = repository.GetByQuery(query); - return contents.SingleOrDefault(); - } - } - - /// - /// Gets a collection of objects by Level - /// - /// The level to retrieve Media from - /// An Enumerable list of objects - public IEnumerable GetByLevel(int level) - { - using (var repository = RepositoryFactory.CreateMediaRepository(UowProvider.GetUnitOfWork())) - { - var query = Query.Builder.Where(x => x.Level == level && !x.Path.StartsWith("-21")); - var contents = repository.GetByQuery(query); - - return contents; - } - } - - /// - /// Gets a specific version of an item. - /// - /// Id of the version to retrieve - /// An item - public IMedia GetByVersion(Guid versionId) - { - using (var repository = RepositoryFactory.CreateMediaRepository(UowProvider.GetUnitOfWork())) - { - return repository.GetByVersion(versionId); - } - } - - /// - /// Gets a collection of an objects versions by Id - /// - /// - /// An Enumerable list of objects - public IEnumerable GetVersions(int id) - { - using (var repository = RepositoryFactory.CreateMediaRepository(UowProvider.GetUnitOfWork())) - { - var versions = repository.GetAllVersions(id); - return versions; - } - } - - /// - /// Gets a collection of objects, which are ancestors of the current media. - /// - /// Id of the to retrieve ancestors for - /// An Enumerable list of objects - public IEnumerable GetAncestors(int id) - { - var media = GetById(id); - return GetAncestors(media); - } - - /// - /// Gets a collection of objects, which are ancestors of the current media. - /// - /// to retrieve ancestors for - /// An Enumerable list of objects - public IEnumerable GetAncestors(IMedia media) - { - var ids = media.Path.Split(',').Where(x => x != "-1" && x != media.Id.ToString(CultureInfo.InvariantCulture)).Select(int.Parse).ToArray(); - if (ids.Any() == false) - return new List(); - - using (var repository = RepositoryFactory.CreateMediaRepository(UowProvider.GetUnitOfWork())) - { - return repository.GetAll(ids); - } - } - - /// - /// Gets a collection of objects by Parent Id - /// - /// Id of the Parent to retrieve Children from - /// An Enumerable list of objects - public IEnumerable GetChildren(int id) - { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMediaRepository(uow)) - { - var query = Query.Builder.Where(x => x.ParentId == id); - var medias = repository.GetByQuery(query); - - return medias; - } - } - - [Obsolete("Use the overload with 'long' parameter types instead")] - [EditorBrowsable(EditorBrowsableState.Never)] - public IEnumerable GetPagedChildren(int id, int pageIndex, int pageSize, out int totalChildren, - string orderBy, Direction orderDirection, bool orderBySystemField = true, string filter = "") - { - Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); - Mandate.ParameterCondition(pageSize > 0, "pageSize"); - using (var repository = RepositoryFactory.CreateMediaRepository(UowProvider.GetUnitOfWork())) - { - var query = Query.Builder; - query.Where(x => x.ParentId == id); - - long total; - var medias = repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out total, orderBy, orderDirection, orderBySystemField, filter); - - totalChildren = Convert.ToInt32(total); - return medias; - } - } - - /// - /// Gets a collection of objects by Parent Id - /// - /// Id of the Parent to retrieve Children from - /// Page index (zero based) - /// Page size - /// Total records query would return without paging - /// Field to order by - /// Direction to order by - /// Flag to indicate when ordering by system field - /// Search text filter - /// An Enumerable list of objects - public IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize, out long totalChildren, - string orderBy, Direction orderDirection, bool orderBySystemField = true, string filter = "") - { - Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); - Mandate.ParameterCondition(pageSize > 0, "pageSize"); - using (var repository = RepositoryFactory.CreateMediaRepository(UowProvider.GetUnitOfWork())) - { - var query = Query.Builder; - query.Where(x => x.ParentId == id); - - var medias = repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, orderBySystemField, filter); - - return medias; - } - } - - [Obsolete("Use the overload with 'long' parameter types instead")] - [EditorBrowsable(EditorBrowsableState.Never)] - public IEnumerable GetPagedDescendants(int id, int pageIndex, int pageSize, out int totalChildren, string orderBy = "Path", Direction orderDirection = Direction.Ascending, bool orderBySystemField = true, string filter = "") - { - Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); - Mandate.ParameterCondition(pageSize > 0, "pageSize"); - using (var repository = RepositoryFactory.CreateMediaRepository(UowProvider.GetUnitOfWork())) - { - - var query = Query.Builder; - //if the id is -1, then just get all - if (id != -1) - { - query.Where(x => x.Path.SqlContains(string.Format(",{0},", id), TextColumnType.NVarchar)); - } - long total; - var contents = repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out total, orderBy, orderDirection, orderBySystemField, filter); - totalChildren = Convert.ToInt32(total); - return contents; - } - } - - /// - /// Gets a collection of objects by Parent Id - /// - /// Id of the Parent to retrieve Descendants from - /// Page number - /// Page size - /// Total records query would return without paging - /// Field to order by - /// Direction to order by - /// Flag to indicate when ordering by system field - /// Search text filter - /// An Enumerable list of objects - public IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalChildren, string orderBy = "Path", Direction orderDirection = Direction.Ascending, bool orderBySystemField = true, string filter = "") - { - Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); - Mandate.ParameterCondition(pageSize > 0, "pageSize"); - using (var repository = RepositoryFactory.CreateMediaRepository(UowProvider.GetUnitOfWork())) - { - - var query = Query.Builder; - //if the id is -1, then just get all - if (id != -1) - { - query.Where(x => x.Path.SqlContains(string.Format(",{0},", id), TextColumnType.NVarchar)); - } - var contents = repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, orderBySystemField, filter); - - return contents; - } - } - - /// - /// Gets descendants of a object by its Id - /// - /// Id of the Parent to retrieve descendants from - /// An Enumerable flat list of objects - public IEnumerable GetDescendants(int id) - { - var media = GetById(id); - if (media == null) - { - return Enumerable.Empty(); - } - return GetDescendants(media); - } - - /// - /// Gets descendants of a object by its Id - /// - /// The Parent object to retrieve descendants from - /// An Enumerable flat list of objects - public IEnumerable GetDescendants(IMedia media) - { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMediaRepository(uow)) - { - var pathMatch = media.Path + ","; - var query = Query.Builder.Where(x => x.Path.StartsWith(pathMatch) && x.Id != media.Id); - var medias = repository.GetByQuery(query); - - return medias; - } - } - - /// - /// Gets the parent of the current media as an item. - /// - /// Id of the to retrieve the parent from - /// Parent object - public IMedia GetParent(int id) - { - var media = GetById(id); - return GetParent(media); - } - - /// - /// Gets the parent of the current media as an item. - /// - /// to retrieve the parent from - /// Parent object - public IMedia GetParent(IMedia media) - { - if (media.ParentId == -1 || media.ParentId == -21) - return null; - - return GetById(media.ParentId); - } - - /// - /// Gets a collection of objects by the Id of the - /// - /// Id of the - /// An Enumerable list of objects - public IEnumerable GetMediaOfMediaType(int id) - { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMediaRepository(uow)) - { - var query = Query.Builder.Where(x => x.ContentTypeId == id); - var medias = repository.GetByQuery(query); - - return medias; - } - } - - /// - /// Gets a collection of objects, which reside at the first level / root - /// - /// An Enumerable list of objects - public IEnumerable GetRootMedia() - { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMediaRepository(uow)) - { - var query = Query.Builder.Where(x => x.ParentId == -1); - var medias = repository.GetByQuery(query); - - return medias; - } - } - - /// - /// Gets a collection of an objects, which resides in the Recycle Bin - /// - /// An Enumerable list of objects - public IEnumerable GetMediaInRecycleBin() - { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMediaRepository(uow)) - { - var query = Query.Builder.Where(x => x.Path.Contains("-21")); - var medias = repository.GetByQuery(query); - - return medias; - } - } - - /// - /// Gets an object from the path stored in the 'umbracoFile' property. - /// - /// Path of the media item to retrieve (for example: /media/1024/koala_403x328.jpg) - /// - public IMedia GetMediaByPath(string mediaPath) - { - var umbracoFileValue = mediaPath; - const string Pattern = ".*[_][0-9]+[x][0-9]+[.].*"; - var isResized = Regex.IsMatch(mediaPath, Pattern); - - // If the image has been resized we strip the "_403x328" of the original "/media/1024/koala_403x328.jpg" url. - if (isResized) - { - var underscoreIndex = mediaPath.LastIndexOf('_'); - var dotIndex = mediaPath.LastIndexOf('.'); - umbracoFileValue = string.Concat(mediaPath.Substring(0, underscoreIndex), mediaPath.Substring(dotIndex)); - } - - Func createSql = url => new Sql().Select("*") - .From() - .InnerJoin() - .On(left => left.PropertyTypeId, right => right.Id) - .Where(x => x.Alias == "umbracoFile") - .Where(x => x.VarChar == url); - - var sql = createSql(umbracoFileValue); - - using (var uow = UowProvider.GetUnitOfWork()) - { - var propertyDataDto = uow.Database.Fetch(sql).FirstOrDefault(); - - // If the stripped-down url returns null, we try again with the original url. - // Previously, the function would fail on e.g. "my_x_image.jpg" - if (propertyDataDto == null) - { - sql = createSql(mediaPath); - propertyDataDto = uow.Database.Fetch(sql).FirstOrDefault(); - } - - return propertyDataDto == null ? null : GetById(propertyDataDto.NodeId); - } - } - - /// - /// Checks whether an item has any children - /// - /// Id of the - /// True if the media has any children otherwise False - public bool HasChildren(int id) - { - using (var repository = RepositoryFactory.CreateMediaRepository(UowProvider.GetUnitOfWork())) - { - var query = Query.Builder.Where(x => x.ParentId == id); - int count = repository.Count(query); - return count > 0; - } - } - - /// - /// Moves an object to a new location - /// - /// The to move - /// Id of the Media's new Parent - /// Id of the User moving the Media - public void Move(IMedia media, int parentId, int userId = 0) - { - //TODO: This all needs to be on the repo layer in one transaction! - - if (media == null) throw new ArgumentNullException("media"); - - using (new WriteLock(Locker)) - { - //This ensures that the correct method is called if this method is used to Move to recycle bin. - if (parentId == -21) - { - MoveToRecycleBin(media, userId); - return; - } - - var originalPath = media.Path; - - if (Moving.IsRaisedEventCancelled( - new MoveEventArgs( - new MoveEventInfo(media, originalPath, parentId)), this)) - { - return; - } - - media.ParentId = parentId; - if (media.Trashed) - { - media.ChangeTrashedState(false, parentId); - } - Save(media, userId, - //no events! - false); - - //used to track all the moved entities to be given to the event - var moveInfo = new List> - { - new MoveEventInfo(media, originalPath, parentId) - }; - - //Ensure that relevant properties are updated on children - var children = GetChildren(media.Id).ToArray(); - if (children.Any()) - { - var parentPath = media.Path; - var parentLevel = media.Level; - var parentTrashed = media.Trashed; - var updatedDescendants = UpdatePropertiesOnChildren(children, parentPath, parentLevel, parentTrashed, moveInfo); - Save(updatedDescendants, userId, - //no events! - false); - } - - Moved.RaiseEvent(new MoveEventArgs(false, moveInfo.ToArray()), this); - - Audit(AuditType.Move, "Move Media performed by user", userId, media.Id); - } - } - - /// - /// Deletes an object by moving it to the Recycle Bin - /// - /// The to delete - /// Id of the User deleting the Media - public void MoveToRecycleBin(IMedia media, int userId = 0) - { - ((IMediaServiceOperations)this).MoveToRecycleBin(media, userId); - } - - /// - /// Permanently deletes an object - /// - /// - /// Please note that this method will completely remove the Media from the database, - /// but current not from the file system. - /// - /// The to delete - /// Id of the User deleting the Media - Attempt IMediaServiceOperations.Delete(IMedia media, int userId) - { - //TODO: IT would be much nicer to mass delete all in one trans in the repo level! - var evtMsgs = EventMessagesFactory.Get(); - - if (Deleting.IsRaisedEventCancelled( - new DeleteEventArgs(media, evtMsgs), this)) - { - return OperationStatus.Cancelled(evtMsgs); - } - - //Delete children before deleting the 'possible parent' - var children = GetChildren(media.Id); - foreach (var child in children) - { - Delete(child, userId); - } - - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMediaRepository(uow)) - { - repository.Delete(media); - uow.Commit(); - - var args = new DeleteEventArgs(media, false, evtMsgs); - Deleted.RaiseEvent(args, this); - - //remove any flagged media files - repository.DeleteMediaFiles(args.MediaFilesToDelete); - } - - Audit(AuditType.Delete, "Delete Media performed by user", userId, media.Id); - - return OperationStatus.Success(evtMsgs); - } - - /// - /// Saves a single object - /// - /// The to save - /// Id of the User saving the Media - /// Optional boolean indicating whether or not to raise events. - Attempt IMediaServiceOperations.Save(IMedia media, int userId, bool raiseEvents) - { - var evtMsgs = EventMessagesFactory.Get(); - - if (raiseEvents) - { - if (Saving.IsRaisedEventCancelled( - new SaveEventArgs(media, evtMsgs), - this)) - { - return OperationStatus.Cancelled(evtMsgs); - } - - } - - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMediaRepository(uow)) - { - media.CreatorId = userId; - repository.AddOrUpdate(media); - repository.AddOrUpdateContentXml(media, m => _entitySerializer.Serialize(this, _dataTypeService, _userService, m)); - // generate preview for blame history? - if (UmbracoConfig.For.UmbracoSettings().Content.GlobalPreviewStorageEnabled) - { - repository.AddOrUpdatePreviewXml(media, m => _entitySerializer.Serialize(this, _dataTypeService, _userService, m)); - } - - uow.Commit(); - } - - if (raiseEvents) - Saved.RaiseEvent(new SaveEventArgs(media, false, evtMsgs), this); - - Audit(AuditType.Save, "Save Media performed by user", userId, media.Id); - - return OperationStatus.Success(evtMsgs); - } - - /// - /// Saves a collection of objects - /// - /// Collection of to save - /// Id of the User saving the Media - /// Optional boolean indicating whether or not to raise events. - Attempt IMediaServiceOperations.Save(IEnumerable medias, int userId, bool raiseEvents) - { - var asArray = medias.ToArray(); - var evtMsgs = EventMessagesFactory.Get(); - - if (raiseEvents) - { - if (Saving.IsRaisedEventCancelled( - new SaveEventArgs(asArray, evtMsgs), - this)) - { - return OperationStatus.Cancelled(evtMsgs); - } - } - - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMediaRepository(uow)) - { - foreach (var media in asArray) - { - media.CreatorId = userId; - repository.AddOrUpdate(media); - repository.AddOrUpdateContentXml(media, m => _entitySerializer.Serialize(this, _dataTypeService, _userService, m)); - // generate preview for blame history? - if (UmbracoConfig.For.UmbracoSettings().Content.GlobalPreviewStorageEnabled) - { - repository.AddOrUpdatePreviewXml(media, m => _entitySerializer.Serialize(this, _dataTypeService, _userService, m)); - } - } - - //commit the whole lot in one go - uow.Commit(); - } - - if (raiseEvents) - Saved.RaiseEvent(new SaveEventArgs(asArray, false, evtMsgs), this); - - Audit(AuditType.Save, "Save Media items performed by user", userId, -1); - - return OperationStatus.Success(evtMsgs); - } - - /// - /// Empties the Recycle Bin by deleting all that resides in the bin - /// - public void EmptyRecycleBin() - { - using (new WriteLock(Locker)) - { - Dictionary> entities; - List files; - bool success; - var nodeObjectType = new Guid(Constants.ObjectTypes.Media); - - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMediaRepository(uow)) - { - //Create a dictionary of ids -> dictionary of property aliases + values - entities = repository.GetEntitiesInRecycleBin() - .ToDictionary( - key => key.Id, - val => (IEnumerable)val.Properties); - - files = ((MediaRepository)repository).GetFilesInRecycleBinForUploadField(); - - if (EmptyingRecycleBin.IsRaisedEventCancelled(new RecycleBinEventArgs(nodeObjectType, entities, files), this)) - return; - - success = repository.EmptyRecycleBin(); - - EmptiedRecycleBin.RaiseEvent(new RecycleBinEventArgs(nodeObjectType, entities, files, success), this); - - if (success) - repository.DeleteMediaFiles(files); - } - } - Audit(AuditType.Delete, "Empty Media Recycle Bin performed by user", 0, -21); - } - - /// - /// Deletes all media of specified type. All children of deleted media is moved to Recycle Bin. - /// - /// This needs extra care and attention as its potentially a dangerous and extensive operation - /// Id of the - /// Optional id of the user deleting the media - public void DeleteMediaOfType(int mediaTypeId, int userId = 0) - { - //TODO: This all needs to be done on the repo level in one trans - - using (new WriteLock(Locker)) - { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMediaRepository(uow)) - { - //NOTE What about media that has the contenttype as part of its composition? - //The ContentType has to be removed from the composition somehow as it would otherwise break - //Dbl.check+test that the ContentType's Id is removed from the ContentType2ContentType table - var query = Query.Builder.Where(x => x.ContentTypeId == mediaTypeId); - var contents = repository.GetByQuery(query).ToArray(); - - if (Deleting.IsRaisedEventCancelled(new DeleteEventArgs(contents), this)) - return; - - foreach (var content in contents.OrderByDescending(x => x.ParentId)) - { - //Look for children of current content and move that to trash before the current content is deleted - var c = content; - var childQuery = Query.Builder.Where(x => x.Path.StartsWith(c.Path)); - var children = repository.GetByQuery(childQuery); - - foreach (var child in children) - { - if (child.ContentType.Id != mediaTypeId) - MoveToRecycleBin(child, userId); - } - - //Permanently delete the content - Delete(content, userId); - } - } - - Audit(AuditType.Delete, "Delete Media items by Type performed by user", userId, -1); - } - } - - /// - /// Deletes an object by moving it to the Recycle Bin - /// - /// The to delete - /// Id of the User deleting the Media - Attempt IMediaServiceOperations.MoveToRecycleBin(IMedia media, int userId) - { - if (media == null) throw new ArgumentNullException("media"); - - var originalPath = media.Path; - - var evtMsgs = EventMessagesFactory.Get(); - - if (Trashing.IsRaisedEventCancelled( - new MoveEventArgs(new MoveEventInfo(media, originalPath, Constants.System.RecycleBinMedia)), this)) - { - return OperationStatus.Cancelled(evtMsgs); - } - - var moveInfo = new List> - { - new MoveEventInfo(media, originalPath, Constants.System.RecycleBinMedia) - }; - - //Find Descendants, which will be moved to the recycle bin along with the parent/grandparent. - var descendants = GetDescendants(media).OrderBy(x => x.Level).ToList(); - - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMediaRepository(uow)) - { - //TODO: This should be part of the repo! - - //Remove 'published' xml from the cmsContentXml table for the unpublished media - uow.Database.Delete("WHERE nodeId = @Id", new { Id = media.Id }); - - media.ChangeTrashedState(true, Constants.System.RecycleBinMedia); - repository.AddOrUpdate(media); - - //Loop through descendants to update their trash state, but ensuring structure by keeping the ParentId - foreach (var descendant in descendants) - { - //Remove 'published' xml from the cmsContentXml table for the unpublished media - uow.Database.Delete("WHERE nodeId = @Id", new { Id = descendant.Id }); - - descendant.ChangeTrashedState(true, descendant.ParentId); - repository.AddOrUpdate(descendant); - - moveInfo.Add(new MoveEventInfo(descendant, descendant.Path, descendant.ParentId)); - } - - uow.Commit(); - } - - Trashed.RaiseEvent( - new MoveEventArgs(false, evtMsgs, moveInfo.ToArray()), this); - - Audit(AuditType.Move, "Move Media to Recycle Bin performed by user", userId, media.Id); - - return OperationStatus.Success(evtMsgs); - } - - /// - /// Permanently deletes an object as well as all of its Children. - /// - /// - /// Please note that this method will completely remove the Media from the database, - /// as well as associated media files from the file system. - /// - /// The to delete - /// Id of the User deleting the Media - public void Delete(IMedia media, int userId = 0) - { - ((IMediaServiceOperations)this).Delete(media, userId); - } - - - - /// - /// Permanently deletes versions from an object prior to a specific date. - /// This method will never delete the latest version of a content item. - /// - /// Id of the object to delete versions from - /// Latest version date - /// Optional Id of the User deleting versions of a Content object - public void DeleteVersions(int id, DateTime versionDate, int userId = 0) - { - if (DeletingVersions.IsRaisedEventCancelled(new DeleteRevisionsEventArgs(id, dateToRetain: versionDate), this)) - return; - - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMediaRepository(uow)) - { - repository.DeleteVersions(id, versionDate); - uow.Commit(); - } - - DeletedVersions.RaiseEvent(new DeleteRevisionsEventArgs(id, false, dateToRetain: versionDate), this); - - Audit(AuditType.Delete, "Delete Media by version date performed by user", userId, -1); - } - - /// - /// Permanently deletes specific version(s) from an object. - /// This method will never delete the latest version of a content item. - /// - /// Id of the object to delete a version from - /// Id of the version to delete - /// Boolean indicating whether to delete versions prior to the versionId - /// Optional Id of the User deleting versions of a Content object - public void DeleteVersion(int id, Guid versionId, bool deletePriorVersions, int userId = 0) - { - if (DeletingVersions.IsRaisedEventCancelled(new DeleteRevisionsEventArgs(id, specificVersion: versionId), this)) - return; - - if (deletePriorVersions) - { - var content = GetByVersion(versionId); - DeleteVersions(id, content.UpdateDate, userId); - } - - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMediaRepository(uow)) - { - repository.DeleteVersion(versionId); - uow.Commit(); - } - - DeletedVersions.RaiseEvent(new DeleteRevisionsEventArgs(id, false, specificVersion: versionId), this); - - Audit(AuditType.Delete, "Delete Media by version performed by user", userId, -1); - } - - /// - /// Saves a single object - /// - /// The to save - /// Id of the User saving the Content - /// Optional boolean indicating whether or not to raise events. - public void Save(IMedia media, int userId = 0, bool raiseEvents = true) - { - ((IMediaServiceOperations)this).Save(media, userId, raiseEvents); - } - - /// - /// Saves a collection of objects - /// - /// Collection of to save - /// Id of the User saving the Content - /// Optional boolean indicating whether or not to raise events. - public void Save(IEnumerable medias, int userId = 0, bool raiseEvents = true) - { - ((IMediaServiceOperations)this).Save(medias, userId, raiseEvents); - } - - /// - /// Sorts a collection of objects by updating the SortOrder according - /// to the ordering of items in the passed in . - /// - /// - /// - /// - /// True if sorting succeeded, otherwise False - public bool Sort(IEnumerable items, int userId = 0, bool raiseEvents = true) - { - var asArray = items.ToArray(); - - if (raiseEvents) - { - if (Saving.IsRaisedEventCancelled(new SaveEventArgs(asArray), this)) - return false; - } - - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMediaRepository(uow)) - { - int i = 0; - foreach (var media in asArray) - { - //If the current sort order equals that of the media - //we don't need to update it, so just increment the sort order - //and continue. - if (media.SortOrder == i) - { - i++; - continue; - } - - media.SortOrder = i; - i++; - - repository.AddOrUpdate(media); - repository.AddOrUpdateContentXml(media, m => _entitySerializer.Serialize(this, _dataTypeService, _userService, m)); - // generate preview for blame history? - if (UmbracoConfig.For.UmbracoSettings().Content.GlobalPreviewStorageEnabled) - { - repository.AddOrUpdatePreviewXml(media, m => _entitySerializer.Serialize(this, _dataTypeService, _userService, m)); - } - } - - uow.Commit(); - } - - if (raiseEvents) - Saved.RaiseEvent(new SaveEventArgs(asArray, false), this); - - Audit(AuditType.Sort, "Sorting Media performed by user", userId, 0); - - return true; - } - - /// - /// Rebuilds all xml content in the cmsContentXml table for all media - /// - /// - /// Only rebuild the xml structures for the content type ids passed in, if none then rebuilds the structures - /// for all media - /// - public void RebuildXmlStructures(params int[] contentTypeIds) - { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMediaRepository(uow)) - { - repository.RebuildXmlStructures( - media => _entitySerializer.Serialize(this, _dataTypeService, _userService, media), - contentTypeIds: contentTypeIds.Length == 0 ? null : contentTypeIds); - } - - Audit(AuditType.Publish, "MediaService.RebuildXmlStructures completed, the xml has been regenerated in the database", 0, -1); - } - - /// - /// Updates the Path and Level on a collection of objects - /// based on the Parent's Path and Level. Also change the trashed state if relevant. - /// - /// Collection of objects to update - /// Path of the Parent media - /// Level of the Parent media - /// Indicates whether the Parent is trashed or not - /// Used to track the objects to be used in the move event - /// Collection of updated objects - private IEnumerable UpdatePropertiesOnChildren(IEnumerable children, string parentPath, int parentLevel, bool parentTrashed, ICollection> eventInfo) - { - var list = new List(); - foreach (var child in children) - { - var originalPath = child.Path; - child.Path = string.Concat(parentPath, ",", child.Id); - child.Level = parentLevel + 1; - if (parentTrashed != child.Trashed) - { - child.ChangeTrashedState(parentTrashed, child.ParentId); - } - - eventInfo.Add(new MoveEventInfo(child, originalPath, child.ParentId)); - list.Add(child); - - var grandkids = GetChildren(child.Id).ToArray(); - if (grandkids.Any()) - { - list.AddRange(UpdatePropertiesOnChildren(grandkids, child.Path, child.Level, child.Trashed, eventInfo)); - } - } - return list; - } - - //private void CreateAndSaveMediaXml(XElement xml, int id, UmbracoDatabase db) - //{ - // var poco = new ContentXmlDto { NodeId = id, Xml = xml.ToDataString() }; - // var exists = db.FirstOrDefault("WHERE nodeId = @Id", new { Id = id }) != null; - // int result = exists ? db.Update(poco) : Convert.ToInt32(db.Insert(poco)); - //} - - private IMediaType FindMediaTypeByAlias(string mediaTypeAlias) - { - Mandate.ParameterNotNullOrEmpty(mediaTypeAlias, "mediaTypeAlias"); - - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMediaTypeRepository(uow)) - { - var query = Query.Builder.Where(x => x.Alias == mediaTypeAlias); - var mediaTypes = repository.GetByQuery(query); - - if (mediaTypes.Any() == false) - throw new Exception(string.Format("No MediaType matching the passed in Alias: '{0}' was found", - mediaTypeAlias)); - - var mediaType = mediaTypes.First(); - - if (mediaType == null) - throw new Exception(string.Format("MediaType matching the passed in Alias: '{0}' was null", - mediaTypeAlias)); - - return mediaType; - } - } - - private void Audit(AuditType type, string message, int userId, int objectId) - { - var uow = UowProvider.GetUnitOfWork(); - using (var auditRepo = RepositoryFactory.CreateAuditRepository(uow)) - { - auditRepo.AddOrUpdate(new AuditItem(objectId, message, type, userId)); - uow.Commit(); - } - } - - #region Event Handlers - - /// - /// Occurs before Delete - /// - public static event TypedEventHandler DeletingVersions; - - /// - /// Occurs after Delete - /// - public static event TypedEventHandler DeletedVersions; - - /// - /// Occurs before Delete - /// - public static event TypedEventHandler> Deleting; - - /// - /// Occurs after Delete - /// - public static event TypedEventHandler> Deleted; - - /// - /// Occurs before Save - /// - public static event TypedEventHandler> Saving; - - /// - /// Occurs after Save - /// - public static event TypedEventHandler> Saved; - - /// - /// Occurs before Create - /// - [Obsolete("Use the Created event instead, the Creating and Created events both offer the same functionality, Creating event has been deprecated.")] - public static event TypedEventHandler> Creating; - - /// - /// Occurs after Create - /// - /// - /// Please note that the Media object has been created, but not saved - /// so it does not have an identity yet (meaning no Id has been set). - /// - public static event TypedEventHandler> Created; - - /// - /// Occurs before Content is moved to Recycle Bin - /// - public static event TypedEventHandler> Trashing; - - /// - /// Occurs after Content is moved to Recycle Bin - /// - public static event TypedEventHandler> Trashed; - - /// - /// Occurs before Move - /// - public static event TypedEventHandler> Moving; - - /// - /// Occurs after Move - /// - public static event TypedEventHandler> Moved; - - /// - /// Occurs before the Recycle Bin is emptied - /// - public static event TypedEventHandler EmptyingRecycleBin; - - /// - /// Occurs after the Recycle Bin has been Emptied - /// - public static event TypedEventHandler EmptiedRecycleBin; - #endregion - } + /// + /// Represents the Media Service, which is an easy access to operations involving + /// + public class MediaService : RepositoryService, IMediaService, IMediaServiceOperations + { + + //Support recursive locks because some of the methods that require locking call other methods that require locking. + //for example, the Move method needs to be locked but this calls the Save method which also needs to be locked. + private static readonly ReaderWriterLockSlim Locker = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion); + + private readonly EntityXmlSerializer _entitySerializer = new EntityXmlSerializer(); + private readonly IDataTypeService _dataTypeService; + private readonly IUserService _userService; + + public MediaService(IDatabaseUnitOfWorkProvider provider, RepositoryFactory repositoryFactory, ILogger logger, IEventMessagesFactory eventMessagesFactory, IDataTypeService dataTypeService, IUserService userService) + : base(provider, repositoryFactory, logger, eventMessagesFactory) + { + if (dataTypeService == null) throw new ArgumentNullException("dataTypeService"); + if (userService == null) throw new ArgumentNullException("userService"); + _dataTypeService = dataTypeService; + _userService = userService; + } + + /// + /// Creates an object using the alias of the + /// that this Media should based on. + /// + /// + /// Note that using this method will simply return a new IMedia without any identity + /// as it has not yet been persisted. It is intended as a shortcut to creating new media objects + /// that does not invoke a save operation against the database. + /// + /// Name of the Media object + /// Id of Parent for the new Media item + /// Alias of the + /// Optional id of the user creating the media item + /// + public IMedia CreateMedia(string name, int parentId, string mediaTypeAlias, int userId = 0) + { + var mediaType = FindMediaTypeByAlias(mediaTypeAlias); + var media = new Models.Media(name, parentId, mediaType); + var parent = GetById(media.ParentId); + media.Path = string.Concat(parent.IfNotNull(x => x.Path, media.ParentId.ToString()), ",", media.Id); + + if (Creating.IsRaisedEventCancelled(new NewEventArgs(media, mediaTypeAlias, parentId), this)) + { + media.WasCancelled = true; + return media; + } + + media.CreatorId = userId; + + Created.RaiseEvent(new NewEventArgs(media, false, mediaTypeAlias, parentId), this); + + Audit(AuditType.New, string.Format("Media '{0}' was created", name), media.CreatorId, media.Id); + + return media; + } + + /// + /// Creates an object using the alias of the + /// that this Media should based on. + /// + /// + /// Note that using this method will simply return a new IMedia without any identity + /// as it has not yet been persisted. It is intended as a shortcut to creating new media objects + /// that does not invoke a save operation against the database. + /// + /// Name of the Media object + /// Parent for the new Media item + /// Alias of the + /// Optional id of the user creating the media item + /// + public IMedia CreateMedia(string name, IMedia parent, string mediaTypeAlias, int userId = 0) + { + if (parent == null) throw new ArgumentNullException("parent"); + + var mediaType = FindMediaTypeByAlias(mediaTypeAlias); + var media = new Models.Media(name, parent, mediaType); + media.Path = string.Concat(parent.Path, ",", media.Id); + + if (Creating.IsRaisedEventCancelled(new NewEventArgs(media, mediaTypeAlias, parent), this)) + { + media.WasCancelled = true; + return media; + } + + media.CreatorId = userId; + + Created.RaiseEvent(new NewEventArgs(media, false, mediaTypeAlias, parent), this); + + Audit(AuditType.New, string.Format("Media '{0}' was created", name), media.CreatorId, media.Id); + + return media; + } + + /// + /// Creates an object using the alias of the + /// that this Media should based on. + /// + /// + /// This method returns an object that has been persisted to the database + /// and therefor has an identity. + /// + /// Name of the Media object + /// Id of Parent for the new Media item + /// Alias of the + /// Optional id of the user creating the media item + /// + public IMedia CreateMediaWithIdentity(string name, int parentId, string mediaTypeAlias, int userId = 0) + { + var mediaType = FindMediaTypeByAlias(mediaTypeAlias); + var media = new Models.Media(name, parentId, mediaType); + + //NOTE: I really hate the notion of these Creating/Created events - they are so inconsistent, I've only just found + // out that in these 'WithIdentity' methods, the Saving/Saved events were not fired, wtf. Anyways, they're added now. + if (Creating.IsRaisedEventCancelled(new NewEventArgs(media, mediaTypeAlias, parentId), this)) + { + media.WasCancelled = true; + return media; + } + + if (Saving.IsRaisedEventCancelled(new SaveEventArgs(media), this)) + { + media.WasCancelled = true; + return media; + } + + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMediaRepository(uow)) + { + media.CreatorId = userId; + repository.AddOrUpdate(media); + + repository.AddOrUpdateContentXml(media, m => _entitySerializer.Serialize(this, _dataTypeService, _userService, m)); + // generate preview for blame history? + if (UmbracoConfig.For.UmbracoSettings().Content.GlobalPreviewStorageEnabled) + { + repository.AddOrUpdatePreviewXml(media, m => _entitySerializer.Serialize(this, _dataTypeService, _userService, m)); + } + + uow.Commit(); + } + + Saved.RaiseEvent(new SaveEventArgs(media, false), this); + + Created.RaiseEvent(new NewEventArgs(media, false, mediaTypeAlias, parentId), this); + + Audit(AuditType.New, string.Format("Media '{0}' was created with Id {1}", name, media.Id), media.CreatorId, media.Id); + + return media; + } + + /// + /// Creates an object using the alias of the + /// that this Media should based on. + /// + /// + /// This method returns an object that has been persisted to the database + /// and therefor has an identity. + /// + /// Name of the Media object + /// Parent for the new Media item + /// Alias of the + /// Optional id of the user creating the media item + /// + public IMedia CreateMediaWithIdentity(string name, IMedia parent, string mediaTypeAlias, int userId = 0) + { + if (parent == null) throw new ArgumentNullException("parent"); + + var mediaType = FindMediaTypeByAlias(mediaTypeAlias); + var media = new Models.Media(name, parent, mediaType); + + //NOTE: I really hate the notion of these Creating/Created events - they are so inconsistent, I've only just found + // out that in these 'WithIdentity' methods, the Saving/Saved events were not fired, wtf. Anyways, they're added now. + if (Creating.IsRaisedEventCancelled(new NewEventArgs(media, mediaTypeAlias, parent), this)) + { + media.WasCancelled = true; + return media; + } + + if (Saving.IsRaisedEventCancelled(new SaveEventArgs(media), this)) + { + media.WasCancelled = true; + return media; + } + + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMediaRepository(uow)) + { + media.CreatorId = userId; + repository.AddOrUpdate(media); + repository.AddOrUpdateContentXml(media, m => _entitySerializer.Serialize(this, _dataTypeService, _userService, m)); + // generate preview for blame history? + if (UmbracoConfig.For.UmbracoSettings().Content.GlobalPreviewStorageEnabled) + { + repository.AddOrUpdatePreviewXml(media, m => _entitySerializer.Serialize(this, _dataTypeService, _userService, m)); + } + + uow.Commit(); + } + + Saved.RaiseEvent(new SaveEventArgs(media, false), this); + + Created.RaiseEvent(new NewEventArgs(media, false, mediaTypeAlias, parent), this); + + Audit(AuditType.New, string.Format("Media '{0}' was created with Id {1}", name, media.Id), media.CreatorId, media.Id); + + return media; + } + + /// + /// Gets an object by Id + /// + /// Id of the Content to retrieve + /// + public IMedia GetById(int id) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMediaRepository(uow)) + { + return repository.Get(id); + } + } + + public int Count(string contentTypeAlias = null) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMediaRepository(uow)) + { + return repository.Count(contentTypeAlias); + } + } + + public int CountChildren(int parentId, string contentTypeAlias = null) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMediaRepository(uow)) + { + return repository.CountChildren(parentId, contentTypeAlias); + } + } + + public int CountDescendants(int parentId, string contentTypeAlias = null) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMediaRepository(uow)) + { + return repository.CountDescendants(parentId, contentTypeAlias); + } + } + + /// + /// Gets an object by Id + /// + /// Ids of the Media to retrieve + /// + public IEnumerable GetByIds(IEnumerable ids) + { + if (ids.Any() == false) return Enumerable.Empty(); + + using (var repository = RepositoryFactory.CreateMediaRepository(UowProvider.GetUnitOfWork())) + { + return repository.GetAll(ids.ToArray()); + } + } + + /// + /// Gets an object by its 'UniqueId' + /// + /// Guid key of the Media to retrieve + /// + public IMedia GetById(Guid key) + { + using (var repository = RepositoryFactory.CreateMediaRepository(UowProvider.GetUnitOfWork())) + { + var query = Query.Builder.Where(x => x.Key == key); + var contents = repository.GetByQuery(query); + return contents.SingleOrDefault(); + } + } + + /// + /// Gets a collection of objects by Level + /// + /// The level to retrieve Media from + /// An Enumerable list of objects + public IEnumerable GetByLevel(int level) + { + using (var repository = RepositoryFactory.CreateMediaRepository(UowProvider.GetUnitOfWork())) + { + var query = Query.Builder.Where(x => x.Level == level && !x.Path.StartsWith("-21")); + var contents = repository.GetByQuery(query); + + return contents; + } + } + + /// + /// Gets a specific version of an item. + /// + /// Id of the version to retrieve + /// An item + public IMedia GetByVersion(Guid versionId) + { + using (var repository = RepositoryFactory.CreateMediaRepository(UowProvider.GetUnitOfWork())) + { + return repository.GetByVersion(versionId); + } + } + + /// + /// Gets a collection of an objects versions by Id + /// + /// + /// An Enumerable list of objects + public IEnumerable GetVersions(int id) + { + using (var repository = RepositoryFactory.CreateMediaRepository(UowProvider.GetUnitOfWork())) + { + var versions = repository.GetAllVersions(id); + return versions; + } + } + + /// + /// Gets a collection of objects, which are ancestors of the current media. + /// + /// Id of the to retrieve ancestors for + /// An Enumerable list of objects + public IEnumerable GetAncestors(int id) + { + var media = GetById(id); + return GetAncestors(media); + } + + /// + /// Gets a collection of objects, which are ancestors of the current media. + /// + /// to retrieve ancestors for + /// An Enumerable list of objects + public IEnumerable GetAncestors(IMedia media) + { + var ids = media.Path.Split(',').Where(x => x != "-1" && x != media.Id.ToString(CultureInfo.InvariantCulture)).Select(int.Parse).ToArray(); + if (ids.Any() == false) + return new List(); + + using (var repository = RepositoryFactory.CreateMediaRepository(UowProvider.GetUnitOfWork())) + { + return repository.GetAll(ids); + } + } + + /// + /// Gets a collection of objects by Parent Id + /// + /// Id of the Parent to retrieve Children from + /// An Enumerable list of objects + public IEnumerable GetChildren(int id) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMediaRepository(uow)) + { + var query = Query.Builder.Where(x => x.ParentId == id); + var medias = repository.GetByQuery(query); + + return medias; + } + } + + [Obsolete("Use the overload with 'long' parameter types instead")] + [EditorBrowsable(EditorBrowsableState.Never)] + public IEnumerable GetPagedChildren(int id, int pageIndex, int pageSize, out int totalChildren, + string orderBy, Direction orderDirection, bool orderBySystemField = true, string filter = "") + { + Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); + Mandate.ParameterCondition(pageSize > 0, "pageSize"); + using (var repository = RepositoryFactory.CreateMediaRepository(UowProvider.GetUnitOfWork())) + { + var query = Query.Builder; + query.Where(x => x.ParentId == id); + + long total; + var medias = repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out total, orderBy, orderDirection, orderBySystemField, filter); + + totalChildren = Convert.ToInt32(total); + return medias; + } + } + + /// + /// Gets a collection of objects by Parent Id + /// + /// Id of the Parent to retrieve Children from + /// Page index (zero based) + /// Page size + /// Total records query would return without paging + /// Field to order by + /// Direction to order by + /// Flag to indicate when ordering by system field + /// Search text filter + /// An Enumerable list of objects + public IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize, out long totalChildren, + string orderBy, Direction orderDirection, bool orderBySystemField = true, string filter = "") + { + Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); + Mandate.ParameterCondition(pageSize > 0, "pageSize"); + using (var repository = RepositoryFactory.CreateMediaRepository(UowProvider.GetUnitOfWork())) + { + var query = Query.Builder; + query.Where(x => x.ParentId == id); + + var medias = repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, orderBySystemField, filter); + + return medias; + } + } + + [Obsolete("Use the overload with 'long' parameter types instead")] + [EditorBrowsable(EditorBrowsableState.Never)] + public IEnumerable GetPagedDescendants(int id, int pageIndex, int pageSize, out int totalChildren, string orderBy = "Path", Direction orderDirection = Direction.Ascending, bool orderBySystemField = true, string filter = "") + { + Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); + Mandate.ParameterCondition(pageSize > 0, "pageSize"); + using (var repository = RepositoryFactory.CreateMediaRepository(UowProvider.GetUnitOfWork())) + { + + var query = Query.Builder; + //if the id is -1, then just get all + if (id != -1) + { + query.Where(x => x.Path.SqlContains(string.Format(",{0},", id), TextColumnType.NVarchar)); + } + long total; + var contents = repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out total, orderBy, orderDirection, orderBySystemField, filter); + totalChildren = Convert.ToInt32(total); + return contents; + } + } + + /// + /// Gets a collection of objects by Parent Id + /// + /// Id of the Parent to retrieve Descendants from + /// Page number + /// Page size + /// Total records query would return without paging + /// Field to order by + /// Direction to order by + /// Flag to indicate when ordering by system field + /// Search text filter + /// An Enumerable list of objects + public IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalChildren, string orderBy = "Path", Direction orderDirection = Direction.Ascending, bool orderBySystemField = true, string filter = "") + { + Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); + Mandate.ParameterCondition(pageSize > 0, "pageSize"); + using (var repository = RepositoryFactory.CreateMediaRepository(UowProvider.GetUnitOfWork())) + { + + var query = Query.Builder; + //if the id is -1, then just get all + if (id != -1) + { + query.Where(x => x.Path.SqlContains(string.Format(",{0},", id), TextColumnType.NVarchar)); + } + var contents = repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, orderBySystemField, filter); + + return contents; + } + } + + /// + /// Gets descendants of a object by its Id + /// + /// Id of the Parent to retrieve descendants from + /// An Enumerable flat list of objects + public IEnumerable GetDescendants(int id) + { + var media = GetById(id); + if (media == null) + { + return Enumerable.Empty(); + } + return GetDescendants(media); + } + + /// + /// Gets descendants of a object by its Id + /// + /// The Parent object to retrieve descendants from + /// An Enumerable flat list of objects + public IEnumerable GetDescendants(IMedia media) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMediaRepository(uow)) + { + var pathMatch = media.Path + ","; + var query = Query.Builder.Where(x => x.Path.StartsWith(pathMatch) && x.Id != media.Id); + var medias = repository.GetByQuery(query); + + return medias; + } + } + + /// + /// Gets the parent of the current media as an item. + /// + /// Id of the to retrieve the parent from + /// Parent object + public IMedia GetParent(int id) + { + var media = GetById(id); + return GetParent(media); + } + + /// + /// Gets the parent of the current media as an item. + /// + /// to retrieve the parent from + /// Parent object + public IMedia GetParent(IMedia media) + { + if (media.ParentId == -1 || media.ParentId == -21) + return null; + + return GetById(media.ParentId); + } + + /// + /// Gets a collection of objects by the Id of the + /// + /// Id of the + /// An Enumerable list of objects + public IEnumerable GetMediaOfMediaType(int id) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMediaRepository(uow)) + { + var query = Query.Builder.Where(x => x.ContentTypeId == id); + var medias = repository.GetByQuery(query); + + return medias; + } + } + + /// + /// Gets a collection of objects, which reside at the first level / root + /// + /// An Enumerable list of objects + public IEnumerable GetRootMedia() + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMediaRepository(uow)) + { + var query = Query.Builder.Where(x => x.ParentId == -1); + var medias = repository.GetByQuery(query); + + return medias; + } + } + + /// + /// Gets a collection of an objects, which resides in the Recycle Bin + /// + /// An Enumerable list of objects + public IEnumerable GetMediaInRecycleBin() + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMediaRepository(uow)) + { + var query = Query.Builder.Where(x => x.Path.Contains("-21")); + var medias = repository.GetByQuery(query); + + return medias; + } + } + + /// + /// Gets an object from the path stored in the 'umbracoFile' property. + /// + /// Path of the media item to retrieve (for example: /media/1024/koala_403x328.jpg) + /// + public IMedia GetMediaByPath(string mediaPath) + { + var umbracoFileValue = mediaPath; + const string Pattern = ".*[_][0-9]+[x][0-9]+[.].*"; + var isResized = Regex.IsMatch(mediaPath, Pattern); + + // If the image has been resized we strip the "_403x328" of the original "/media/1024/koala_403x328.jpg" url. + if (isResized) + { + var underscoreIndex = mediaPath.LastIndexOf('_'); + var dotIndex = mediaPath.LastIndexOf('.'); + umbracoFileValue = string.Concat(mediaPath.Substring(0, underscoreIndex), mediaPath.Substring(dotIndex)); + } + + Func createSql = url => new Sql().Select("*") + .From() + .InnerJoin() + .On(left => left.PropertyTypeId, right => right.Id) + .Where(x => x.Alias == "umbracoFile") + .Where(x => x.VarChar == url); + + var sql = createSql(umbracoFileValue); + + using (var uow = UowProvider.GetUnitOfWork()) + { + var propertyDataDto = uow.Database.Fetch(sql).FirstOrDefault(); + + // If the stripped-down url returns null, we try again with the original url. + // Previously, the function would fail on e.g. "my_x_image.jpg" + if (propertyDataDto == null) + { + sql = createSql(mediaPath); + propertyDataDto = uow.Database.Fetch(sql).FirstOrDefault(); + } + + return propertyDataDto == null ? null : GetById(propertyDataDto.NodeId); + } + } + + /// + /// Checks whether an item has any children + /// + /// Id of the + /// True if the media has any children otherwise False + public bool HasChildren(int id) + { + using (var repository = RepositoryFactory.CreateMediaRepository(UowProvider.GetUnitOfWork())) + { + var query = Query.Builder.Where(x => x.ParentId == id); + int count = repository.Count(query); + return count > 0; + } + } + + /// + /// Moves an object to a new location + /// + /// The to move + /// Id of the Media's new Parent + /// Id of the User moving the Media + public void Move(IMedia media, int parentId, int userId = 0) + { + //TODO: This all needs to be on the repo layer in one transaction! + + if (media == null) throw new ArgumentNullException("media"); + + using (new WriteLock(Locker)) + { + //This ensures that the correct method is called if this method is used to Move to recycle bin. + if (parentId == -21) + { + MoveToRecycleBin(media, userId); + return; + } + + var originalPath = media.Path; + + if (Moving.IsRaisedEventCancelled( + new MoveEventArgs( + new MoveEventInfo(media, originalPath, parentId)), this)) + { + return; + } + + media.ParentId = parentId; + if (media.Trashed) + { + media.ChangeTrashedState(false, parentId); + } + Save(media, userId, + //no events! + false); + + //used to track all the moved entities to be given to the event + var moveInfo = new List> + { + new MoveEventInfo(media, originalPath, parentId) + }; + + //Ensure that relevant properties are updated on children + var children = GetChildren(media.Id).ToArray(); + if (children.Any()) + { + var parentPath = media.Path; + var parentLevel = media.Level; + var parentTrashed = media.Trashed; + var updatedDescendants = UpdatePropertiesOnChildren(children, parentPath, parentLevel, parentTrashed, moveInfo); + Save(updatedDescendants, userId, + //no events! + false); + } + + Moved.RaiseEvent(new MoveEventArgs(false, moveInfo.ToArray()), this); + + Audit(AuditType.Move, "Move Media performed by user", userId, media.Id); + } + } + + /// + /// Deletes an object by moving it to the Recycle Bin + /// + /// The to delete + /// Id of the User deleting the Media + public void MoveToRecycleBin(IMedia media, int userId = 0) + { + ((IMediaServiceOperations)this).MoveToRecycleBin(media, userId); + } + + /// + /// Permanently deletes an object + /// + /// + /// Please note that this method will completely remove the Media from the database, + /// but current not from the file system. + /// + /// The to delete + /// Id of the User deleting the Media + Attempt IMediaServiceOperations.Delete(IMedia media, int userId) + { + //TODO: IT would be much nicer to mass delete all in one trans in the repo level! + var evtMsgs = EventMessagesFactory.Get(); + + if (Deleting.IsRaisedEventCancelled( + new DeleteEventArgs(media, evtMsgs), this)) + { + return OperationStatus.Cancelled(evtMsgs); + } + + //Delete children before deleting the 'possible parent' + var children = GetChildren(media.Id); + foreach (var child in children) + { + Delete(child, userId); + } + + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMediaRepository(uow)) + { + repository.Delete(media); + uow.Commit(); + + var args = new DeleteEventArgs(media, false, evtMsgs); + Deleted.RaiseEvent(args, this); + + //remove any flagged media files + repository.DeleteMediaFiles(args.MediaFilesToDelete); + } + + Audit(AuditType.Delete, "Delete Media performed by user", userId, media.Id); + + return OperationStatus.Success(evtMsgs); + } + + /// + /// Saves a single object + /// + /// The to save + /// Id of the User saving the Media + /// Optional boolean indicating whether or not to raise events. + Attempt IMediaServiceOperations.Save(IMedia media, int userId, bool raiseEvents) + { + var evtMsgs = EventMessagesFactory.Get(); + + if (raiseEvents) + { + if (Saving.IsRaisedEventCancelled( + new SaveEventArgs(media, evtMsgs), + this)) + { + return OperationStatus.Cancelled(evtMsgs); + } + + } + + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMediaRepository(uow)) + { + media.CreatorId = userId; + repository.AddOrUpdate(media); + repository.AddOrUpdateContentXml(media, m => _entitySerializer.Serialize(this, _dataTypeService, _userService, m)); + // generate preview for blame history? + if (UmbracoConfig.For.UmbracoSettings().Content.GlobalPreviewStorageEnabled) + { + repository.AddOrUpdatePreviewXml(media, m => _entitySerializer.Serialize(this, _dataTypeService, _userService, m)); + } + + uow.Commit(); + } + + if (raiseEvents) + Saved.RaiseEvent(new SaveEventArgs(media, false, evtMsgs), this); + + Audit(AuditType.Save, "Save Media performed by user", userId, media.Id); + + return OperationStatus.Success(evtMsgs); + } + + /// + /// Saves a collection of objects + /// + /// Collection of to save + /// Id of the User saving the Media + /// Optional boolean indicating whether or not to raise events. + Attempt IMediaServiceOperations.Save(IEnumerable medias, int userId, bool raiseEvents) + { + var asArray = medias.ToArray(); + var evtMsgs = EventMessagesFactory.Get(); + + if (raiseEvents) + { + if (Saving.IsRaisedEventCancelled( + new SaveEventArgs(asArray, evtMsgs), + this)) + { + return OperationStatus.Cancelled(evtMsgs); + } + } + + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMediaRepository(uow)) + { + foreach (var media in asArray) + { + media.CreatorId = userId; + repository.AddOrUpdate(media); + repository.AddOrUpdateContentXml(media, m => _entitySerializer.Serialize(this, _dataTypeService, _userService, m)); + // generate preview for blame history? + if (UmbracoConfig.For.UmbracoSettings().Content.GlobalPreviewStorageEnabled) + { + repository.AddOrUpdatePreviewXml(media, m => _entitySerializer.Serialize(this, _dataTypeService, _userService, m)); + } + } + + //commit the whole lot in one go + uow.Commit(); + } + + if (raiseEvents) + Saved.RaiseEvent(new SaveEventArgs(asArray, false, evtMsgs), this); + + Audit(AuditType.Save, "Save Media items performed by user", userId, -1); + + return OperationStatus.Success(evtMsgs); + } + + /// + /// Empties the Recycle Bin by deleting all that resides in the bin + /// + public void EmptyRecycleBin() + { + using (new WriteLock(Locker)) + { + Dictionary> entities; + List files; + bool success; + var nodeObjectType = new Guid(Constants.ObjectTypes.Media); + + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMediaRepository(uow)) + { + //Create a dictionary of ids -> dictionary of property aliases + values + entities = repository.GetEntitiesInRecycleBin() + .ToDictionary( + key => key.Id, + val => (IEnumerable)val.Properties); + + files = ((MediaRepository)repository).GetFilesInRecycleBinForUploadField(); + + if (EmptyingRecycleBin.IsRaisedEventCancelled(new RecycleBinEventArgs(nodeObjectType, entities, files), this)) + return; + + success = repository.EmptyRecycleBin(); + + EmptiedRecycleBin.RaiseEvent(new RecycleBinEventArgs(nodeObjectType, entities, files, success), this); + + if (success) + repository.DeleteMediaFiles(files); + } + } + Audit(AuditType.Delete, "Empty Media Recycle Bin performed by user", 0, -21); + } + + /// + /// Deletes all media of specified type. All children of deleted media is moved to Recycle Bin. + /// + /// This needs extra care and attention as its potentially a dangerous and extensive operation + /// Id of the + /// Optional id of the user deleting the media + public void DeleteMediaOfType(int mediaTypeId, int userId = 0) + { + //TODO: This all needs to be done on the repo level in one trans + + using (new WriteLock(Locker)) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMediaRepository(uow)) + { + //NOTE What about media that has the contenttype as part of its composition? + //The ContentType has to be removed from the composition somehow as it would otherwise break + //Dbl.check+test that the ContentType's Id is removed from the ContentType2ContentType table + var query = Query.Builder.Where(x => x.ContentTypeId == mediaTypeId); + var contents = repository.GetByQuery(query).ToArray(); + + if (Deleting.IsRaisedEventCancelled(new DeleteEventArgs(contents), this)) + return; + + foreach (var content in contents.OrderByDescending(x => x.ParentId)) + { + //Look for children of current content and move that to trash before the current content is deleted + var c = content; + var childQuery = Query.Builder.Where(x => x.Path.StartsWith(c.Path)); + var children = repository.GetByQuery(childQuery); + + foreach (var child in children) + { + if (child.ContentType.Id != mediaTypeId) + MoveToRecycleBin(child, userId); + } + + //Permanently delete the content + Delete(content, userId); + } + } + + Audit(AuditType.Delete, "Delete Media items by Type performed by user", userId, -1); + } + } + + /// + /// Deletes an object by moving it to the Recycle Bin + /// + /// The to delete + /// Id of the User deleting the Media + Attempt IMediaServiceOperations.MoveToRecycleBin(IMedia media, int userId) + { + if (media == null) throw new ArgumentNullException("media"); + + var originalPath = media.Path; + + var evtMsgs = EventMessagesFactory.Get(); + + if (Trashing.IsRaisedEventCancelled( + new MoveEventArgs(new MoveEventInfo(media, originalPath, Constants.System.RecycleBinMedia)), this)) + { + return OperationStatus.Cancelled(evtMsgs); + } + + var moveInfo = new List> + { + new MoveEventInfo(media, originalPath, Constants.System.RecycleBinMedia) + }; + + //Find Descendants, which will be moved to the recycle bin along with the parent/grandparent. + var descendants = GetDescendants(media).OrderBy(x => x.Level).ToList(); + + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMediaRepository(uow)) + { + //TODO: This should be part of the repo! + + //Remove 'published' xml from the cmsContentXml table for the unpublished media + uow.Database.Delete("WHERE nodeId = @Id", new { Id = media.Id }); + + media.ChangeTrashedState(true, Constants.System.RecycleBinMedia); + repository.AddOrUpdate(media); + + //Loop through descendants to update their trash state, but ensuring structure by keeping the ParentId + foreach (var descendant in descendants) + { + //Remove 'published' xml from the cmsContentXml table for the unpublished media + uow.Database.Delete("WHERE nodeId = @Id", new { Id = descendant.Id }); + + descendant.ChangeTrashedState(true, descendant.ParentId); + repository.AddOrUpdate(descendant); + + moveInfo.Add(new MoveEventInfo(descendant, descendant.Path, descendant.ParentId)); + } + + uow.Commit(); + } + + Trashed.RaiseEvent( + new MoveEventArgs(false, evtMsgs, moveInfo.ToArray()), this); + + Audit(AuditType.Move, "Move Media to Recycle Bin performed by user", userId, media.Id); + + return OperationStatus.Success(evtMsgs); + } + + /// + /// Permanently deletes an object as well as all of its Children. + /// + /// + /// Please note that this method will completely remove the Media from the database, + /// as well as associated media files from the file system. + /// + /// The to delete + /// Id of the User deleting the Media + public void Delete(IMedia media, int userId = 0) + { + ((IMediaServiceOperations)this).Delete(media, userId); + } + + + + /// + /// Permanently deletes versions from an object prior to a specific date. + /// This method will never delete the latest version of a content item. + /// + /// Id of the object to delete versions from + /// Latest version date + /// Optional Id of the User deleting versions of a Content object + public void DeleteVersions(int id, DateTime versionDate, int userId = 0) + { + if (DeletingVersions.IsRaisedEventCancelled(new DeleteRevisionsEventArgs(id, dateToRetain: versionDate), this)) + return; + + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMediaRepository(uow)) + { + repository.DeleteVersions(id, versionDate); + uow.Commit(); + } + + DeletedVersions.RaiseEvent(new DeleteRevisionsEventArgs(id, false, dateToRetain: versionDate), this); + + Audit(AuditType.Delete, "Delete Media by version date performed by user", userId, -1); + } + + /// + /// Permanently deletes specific version(s) from an object. + /// This method will never delete the latest version of a content item. + /// + /// Id of the object to delete a version from + /// Id of the version to delete + /// Boolean indicating whether to delete versions prior to the versionId + /// Optional Id of the User deleting versions of a Content object + public void DeleteVersion(int id, Guid versionId, bool deletePriorVersions, int userId = 0) + { + if (DeletingVersions.IsRaisedEventCancelled(new DeleteRevisionsEventArgs(id, specificVersion: versionId), this)) + return; + + if (deletePriorVersions) + { + var content = GetByVersion(versionId); + DeleteVersions(id, content.UpdateDate, userId); + } + + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMediaRepository(uow)) + { + repository.DeleteVersion(versionId); + uow.Commit(); + } + + DeletedVersions.RaiseEvent(new DeleteRevisionsEventArgs(id, false, specificVersion: versionId), this); + + Audit(AuditType.Delete, "Delete Media by version performed by user", userId, -1); + } + + /// + /// Saves a single object + /// + /// The to save + /// Id of the User saving the Content + /// Optional boolean indicating whether or not to raise events. + public void Save(IMedia media, int userId = 0, bool raiseEvents = true) + { + ((IMediaServiceOperations)this).Save(media, userId, raiseEvents); + } + + /// + /// Saves a collection of objects + /// + /// Collection of to save + /// Id of the User saving the Content + /// Optional boolean indicating whether or not to raise events. + public void Save(IEnumerable medias, int userId = 0, bool raiseEvents = true) + { + ((IMediaServiceOperations)this).Save(medias, userId, raiseEvents); + } + + /// + /// Sorts a collection of objects by updating the SortOrder according + /// to the ordering of items in the passed in . + /// + /// + /// + /// + /// True if sorting succeeded, otherwise False + public bool Sort(IEnumerable items, int userId = 0, bool raiseEvents = true) + { + var asArray = items.ToArray(); + + if (raiseEvents) + { + if (Saving.IsRaisedEventCancelled(new SaveEventArgs(asArray), this)) + return false; + } + + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMediaRepository(uow)) + { + int i = 0; + foreach (var media in asArray) + { + //If the current sort order equals that of the media + //we don't need to update it, so just increment the sort order + //and continue. + if (media.SortOrder == i) + { + i++; + continue; + } + + media.SortOrder = i; + i++; + + repository.AddOrUpdate(media); + repository.AddOrUpdateContentXml(media, m => _entitySerializer.Serialize(this, _dataTypeService, _userService, m)); + // generate preview for blame history? + if (UmbracoConfig.For.UmbracoSettings().Content.GlobalPreviewStorageEnabled) + { + repository.AddOrUpdatePreviewXml(media, m => _entitySerializer.Serialize(this, _dataTypeService, _userService, m)); + } + } + + uow.Commit(); + } + + if (raiseEvents) + Saved.RaiseEvent(new SaveEventArgs(asArray, false), this); + + Audit(AuditType.Sort, "Sorting Media performed by user", userId, 0); + + return true; + } + + /// + /// Rebuilds all xml content in the cmsContentXml table for all media + /// + /// + /// Only rebuild the xml structures for the content type ids passed in, if none then rebuilds the structures + /// for all media + /// + public void RebuildXmlStructures(params int[] contentTypeIds) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMediaRepository(uow)) + { + repository.RebuildXmlStructures( + media => _entitySerializer.Serialize(this, _dataTypeService, _userService, media), + contentTypeIds: contentTypeIds.Length == 0 ? null : contentTypeIds); + } + + Audit(AuditType.Publish, "MediaService.RebuildXmlStructures completed, the xml has been regenerated in the database", 0, -1); + } + + /// + /// Updates the Path and Level on a collection of objects + /// based on the Parent's Path and Level. Also change the trashed state if relevant. + /// + /// Collection of objects to update + /// Path of the Parent media + /// Level of the Parent media + /// Indicates whether the Parent is trashed or not + /// Used to track the objects to be used in the move event + /// Collection of updated objects + private IEnumerable UpdatePropertiesOnChildren(IEnumerable children, string parentPath, int parentLevel, bool parentTrashed, ICollection> eventInfo) + { + var list = new List(); + foreach (var child in children) + { + var originalPath = child.Path; + child.Path = string.Concat(parentPath, ",", child.Id); + child.Level = parentLevel + 1; + if (parentTrashed != child.Trashed) + { + child.ChangeTrashedState(parentTrashed, child.ParentId); + } + + eventInfo.Add(new MoveEventInfo(child, originalPath, child.ParentId)); + list.Add(child); + + var grandkids = GetChildren(child.Id).ToArray(); + if (grandkids.Any()) + { + list.AddRange(UpdatePropertiesOnChildren(grandkids, child.Path, child.Level, child.Trashed, eventInfo)); + } + } + return list; + } + + //private void CreateAndSaveMediaXml(XElement xml, int id, UmbracoDatabase db) + //{ + // var poco = new ContentXmlDto { NodeId = id, Xml = xml.ToDataString() }; + // var exists = db.FirstOrDefault("WHERE nodeId = @Id", new { Id = id }) != null; + // int result = exists ? db.Update(poco) : Convert.ToInt32(db.Insert(poco)); + //} + + private IMediaType FindMediaTypeByAlias(string mediaTypeAlias) + { + Mandate.ParameterNotNullOrEmpty(mediaTypeAlias, "mediaTypeAlias"); + + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMediaTypeRepository(uow)) + { + var query = Query.Builder.Where(x => x.Alias == mediaTypeAlias); + var mediaTypes = repository.GetByQuery(query); + + if (mediaTypes.Any() == false) + throw new Exception(string.Format("No MediaType matching the passed in Alias: '{0}' was found", + mediaTypeAlias)); + + var mediaType = mediaTypes.First(); + + if (mediaType == null) + throw new Exception(string.Format("MediaType matching the passed in Alias: '{0}' was null", + mediaTypeAlias)); + + return mediaType; + } + } + + private void Audit(AuditType type, string message, int userId, int objectId) + { + var uow = UowProvider.GetUnitOfWork(); + using (var auditRepo = RepositoryFactory.CreateAuditRepository(uow)) + { + auditRepo.AddOrUpdate(new AuditItem(objectId, message, type, userId)); + uow.Commit(); + } + } + + #region Event Handlers + + /// + /// Occurs before Delete + /// + public static event TypedEventHandler DeletingVersions; + + /// + /// Occurs after Delete + /// + public static event TypedEventHandler DeletedVersions; + + /// + /// Occurs before Delete + /// + public static event TypedEventHandler> Deleting; + + /// + /// Occurs after Delete + /// + public static event TypedEventHandler> Deleted; + + /// + /// Occurs before Save + /// + public static event TypedEventHandler> Saving; + + /// + /// Occurs after Save + /// + public static event TypedEventHandler> Saved; + + /// + /// Occurs before Create + /// + [Obsolete("Use the Created event instead, the Creating and Created events both offer the same functionality, Creating event has been deprecated.")] + public static event TypedEventHandler> Creating; + + /// + /// Occurs after Create + /// + /// + /// Please note that the Media object has been created, but not saved + /// so it does not have an identity yet (meaning no Id has been set). + /// + public static event TypedEventHandler> Created; + + /// + /// Occurs before Content is moved to Recycle Bin + /// + public static event TypedEventHandler> Trashing; + + /// + /// Occurs after Content is moved to Recycle Bin + /// + public static event TypedEventHandler> Trashed; + + /// + /// Occurs before Move + /// + public static event TypedEventHandler> Moving; + + /// + /// Occurs after Move + /// + public static event TypedEventHandler> Moved; + + /// + /// Occurs before the Recycle Bin is emptied + /// + public static event TypedEventHandler EmptyingRecycleBin; + + /// + /// Occurs after the Recycle Bin has been Emptied + /// + public static event TypedEventHandler EmptiedRecycleBin; + #endregion + } } diff --git a/src/Umbraco.Core/Services/MemberService.cs b/src/Umbraco.Core/Services/MemberService.cs index 6216bdfbe9..9991583719 100644 --- a/src/Umbraco.Core/Services/MemberService.cs +++ b/src/Umbraco.Core/Services/MemberService.cs @@ -21,1296 +21,1296 @@ using Umbraco.Core.Security; namespace Umbraco.Core.Services { - /// - /// Represents the MemberService. - /// - public class MemberService : RepositoryService, IMemberService - { - private readonly IMemberGroupService _memberGroupService; - private readonly EntityXmlSerializer _entitySerializer = new EntityXmlSerializer(); - private readonly IDataTypeService _dataTypeService; - private static readonly ReaderWriterLockSlim Locker = new ReaderWriterLockSlim(); - - public MemberService(IDatabaseUnitOfWorkProvider provider, RepositoryFactory repositoryFactory, ILogger logger, IEventMessagesFactory eventMessagesFactory, IMemberGroupService memberGroupService, IDataTypeService dataTypeService) - : base(provider, repositoryFactory, logger, eventMessagesFactory) - { - if (memberGroupService == null) throw new ArgumentNullException("memberGroupService"); - if (dataTypeService == null) throw new ArgumentNullException("dataTypeService"); - _memberGroupService = memberGroupService; - _dataTypeService = dataTypeService; - } - - #region IMemberService Implementation - - /// - /// Gets the default MemberType alias - /// - /// By default we'll return the 'writer', but we need to check it exists. If it doesn't we'll - /// return the first type that is not an admin, otherwise if there's only one we will return that one. - /// Alias of the default MemberType - public string GetDefaultMemberType() - { - using (var repository = RepositoryFactory.CreateMemberTypeRepository(UowProvider.GetUnitOfWork())) - { - var types = repository.GetAll(new int[] { }).Select(x => x.Alias).ToArray(); - - if (types.Any() == false) - { - throw new InvalidOperationException("No member types could be resolved"); - } - - if (types.InvariantContains("Member")) - { - return types.First(x => x.InvariantEquals("Member")); - } - - return types.First(); - } - } - - /// - /// Checks if a Member with the username exists - /// - /// Username to check - /// True if the Member exists otherwise False - public bool Exists(string username) - { - using (var repository = RepositoryFactory.CreateMemberRepository(UowProvider.GetUnitOfWork())) - { - return repository.Exists(username); - } - } - - /// - /// This is simply a helper method which essentially just wraps the MembershipProvider's ChangePassword method - /// - /// This method exists so that Umbraco developers can use one entry point to create/update - /// Members if they choose to. - /// The Member to save the password for - /// The password to encrypt and save - public void SavePassword(IMember member, string password) - { - if (member == null) throw new ArgumentNullException("member"); - - var provider = MembershipProviderExtensions.GetMembersMembershipProvider(); - if (provider.IsUmbracoMembershipProvider()) - { - provider.ChangePassword(member.Username, "", password); - } - else - { - throw new NotSupportedException("When using a non-Umbraco membership provider you must change the member password by using the MembershipProvider.ChangePassword method"); - } - - //go re-fetch the member and update the properties that may have changed - var result = GetByUsername(member.Username); - - //should never be null but it could have been deleted by another thread. - if (result == null) - return; - - member.RawPasswordValue = result.RawPasswordValue; - member.LastPasswordChangeDate = result.LastPasswordChangeDate; - member.UpdateDate = result.UpdateDate; - } - - /// - /// Checks if a Member with the id exists - /// - /// Id of the Member - /// True if the Member exists otherwise False - public bool Exists(int id) - { - using (var repository = RepositoryFactory.CreateMemberRepository(UowProvider.GetUnitOfWork())) - { - return repository.Exists(id); - } - } - - /// - /// Gets a Member by its integer id - /// - /// Id - /// - public IMember GetById(int id) - { - using (var repository = RepositoryFactory.CreateMemberRepository(UowProvider.GetUnitOfWork())) - { - return repository.Get(id); - } - } - - /// - /// Gets a Member by the unique key - /// - /// The guid key corresponds to the unique id in the database - /// and the user id in the membership provider. - /// Id - /// - public IMember GetByKey(Guid id) - { - using (var repository = RepositoryFactory.CreateMemberRepository(UowProvider.GetUnitOfWork())) - { - var query = Query.Builder.Where(x => x.Key == id); - var member = repository.GetByQuery(query).FirstOrDefault(); - return member; - } - } - - /// - /// Gets all Members for the specified MemberType alias - /// - /// Alias of the MemberType - /// - public IEnumerable GetMembersByMemberType(string memberTypeAlias) - { - using (var repository = RepositoryFactory.CreateMemberRepository(UowProvider.GetUnitOfWork())) - { - var query = Query.Builder.Where(x => x.ContentTypeAlias == memberTypeAlias); - var members = repository.GetByQuery(query); - return members; - } - } - - /// - /// Gets all Members for the MemberType id - /// - /// Id of the MemberType - /// - public IEnumerable GetMembersByMemberType(int memberTypeId) - { - using (var repository = RepositoryFactory.CreateMemberRepository(UowProvider.GetUnitOfWork())) - { - repository.Get(memberTypeId); - var query = Query.Builder.Where(x => x.ContentTypeId == memberTypeId); - var members = repository.GetByQuery(query); - return members; - } - } - - /// - /// Gets all Members within the specified MemberGroup name - /// - /// Name of the MemberGroup - /// - public IEnumerable GetMembersByGroup(string memberGroupName) - { - using (var repository = RepositoryFactory.CreateMemberRepository(UowProvider.GetUnitOfWork())) - { - return repository.GetByMemberGroup(memberGroupName); - } - } - - /// - /// Gets all Members with the ids specified - /// - /// If no Ids are specified all Members will be retrieved - /// Optional list of Member Ids - /// - public IEnumerable GetAllMembers(params int[] ids) - { - using (var repository = RepositoryFactory.CreateMemberRepository(UowProvider.GetUnitOfWork())) - { - return repository.GetAll(ids); - } - } - - /// - /// Delete Members of the specified MemberType id - /// - /// Id of the MemberType - public void DeleteMembersOfType(int memberTypeId) - { - using (new WriteLock(Locker)) - { - using (var uow = UowProvider.GetUnitOfWork()) - { - var repository = RepositoryFactory.CreateMemberRepository(uow); - //TODO: What about content that has the contenttype as part of its composition? - var query = Query.Builder.Where(x => x.ContentTypeId == memberTypeId); - var members = repository.GetByQuery(query).ToArray(); - - if (Deleting.IsRaisedEventCancelled(new DeleteEventArgs(members), this)) - return; - - foreach (var member in members) - { - //Permantly delete the member - Delete(member); - } - } - } - } - - [Obsolete("Use the overload with 'long' parameter types instead")] - [EditorBrowsable(EditorBrowsableState.Never)] - public IEnumerable FindMembersByDisplayName(string displayNameToMatch, int pageIndex, int pageSize, out int totalRecords, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith) - { - long total; - var result = FindMembersByDisplayName(displayNameToMatch, Convert.ToInt64(pageIndex), pageSize, out total, matchType); - totalRecords = Convert.ToInt32(total); - return result; - } - - /// - /// Finds Members based on their display name - /// - /// Display name to match - /// Current page index - /// Size of the page - /// Total number of records found (out) - /// The type of match to make as . Default is - /// - public IEnumerable FindMembersByDisplayName(string displayNameToMatch, long pageIndex, int pageSize, out long totalRecords, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith) - { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMemberRepository(uow)) - { - var query = new Query(); - - switch (matchType) - { - case StringPropertyMatchType.Exact: - query.Where(member => member.Name.Equals(displayNameToMatch)); - break; - case StringPropertyMatchType.Contains: - query.Where(member => member.Name.Contains(displayNameToMatch)); - break; - case StringPropertyMatchType.StartsWith: - query.Where(member => member.Name.StartsWith(displayNameToMatch)); - break; - case StringPropertyMatchType.EndsWith: - query.Where(member => member.Name.EndsWith(displayNameToMatch)); - break; - case StringPropertyMatchType.Wildcard: - query.Where(member => member.Name.SqlWildcard(displayNameToMatch, TextColumnType.NVarchar)); - break; - default: - throw new ArgumentOutOfRangeException("matchType"); - } - - return repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, "Name", Direction.Ascending, orderBySystemField: true); - } - } - - [Obsolete("Use the overload with 'long' parameter types instead")] - [EditorBrowsable(EditorBrowsableState.Never)] - public IEnumerable FindByEmail(string emailStringToMatch, int pageIndex, int pageSize, out int totalRecords, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith) - { - long total; - var result = FindByEmail(emailStringToMatch, Convert.ToInt64(pageIndex), pageSize, out total, matchType); - totalRecords = Convert.ToInt32(total); - return result; - } - - /// - /// Finds a list of objects by a partial email string - /// - /// Partial email string to match - /// Current page index - /// Size of the page - /// Total number of records found (out) - /// The type of match to make as . Default is - /// - public IEnumerable FindByEmail(string emailStringToMatch, long pageIndex, int pageSize, out long totalRecords, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith) - { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMemberRepository(uow)) - { - var query = new Query(); - - switch (matchType) - { - case StringPropertyMatchType.Exact: - query.Where(member => member.Email.Equals(emailStringToMatch)); - break; - case StringPropertyMatchType.Contains: - query.Where(member => member.Email.Contains(emailStringToMatch)); - break; - case StringPropertyMatchType.StartsWith: - query.Where(member => member.Email.StartsWith(emailStringToMatch)); - break; - case StringPropertyMatchType.EndsWith: - query.Where(member => member.Email.EndsWith(emailStringToMatch)); - break; - case StringPropertyMatchType.Wildcard: - query.Where(member => member.Email.SqlWildcard(emailStringToMatch, TextColumnType.NVarchar)); - break; - default: - throw new ArgumentOutOfRangeException("matchType"); - } - - return repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, "Email", Direction.Ascending, orderBySystemField: true); - } - } - - [Obsolete("Use the overload with 'long' parameter types instead")] - [EditorBrowsable(EditorBrowsableState.Never)] - public IEnumerable FindByUsername(string login, int pageIndex, int pageSize, out int totalRecords, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith) - { - long total; - var result = FindByUsername(login, Convert.ToInt64(pageIndex), pageSize, out total, matchType); - totalRecords = Convert.ToInt32(total); - return result; - } - - /// - /// Finds a list of objects by a partial username - /// - /// Partial username to match - /// Current page index - /// Size of the page - /// Total number of records found (out) - /// The type of match to make as . Default is - /// - public IEnumerable FindByUsername(string login, long pageIndex, int pageSize, out long totalRecords, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith) - { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMemberRepository(uow)) - { - var query = new Query(); - - switch (matchType) - { - case StringPropertyMatchType.Exact: - query.Where(member => member.Username.Equals(login)); - break; - case StringPropertyMatchType.Contains: - query.Where(member => member.Username.Contains(login)); - break; - case StringPropertyMatchType.StartsWith: - query.Where(member => member.Username.StartsWith(login)); - break; - case StringPropertyMatchType.EndsWith: - query.Where(member => member.Username.EndsWith(login)); - break; - case StringPropertyMatchType.Wildcard: - query.Where(member => member.Email.SqlWildcard(login, TextColumnType.NVarchar)); - break; - default: - throw new ArgumentOutOfRangeException("matchType"); - } - - return repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, "LoginName", Direction.Ascending, orderBySystemField: true); - } - } - - /// - /// Gets a list of Members based on a property search - /// - /// Alias of the PropertyType to search for - /// Value to match - /// The type of match to make as . Default is - /// - public IEnumerable GetMembersByPropertyValue(string propertyTypeAlias, string value, StringPropertyMatchType matchType = StringPropertyMatchType.Exact) - { - using (var repository = RepositoryFactory.CreateMemberRepository(UowProvider.GetUnitOfWork())) - { - IQuery query; - - switch (matchType) - { - case StringPropertyMatchType.Exact: - query = - Query.Builder.Where( - x => - ((Member)x).PropertyTypeAlias == propertyTypeAlias && - (((Member)x).LongStringPropertyValue.SqlEquals(value, TextColumnType.NText) || - ((Member)x).ShortStringPropertyValue.SqlEquals(value, TextColumnType.NVarchar))); - break; - case StringPropertyMatchType.Contains: - query = - Query.Builder.Where( - x => - ((Member)x).PropertyTypeAlias == propertyTypeAlias && - (((Member)x).LongStringPropertyValue.SqlContains(value, TextColumnType.NText) || - ((Member)x).ShortStringPropertyValue.SqlContains(value, TextColumnType.NVarchar))); - break; - case StringPropertyMatchType.StartsWith: - query = - Query.Builder.Where( - x => - ((Member)x).PropertyTypeAlias == propertyTypeAlias && - (((Member)x).LongStringPropertyValue.SqlStartsWith(value, TextColumnType.NText) || - ((Member)x).ShortStringPropertyValue.SqlStartsWith(value, TextColumnType.NVarchar))); - break; - case StringPropertyMatchType.EndsWith: - query = - Query.Builder.Where( - x => - ((Member)x).PropertyTypeAlias == propertyTypeAlias && - (((Member)x).LongStringPropertyValue.SqlEndsWith(value, TextColumnType.NText) || - ((Member)x).ShortStringPropertyValue.SqlEndsWith(value, TextColumnType.NVarchar))); - break; - default: - throw new ArgumentOutOfRangeException("matchType"); - } - - var members = repository.GetByQuery(query); - return members; - } - } - - /// - /// Gets a list of Members based on a property search - /// - /// Alias of the PropertyType to search for - /// Value to match - /// The type of match to make as . Default is - /// - public IEnumerable GetMembersByPropertyValue(string propertyTypeAlias, int value, ValuePropertyMatchType matchType = ValuePropertyMatchType.Exact) - { - using (var repository = RepositoryFactory.CreateMemberRepository(UowProvider.GetUnitOfWork())) - { - IQuery query; - - switch (matchType) - { - case ValuePropertyMatchType.Exact: - query = - Query.Builder.Where( - x => - ((Member)x).PropertyTypeAlias == propertyTypeAlias && - ((Member)x).IntegerPropertyValue == value); - break; - case ValuePropertyMatchType.GreaterThan: - query = - Query.Builder.Where( - x => - ((Member)x).PropertyTypeAlias == propertyTypeAlias && - ((Member)x).IntegerPropertyValue > value); - break; - case ValuePropertyMatchType.LessThan: - query = - Query.Builder.Where( - x => - ((Member)x).PropertyTypeAlias == propertyTypeAlias && - ((Member)x).IntegerPropertyValue < value); - break; - case ValuePropertyMatchType.GreaterThanOrEqualTo: - query = - Query.Builder.Where( - x => - ((Member)x).PropertyTypeAlias == propertyTypeAlias && - ((Member)x).IntegerPropertyValue >= value); - break; - case ValuePropertyMatchType.LessThanOrEqualTo: - query = - Query.Builder.Where( - x => - ((Member)x).PropertyTypeAlias == propertyTypeAlias && - ((Member)x).IntegerPropertyValue <= value); - break; - default: - throw new ArgumentOutOfRangeException("matchType"); - } - - var members = repository.GetByQuery(query); - return members; - } - } - - /// - /// Gets a list of Members based on a property search - /// - /// Alias of the PropertyType to search for - /// Value to match - /// - public IEnumerable GetMembersByPropertyValue(string propertyTypeAlias, bool value) - { - using (var repository = RepositoryFactory.CreateMemberRepository(UowProvider.GetUnitOfWork())) - { - var query = - Query.Builder.Where( - x => - ((Member)x).PropertyTypeAlias == propertyTypeAlias && - ((Member)x).BoolPropertyValue == value); - - var members = repository.GetByQuery(query); - return members; - } - } - - /// - /// Gets a list of Members based on a property search - /// - /// Alias of the PropertyType to search for - /// Value to match - /// The type of match to make as . Default is - /// - public IEnumerable GetMembersByPropertyValue(string propertyTypeAlias, DateTime value, ValuePropertyMatchType matchType = ValuePropertyMatchType.Exact) - { - using (var repository = RepositoryFactory.CreateMemberRepository(UowProvider.GetUnitOfWork())) - { - IQuery query; - - switch (matchType) - { - case ValuePropertyMatchType.Exact: - query = - Query.Builder.Where( - x => - ((Member)x).PropertyTypeAlias == propertyTypeAlias && - ((Member)x).DateTimePropertyValue == value); - break; - case ValuePropertyMatchType.GreaterThan: - query = - Query.Builder.Where( - x => - ((Member)x).PropertyTypeAlias == propertyTypeAlias && - ((Member)x).DateTimePropertyValue > value); - break; - case ValuePropertyMatchType.LessThan: - query = - Query.Builder.Where( - x => - ((Member)x).PropertyTypeAlias == propertyTypeAlias && - ((Member)x).DateTimePropertyValue < value); - break; - case ValuePropertyMatchType.GreaterThanOrEqualTo: - query = - Query.Builder.Where( - x => - ((Member)x).PropertyTypeAlias == propertyTypeAlias && - ((Member)x).DateTimePropertyValue >= value); - break; - case ValuePropertyMatchType.LessThanOrEqualTo: - query = - Query.Builder.Where( - x => - ((Member)x).PropertyTypeAlias == propertyTypeAlias && - ((Member)x).DateTimePropertyValue <= value); - break; - default: - throw new ArgumentOutOfRangeException("matchType"); - } - - //TODO: Since this is by property value, we need a GetByPropertyQuery on the repo! - var members = repository.GetByQuery(query); - return members; - } - } - - /// - /// Rebuilds all xml content in the cmsContentXml table for all members - /// - /// - /// Only rebuild the xml structures for the content type ids passed in, if none then rebuilds the structures - /// for all members = USE WITH CARE! - /// - /// True if publishing succeeded, otherwise False - public void RebuildXmlStructures(params int[] memberTypeIds) - { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMemberRepository(uow)) - { - repository.RebuildXmlStructures( - member => _entitySerializer.Serialize(_dataTypeService, member), - contentTypeIds: memberTypeIds.Length == 0 ? null : memberTypeIds); - } - - Audit(AuditType.Publish, "MemberService.RebuildXmlStructures completed, the xml has been regenerated in the database", 0, -1); - } - - #endregion - - #region IMembershipMemberService Implementation - - /// - /// Gets the total number of Members based on the count type - /// - /// - /// The way the Online count is done is the same way that it is done in the MS SqlMembershipProvider - We query for any members - /// that have their last active date within the Membership.UserIsOnlineTimeWindow (which is in minutes). It isn't exact science - /// but that is how MS have made theirs so we'll follow that principal. - /// - /// to count by - /// with number of Members for passed in type - public int GetCount(MemberCountType countType) - { - using (var repository = RepositoryFactory.CreateMemberRepository(UowProvider.GetUnitOfWork())) - { - IQuery query; - - switch (countType) - { - case MemberCountType.All: - query = new Query(); - return repository.Count(query); - case MemberCountType.Online: - var fromDate = DateTime.Now.AddMinutes(-Membership.UserIsOnlineTimeWindow); - query = - Query.Builder.Where( - x => - ((Member)x).PropertyTypeAlias == Constants.Conventions.Member.LastLoginDate && - ((Member)x).DateTimePropertyValue > fromDate); - return repository.GetCountByQuery(query); - case MemberCountType.LockedOut: - query = - Query.Builder.Where( - x => - ((Member)x).PropertyTypeAlias == Constants.Conventions.Member.IsLockedOut && - ((Member)x).BoolPropertyValue == true); - return repository.GetCountByQuery(query); - case MemberCountType.Approved: - query = - Query.Builder.Where( - x => - ((Member)x).PropertyTypeAlias == Constants.Conventions.Member.IsApproved && - ((Member)x).BoolPropertyValue == true); - return repository.GetCountByQuery(query); - default: - throw new ArgumentOutOfRangeException("countType"); - } - } - - } - - [Obsolete("Use the overload with 'long' parameter types instead")] - [EditorBrowsable(EditorBrowsableState.Never)] - public IEnumerable GetAll(int pageIndex, int pageSize, out int totalRecords) - { - long total; - var result = GetAll(Convert.ToInt64(pageIndex), pageSize, out total); - totalRecords = Convert.ToInt32(total); - return result; - } - - /// - /// Gets a list of paged objects - /// - /// Current page index - /// Size of the page - /// Total number of records found (out) - /// - public IEnumerable GetAll(long pageIndex, int pageSize, out long totalRecords) - { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMemberRepository(uow)) - { - return repository.GetPagedResultsByQuery(null, pageIndex, pageSize, out totalRecords, "LoginName", Direction.Ascending, orderBySystemField: true); - } - } - - [Obsolete("Use the overload with 'long' parameter types instead")] - [EditorBrowsable(EditorBrowsableState.Never)] - public IEnumerable GetAll(int pageIndex, int pageSize, out int totalRecords, - string orderBy, Direction orderDirection, bool orderBySystemField = true, string memberTypeAlias = null, string filter = "") - { - long total; - var result = GetAll(Convert.ToInt64(pageIndex), pageSize, out total, orderBy, orderDirection, orderBySystemField, memberTypeAlias, filter); - totalRecords = Convert.ToInt32(total); - return result; - } - - public IEnumerable GetAll(long pageIndex, int pageSize, out long totalRecords, - string orderBy, Direction orderDirection, bool orderBySystemField = true, string memberTypeAlias = null, string filter = "") - { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMemberRepository(uow)) - { - if (memberTypeAlias == null) - { - return repository.GetPagedResultsByQuery(null, pageIndex, pageSize, out totalRecords, orderBy, orderDirection, orderBySystemField, filter); - } - var query = new Query().Where(x => x.ContentTypeAlias == memberTypeAlias); - return repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, orderBy, orderDirection, orderBySystemField, filter); - } - } - - /// - /// Gets the count of Members by an optional MemberType alias - /// - /// If no alias is supplied then the count for all Member will be returned - /// Optional alias for the MemberType when counting number of Members - /// with number of Members - public int Count(string memberTypeAlias = null) - { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMemberRepository(uow)) - { - return repository.Count(memberTypeAlias); - } - } - - /// - /// Creates an object without persisting it - /// - /// This method is convenient for when you need to add properties to a new Member - /// before persisting it in order to limit the amount of times its saved. - /// Also note that the returned will not have an Id until its saved. - /// Username of the Member to create - /// Email of the Member to create - /// Name of the Member to create - /// Alias of the MemberType the Member should be based on - /// - public IMember CreateMember(string username, string email, string name, string memberTypeAlias) - { - var memberType = FindMemberTypeByAlias(memberTypeAlias); - return CreateMember(username, email, name, memberType); - } - - /// - /// Creates an object without persisting it - /// - /// This method is convenient for when you need to add properties to a new Member - /// before persisting it in order to limit the amount of times its saved. - /// Also note that the returned will not have an Id until its saved. - /// Username of the Member to create - /// Email of the Member to create - /// Name of the Member to create - /// MemberType the Member should be based on - /// - public IMember CreateMember(string username, string email, string name, IMemberType memberType) - { - var member = new Member(name, email.ToLower().Trim(), username, memberType); - - Created.RaiseEvent(new NewEventArgs(member, false, memberType.Alias, -1), this); - - return member; - } - - /// - /// Creates and persists a Member - /// - /// Using this method will persist the Member object before its returned - /// meaning that it will have an Id available (unlike the CreateMember method) - /// Username of the Member to create - /// Email of the Member to create - /// Name of the Member to create - /// Alias of the MemberType the Member should be based on - /// - public IMember CreateMemberWithIdentity(string username, string email, string name, string memberTypeAlias) - { - var memberType = FindMemberTypeByAlias(memberTypeAlias); - return CreateMemberWithIdentity(username, email, name, memberType); - } - - /// - /// Creates and persists a Member - /// - /// Using this method will persist the Member object before its returned - /// meaning that it will have an Id available (unlike the CreateMember method) - /// Username of the Member to create - /// Email of the Member to create - /// MemberType the Member should be based on - /// - public IMember CreateMemberWithIdentity(string username, string email, IMemberType memberType) - { - return CreateMemberWithIdentity(username, email, username, memberType); - } - - /// - /// Creates and persists a Member - /// - /// Using this method will persist the Member object before its returned - /// meaning that it will have an Id available (unlike the CreateMember method) - /// Username of the Member to create - /// Email of the Member to create - /// Name of the Member to create - /// MemberType the Member should be based on - /// - public IMember CreateMemberWithIdentity(string username, string email, string name, IMemberType memberType) - { - return CreateMemberWithIdentity(username, email, name, "", memberType); - } - - /// - /// Creates and persists a new - /// - /// An can be of type or - /// Username of the to create - /// Email of the to create - /// This value should be the encoded/encrypted/hashed value for the password that will be stored in the database - /// Alias of the Type - /// - IMember IMembershipMemberService.CreateWithIdentity(string username, string email, string passwordValue, string memberTypeAlias) - { - var memberType = FindMemberTypeByAlias(memberTypeAlias); - return CreateMemberWithIdentity(username, email, username, passwordValue, memberType); - } - - /// - /// Creates and persists a Member - /// - /// Using this method will persist the Member object before its returned - /// meaning that it will have an Id available (unlike the CreateMember method) - /// Username of the Member to create - /// Email of the Member to create - /// Name of the Member to create - /// This value should be the encoded/encrypted/hashed value for the password that will be stored in the database - /// MemberType the Member should be based on - /// - private IMember CreateMemberWithIdentity(string username, string email, string name, string passwordValue, IMemberType memberType) - { - if (memberType == null) throw new ArgumentNullException("memberType"); - - var member = new Member(name, email.ToLower().Trim(), username, passwordValue, memberType); - - if (Saving.IsRaisedEventCancelled(new SaveEventArgs(member), this)) - { - member.WasCancelled = true; - return member; - } - - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMemberRepository(uow)) - { - repository.AddOrUpdate(member); - //insert the xml - repository.AddOrUpdateContentXml(member, m => _entitySerializer.Serialize(_dataTypeService, m)); - // generate preview for blame history? - if (UmbracoConfig.For.UmbracoSettings().Content.GlobalPreviewStorageEnabled) - { - repository.AddOrUpdatePreviewXml(member, m => _entitySerializer.Serialize(_dataTypeService, m)); - } - - uow.Commit(); - } - - Saved.RaiseEvent(new SaveEventArgs(member, false), this); - Created.RaiseEvent(new NewEventArgs(member, false, memberType.Alias, -1), this); - - return member; - } - - /// - /// Gets an by its provider key - /// - /// Id to use for retrieval - /// - public IMember GetByProviderKey(object id) - { - var asGuid = id.TryConvertTo(); - if (asGuid.Success) - { - return GetByKey((Guid)id); - } - var asInt = id.TryConvertTo(); - if (asInt.Success) - { - return GetById((int)id); - } - - return null; - } - - /// - /// Get an by email - /// - /// Email to use for retrieval - /// - public IMember GetByEmail(string email) - { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMemberRepository(uow)) - { - var query = Query.Builder.Where(x => x.Email.Equals(email)); - var member = repository.GetByQuery(query).FirstOrDefault(); - - return member; - } - } - - /// - /// Get an by username - /// - /// Username to use for retrieval - /// - public IMember GetByUsername(string username) - { - //TODO: Somewhere in here, whether at this level or the repository level, we need to add - // a caching mechanism since this method is used by all the membership providers and could be - // called quite a bit when dealing with members. - - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMemberRepository(uow)) - { - var query = Query.Builder.Where(x => x.Username.Equals(username)); - var member = repository.GetByQuery(query).FirstOrDefault(); - - return member; - } - } - - /// - /// Deletes an - /// - /// to Delete - public void Delete(IMember member) - { - if (Deleting.IsRaisedEventCancelled(new DeleteEventArgs(member), this)) - return; - - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMemberRepository(uow)) - { - repository.Delete(member); - uow.Commit(); - - var args = new DeleteEventArgs(member, false); - Deleted.RaiseEvent(args, this); - - //remove any flagged media files - repository.DeleteMediaFiles(args.MediaFilesToDelete); - } - } - - /// - /// Saves an - /// - /// to Save - /// Optional parameter to raise events. - /// Default is True otherwise set to False to not raise events - public void Save(IMember entity, bool raiseEvents = true) - { - if (raiseEvents) - { - if (Saving.IsRaisedEventCancelled(new SaveEventArgs(entity), this)) - { - return; - } - } - - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMemberRepository(uow)) - { - repository.AddOrUpdate(entity); - repository.AddOrUpdateContentXml(entity, m => _entitySerializer.Serialize(_dataTypeService, m)); - // generate preview for blame history? - if (UmbracoConfig.For.UmbracoSettings().Content.GlobalPreviewStorageEnabled) - { - repository.AddOrUpdatePreviewXml(entity, m => _entitySerializer.Serialize(_dataTypeService, m)); - } - - uow.Commit(); - } - - if (raiseEvents) - Saved.RaiseEvent(new SaveEventArgs(entity, false), this); - } - - /// - /// Saves a list of objects - /// - /// to save - /// Optional parameter to raise events. - /// Default is True otherwise set to False to not raise events - public void Save(IEnumerable entities, bool raiseEvents = true) - { - var asArray = entities.ToArray(); - - if (raiseEvents) - { - if (Saving.IsRaisedEventCancelled(new SaveEventArgs(asArray), this)) - return; - } - using (new WriteLock(Locker)) - { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMemberRepository(uow)) - { - foreach (var member in asArray) - { - repository.AddOrUpdate(member); - repository.AddOrUpdateContentXml(member, m => _entitySerializer.Serialize(_dataTypeService, m)); - // generate preview for blame history? - if (UmbracoConfig.For.UmbracoSettings().Content.GlobalPreviewStorageEnabled) - { - repository.AddOrUpdatePreviewXml(member, m => _entitySerializer.Serialize(_dataTypeService, m)); - } - } - - //commit the whole lot in one go - uow.Commit(); - } - - if (raiseEvents) - Saved.RaiseEvent(new SaveEventArgs(asArray, false), this); - } - } - - #endregion - - #region IMembershipRoleService Implementation - - public void AddRole(string roleName) - { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMemberGroupRepository(uow)) - { - repository.CreateIfNotExists(roleName); - } - } - - public IEnumerable GetAllRoles() - { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMemberGroupRepository(uow)) - { - var result = repository.GetAll(); - return result.Select(x => x.Name).Distinct(); - } - } - - public IEnumerable GetAllRoles(int memberId) - { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMemberGroupRepository(uow)) - { - var result = repository.GetMemberGroupsForMember(memberId); - return result.Select(x => x.Name).Distinct(); - } - } - - public IEnumerable GetAllRoles(string username) - { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMemberGroupRepository(uow)) - { - var result = repository.GetMemberGroupsForMember(username); - return result.Select(x => x.Name).Distinct(); - } - } - - public IEnumerable GetMembersInRole(string roleName) - { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMemberRepository(uow)) - { - return repository.GetByMemberGroup(roleName); - } - } - - public IEnumerable FindMembersInRole(string roleName, string usernameToMatch, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith) - { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMemberRepository(uow)) - { - return repository.FindMembersInRole(roleName, usernameToMatch, matchType); - } - } - - public bool DeleteRole(string roleName, bool throwIfBeingUsed) - { - using (new WriteLock(Locker)) - { - if (throwIfBeingUsed) - { - var inRole = GetMembersInRole(roleName); - if (inRole.Any()) - { - throw new InvalidOperationException("The role " + roleName + " is currently assigned to members"); - } - } - - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMemberGroupRepository(uow)) - { - var qry = new Query().Where(g => g.Name == roleName); - var found = repository.GetByQuery(qry).ToArray(); - - foreach (var memberGroup in found) - { - _memberGroupService.Delete(memberGroup); - } - return found.Any(); - } - } - } - public void AssignRole(string username, string roleName) - { - AssignRoles(new[] { username }, new[] { roleName }); - } - - public void AssignRoles(string[] usernames, string[] roleNames) - { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMemberGroupRepository(uow)) - { - repository.AssignRoles(usernames, roleNames); - } - } - - public void DissociateRole(string username, string roleName) - { - DissociateRoles(new[] { username }, new[] { roleName }); - } - - public void DissociateRoles(string[] usernames, string[] roleNames) - { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMemberGroupRepository(uow)) - { - repository.DissociateRoles(usernames, roleNames); - } - } - - public void AssignRole(int memberId, string roleName) - { - AssignRoles(new[] { memberId }, new[] { roleName }); - } - - public void AssignRoles(int[] memberIds, string[] roleNames) - { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMemberGroupRepository(uow)) - { - repository.AssignRoles(memberIds, roleNames); - } - } - - public void DissociateRole(int memberId, string roleName) - { - DissociateRoles(new[] { memberId }, new[] { roleName }); - } - - public void DissociateRoles(int[] memberIds, string[] roleNames) - { - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMemberGroupRepository(uow)) - { - repository.DissociateRoles(memberIds, roleNames); - } - } - - - - #endregion - - private IMemberType FindMemberTypeByAlias(string memberTypeAlias) - { - using (var repository = RepositoryFactory.CreateMemberTypeRepository(UowProvider.GetUnitOfWork())) - { - var query = Query.Builder.Where(x => x.Alias == memberTypeAlias); - var types = repository.GetByQuery(query); - - if (types.Any() == false) - throw new Exception( - string.Format("No MemberType matching the passed in Alias: '{0}' was found", - memberTypeAlias)); - - var contentType = types.First(); - - if (contentType == null) - throw new Exception(string.Format("MemberType matching the passed in Alias: '{0}' was null", - memberTypeAlias)); - - return contentType; - } - } - - private void Audit(AuditType type, string message, int userId, int objectId) - { - var uow = UowProvider.GetUnitOfWork(); - using (var auditRepo = RepositoryFactory.CreateAuditRepository(uow)) - { - auditRepo.AddOrUpdate(new AuditItem(objectId, message, type, userId)); - uow.Commit(); - } - } - - #region Event Handlers - - /// - /// Occurs before Delete - /// - public static event TypedEventHandler> Deleting; - - /// - /// Occurs after Delete - /// - public static event TypedEventHandler> Deleted; - - /// - /// Occurs before Save - /// - public static event TypedEventHandler> Saving; - - /// - /// Occurs after Create - /// - /// - /// Please note that the Member object has been created, but might not have been saved - /// so it does not have an identity yet (meaning no Id has been set). - /// - public static event TypedEventHandler> Created; - - /// - /// Occurs after Save - /// - public static event TypedEventHandler> Saved; - - #endregion - - /// - /// A helper method that will create a basic/generic member for use with a generic membership provider - /// - /// - internal static IMember CreateGenericMembershipProviderMember(string name, string email, string username, string password) - { - var identity = int.MaxValue; - - var memType = new MemberType(-1); - var propGroup = new PropertyGroup - { - Name = "Membership", - Id = --identity - }; - propGroup.PropertyTypes.Add(new PropertyType(Constants.PropertyEditors.TextboxAlias, DataTypeDatabaseType.Ntext, Constants.Conventions.Member.Comments) - { - Name = Constants.Conventions.Member.CommentsLabel, - SortOrder = 0, - Id = --identity, - Key = identity.ToGuid() - }); - propGroup.PropertyTypes.Add(new PropertyType(Constants.PropertyEditors.TrueFalseAlias, DataTypeDatabaseType.Integer, Constants.Conventions.Member.IsApproved) - { - Name = Constants.Conventions.Member.IsApprovedLabel, - SortOrder = 3, - Id = --identity, - Key = identity.ToGuid() - }); - propGroup.PropertyTypes.Add(new PropertyType(Constants.PropertyEditors.TrueFalseAlias, DataTypeDatabaseType.Integer, Constants.Conventions.Member.IsLockedOut) - { - Name = Constants.Conventions.Member.IsLockedOutLabel, - SortOrder = 4, - Id = --identity, - Key = identity.ToGuid() - }); - propGroup.PropertyTypes.Add(new PropertyType(Constants.PropertyEditors.NoEditAlias, DataTypeDatabaseType.Date, Constants.Conventions.Member.LastLockoutDate) - { - Name = Constants.Conventions.Member.LastLockoutDateLabel, - SortOrder = 5, - Id = --identity, - Key = identity.ToGuid() - }); - propGroup.PropertyTypes.Add(new PropertyType(Constants.PropertyEditors.NoEditAlias, DataTypeDatabaseType.Date, Constants.Conventions.Member.LastLoginDate) - { - Name = Constants.Conventions.Member.LastLoginDateLabel, - SortOrder = 6, - Id = --identity, - Key = identity.ToGuid() - }); - propGroup.PropertyTypes.Add(new PropertyType(Constants.PropertyEditors.NoEditAlias, DataTypeDatabaseType.Date, Constants.Conventions.Member.LastPasswordChangeDate) - { - Name = Constants.Conventions.Member.LastPasswordChangeDateLabel, - SortOrder = 7, - Id = --identity, - Key = identity.ToGuid() - }); - - memType.PropertyGroups.Add(propGroup); - - var member = new Member(name, email, username, password, memType); - - //we've assigned ids to the property types and groups but we also need to assign fake ids to the properties themselves. - foreach (var property in member.Properties) - { - property.Id = --identity; - } - - return member; - } - } + /// + /// Represents the MemberService. + /// + public class MemberService : RepositoryService, IMemberService + { + private readonly IMemberGroupService _memberGroupService; + private readonly EntityXmlSerializer _entitySerializer = new EntityXmlSerializer(); + private readonly IDataTypeService _dataTypeService; + private static readonly ReaderWriterLockSlim Locker = new ReaderWriterLockSlim(); + + public MemberService(IDatabaseUnitOfWorkProvider provider, RepositoryFactory repositoryFactory, ILogger logger, IEventMessagesFactory eventMessagesFactory, IMemberGroupService memberGroupService, IDataTypeService dataTypeService) + : base(provider, repositoryFactory, logger, eventMessagesFactory) + { + if (memberGroupService == null) throw new ArgumentNullException("memberGroupService"); + if (dataTypeService == null) throw new ArgumentNullException("dataTypeService"); + _memberGroupService = memberGroupService; + _dataTypeService = dataTypeService; + } + + #region IMemberService Implementation + + /// + /// Gets the default MemberType alias + /// + /// By default we'll return the 'writer', but we need to check it exists. If it doesn't we'll + /// return the first type that is not an admin, otherwise if there's only one we will return that one. + /// Alias of the default MemberType + public string GetDefaultMemberType() + { + using (var repository = RepositoryFactory.CreateMemberTypeRepository(UowProvider.GetUnitOfWork())) + { + var types = repository.GetAll(new int[] { }).Select(x => x.Alias).ToArray(); + + if (types.Any() == false) + { + throw new InvalidOperationException("No member types could be resolved"); + } + + if (types.InvariantContains("Member")) + { + return types.First(x => x.InvariantEquals("Member")); + } + + return types.First(); + } + } + + /// + /// Checks if a Member with the username exists + /// + /// Username to check + /// True if the Member exists otherwise False + public bool Exists(string username) + { + using (var repository = RepositoryFactory.CreateMemberRepository(UowProvider.GetUnitOfWork())) + { + return repository.Exists(username); + } + } + + /// + /// This is simply a helper method which essentially just wraps the MembershipProvider's ChangePassword method + /// + /// This method exists so that Umbraco developers can use one entry point to create/update + /// Members if they choose to. + /// The Member to save the password for + /// The password to encrypt and save + public void SavePassword(IMember member, string password) + { + if (member == null) throw new ArgumentNullException("member"); + + var provider = MembershipProviderExtensions.GetMembersMembershipProvider(); + if (provider.IsUmbracoMembershipProvider()) + { + provider.ChangePassword(member.Username, "", password); + } + else + { + throw new NotSupportedException("When using a non-Umbraco membership provider you must change the member password by using the MembershipProvider.ChangePassword method"); + } + + //go re-fetch the member and update the properties that may have changed + var result = GetByUsername(member.Username); + + //should never be null but it could have been deleted by another thread. + if (result == null) + return; + + member.RawPasswordValue = result.RawPasswordValue; + member.LastPasswordChangeDate = result.LastPasswordChangeDate; + member.UpdateDate = result.UpdateDate; + } + + /// + /// Checks if a Member with the id exists + /// + /// Id of the Member + /// True if the Member exists otherwise False + public bool Exists(int id) + { + using (var repository = RepositoryFactory.CreateMemberRepository(UowProvider.GetUnitOfWork())) + { + return repository.Exists(id); + } + } + + /// + /// Gets a Member by its integer id + /// + /// Id + /// + public IMember GetById(int id) + { + using (var repository = RepositoryFactory.CreateMemberRepository(UowProvider.GetUnitOfWork())) + { + return repository.Get(id); + } + } + + /// + /// Gets a Member by the unique key + /// + /// The guid key corresponds to the unique id in the database + /// and the user id in the membership provider. + /// Id + /// + public IMember GetByKey(Guid id) + { + using (var repository = RepositoryFactory.CreateMemberRepository(UowProvider.GetUnitOfWork())) + { + var query = Query.Builder.Where(x => x.Key == id); + var member = repository.GetByQuery(query).FirstOrDefault(); + return member; + } + } + + /// + /// Gets all Members for the specified MemberType alias + /// + /// Alias of the MemberType + /// + public IEnumerable GetMembersByMemberType(string memberTypeAlias) + { + using (var repository = RepositoryFactory.CreateMemberRepository(UowProvider.GetUnitOfWork())) + { + var query = Query.Builder.Where(x => x.ContentTypeAlias == memberTypeAlias); + var members = repository.GetByQuery(query); + return members; + } + } + + /// + /// Gets all Members for the MemberType id + /// + /// Id of the MemberType + /// + public IEnumerable GetMembersByMemberType(int memberTypeId) + { + using (var repository = RepositoryFactory.CreateMemberRepository(UowProvider.GetUnitOfWork())) + { + repository.Get(memberTypeId); + var query = Query.Builder.Where(x => x.ContentTypeId == memberTypeId); + var members = repository.GetByQuery(query); + return members; + } + } + + /// + /// Gets all Members within the specified MemberGroup name + /// + /// Name of the MemberGroup + /// + public IEnumerable GetMembersByGroup(string memberGroupName) + { + using (var repository = RepositoryFactory.CreateMemberRepository(UowProvider.GetUnitOfWork())) + { + return repository.GetByMemberGroup(memberGroupName); + } + } + + /// + /// Gets all Members with the ids specified + /// + /// If no Ids are specified all Members will be retrieved + /// Optional list of Member Ids + /// + public IEnumerable GetAllMembers(params int[] ids) + { + using (var repository = RepositoryFactory.CreateMemberRepository(UowProvider.GetUnitOfWork())) + { + return repository.GetAll(ids); + } + } + + /// + /// Delete Members of the specified MemberType id + /// + /// Id of the MemberType + public void DeleteMembersOfType(int memberTypeId) + { + using (new WriteLock(Locker)) + { + using (var uow = UowProvider.GetUnitOfWork()) + { + var repository = RepositoryFactory.CreateMemberRepository(uow); + //TODO: What about content that has the contenttype as part of its composition? + var query = Query.Builder.Where(x => x.ContentTypeId == memberTypeId); + var members = repository.GetByQuery(query).ToArray(); + + if (Deleting.IsRaisedEventCancelled(new DeleteEventArgs(members), this)) + return; + + foreach (var member in members) + { + //Permantly delete the member + Delete(member); + } + } + } + } + + [Obsolete("Use the overload with 'long' parameter types instead")] + [EditorBrowsable(EditorBrowsableState.Never)] + public IEnumerable FindMembersByDisplayName(string displayNameToMatch, int pageIndex, int pageSize, out int totalRecords, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith) + { + long total; + var result = FindMembersByDisplayName(displayNameToMatch, Convert.ToInt64(pageIndex), pageSize, out total, matchType); + totalRecords = Convert.ToInt32(total); + return result; + } + + /// + /// Finds Members based on their display name + /// + /// Display name to match + /// Current page index + /// Size of the page + /// Total number of records found (out) + /// The type of match to make as . Default is + /// + public IEnumerable FindMembersByDisplayName(string displayNameToMatch, long pageIndex, int pageSize, out long totalRecords, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMemberRepository(uow)) + { + var query = new Query(); + + switch (matchType) + { + case StringPropertyMatchType.Exact: + query.Where(member => member.Name.Equals(displayNameToMatch)); + break; + case StringPropertyMatchType.Contains: + query.Where(member => member.Name.Contains(displayNameToMatch)); + break; + case StringPropertyMatchType.StartsWith: + query.Where(member => member.Name.StartsWith(displayNameToMatch)); + break; + case StringPropertyMatchType.EndsWith: + query.Where(member => member.Name.EndsWith(displayNameToMatch)); + break; + case StringPropertyMatchType.Wildcard: + query.Where(member => member.Name.SqlWildcard(displayNameToMatch, TextColumnType.NVarchar)); + break; + default: + throw new ArgumentOutOfRangeException("matchType"); + } + + return repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, "Name", Direction.Ascending, orderBySystemField: true); + } + } + + [Obsolete("Use the overload with 'long' parameter types instead")] + [EditorBrowsable(EditorBrowsableState.Never)] + public IEnumerable FindByEmail(string emailStringToMatch, int pageIndex, int pageSize, out int totalRecords, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith) + { + long total; + var result = FindByEmail(emailStringToMatch, Convert.ToInt64(pageIndex), pageSize, out total, matchType); + totalRecords = Convert.ToInt32(total); + return result; + } + + /// + /// Finds a list of objects by a partial email string + /// + /// Partial email string to match + /// Current page index + /// Size of the page + /// Total number of records found (out) + /// The type of match to make as . Default is + /// + public IEnumerable FindByEmail(string emailStringToMatch, long pageIndex, int pageSize, out long totalRecords, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMemberRepository(uow)) + { + var query = new Query(); + + switch (matchType) + { + case StringPropertyMatchType.Exact: + query.Where(member => member.Email.Equals(emailStringToMatch)); + break; + case StringPropertyMatchType.Contains: + query.Where(member => member.Email.Contains(emailStringToMatch)); + break; + case StringPropertyMatchType.StartsWith: + query.Where(member => member.Email.StartsWith(emailStringToMatch)); + break; + case StringPropertyMatchType.EndsWith: + query.Where(member => member.Email.EndsWith(emailStringToMatch)); + break; + case StringPropertyMatchType.Wildcard: + query.Where(member => member.Email.SqlWildcard(emailStringToMatch, TextColumnType.NVarchar)); + break; + default: + throw new ArgumentOutOfRangeException("matchType"); + } + + return repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, "Email", Direction.Ascending, orderBySystemField: true); + } + } + + [Obsolete("Use the overload with 'long' parameter types instead")] + [EditorBrowsable(EditorBrowsableState.Never)] + public IEnumerable FindByUsername(string login, int pageIndex, int pageSize, out int totalRecords, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith) + { + long total; + var result = FindByUsername(login, Convert.ToInt64(pageIndex), pageSize, out total, matchType); + totalRecords = Convert.ToInt32(total); + return result; + } + + /// + /// Finds a list of objects by a partial username + /// + /// Partial username to match + /// Current page index + /// Size of the page + /// Total number of records found (out) + /// The type of match to make as . Default is + /// + public IEnumerable FindByUsername(string login, long pageIndex, int pageSize, out long totalRecords, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMemberRepository(uow)) + { + var query = new Query(); + + switch (matchType) + { + case StringPropertyMatchType.Exact: + query.Where(member => member.Username.Equals(login)); + break; + case StringPropertyMatchType.Contains: + query.Where(member => member.Username.Contains(login)); + break; + case StringPropertyMatchType.StartsWith: + query.Where(member => member.Username.StartsWith(login)); + break; + case StringPropertyMatchType.EndsWith: + query.Where(member => member.Username.EndsWith(login)); + break; + case StringPropertyMatchType.Wildcard: + query.Where(member => member.Email.SqlWildcard(login, TextColumnType.NVarchar)); + break; + default: + throw new ArgumentOutOfRangeException("matchType"); + } + + return repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, "LoginName", Direction.Ascending, orderBySystemField: true); + } + } + + /// + /// Gets a list of Members based on a property search + /// + /// Alias of the PropertyType to search for + /// Value to match + /// The type of match to make as . Default is + /// + public IEnumerable GetMembersByPropertyValue(string propertyTypeAlias, string value, StringPropertyMatchType matchType = StringPropertyMatchType.Exact) + { + using (var repository = RepositoryFactory.CreateMemberRepository(UowProvider.GetUnitOfWork())) + { + IQuery query; + + switch (matchType) + { + case StringPropertyMatchType.Exact: + query = + Query.Builder.Where( + x => + ((Member)x).PropertyTypeAlias == propertyTypeAlias && + (((Member)x).LongStringPropertyValue.SqlEquals(value, TextColumnType.NText) || + ((Member)x).ShortStringPropertyValue.SqlEquals(value, TextColumnType.NVarchar))); + break; + case StringPropertyMatchType.Contains: + query = + Query.Builder.Where( + x => + ((Member)x).PropertyTypeAlias == propertyTypeAlias && + (((Member)x).LongStringPropertyValue.SqlContains(value, TextColumnType.NText) || + ((Member)x).ShortStringPropertyValue.SqlContains(value, TextColumnType.NVarchar))); + break; + case StringPropertyMatchType.StartsWith: + query = + Query.Builder.Where( + x => + ((Member)x).PropertyTypeAlias == propertyTypeAlias && + (((Member)x).LongStringPropertyValue.SqlStartsWith(value, TextColumnType.NText) || + ((Member)x).ShortStringPropertyValue.SqlStartsWith(value, TextColumnType.NVarchar))); + break; + case StringPropertyMatchType.EndsWith: + query = + Query.Builder.Where( + x => + ((Member)x).PropertyTypeAlias == propertyTypeAlias && + (((Member)x).LongStringPropertyValue.SqlEndsWith(value, TextColumnType.NText) || + ((Member)x).ShortStringPropertyValue.SqlEndsWith(value, TextColumnType.NVarchar))); + break; + default: + throw new ArgumentOutOfRangeException("matchType"); + } + + var members = repository.GetByQuery(query); + return members; + } + } + + /// + /// Gets a list of Members based on a property search + /// + /// Alias of the PropertyType to search for + /// Value to match + /// The type of match to make as . Default is + /// + public IEnumerable GetMembersByPropertyValue(string propertyTypeAlias, int value, ValuePropertyMatchType matchType = ValuePropertyMatchType.Exact) + { + using (var repository = RepositoryFactory.CreateMemberRepository(UowProvider.GetUnitOfWork())) + { + IQuery query; + + switch (matchType) + { + case ValuePropertyMatchType.Exact: + query = + Query.Builder.Where( + x => + ((Member)x).PropertyTypeAlias == propertyTypeAlias && + ((Member)x).IntegerPropertyValue == value); + break; + case ValuePropertyMatchType.GreaterThan: + query = + Query.Builder.Where( + x => + ((Member)x).PropertyTypeAlias == propertyTypeAlias && + ((Member)x).IntegerPropertyValue > value); + break; + case ValuePropertyMatchType.LessThan: + query = + Query.Builder.Where( + x => + ((Member)x).PropertyTypeAlias == propertyTypeAlias && + ((Member)x).IntegerPropertyValue < value); + break; + case ValuePropertyMatchType.GreaterThanOrEqualTo: + query = + Query.Builder.Where( + x => + ((Member)x).PropertyTypeAlias == propertyTypeAlias && + ((Member)x).IntegerPropertyValue >= value); + break; + case ValuePropertyMatchType.LessThanOrEqualTo: + query = + Query.Builder.Where( + x => + ((Member)x).PropertyTypeAlias == propertyTypeAlias && + ((Member)x).IntegerPropertyValue <= value); + break; + default: + throw new ArgumentOutOfRangeException("matchType"); + } + + var members = repository.GetByQuery(query); + return members; + } + } + + /// + /// Gets a list of Members based on a property search + /// + /// Alias of the PropertyType to search for + /// Value to match + /// + public IEnumerable GetMembersByPropertyValue(string propertyTypeAlias, bool value) + { + using (var repository = RepositoryFactory.CreateMemberRepository(UowProvider.GetUnitOfWork())) + { + var query = + Query.Builder.Where( + x => + ((Member)x).PropertyTypeAlias == propertyTypeAlias && + ((Member)x).BoolPropertyValue == value); + + var members = repository.GetByQuery(query); + return members; + } + } + + /// + /// Gets a list of Members based on a property search + /// + /// Alias of the PropertyType to search for + /// Value to match + /// The type of match to make as . Default is + /// + public IEnumerable GetMembersByPropertyValue(string propertyTypeAlias, DateTime value, ValuePropertyMatchType matchType = ValuePropertyMatchType.Exact) + { + using (var repository = RepositoryFactory.CreateMemberRepository(UowProvider.GetUnitOfWork())) + { + IQuery query; + + switch (matchType) + { + case ValuePropertyMatchType.Exact: + query = + Query.Builder.Where( + x => + ((Member)x).PropertyTypeAlias == propertyTypeAlias && + ((Member)x).DateTimePropertyValue == value); + break; + case ValuePropertyMatchType.GreaterThan: + query = + Query.Builder.Where( + x => + ((Member)x).PropertyTypeAlias == propertyTypeAlias && + ((Member)x).DateTimePropertyValue > value); + break; + case ValuePropertyMatchType.LessThan: + query = + Query.Builder.Where( + x => + ((Member)x).PropertyTypeAlias == propertyTypeAlias && + ((Member)x).DateTimePropertyValue < value); + break; + case ValuePropertyMatchType.GreaterThanOrEqualTo: + query = + Query.Builder.Where( + x => + ((Member)x).PropertyTypeAlias == propertyTypeAlias && + ((Member)x).DateTimePropertyValue >= value); + break; + case ValuePropertyMatchType.LessThanOrEqualTo: + query = + Query.Builder.Where( + x => + ((Member)x).PropertyTypeAlias == propertyTypeAlias && + ((Member)x).DateTimePropertyValue <= value); + break; + default: + throw new ArgumentOutOfRangeException("matchType"); + } + + //TODO: Since this is by property value, we need a GetByPropertyQuery on the repo! + var members = repository.GetByQuery(query); + return members; + } + } + + /// + /// Rebuilds all xml content in the cmsContentXml table for all members + /// + /// + /// Only rebuild the xml structures for the content type ids passed in, if none then rebuilds the structures + /// for all members = USE WITH CARE! + /// + /// True if publishing succeeded, otherwise False + public void RebuildXmlStructures(params int[] memberTypeIds) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMemberRepository(uow)) + { + repository.RebuildXmlStructures( + member => _entitySerializer.Serialize(_dataTypeService, member), + contentTypeIds: memberTypeIds.Length == 0 ? null : memberTypeIds); + } + + Audit(AuditType.Publish, "MemberService.RebuildXmlStructures completed, the xml has been regenerated in the database", 0, -1); + } + + #endregion + + #region IMembershipMemberService Implementation + + /// + /// Gets the total number of Members based on the count type + /// + /// + /// The way the Online count is done is the same way that it is done in the MS SqlMembershipProvider - We query for any members + /// that have their last active date within the Membership.UserIsOnlineTimeWindow (which is in minutes). It isn't exact science + /// but that is how MS have made theirs so we'll follow that principal. + /// + /// to count by + /// with number of Members for passed in type + public int GetCount(MemberCountType countType) + { + using (var repository = RepositoryFactory.CreateMemberRepository(UowProvider.GetUnitOfWork())) + { + IQuery query; + + switch (countType) + { + case MemberCountType.All: + query = new Query(); + return repository.Count(query); + case MemberCountType.Online: + var fromDate = DateTime.Now.AddMinutes(-Membership.UserIsOnlineTimeWindow); + query = + Query.Builder.Where( + x => + ((Member)x).PropertyTypeAlias == Constants.Conventions.Member.LastLoginDate && + ((Member)x).DateTimePropertyValue > fromDate); + return repository.GetCountByQuery(query); + case MemberCountType.LockedOut: + query = + Query.Builder.Where( + x => + ((Member)x).PropertyTypeAlias == Constants.Conventions.Member.IsLockedOut && + ((Member)x).BoolPropertyValue == true); + return repository.GetCountByQuery(query); + case MemberCountType.Approved: + query = + Query.Builder.Where( + x => + ((Member)x).PropertyTypeAlias == Constants.Conventions.Member.IsApproved && + ((Member)x).BoolPropertyValue == true); + return repository.GetCountByQuery(query); + default: + throw new ArgumentOutOfRangeException("countType"); + } + } + + } + + [Obsolete("Use the overload with 'long' parameter types instead")] + [EditorBrowsable(EditorBrowsableState.Never)] + public IEnumerable GetAll(int pageIndex, int pageSize, out int totalRecords) + { + long total; + var result = GetAll(Convert.ToInt64(pageIndex), pageSize, out total); + totalRecords = Convert.ToInt32(total); + return result; + } + + /// + /// Gets a list of paged objects + /// + /// Current page index + /// Size of the page + /// Total number of records found (out) + /// + public IEnumerable GetAll(long pageIndex, int pageSize, out long totalRecords) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMemberRepository(uow)) + { + return repository.GetPagedResultsByQuery(null, pageIndex, pageSize, out totalRecords, "LoginName", Direction.Ascending, orderBySystemField: true); + } + } + + [Obsolete("Use the overload with 'long' parameter types instead")] + [EditorBrowsable(EditorBrowsableState.Never)] + public IEnumerable GetAll(int pageIndex, int pageSize, out int totalRecords, + string orderBy, Direction orderDirection, bool orderBySystemField = true, string memberTypeAlias = null, string filter = "") + { + long total; + var result = GetAll(Convert.ToInt64(pageIndex), pageSize, out total, orderBy, orderDirection, orderBySystemField, memberTypeAlias, filter); + totalRecords = Convert.ToInt32(total); + return result; + } + + public IEnumerable GetAll(long pageIndex, int pageSize, out long totalRecords, + string orderBy, Direction orderDirection, bool orderBySystemField = true, string memberTypeAlias = null, string filter = "") + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMemberRepository(uow)) + { + if (memberTypeAlias == null) + { + return repository.GetPagedResultsByQuery(null, pageIndex, pageSize, out totalRecords, orderBy, orderDirection, orderBySystemField, filter); + } + var query = new Query().Where(x => x.ContentTypeAlias == memberTypeAlias); + return repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, orderBy, orderDirection, orderBySystemField, filter); + } + } + + /// + /// Gets the count of Members by an optional MemberType alias + /// + /// If no alias is supplied then the count for all Member will be returned + /// Optional alias for the MemberType when counting number of Members + /// with number of Members + public int Count(string memberTypeAlias = null) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMemberRepository(uow)) + { + return repository.Count(memberTypeAlias); + } + } + + /// + /// Creates an object without persisting it + /// + /// This method is convenient for when you need to add properties to a new Member + /// before persisting it in order to limit the amount of times its saved. + /// Also note that the returned will not have an Id until its saved. + /// Username of the Member to create + /// Email of the Member to create + /// Name of the Member to create + /// Alias of the MemberType the Member should be based on + /// + public IMember CreateMember(string username, string email, string name, string memberTypeAlias) + { + var memberType = FindMemberTypeByAlias(memberTypeAlias); + return CreateMember(username, email, name, memberType); + } + + /// + /// Creates an object without persisting it + /// + /// This method is convenient for when you need to add properties to a new Member + /// before persisting it in order to limit the amount of times its saved. + /// Also note that the returned will not have an Id until its saved. + /// Username of the Member to create + /// Email of the Member to create + /// Name of the Member to create + /// MemberType the Member should be based on + /// + public IMember CreateMember(string username, string email, string name, IMemberType memberType) + { + var member = new Member(name, email.ToLower().Trim(), username, memberType); + + Created.RaiseEvent(new NewEventArgs(member, false, memberType.Alias, -1), this); + + return member; + } + + /// + /// Creates and persists a Member + /// + /// Using this method will persist the Member object before its returned + /// meaning that it will have an Id available (unlike the CreateMember method) + /// Username of the Member to create + /// Email of the Member to create + /// Name of the Member to create + /// Alias of the MemberType the Member should be based on + /// + public IMember CreateMemberWithIdentity(string username, string email, string name, string memberTypeAlias) + { + var memberType = FindMemberTypeByAlias(memberTypeAlias); + return CreateMemberWithIdentity(username, email, name, memberType); + } + + /// + /// Creates and persists a Member + /// + /// Using this method will persist the Member object before its returned + /// meaning that it will have an Id available (unlike the CreateMember method) + /// Username of the Member to create + /// Email of the Member to create + /// MemberType the Member should be based on + /// + public IMember CreateMemberWithIdentity(string username, string email, IMemberType memberType) + { + return CreateMemberWithIdentity(username, email, username, memberType); + } + + /// + /// Creates and persists a Member + /// + /// Using this method will persist the Member object before its returned + /// meaning that it will have an Id available (unlike the CreateMember method) + /// Username of the Member to create + /// Email of the Member to create + /// Name of the Member to create + /// MemberType the Member should be based on + /// + public IMember CreateMemberWithIdentity(string username, string email, string name, IMemberType memberType) + { + return CreateMemberWithIdentity(username, email, name, "", memberType); + } + + /// + /// Creates and persists a new + /// + /// An can be of type or + /// Username of the to create + /// Email of the to create + /// This value should be the encoded/encrypted/hashed value for the password that will be stored in the database + /// Alias of the Type + /// + IMember IMembershipMemberService.CreateWithIdentity(string username, string email, string passwordValue, string memberTypeAlias) + { + var memberType = FindMemberTypeByAlias(memberTypeAlias); + return CreateMemberWithIdentity(username, email, username, passwordValue, memberType); + } + + /// + /// Creates and persists a Member + /// + /// Using this method will persist the Member object before its returned + /// meaning that it will have an Id available (unlike the CreateMember method) + /// Username of the Member to create + /// Email of the Member to create + /// Name of the Member to create + /// This value should be the encoded/encrypted/hashed value for the password that will be stored in the database + /// MemberType the Member should be based on + /// + private IMember CreateMemberWithIdentity(string username, string email, string name, string passwordValue, IMemberType memberType) + { + if (memberType == null) throw new ArgumentNullException("memberType"); + + var member = new Member(name, email.ToLower().Trim(), username, passwordValue, memberType); + + if (Saving.IsRaisedEventCancelled(new SaveEventArgs(member), this)) + { + member.WasCancelled = true; + return member; + } + + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMemberRepository(uow)) + { + repository.AddOrUpdate(member); + //insert the xml + repository.AddOrUpdateContentXml(member, m => _entitySerializer.Serialize(_dataTypeService, m)); + // generate preview for blame history? + if (UmbracoConfig.For.UmbracoSettings().Content.GlobalPreviewStorageEnabled) + { + repository.AddOrUpdatePreviewXml(member, m => _entitySerializer.Serialize(_dataTypeService, m)); + } + + uow.Commit(); + } + + Saved.RaiseEvent(new SaveEventArgs(member, false), this); + Created.RaiseEvent(new NewEventArgs(member, false, memberType.Alias, -1), this); + + return member; + } + + /// + /// Gets an by its provider key + /// + /// Id to use for retrieval + /// + public IMember GetByProviderKey(object id) + { + var asGuid = id.TryConvertTo(); + if (asGuid.Success) + { + return GetByKey((Guid)id); + } + var asInt = id.TryConvertTo(); + if (asInt.Success) + { + return GetById((int)id); + } + + return null; + } + + /// + /// Get an by email + /// + /// Email to use for retrieval + /// + public IMember GetByEmail(string email) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMemberRepository(uow)) + { + var query = Query.Builder.Where(x => x.Email.Equals(email)); + var member = repository.GetByQuery(query).FirstOrDefault(); + + return member; + } + } + + /// + /// Get an by username + /// + /// Username to use for retrieval + /// + public IMember GetByUsername(string username) + { + //TODO: Somewhere in here, whether at this level or the repository level, we need to add + // a caching mechanism since this method is used by all the membership providers and could be + // called quite a bit when dealing with members. + + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMemberRepository(uow)) + { + var query = Query.Builder.Where(x => x.Username.Equals(username)); + var member = repository.GetByQuery(query).FirstOrDefault(); + + return member; + } + } + + /// + /// Deletes an + /// + /// to Delete + public void Delete(IMember member) + { + if (Deleting.IsRaisedEventCancelled(new DeleteEventArgs(member), this)) + return; + + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMemberRepository(uow)) + { + repository.Delete(member); + uow.Commit(); + + var args = new DeleteEventArgs(member, false); + Deleted.RaiseEvent(args, this); + + //remove any flagged media files + repository.DeleteMediaFiles(args.MediaFilesToDelete); + } + } + + /// + /// Saves an + /// + /// to Save + /// Optional parameter to raise events. + /// Default is True otherwise set to False to not raise events + public void Save(IMember entity, bool raiseEvents = true) + { + if (raiseEvents) + { + if (Saving.IsRaisedEventCancelled(new SaveEventArgs(entity), this)) + { + return; + } + } + + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMemberRepository(uow)) + { + repository.AddOrUpdate(entity); + repository.AddOrUpdateContentXml(entity, m => _entitySerializer.Serialize(_dataTypeService, m)); + // generate preview for blame history? + if (UmbracoConfig.For.UmbracoSettings().Content.GlobalPreviewStorageEnabled) + { + repository.AddOrUpdatePreviewXml(entity, m => _entitySerializer.Serialize(_dataTypeService, m)); + } + + uow.Commit(); + } + + if (raiseEvents) + Saved.RaiseEvent(new SaveEventArgs(entity, false), this); + } + + /// + /// Saves a list of objects + /// + /// to save + /// Optional parameter to raise events. + /// Default is True otherwise set to False to not raise events + public void Save(IEnumerable entities, bool raiseEvents = true) + { + var asArray = entities.ToArray(); + + if (raiseEvents) + { + if (Saving.IsRaisedEventCancelled(new SaveEventArgs(asArray), this)) + return; + } + using (new WriteLock(Locker)) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMemberRepository(uow)) + { + foreach (var member in asArray) + { + repository.AddOrUpdate(member); + repository.AddOrUpdateContentXml(member, m => _entitySerializer.Serialize(_dataTypeService, m)); + // generate preview for blame history? + if (UmbracoConfig.For.UmbracoSettings().Content.GlobalPreviewStorageEnabled) + { + repository.AddOrUpdatePreviewXml(member, m => _entitySerializer.Serialize(_dataTypeService, m)); + } + } + + //commit the whole lot in one go + uow.Commit(); + } + + if (raiseEvents) + Saved.RaiseEvent(new SaveEventArgs(asArray, false), this); + } + } + + #endregion + + #region IMembershipRoleService Implementation + + public void AddRole(string roleName) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMemberGroupRepository(uow)) + { + repository.CreateIfNotExists(roleName); + } + } + + public IEnumerable GetAllRoles() + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMemberGroupRepository(uow)) + { + var result = repository.GetAll(); + return result.Select(x => x.Name).Distinct(); + } + } + + public IEnumerable GetAllRoles(int memberId) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMemberGroupRepository(uow)) + { + var result = repository.GetMemberGroupsForMember(memberId); + return result.Select(x => x.Name).Distinct(); + } + } + + public IEnumerable GetAllRoles(string username) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMemberGroupRepository(uow)) + { + var result = repository.GetMemberGroupsForMember(username); + return result.Select(x => x.Name).Distinct(); + } + } + + public IEnumerable GetMembersInRole(string roleName) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMemberRepository(uow)) + { + return repository.GetByMemberGroup(roleName); + } + } + + public IEnumerable FindMembersInRole(string roleName, string usernameToMatch, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMemberRepository(uow)) + { + return repository.FindMembersInRole(roleName, usernameToMatch, matchType); + } + } + + public bool DeleteRole(string roleName, bool throwIfBeingUsed) + { + using (new WriteLock(Locker)) + { + if (throwIfBeingUsed) + { + var inRole = GetMembersInRole(roleName); + if (inRole.Any()) + { + throw new InvalidOperationException("The role " + roleName + " is currently assigned to members"); + } + } + + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMemberGroupRepository(uow)) + { + var qry = new Query().Where(g => g.Name == roleName); + var found = repository.GetByQuery(qry).ToArray(); + + foreach (var memberGroup in found) + { + _memberGroupService.Delete(memberGroup); + } + return found.Any(); + } + } + } + public void AssignRole(string username, string roleName) + { + AssignRoles(new[] { username }, new[] { roleName }); + } + + public void AssignRoles(string[] usernames, string[] roleNames) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMemberGroupRepository(uow)) + { + repository.AssignRoles(usernames, roleNames); + } + } + + public void DissociateRole(string username, string roleName) + { + DissociateRoles(new[] { username }, new[] { roleName }); + } + + public void DissociateRoles(string[] usernames, string[] roleNames) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMemberGroupRepository(uow)) + { + repository.DissociateRoles(usernames, roleNames); + } + } + + public void AssignRole(int memberId, string roleName) + { + AssignRoles(new[] { memberId }, new[] { roleName }); + } + + public void AssignRoles(int[] memberIds, string[] roleNames) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMemberGroupRepository(uow)) + { + repository.AssignRoles(memberIds, roleNames); + } + } + + public void DissociateRole(int memberId, string roleName) + { + DissociateRoles(new[] { memberId }, new[] { roleName }); + } + + public void DissociateRoles(int[] memberIds, string[] roleNames) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMemberGroupRepository(uow)) + { + repository.DissociateRoles(memberIds, roleNames); + } + } + + + + #endregion + + private IMemberType FindMemberTypeByAlias(string memberTypeAlias) + { + using (var repository = RepositoryFactory.CreateMemberTypeRepository(UowProvider.GetUnitOfWork())) + { + var query = Query.Builder.Where(x => x.Alias == memberTypeAlias); + var types = repository.GetByQuery(query); + + if (types.Any() == false) + throw new Exception( + string.Format("No MemberType matching the passed in Alias: '{0}' was found", + memberTypeAlias)); + + var contentType = types.First(); + + if (contentType == null) + throw new Exception(string.Format("MemberType matching the passed in Alias: '{0}' was null", + memberTypeAlias)); + + return contentType; + } + } + + private void Audit(AuditType type, string message, int userId, int objectId) + { + var uow = UowProvider.GetUnitOfWork(); + using (var auditRepo = RepositoryFactory.CreateAuditRepository(uow)) + { + auditRepo.AddOrUpdate(new AuditItem(objectId, message, type, userId)); + uow.Commit(); + } + } + + #region Event Handlers + + /// + /// Occurs before Delete + /// + public static event TypedEventHandler> Deleting; + + /// + /// Occurs after Delete + /// + public static event TypedEventHandler> Deleted; + + /// + /// Occurs before Save + /// + public static event TypedEventHandler> Saving; + + /// + /// Occurs after Create + /// + /// + /// Please note that the Member object has been created, but might not have been saved + /// so it does not have an identity yet (meaning no Id has been set). + /// + public static event TypedEventHandler> Created; + + /// + /// Occurs after Save + /// + public static event TypedEventHandler> Saved; + + #endregion + + /// + /// A helper method that will create a basic/generic member for use with a generic membership provider + /// + /// + internal static IMember CreateGenericMembershipProviderMember(string name, string email, string username, string password) + { + var identity = int.MaxValue; + + var memType = new MemberType(-1); + var propGroup = new PropertyGroup + { + Name = "Membership", + Id = --identity + }; + propGroup.PropertyTypes.Add(new PropertyType(Constants.PropertyEditors.TextboxAlias, DataTypeDatabaseType.Ntext, Constants.Conventions.Member.Comments) + { + Name = Constants.Conventions.Member.CommentsLabel, + SortOrder = 0, + Id = --identity, + Key = identity.ToGuid() + }); + propGroup.PropertyTypes.Add(new PropertyType(Constants.PropertyEditors.TrueFalseAlias, DataTypeDatabaseType.Integer, Constants.Conventions.Member.IsApproved) + { + Name = Constants.Conventions.Member.IsApprovedLabel, + SortOrder = 3, + Id = --identity, + Key = identity.ToGuid() + }); + propGroup.PropertyTypes.Add(new PropertyType(Constants.PropertyEditors.TrueFalseAlias, DataTypeDatabaseType.Integer, Constants.Conventions.Member.IsLockedOut) + { + Name = Constants.Conventions.Member.IsLockedOutLabel, + SortOrder = 4, + Id = --identity, + Key = identity.ToGuid() + }); + propGroup.PropertyTypes.Add(new PropertyType(Constants.PropertyEditors.NoEditAlias, DataTypeDatabaseType.Date, Constants.Conventions.Member.LastLockoutDate) + { + Name = Constants.Conventions.Member.LastLockoutDateLabel, + SortOrder = 5, + Id = --identity, + Key = identity.ToGuid() + }); + propGroup.PropertyTypes.Add(new PropertyType(Constants.PropertyEditors.NoEditAlias, DataTypeDatabaseType.Date, Constants.Conventions.Member.LastLoginDate) + { + Name = Constants.Conventions.Member.LastLoginDateLabel, + SortOrder = 6, + Id = --identity, + Key = identity.ToGuid() + }); + propGroup.PropertyTypes.Add(new PropertyType(Constants.PropertyEditors.NoEditAlias, DataTypeDatabaseType.Date, Constants.Conventions.Member.LastPasswordChangeDate) + { + Name = Constants.Conventions.Member.LastPasswordChangeDateLabel, + SortOrder = 7, + Id = --identity, + Key = identity.ToGuid() + }); + + memType.PropertyGroups.Add(propGroup); + + var member = new Member(name, email, username, password, memType); + + //we've assigned ids to the property types and groups but we also need to assign fake ids to the properties themselves. + foreach (var property in member.Properties) + { + property.Id = --identity; + } + + return member; + } + } } \ No newline at end of file diff --git a/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs index f21371ef2b..e256e4082a 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs @@ -24,795 +24,795 @@ using Umbraco.Core.Persistence.DatabaseModelDefinitions; namespace Umbraco.Tests.Persistence.Repositories { - [DatabaseTestBehavior(DatabaseBehavior.NewDbFileAndSchemaPerTest)] - [TestFixture] - public class ContentRepositoryTest : BaseDatabaseFactoryTest - { - [SetUp] - public override void Initialize() - { - base.Initialize(); - - CreateTestData(); - } - - [TearDown] - public override void TearDown() - { - base.TearDown(); - } - - private ContentRepository CreateRepository(IDatabaseUnitOfWork unitOfWork, out ContentTypeRepository contentTypeRepository) - { - TemplateRepository tr; - return CreateRepository(unitOfWork, out contentTypeRepository, out tr); - } - - private ContentRepository CreateRepository(IDatabaseUnitOfWork unitOfWork, out ContentTypeRepository contentTypeRepository, out TemplateRepository templateRepository) - { - templateRepository = new TemplateRepository(unitOfWork, CacheHelper, Logger, SqlSyntax, Mock.Of(), Mock.Of(), Mock.Of()); - var tagRepository = new TagRepository(unitOfWork, CacheHelper, Logger, SqlSyntax); - contentTypeRepository = new ContentTypeRepository(unitOfWork, CacheHelper, Logger, SqlSyntax, templateRepository); - var repository = new ContentRepository(unitOfWork, CacheHelper, Logger, SqlSyntax, contentTypeRepository, templateRepository, tagRepository, Mock.Of()); - return repository; - } - - [Test] - public void Rebuild_Xml_Structures_With_Non_Latest_Version() - { - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - ContentTypeRepository contentTypeRepository; - using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) - { - var contentType1 = MockedContentTypes.CreateSimpleContentType("Textpage1", "Textpage1"); - contentTypeRepository.AddOrUpdate(contentType1); - - var allCreated = new List(); - - //create 100 non published - for (var i = 0; i < 100; i++) - { - var c1 = MockedContent.CreateSimpleContent(contentType1); - repository.AddOrUpdate(c1); - allCreated.Add(c1); - } - //create 100 published - for (var i = 0; i < 100; i++) - { - var c1 = MockedContent.CreateSimpleContent(contentType1); - c1.ChangePublishedState(PublishedState.Published); - repository.AddOrUpdate(c1); - allCreated.Add(c1); - } - unitOfWork.Commit(); - - //now create some versions of this content - this shouldn't affect the xml structures saved - for (int i = 0; i < allCreated.Count; i++) - { - allCreated[i].Name = "blah" + i; - //IMPORTANT testing note here: We need to changed the published state here so that - // it doesn't automatically think this is simply publishing again - this forces the latest - // version to be Saved and not published - allCreated[i].ChangePublishedState(PublishedState.Saved); - repository.AddOrUpdate(allCreated[i]); - } - unitOfWork.Commit(); - - //delete all xml - unitOfWork.Database.Execute("DELETE FROM cmsContentXml"); - Assert.AreEqual(0, unitOfWork.Database.ExecuteScalar("SELECT COUNT(*) FROM cmsContentXml")); - - repository.RebuildXmlStructures(media => new XElement("test"), 10); - - Assert.AreEqual(100, unitOfWork.Database.ExecuteScalar("SELECT COUNT(*) FROM cmsContentXml")); - } - } - - [Test] - public void Rebuild_All_Xml_Structures() - { - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - ContentTypeRepository contentTypeRepository; - using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) - { - - var contentType1 = MockedContentTypes.CreateSimpleContentType("Textpage1", "Textpage1"); - contentTypeRepository.AddOrUpdate(contentType1); - var allCreated = new List(); - - for (var i = 0; i < 100; i++) - { - //These will be non-published so shouldn't show up - var c1 = MockedContent.CreateSimpleContent(contentType1); - repository.AddOrUpdate(c1); - allCreated.Add(c1); - } - for (var i = 0; i < 100; i++) - { - var c1 = MockedContent.CreateSimpleContent(contentType1); - c1.ChangePublishedState(PublishedState.Published); - repository.AddOrUpdate(c1); - allCreated.Add(c1); - } - unitOfWork.Commit(); - - //now create some versions of this content - this shouldn't affect the xml structures saved - for (int i = 0; i < allCreated.Count; i++) - { - allCreated[i].Name = "blah" + i; - repository.AddOrUpdate(allCreated[i]); - } - unitOfWork.Commit(); - - //delete all xml - unitOfWork.Database.Execute("DELETE FROM cmsContentXml"); - Assert.AreEqual(0, unitOfWork.Database.ExecuteScalar("SELECT COUNT(*) FROM cmsContentXml")); - - repository.RebuildXmlStructures(media => new XElement("test"), 10); - - Assert.AreEqual(100, unitOfWork.Database.ExecuteScalar("SELECT COUNT(*) FROM cmsContentXml")); - } - } - - [Test] - public void Rebuild_All_Xml_Structures_For_Content_Type() - { - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - ContentTypeRepository contentTypeRepository; - using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) - { - var contentType1 = MockedContentTypes.CreateSimpleContentType("Textpage1", "Textpage1"); - var contentType2 = MockedContentTypes.CreateSimpleContentType("Textpage2", "Textpage2"); - var contentType3 = MockedContentTypes.CreateSimpleContentType("Textpage3", "Textpage3"); - contentTypeRepository.AddOrUpdate(contentType1); - contentTypeRepository.AddOrUpdate(contentType2); - contentTypeRepository.AddOrUpdate(contentType3); - - var allCreated = new List(); - - for (var i = 0; i < 30; i++) - { - //These will be non-published so shouldn't show up - var c1 = MockedContent.CreateSimpleContent(contentType1); - repository.AddOrUpdate(c1); - allCreated.Add(c1); - } - for (var i = 0; i < 30; i++) - { - var c1 = MockedContent.CreateSimpleContent(contentType1); - c1.ChangePublishedState(PublishedState.Published); - repository.AddOrUpdate(c1); - allCreated.Add(c1); - } - for (var i = 0; i < 30; i++) - { - var c1 = MockedContent.CreateSimpleContent(contentType2); - c1.ChangePublishedState(PublishedState.Published); - repository.AddOrUpdate(c1); - allCreated.Add(c1); - } - for (var i = 0; i < 30; i++) - { - var c1 = MockedContent.CreateSimpleContent(contentType3); - c1.ChangePublishedState(PublishedState.Published); - repository.AddOrUpdate(c1); - allCreated.Add(c1); - } - unitOfWork.Commit(); - - //now create some versions of this content - this shouldn't affect the xml structures saved - for (int i = 0; i < allCreated.Count; i++) - { - allCreated[i].Name = "blah" + i; - repository.AddOrUpdate(allCreated[i]); - } - unitOfWork.Commit(); - - //delete all xml - unitOfWork.Database.Execute("DELETE FROM cmsContentXml"); - Assert.AreEqual(0, unitOfWork.Database.ExecuteScalar("SELECT COUNT(*) FROM cmsContentXml")); - - repository.RebuildXmlStructures(media => new XElement("test"), 10, contentTypeIds: new[] { contentType1.Id, contentType2.Id }); - - Assert.AreEqual(60, unitOfWork.Database.ExecuteScalar("SELECT COUNT(*) FROM cmsContentXml")); - } - } - - [Test] - public void Ensures_Permissions_Are_Set_If_Parent_Entity_Permissions_Exist() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - - ContentTypeRepository contentTypeRepository; - using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) - { - var contentType = MockedContentTypes.CreateSimpleContentType("umbTextpage1", "Textpage"); - contentType.AllowedContentTypes = new List - { - new ContentTypeSort(new Lazy(() => contentType.Id), 0, contentType.Alias) - }; - var parentPage = MockedContent.CreateSimpleContent(contentType); - contentTypeRepository.AddOrUpdate(contentType); - repository.AddOrUpdate(parentPage); - unitOfWork.Commit(); - - // Act - repository.AssignEntityPermission(parentPage, 'A', new int[] { 0 }); - var childPage = MockedContent.CreateSimpleContent(contentType, "child", parentPage); - repository.AddOrUpdate(childPage); - unitOfWork.Commit(); - - // Assert - var permissions = repository.GetPermissionsForEntity(childPage.Id); - Assert.AreEqual(1, permissions.Count()); - Assert.AreEqual("A", permissions.Single().AssignedPermissions.First()); - } - - } - - [Test] - public void Can_Perform_Add_On_ContentRepository() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - ContentTypeRepository contentTypeRepository; - using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) - { - ContentType contentType = MockedContentTypes.CreateSimpleContentType("umbTextpage2", "Textpage"); - Content textpage = MockedContent.CreateSimpleContent(contentType); - - // Act - contentTypeRepository.AddOrUpdate(contentType); - repository.AddOrUpdate(textpage); - unitOfWork.Commit(); - - // Assert - Assert.That(contentType.HasIdentity, Is.True); - Assert.That(textpage.HasIdentity, Is.True); - - } - } - - [Test] - public void Can_Perform_Add_With_Default_Template() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - ContentTypeRepository contentTypeRepository; - TemplateRepository templateRepository; - using (var repository = CreateRepository(unitOfWork, out contentTypeRepository, out templateRepository)) - { - var template = new Template("hello", "hello"); - templateRepository.AddOrUpdate(template); - unitOfWork.Commit(); - - ContentType contentType = MockedContentTypes.CreateSimpleContentType("umbTextpage2", "Textpage"); - contentType.AllowedTemplates = Enumerable.Empty(); // because CreateSimple... assigns one - contentType.SetDefaultTemplate(template); - Content textpage = MockedContent.CreateSimpleContent(contentType); - - // Act - - contentTypeRepository.AddOrUpdate(contentType); - repository.AddOrUpdate(textpage); - unitOfWork.Commit(); - - // Assert - Assert.That(textpage.Template, Is.Not.Null); - Assert.That(textpage.Template, Is.EqualTo(contentType.DefaultTemplate)); - } - } - - //Covers issue U4-2791 and U4-2607 - [Test] - public void Can_Save_Content_With_AtSign_In_Name_On_ContentRepository() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - ContentTypeRepository contentTypeRepository; - using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) - { - var contentType = MockedContentTypes.CreateSimpleContentType("umbTextpage1", "Textpage"); - contentTypeRepository.AddOrUpdate(contentType); - unitOfWork.Commit(); - - var textpage = MockedContent.CreateSimpleContent(contentType, "test@umbraco.org", -1); - var anotherTextpage = MockedContent.CreateSimpleContent(contentType, "@lightgiants", -1); - - // Act - - repository.AddOrUpdate(textpage); - repository.AddOrUpdate(anotherTextpage); - unitOfWork.Commit(); - - // Assert - Assert.That(contentType.HasIdentity, Is.True); - Assert.That(textpage.HasIdentity, Is.True); - - var content = repository.Get(textpage.Id); - Assert.That(content.Name, Is.EqualTo(textpage.Name)); - - var content2 = repository.Get(anotherTextpage.Id); - Assert.That(content2.Name, Is.EqualTo(anotherTextpage.Name)); - } - } - - [Test] - public void Can_Perform_Multiple_Adds_On_ContentRepository() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - ContentTypeRepository contentTypeRepository; - using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) - { - ContentType contentType = MockedContentTypes.CreateSimpleContentType("umbTextpage1", "Textpage"); - Content textpage = MockedContent.CreateSimpleContent(contentType); - - // Act - contentTypeRepository.AddOrUpdate(contentType); - repository.AddOrUpdate(textpage); - unitOfWork.Commit(); - - Content subpage = MockedContent.CreateSimpleContent(contentType, "Text Page 1", textpage.Id); - repository.AddOrUpdate(subpage); - unitOfWork.Commit(); - - // Assert - Assert.That(contentType.HasIdentity, Is.True); - Assert.That(textpage.HasIdentity, Is.True); - Assert.That(subpage.HasIdentity, Is.True); - Assert.That(textpage.Id, Is.EqualTo(subpage.ParentId)); - } - - } - - - [Test] - public void Can_Verify_Fresh_Entity_Is_Not_Dirty() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - ContentTypeRepository contentTypeRepository; - using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) - { - // Act - var content = repository.Get(NodeDto.NodeIdSeed + 3); - bool dirty = ((Content)content).IsDirty(); - - // Assert - Assert.That(dirty, Is.False); - } - } - - [Test] - public void Can_Perform_Update_On_ContentRepository() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - ContentTypeRepository contentTypeRepository; - using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) - { - // Act - var content = repository.Get(NodeDto.NodeIdSeed + 2); - content.Name = "About 2"; - repository.AddOrUpdate(content); - unitOfWork.Commit(); - var updatedContent = repository.Get(NodeDto.NodeIdSeed + 2); - - // Assert - Assert.That(updatedContent.Id, Is.EqualTo(content.Id)); - Assert.That(updatedContent.Name, Is.EqualTo(content.Name)); - } - - } - - [Test] - public void Can_Update_With_Null_Template() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - ContentTypeRepository contentTypeRepository; - using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) - { - // Act - var content = repository.Get(NodeDto.NodeIdSeed + 2); - content.Template = null; - repository.AddOrUpdate(content); - unitOfWork.Commit(); - var updatedContent = repository.Get(NodeDto.NodeIdSeed + 2); - - // Assert - Assert.That(updatedContent.Template, Is.Null); - } - - } - - [Test] - public void Can_Perform_Delete_On_ContentRepository() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - ContentTypeRepository contentTypeRepository; - using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) - { - var contentType = contentTypeRepository.Get(NodeDto.NodeIdSeed); - var content = new Content("Textpage 2 Child Node", NodeDto.NodeIdSeed + 3, contentType); - content.CreatorId = 0; - content.WriterId = 0; - - // Act - repository.AddOrUpdate(content); - unitOfWork.Commit(); - var id = content.Id; - - repository.Delete(content); - unitOfWork.Commit(); - - var content1 = repository.Get(id); - - // Assert - Assert.That(content1, Is.Null); - } - } - - [Test] - public void Can_Perform_Get_On_ContentRepository() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - ContentTypeRepository contentTypeRepository; - using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) - { - // Act - var content = repository.Get(NodeDto.NodeIdSeed + 3); - - // Assert - Assert.That(content.Id, Is.EqualTo(NodeDto.NodeIdSeed + 3)); - Assert.That(content.CreateDate, Is.GreaterThan(DateTime.MinValue)); - Assert.That(content.UpdateDate, Is.GreaterThan(DateTime.MinValue)); - Assert.That(content.ParentId, Is.Not.EqualTo(0)); - Assert.That(content.Name, Is.EqualTo("Text Page 2")); - //Assert.That(content.SortOrder, Is.EqualTo(1)); - Assert.That(content.Version, Is.Not.EqualTo(Guid.Empty)); - Assert.That(content.ContentTypeId, Is.EqualTo(NodeDto.NodeIdSeed)); - Assert.That(content.Path, Is.Not.Empty); - Assert.That(content.Properties.Any(), Is.True); - } - } - - [Test] - public void Can_Perform_GetByQuery_On_ContentRepository() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - ContentTypeRepository contentTypeRepository; - using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) - { - // Act - var query = Query.Builder.Where(x => x.Level == 2); - var result = repository.GetByQuery(query); - - // Assert - Assert.That(result.Count(), Is.GreaterThanOrEqualTo(2)); - } - } - - [Test] - public void Can_Perform_Get_All_With_Many_Version() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - ContentTypeRepository contentTypeRepository; - using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) - { - var result = repository.GetAll().ToArray(); - foreach (var content in result) - { - content.ChangePublishedState(PublishedState.Saved); - repository.AddOrUpdate(content); - } - unitOfWork.Commit(); - foreach (var content in result) - { - content.ChangePublishedState(PublishedState.Published); - repository.AddOrUpdate(content); - } - unitOfWork.Commit(); - - //re-get - - var result2 = repository.GetAll().ToArray(); - - Assert.AreEqual(result.Count(), result2.Count()); - } - } - - [Test] - public void Can_Perform_GetPagedResultsByQuery_ForFirstPage_On_ContentRepository() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - ContentTypeRepository contentTypeRepository; - using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) - { - // Act - var query = Query.Builder.Where(x => x.Level == 2); - long totalRecords; - var result = repository.GetPagedResultsByQuery(query, 0, 1, out totalRecords, "Name", Direction.Ascending, orderBySystemField: true); - - // Assert - Assert.That(totalRecords, Is.GreaterThanOrEqualTo(2)); - Assert.That(result.Count(), Is.EqualTo(1)); - Assert.That(result.First().Name, Is.EqualTo("Text Page 1")); - } - } - - [Test] - public void Can_Perform_GetPagedResultsByQuery_ForSecondPage_On_ContentRepository() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - ContentTypeRepository contentTypeRepository; - using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) - { - // Act - var query = Query.Builder.Where(x => x.Level == 2); - long totalRecords; - var result = repository.GetPagedResultsByQuery(query, 1, 1, out totalRecords, "Name", Direction.Ascending, orderBySystemField: true); - - // Assert - Assert.That(totalRecords, Is.GreaterThanOrEqualTo(2)); - Assert.That(result.Count(), Is.EqualTo(1)); - Assert.That(result.First().Name, Is.EqualTo("Text Page 2")); - } - } - - [Test] - public void Can_Perform_GetPagedResultsByQuery_WithSinglePage_On_ContentRepository() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - ContentTypeRepository contentTypeRepository; - using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) - { - // Act - var query = Query.Builder.Where(x => x.Level == 2); - long totalRecords; - var result = repository.GetPagedResultsByQuery(query, 0, 2, out totalRecords, "Name", Direction.Ascending, orderBySystemField: true); - - // Assert - Assert.That(totalRecords, Is.GreaterThanOrEqualTo(2)); - Assert.That(result.Count(), Is.EqualTo(2)); - Assert.That(result.First().Name, Is.EqualTo("Text Page 1")); - } - } - - [Test] - public void Can_Perform_GetPagedResultsByQuery_WithDescendingOrder_On_ContentRepository() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - ContentTypeRepository contentTypeRepository; - using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) - { - // Act - var query = Query.Builder.Where(x => x.Level == 2); - long totalRecords; - var result = repository.GetPagedResultsByQuery(query, 0, 1, out totalRecords, "Name", Direction.Descending, orderBySystemField: true); - - // Assert - Assert.That(totalRecords, Is.GreaterThanOrEqualTo(2)); - Assert.That(result.Count(), Is.EqualTo(1)); - Assert.That(result.First().Name, Is.EqualTo("Text Page 2")); - } - } - - [Test] - public void Can_Perform_GetPagedResultsByQuery_WithFilterMatchingSome_On_ContentRepository() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - ContentTypeRepository contentTypeRepository; - using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) - { - // Act - var query = Query.Builder.Where(x => x.Level == 2); - long totalRecords; - var result = repository.GetPagedResultsByQuery(query, 0, 1, out totalRecords, "Name", Direction.Ascending, true, "Page 2"); - - // Assert - Assert.That(totalRecords, Is.EqualTo(1)); - Assert.That(result.Count(), Is.EqualTo(1)); - Assert.That(result.First().Name, Is.EqualTo("Text Page 2")); - } - } - - [Test] - public void Can_Perform_GetPagedResultsByQuery_WithFilterMatchingAll_On_ContentRepository() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - ContentTypeRepository contentTypeRepository; - using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) - { - // Act - var query = Query.Builder.Where(x => x.Level == 2); - long totalRecords; - var result = repository.GetPagedResultsByQuery(query, 0, 1, out totalRecords, "Name", Direction.Ascending, true, "Page"); - - // Assert - Assert.That(totalRecords, Is.EqualTo(2)); - Assert.That(result.Count(), Is.EqualTo(1)); - Assert.That(result.First().Name, Is.EqualTo("Text Page 1")); - } - } - - [Test] - public void Can_Perform_GetAll_By_Param_Ids_On_ContentRepository() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - ContentTypeRepository contentTypeRepository; - using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) - { - // Act - var contents = repository.GetAll(NodeDto.NodeIdSeed + 2, NodeDto.NodeIdSeed + 3); - - // Assert - Assert.That(contents, Is.Not.Null); - Assert.That(contents.Any(), Is.True); - Assert.That(contents.Count(), Is.EqualTo(2)); - } - } - - [Test] - public void Can_Perform_GetAll_On_ContentRepository() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - ContentTypeRepository contentTypeRepository; - using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) - { - // Act - var contents = repository.GetAll(); - - // Assert - Assert.That(contents, Is.Not.Null); - Assert.That(contents.Any(), Is.True); - Assert.That(contents.Count(), Is.GreaterThanOrEqualTo(4)); - } - - - } - - [Test] - public void Can_Perform_Exists_On_ContentRepository() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - ContentTypeRepository contentTypeRepository; - using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) - { - // Act - var exists = repository.Exists(NodeDto.NodeIdSeed + 1); - - // Assert - Assert.That(exists, Is.True); - } - - - } - - [Test] - public void Can_Perform_Count_On_ContentRepository() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - ContentTypeRepository contentTypeRepository; - using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) - { - // Act - int level = 2; - var query = Query.Builder.Where(x => x.Level == level); - var result = repository.Count(query); - - // Assert - Assert.That(result, Is.GreaterThanOrEqualTo(2)); - } - } - - [Test] - public void Can_Verify_Keys_Set() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - ContentTypeRepository contentTypeRepository; - using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) - { - // Act - var textpage = repository.Get(NodeDto.NodeIdSeed + 1); - var subpage = repository.Get(NodeDto.NodeIdSeed + 2); - var trashed = repository.Get(NodeDto.NodeIdSeed + 4); - - // Assert - Assert.That(textpage.Key.ToString().ToUpper(), Is.EqualTo("B58B3AD4-62C2-4E27-B1BE-837BD7C533E0")); - Assert.That(subpage.Key.ToString().ToUpper(), Is.EqualTo("FF11402B-7E53-4654-81A7-462AC2108059")); - Assert.That(trashed.Key, Is.Not.EqualTo(Guid.Empty)); - } - } - - [Test] - public void Can_Get_Content_By_Guid_Key() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - ContentTypeRepository contentTypeRepository; - using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) - { - // Act - var query = Query.Builder.Where(x => x.Key == new Guid("B58B3AD4-62C2-4E27-B1BE-837BD7C533E0")); - var content = repository.GetByQuery(query).SingleOrDefault(); - - // Assert - Assert.That(content, Is.Not.Null); - Assert.That(content.Id, Is.EqualTo(NodeDto.NodeIdSeed + 1)); - } - - } - - public void CreateTestData() - { - //Create and Save ContentType "umbTextpage" -> (NodeDto.NodeIdSeed) - ContentType contentType = MockedContentTypes.CreateSimpleContentType("umbTextpage", "Textpage"); - contentType.Key = new Guid("1D3A8E6E-2EA9-4CC1-B229-1AEE19821522"); - ServiceContext.ContentTypeService.Save(contentType); - - //Create and Save Content "Homepage" based on "umbTextpage" -> (NodeDto.NodeIdSeed + 1) - Content textpage = MockedContent.CreateSimpleContent(contentType); - textpage.Key = new Guid("B58B3AD4-62C2-4E27-B1BE-837BD7C533E0"); - ServiceContext.ContentService.Save(textpage, 0); - - //Create and Save Content "Text Page 1" based on "umbTextpage" -> (NodeDto.NodeIdSeed + 2) - Content subpage = MockedContent.CreateSimpleContent(contentType, "Text Page 1", textpage.Id); - subpage.Key = new Guid("FF11402B-7E53-4654-81A7-462AC2108059"); - ServiceContext.ContentService.Save(subpage, 0); - - //Create and Save Content "Text Page 1" based on "umbTextpage" -> (NodeDto.NodeIdSeed + 3) - Content subpage2 = MockedContent.CreateSimpleContent(contentType, "Text Page 2", textpage.Id); - ServiceContext.ContentService.Save(subpage2, 0); - - //Create and Save Content "Text Page Deleted" based on "umbTextpage" -> (NodeDto.NodeIdSeed + 4) - Content trashed = MockedContent.CreateSimpleContent(contentType, "Text Page Deleted", -20); - trashed.Trashed = true; - ServiceContext.ContentService.Save(trashed, 0); - } - } + [DatabaseTestBehavior(DatabaseBehavior.NewDbFileAndSchemaPerTest)] + [TestFixture] + public class ContentRepositoryTest : BaseDatabaseFactoryTest + { + [SetUp] + public override void Initialize() + { + base.Initialize(); + + CreateTestData(); + } + + [TearDown] + public override void TearDown() + { + base.TearDown(); + } + + private ContentRepository CreateRepository(IDatabaseUnitOfWork unitOfWork, out ContentTypeRepository contentTypeRepository) + { + TemplateRepository tr; + return CreateRepository(unitOfWork, out contentTypeRepository, out tr); + } + + private ContentRepository CreateRepository(IDatabaseUnitOfWork unitOfWork, out ContentTypeRepository contentTypeRepository, out TemplateRepository templateRepository) + { + templateRepository = new TemplateRepository(unitOfWork, CacheHelper, Logger, SqlSyntax, Mock.Of(), Mock.Of(), Mock.Of()); + var tagRepository = new TagRepository(unitOfWork, CacheHelper, Logger, SqlSyntax); + contentTypeRepository = new ContentTypeRepository(unitOfWork, CacheHelper, Logger, SqlSyntax, templateRepository); + var repository = new ContentRepository(unitOfWork, CacheHelper, Logger, SqlSyntax, contentTypeRepository, templateRepository, tagRepository, Mock.Of()); + return repository; + } + + [Test] + public void Rebuild_Xml_Structures_With_Non_Latest_Version() + { + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + ContentTypeRepository contentTypeRepository; + using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) + { + var contentType1 = MockedContentTypes.CreateSimpleContentType("Textpage1", "Textpage1"); + contentTypeRepository.AddOrUpdate(contentType1); + + var allCreated = new List(); + + //create 100 non published + for (var i = 0; i < 100; i++) + { + var c1 = MockedContent.CreateSimpleContent(contentType1); + repository.AddOrUpdate(c1); + allCreated.Add(c1); + } + //create 100 published + for (var i = 0; i < 100; i++) + { + var c1 = MockedContent.CreateSimpleContent(contentType1); + c1.ChangePublishedState(PublishedState.Published); + repository.AddOrUpdate(c1); + allCreated.Add(c1); + } + unitOfWork.Commit(); + + //now create some versions of this content - this shouldn't affect the xml structures saved + for (int i = 0; i < allCreated.Count; i++) + { + allCreated[i].Name = "blah" + i; + //IMPORTANT testing note here: We need to changed the published state here so that + // it doesn't automatically think this is simply publishing again - this forces the latest + // version to be Saved and not published + allCreated[i].ChangePublishedState(PublishedState.Saved); + repository.AddOrUpdate(allCreated[i]); + } + unitOfWork.Commit(); + + //delete all xml + unitOfWork.Database.Execute("DELETE FROM cmsContentXml"); + Assert.AreEqual(0, unitOfWork.Database.ExecuteScalar("SELECT COUNT(*) FROM cmsContentXml")); + + repository.RebuildXmlStructures(media => new XElement("test"), 10); + + Assert.AreEqual(100, unitOfWork.Database.ExecuteScalar("SELECT COUNT(*) FROM cmsContentXml")); + } + } + + [Test] + public void Rebuild_All_Xml_Structures() + { + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + ContentTypeRepository contentTypeRepository; + using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) + { + + var contentType1 = MockedContentTypes.CreateSimpleContentType("Textpage1", "Textpage1"); + contentTypeRepository.AddOrUpdate(contentType1); + var allCreated = new List(); + + for (var i = 0; i < 100; i++) + { + //These will be non-published so shouldn't show up + var c1 = MockedContent.CreateSimpleContent(contentType1); + repository.AddOrUpdate(c1); + allCreated.Add(c1); + } + for (var i = 0; i < 100; i++) + { + var c1 = MockedContent.CreateSimpleContent(contentType1); + c1.ChangePublishedState(PublishedState.Published); + repository.AddOrUpdate(c1); + allCreated.Add(c1); + } + unitOfWork.Commit(); + + //now create some versions of this content - this shouldn't affect the xml structures saved + for (int i = 0; i < allCreated.Count; i++) + { + allCreated[i].Name = "blah" + i; + repository.AddOrUpdate(allCreated[i]); + } + unitOfWork.Commit(); + + //delete all xml + unitOfWork.Database.Execute("DELETE FROM cmsContentXml"); + Assert.AreEqual(0, unitOfWork.Database.ExecuteScalar("SELECT COUNT(*) FROM cmsContentXml")); + + repository.RebuildXmlStructures(media => new XElement("test"), 10); + + Assert.AreEqual(100, unitOfWork.Database.ExecuteScalar("SELECT COUNT(*) FROM cmsContentXml")); + } + } + + [Test] + public void Rebuild_All_Xml_Structures_For_Content_Type() + { + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + ContentTypeRepository contentTypeRepository; + using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) + { + var contentType1 = MockedContentTypes.CreateSimpleContentType("Textpage1", "Textpage1"); + var contentType2 = MockedContentTypes.CreateSimpleContentType("Textpage2", "Textpage2"); + var contentType3 = MockedContentTypes.CreateSimpleContentType("Textpage3", "Textpage3"); + contentTypeRepository.AddOrUpdate(contentType1); + contentTypeRepository.AddOrUpdate(contentType2); + contentTypeRepository.AddOrUpdate(contentType3); + + var allCreated = new List(); + + for (var i = 0; i < 30; i++) + { + //These will be non-published so shouldn't show up + var c1 = MockedContent.CreateSimpleContent(contentType1); + repository.AddOrUpdate(c1); + allCreated.Add(c1); + } + for (var i = 0; i < 30; i++) + { + var c1 = MockedContent.CreateSimpleContent(contentType1); + c1.ChangePublishedState(PublishedState.Published); + repository.AddOrUpdate(c1); + allCreated.Add(c1); + } + for (var i = 0; i < 30; i++) + { + var c1 = MockedContent.CreateSimpleContent(contentType2); + c1.ChangePublishedState(PublishedState.Published); + repository.AddOrUpdate(c1); + allCreated.Add(c1); + } + for (var i = 0; i < 30; i++) + { + var c1 = MockedContent.CreateSimpleContent(contentType3); + c1.ChangePublishedState(PublishedState.Published); + repository.AddOrUpdate(c1); + allCreated.Add(c1); + } + unitOfWork.Commit(); + + //now create some versions of this content - this shouldn't affect the xml structures saved + for (int i = 0; i < allCreated.Count; i++) + { + allCreated[i].Name = "blah" + i; + repository.AddOrUpdate(allCreated[i]); + } + unitOfWork.Commit(); + + //delete all xml + unitOfWork.Database.Execute("DELETE FROM cmsContentXml"); + Assert.AreEqual(0, unitOfWork.Database.ExecuteScalar("SELECT COUNT(*) FROM cmsContentXml")); + + repository.RebuildXmlStructures(media => new XElement("test"), 10, contentTypeIds: new[] { contentType1.Id, contentType2.Id }); + + Assert.AreEqual(60, unitOfWork.Database.ExecuteScalar("SELECT COUNT(*) FROM cmsContentXml")); + } + } + + [Test] + public void Ensures_Permissions_Are_Set_If_Parent_Entity_Permissions_Exist() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + + ContentTypeRepository contentTypeRepository; + using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) + { + var contentType = MockedContentTypes.CreateSimpleContentType("umbTextpage1", "Textpage"); + contentType.AllowedContentTypes = new List + { + new ContentTypeSort(new Lazy(() => contentType.Id), 0, contentType.Alias) + }; + var parentPage = MockedContent.CreateSimpleContent(contentType); + contentTypeRepository.AddOrUpdate(contentType); + repository.AddOrUpdate(parentPage); + unitOfWork.Commit(); + + // Act + repository.AssignEntityPermission(parentPage, 'A', new int[] { 0 }); + var childPage = MockedContent.CreateSimpleContent(contentType, "child", parentPage); + repository.AddOrUpdate(childPage); + unitOfWork.Commit(); + + // Assert + var permissions = repository.GetPermissionsForEntity(childPage.Id); + Assert.AreEqual(1, permissions.Count()); + Assert.AreEqual("A", permissions.Single().AssignedPermissions.First()); + } + + } + + [Test] + public void Can_Perform_Add_On_ContentRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + ContentTypeRepository contentTypeRepository; + using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) + { + ContentType contentType = MockedContentTypes.CreateSimpleContentType("umbTextpage2", "Textpage"); + Content textpage = MockedContent.CreateSimpleContent(contentType); + + // Act + contentTypeRepository.AddOrUpdate(contentType); + repository.AddOrUpdate(textpage); + unitOfWork.Commit(); + + // Assert + Assert.That(contentType.HasIdentity, Is.True); + Assert.That(textpage.HasIdentity, Is.True); + + } + } + + [Test] + public void Can_Perform_Add_With_Default_Template() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + ContentTypeRepository contentTypeRepository; + TemplateRepository templateRepository; + using (var repository = CreateRepository(unitOfWork, out contentTypeRepository, out templateRepository)) + { + var template = new Template("hello", "hello"); + templateRepository.AddOrUpdate(template); + unitOfWork.Commit(); + + ContentType contentType = MockedContentTypes.CreateSimpleContentType("umbTextpage2", "Textpage"); + contentType.AllowedTemplates = Enumerable.Empty(); // because CreateSimple... assigns one + contentType.SetDefaultTemplate(template); + Content textpage = MockedContent.CreateSimpleContent(contentType); + + // Act + + contentTypeRepository.AddOrUpdate(contentType); + repository.AddOrUpdate(textpage); + unitOfWork.Commit(); + + // Assert + Assert.That(textpage.Template, Is.Not.Null); + Assert.That(textpage.Template, Is.EqualTo(contentType.DefaultTemplate)); + } + } + + //Covers issue U4-2791 and U4-2607 + [Test] + public void Can_Save_Content_With_AtSign_In_Name_On_ContentRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + ContentTypeRepository contentTypeRepository; + using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) + { + var contentType = MockedContentTypes.CreateSimpleContentType("umbTextpage1", "Textpage"); + contentTypeRepository.AddOrUpdate(contentType); + unitOfWork.Commit(); + + var textpage = MockedContent.CreateSimpleContent(contentType, "test@umbraco.org", -1); + var anotherTextpage = MockedContent.CreateSimpleContent(contentType, "@lightgiants", -1); + + // Act + + repository.AddOrUpdate(textpage); + repository.AddOrUpdate(anotherTextpage); + unitOfWork.Commit(); + + // Assert + Assert.That(contentType.HasIdentity, Is.True); + Assert.That(textpage.HasIdentity, Is.True); + + var content = repository.Get(textpage.Id); + Assert.That(content.Name, Is.EqualTo(textpage.Name)); + + var content2 = repository.Get(anotherTextpage.Id); + Assert.That(content2.Name, Is.EqualTo(anotherTextpage.Name)); + } + } + + [Test] + public void Can_Perform_Multiple_Adds_On_ContentRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + ContentTypeRepository contentTypeRepository; + using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) + { + ContentType contentType = MockedContentTypes.CreateSimpleContentType("umbTextpage1", "Textpage"); + Content textpage = MockedContent.CreateSimpleContent(contentType); + + // Act + contentTypeRepository.AddOrUpdate(contentType); + repository.AddOrUpdate(textpage); + unitOfWork.Commit(); + + Content subpage = MockedContent.CreateSimpleContent(contentType, "Text Page 1", textpage.Id); + repository.AddOrUpdate(subpage); + unitOfWork.Commit(); + + // Assert + Assert.That(contentType.HasIdentity, Is.True); + Assert.That(textpage.HasIdentity, Is.True); + Assert.That(subpage.HasIdentity, Is.True); + Assert.That(textpage.Id, Is.EqualTo(subpage.ParentId)); + } + + } + + + [Test] + public void Can_Verify_Fresh_Entity_Is_Not_Dirty() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + ContentTypeRepository contentTypeRepository; + using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) + { + // Act + var content = repository.Get(NodeDto.NodeIdSeed + 3); + bool dirty = ((Content)content).IsDirty(); + + // Assert + Assert.That(dirty, Is.False); + } + } + + [Test] + public void Can_Perform_Update_On_ContentRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + ContentTypeRepository contentTypeRepository; + using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) + { + // Act + var content = repository.Get(NodeDto.NodeIdSeed + 2); + content.Name = "About 2"; + repository.AddOrUpdate(content); + unitOfWork.Commit(); + var updatedContent = repository.Get(NodeDto.NodeIdSeed + 2); + + // Assert + Assert.That(updatedContent.Id, Is.EqualTo(content.Id)); + Assert.That(updatedContent.Name, Is.EqualTo(content.Name)); + } + + } + + [Test] + public void Can_Update_With_Null_Template() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + ContentTypeRepository contentTypeRepository; + using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) + { + // Act + var content = repository.Get(NodeDto.NodeIdSeed + 2); + content.Template = null; + repository.AddOrUpdate(content); + unitOfWork.Commit(); + var updatedContent = repository.Get(NodeDto.NodeIdSeed + 2); + + // Assert + Assert.That(updatedContent.Template, Is.Null); + } + + } + + [Test] + public void Can_Perform_Delete_On_ContentRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + ContentTypeRepository contentTypeRepository; + using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) + { + var contentType = contentTypeRepository.Get(NodeDto.NodeIdSeed); + var content = new Content("Textpage 2 Child Node", NodeDto.NodeIdSeed + 3, contentType); + content.CreatorId = 0; + content.WriterId = 0; + + // Act + repository.AddOrUpdate(content); + unitOfWork.Commit(); + var id = content.Id; + + repository.Delete(content); + unitOfWork.Commit(); + + var content1 = repository.Get(id); + + // Assert + Assert.That(content1, Is.Null); + } + } + + [Test] + public void Can_Perform_Get_On_ContentRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + ContentTypeRepository contentTypeRepository; + using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) + { + // Act + var content = repository.Get(NodeDto.NodeIdSeed + 3); + + // Assert + Assert.That(content.Id, Is.EqualTo(NodeDto.NodeIdSeed + 3)); + Assert.That(content.CreateDate, Is.GreaterThan(DateTime.MinValue)); + Assert.That(content.UpdateDate, Is.GreaterThan(DateTime.MinValue)); + Assert.That(content.ParentId, Is.Not.EqualTo(0)); + Assert.That(content.Name, Is.EqualTo("Text Page 2")); + //Assert.That(content.SortOrder, Is.EqualTo(1)); + Assert.That(content.Version, Is.Not.EqualTo(Guid.Empty)); + Assert.That(content.ContentTypeId, Is.EqualTo(NodeDto.NodeIdSeed)); + Assert.That(content.Path, Is.Not.Empty); + Assert.That(content.Properties.Any(), Is.True); + } + } + + [Test] + public void Can_Perform_GetByQuery_On_ContentRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + ContentTypeRepository contentTypeRepository; + using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) + { + // Act + var query = Query.Builder.Where(x => x.Level == 2); + var result = repository.GetByQuery(query); + + // Assert + Assert.That(result.Count(), Is.GreaterThanOrEqualTo(2)); + } + } + + [Test] + public void Can_Perform_Get_All_With_Many_Version() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + ContentTypeRepository contentTypeRepository; + using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) + { + var result = repository.GetAll().ToArray(); + foreach (var content in result) + { + content.ChangePublishedState(PublishedState.Saved); + repository.AddOrUpdate(content); + } + unitOfWork.Commit(); + foreach (var content in result) + { + content.ChangePublishedState(PublishedState.Published); + repository.AddOrUpdate(content); + } + unitOfWork.Commit(); + + //re-get + + var result2 = repository.GetAll().ToArray(); + + Assert.AreEqual(result.Count(), result2.Count()); + } + } + + [Test] + public void Can_Perform_GetPagedResultsByQuery_ForFirstPage_On_ContentRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + ContentTypeRepository contentTypeRepository; + using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) + { + // Act + var query = Query.Builder.Where(x => x.Level == 2); + long totalRecords; + var result = repository.GetPagedResultsByQuery(query, 0, 1, out totalRecords, "Name", Direction.Ascending, orderBySystemField: true); + + // Assert + Assert.That(totalRecords, Is.GreaterThanOrEqualTo(2)); + Assert.That(result.Count(), Is.EqualTo(1)); + Assert.That(result.First().Name, Is.EqualTo("Text Page 1")); + } + } + + [Test] + public void Can_Perform_GetPagedResultsByQuery_ForSecondPage_On_ContentRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + ContentTypeRepository contentTypeRepository; + using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) + { + // Act + var query = Query.Builder.Where(x => x.Level == 2); + long totalRecords; + var result = repository.GetPagedResultsByQuery(query, 1, 1, out totalRecords, "Name", Direction.Ascending, orderBySystemField: true); + + // Assert + Assert.That(totalRecords, Is.GreaterThanOrEqualTo(2)); + Assert.That(result.Count(), Is.EqualTo(1)); + Assert.That(result.First().Name, Is.EqualTo("Text Page 2")); + } + } + + [Test] + public void Can_Perform_GetPagedResultsByQuery_WithSinglePage_On_ContentRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + ContentTypeRepository contentTypeRepository; + using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) + { + // Act + var query = Query.Builder.Where(x => x.Level == 2); + long totalRecords; + var result = repository.GetPagedResultsByQuery(query, 0, 2, out totalRecords, "Name", Direction.Ascending, orderBySystemField: true); + + // Assert + Assert.That(totalRecords, Is.GreaterThanOrEqualTo(2)); + Assert.That(result.Count(), Is.EqualTo(2)); + Assert.That(result.First().Name, Is.EqualTo("Text Page 1")); + } + } + + [Test] + public void Can_Perform_GetPagedResultsByQuery_WithDescendingOrder_On_ContentRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + ContentTypeRepository contentTypeRepository; + using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) + { + // Act + var query = Query.Builder.Where(x => x.Level == 2); + long totalRecords; + var result = repository.GetPagedResultsByQuery(query, 0, 1, out totalRecords, "Name", Direction.Descending, orderBySystemField: true); + + // Assert + Assert.That(totalRecords, Is.GreaterThanOrEqualTo(2)); + Assert.That(result.Count(), Is.EqualTo(1)); + Assert.That(result.First().Name, Is.EqualTo("Text Page 2")); + } + } + + [Test] + public void Can_Perform_GetPagedResultsByQuery_WithFilterMatchingSome_On_ContentRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + ContentTypeRepository contentTypeRepository; + using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) + { + // Act + var query = Query.Builder.Where(x => x.Level == 2); + long totalRecords; + var result = repository.GetPagedResultsByQuery(query, 0, 1, out totalRecords, "Name", Direction.Ascending, true, "Page 2"); + + // Assert + Assert.That(totalRecords, Is.EqualTo(1)); + Assert.That(result.Count(), Is.EqualTo(1)); + Assert.That(result.First().Name, Is.EqualTo("Text Page 2")); + } + } + + [Test] + public void Can_Perform_GetPagedResultsByQuery_WithFilterMatchingAll_On_ContentRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + ContentTypeRepository contentTypeRepository; + using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) + { + // Act + var query = Query.Builder.Where(x => x.Level == 2); + long totalRecords; + var result = repository.GetPagedResultsByQuery(query, 0, 1, out totalRecords, "Name", Direction.Ascending, true, "Page"); + + // Assert + Assert.That(totalRecords, Is.EqualTo(2)); + Assert.That(result.Count(), Is.EqualTo(1)); + Assert.That(result.First().Name, Is.EqualTo("Text Page 1")); + } + } + + [Test] + public void Can_Perform_GetAll_By_Param_Ids_On_ContentRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + ContentTypeRepository contentTypeRepository; + using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) + { + // Act + var contents = repository.GetAll(NodeDto.NodeIdSeed + 2, NodeDto.NodeIdSeed + 3); + + // Assert + Assert.That(contents, Is.Not.Null); + Assert.That(contents.Any(), Is.True); + Assert.That(contents.Count(), Is.EqualTo(2)); + } + } + + [Test] + public void Can_Perform_GetAll_On_ContentRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + ContentTypeRepository contentTypeRepository; + using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) + { + // Act + var contents = repository.GetAll(); + + // Assert + Assert.That(contents, Is.Not.Null); + Assert.That(contents.Any(), Is.True); + Assert.That(contents.Count(), Is.GreaterThanOrEqualTo(4)); + } + + + } + + [Test] + public void Can_Perform_Exists_On_ContentRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + ContentTypeRepository contentTypeRepository; + using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) + { + // Act + var exists = repository.Exists(NodeDto.NodeIdSeed + 1); + + // Assert + Assert.That(exists, Is.True); + } + + + } + + [Test] + public void Can_Perform_Count_On_ContentRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + ContentTypeRepository contentTypeRepository; + using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) + { + // Act + int level = 2; + var query = Query.Builder.Where(x => x.Level == level); + var result = repository.Count(query); + + // Assert + Assert.That(result, Is.GreaterThanOrEqualTo(2)); + } + } + + [Test] + public void Can_Verify_Keys_Set() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + ContentTypeRepository contentTypeRepository; + using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) + { + // Act + var textpage = repository.Get(NodeDto.NodeIdSeed + 1); + var subpage = repository.Get(NodeDto.NodeIdSeed + 2); + var trashed = repository.Get(NodeDto.NodeIdSeed + 4); + + // Assert + Assert.That(textpage.Key.ToString().ToUpper(), Is.EqualTo("B58B3AD4-62C2-4E27-B1BE-837BD7C533E0")); + Assert.That(subpage.Key.ToString().ToUpper(), Is.EqualTo("FF11402B-7E53-4654-81A7-462AC2108059")); + Assert.That(trashed.Key, Is.Not.EqualTo(Guid.Empty)); + } + } + + [Test] + public void Can_Get_Content_By_Guid_Key() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + ContentTypeRepository contentTypeRepository; + using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) + { + // Act + var query = Query.Builder.Where(x => x.Key == new Guid("B58B3AD4-62C2-4E27-B1BE-837BD7C533E0")); + var content = repository.GetByQuery(query).SingleOrDefault(); + + // Assert + Assert.That(content, Is.Not.Null); + Assert.That(content.Id, Is.EqualTo(NodeDto.NodeIdSeed + 1)); + } + + } + + public void CreateTestData() + { + //Create and Save ContentType "umbTextpage" -> (NodeDto.NodeIdSeed) + ContentType contentType = MockedContentTypes.CreateSimpleContentType("umbTextpage", "Textpage"); + contentType.Key = new Guid("1D3A8E6E-2EA9-4CC1-B229-1AEE19821522"); + ServiceContext.ContentTypeService.Save(contentType); + + //Create and Save Content "Homepage" based on "umbTextpage" -> (NodeDto.NodeIdSeed + 1) + Content textpage = MockedContent.CreateSimpleContent(contentType); + textpage.Key = new Guid("B58B3AD4-62C2-4E27-B1BE-837BD7C533E0"); + ServiceContext.ContentService.Save(textpage, 0); + + //Create and Save Content "Text Page 1" based on "umbTextpage" -> (NodeDto.NodeIdSeed + 2) + Content subpage = MockedContent.CreateSimpleContent(contentType, "Text Page 1", textpage.Id); + subpage.Key = new Guid("FF11402B-7E53-4654-81A7-462AC2108059"); + ServiceContext.ContentService.Save(subpage, 0); + + //Create and Save Content "Text Page 1" based on "umbTextpage" -> (NodeDto.NodeIdSeed + 3) + Content subpage2 = MockedContent.CreateSimpleContent(contentType, "Text Page 2", textpage.Id); + ServiceContext.ContentService.Save(subpage2, 0); + + //Create and Save Content "Text Page Deleted" based on "umbTextpage" -> (NodeDto.NodeIdSeed + 4) + Content trashed = MockedContent.CreateSimpleContent(contentType, "Text Page Deleted", -20); + trashed.Trashed = true; + ServiceContext.ContentService.Save(trashed, 0); + } + } } \ No newline at end of file diff --git a/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs index 4ca987fe26..32258a9961 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs @@ -17,547 +17,547 @@ using Umbraco.Core.Persistence.DatabaseModelDefinitions; namespace Umbraco.Tests.Persistence.Repositories { - [DatabaseTestBehavior(DatabaseBehavior.NewDbFileAndSchemaPerTest)] - [TestFixture] - public class MediaRepositoryTest : BaseDatabaseFactoryTest - { - public MediaRepositoryTest() - { - } - - [SetUp] - public override void Initialize() - { - base.Initialize(); - - CreateTestData(); - } - - private MediaRepository CreateRepository(IDatabaseUnitOfWork unitOfWork, out MediaTypeRepository mediaTypeRepository) - { - mediaTypeRepository = new MediaTypeRepository(unitOfWork, CacheHelper, Mock.Of(), SqlSyntax); - var tagRepository = new TagRepository(unitOfWork, CacheHelper, Mock.Of(), SqlSyntax); - var repository = new MediaRepository(unitOfWork, CacheHelper, Mock.Of(), SqlSyntax, mediaTypeRepository, tagRepository, Mock.Of()); - return repository; - } - - [Test] - public void Rebuild_All_Xml_Structures() - { - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - MediaTypeRepository mediaTypeRepository; - using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository)) - { - - var mediaType = mediaTypeRepository.Get(1032); - - for (var i = 0; i < 100; i++) - { - var image = MockedMedia.CreateMediaImage(mediaType, -1); - repository.AddOrUpdate(image); - } - unitOfWork.Commit(); - - //delete all xml - unitOfWork.Database.Execute("DELETE FROM cmsContentXml"); - Assert.AreEqual(0, unitOfWork.Database.ExecuteScalar("SELECT COUNT(*) FROM cmsContentXml")); - - repository.RebuildXmlStructures(media => new XElement("test"), 10); - - Assert.AreEqual(103, unitOfWork.Database.ExecuteScalar("SELECT COUNT(*) FROM cmsContentXml")); - } - } - - [Test] - public void Rebuild_All_Xml_Structures_For_Content_Type() - { - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - MediaTypeRepository mediaTypeRepository; - using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository)) - { - - var imageMediaType = mediaTypeRepository.Get(1032); - var fileMediaType = mediaTypeRepository.Get(1033); - var folderMediaType = mediaTypeRepository.Get(1031); - - for (var i = 0; i < 30; i++) - { - var image = MockedMedia.CreateMediaImage(imageMediaType, -1); - repository.AddOrUpdate(image); - } - for (var i = 0; i < 30; i++) - { - var file = MockedMedia.CreateMediaFile(fileMediaType, -1); - repository.AddOrUpdate(file); - } - for (var i = 0; i < 30; i++) - { - var folder = MockedMedia.CreateMediaFolder(folderMediaType, -1); - repository.AddOrUpdate(folder); - } - unitOfWork.Commit(); - - //delete all xml - unitOfWork.Database.Execute("DELETE FROM cmsContentXml"); - Assert.AreEqual(0, unitOfWork.Database.ExecuteScalar("SELECT COUNT(*) FROM cmsContentXml")); - - repository.RebuildXmlStructures(media => new XElement("test"), 10, contentTypeIds: new[] { 1032, 1033 }); - - Assert.AreEqual(62, unitOfWork.Database.ExecuteScalar("SELECT COUNT(*) FROM cmsContentXml")); - } - } - - [Test] - public void Can_Perform_Add_On_MediaRepository() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - MediaTypeRepository mediaTypeRepository; - using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository)) - { - - var mediaType = mediaTypeRepository.Get(1032); - var image = MockedMedia.CreateMediaImage(mediaType, -1); - - // Act - mediaTypeRepository.AddOrUpdate(mediaType); - repository.AddOrUpdate(image); - unitOfWork.Commit(); - - // Assert - Assert.That(mediaType.HasIdentity, Is.True); - Assert.That(image.HasIdentity, Is.True); - } - } - - [Test] - public void Can_Perform_Multiple_Adds_On_MediaRepository() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - MediaTypeRepository mediaTypeRepository; - using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository)) - { - - var mediaType = mediaTypeRepository.Get(1032); - var file = MockedMedia.CreateMediaFile(mediaType, -1); - - // Act - repository.AddOrUpdate(file); - unitOfWork.Commit(); - - var image = MockedMedia.CreateMediaImage(mediaType, -1); - repository.AddOrUpdate(image); - unitOfWork.Commit(); - - // Assert - Assert.That(file.HasIdentity, Is.True); - Assert.That(image.HasIdentity, Is.True); - Assert.That(file.Name, Is.EqualTo("Test File")); - Assert.That(image.Name, Is.EqualTo("Test Image")); - Assert.That(file.ContentTypeId, Is.EqualTo(mediaType.Id)); - Assert.That(image.ContentTypeId, Is.EqualTo(mediaType.Id)); - } - } - - [Test] - public void Can_Perform_Multiple_Adds_On_MediaRepository_With_RepositoryResolver() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - MediaTypeRepository mediaTypeRepository; - using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository)) - { - - var mediaType = mediaTypeRepository.Get(1032); - var file = MockedMedia.CreateMediaFile(mediaType, -1); - - // Act - repository.AddOrUpdate(file); - unitOfWork.Commit(); - - var image = MockedMedia.CreateMediaImage(mediaType, -1); - repository.AddOrUpdate(image); - unitOfWork.Commit(); - - // Assert - Assert.That(file.HasIdentity, Is.True); - Assert.That(image.HasIdentity, Is.True); - Assert.That(file.Name, Is.EqualTo("Test File")); - Assert.That(image.Name, Is.EqualTo("Test Image")); - Assert.That(file.ContentTypeId, Is.EqualTo(mediaType.Id)); - Assert.That(image.ContentTypeId, Is.EqualTo(mediaType.Id)); - } - } - - [Test] - public void Can_Verify_Fresh_Entity_Is_Not_Dirty() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - MediaTypeRepository mediaTypeRepository; - using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository)) - { - - // Act - var media = repository.Get(NodeDto.NodeIdSeed + 1); - bool dirty = ((ICanBeDirty)media).IsDirty(); - - // Assert - Assert.That(dirty, Is.False); - } - } - - [Test] - public void Can_Perform_Update_On_MediaRepository() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - MediaTypeRepository mediaTypeRepository; - using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository)) - { - - // Act - var content = repository.Get(NodeDto.NodeIdSeed + 2); - content.Name = "Test File Updated"; - repository.AddOrUpdate(content); - unitOfWork.Commit(); - - var updatedContent = repository.Get(NodeDto.NodeIdSeed + 2); - - // Assert - Assert.That(updatedContent.Id, Is.EqualTo(content.Id)); - Assert.That(updatedContent.Name, Is.EqualTo(content.Name)); - } - } - - [Test] - public void Can_Perform_Delete_On_MediaRepository() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - MediaTypeRepository mediaTypeRepository; - using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository)) - { - - // Act - var media = repository.Get(NodeDto.NodeIdSeed + 2); - repository.Delete(media); - unitOfWork.Commit(); - - var deleted = repository.Get(NodeDto.NodeIdSeed + 2); - var exists = repository.Exists(NodeDto.NodeIdSeed + 2); - - // Assert - Assert.That(deleted, Is.Null); - Assert.That(exists, Is.False); - } - } - - [Test] - public void Can_Perform_Get_On_MediaRepository() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - MediaTypeRepository mediaTypeRepository; - using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository)) - { - - // Act - var media = repository.Get(NodeDto.NodeIdSeed + 1); - - // Assert - Assert.That(media.Id, Is.EqualTo(NodeDto.NodeIdSeed + 1)); - Assert.That(media.CreateDate, Is.GreaterThan(DateTime.MinValue)); - Assert.That(media.UpdateDate, Is.GreaterThan(DateTime.MinValue)); - Assert.That(media.ParentId, Is.Not.EqualTo(0)); - Assert.That(media.Name, Is.EqualTo("Test Image")); - Assert.That(media.SortOrder, Is.EqualTo(0)); - Assert.That(media.Version, Is.Not.EqualTo(Guid.Empty)); - Assert.That(media.ContentTypeId, Is.EqualTo(1032)); - Assert.That(media.Path, Is.Not.Empty); - Assert.That(media.Properties.Any(), Is.True); - } - } - - [Test] - public void Can_Perform_GetByQuery_On_MediaRepository() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - MediaTypeRepository mediaTypeRepository; - using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository)) - { - // Act - var query = Query.Builder.Where(x => x.Level == 2); - var result = repository.GetByQuery(query); - - // Assert - Assert.That(result.Count(), Is.GreaterThanOrEqualTo(2)); //There should be two entities on level 2: File and Media - } - } - - [Test] - public void Can_Perform_GetPagedResultsByQuery_ForFirstPage_On_MediaRepository() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - MediaTypeRepository mediaTypeRepository; - using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository)) - { - // Act - var query = Query.Builder.Where(x => x.Level == 2); - long totalRecords; - var result = repository.GetPagedResultsByQuery(query, 0, 1, out totalRecords, "SortOrder", Direction.Ascending, orderBySystemField: true); - - // Assert - Assert.That(totalRecords, Is.GreaterThanOrEqualTo(2)); - Assert.That(result.Count(), Is.EqualTo(1)); - Assert.That(result.First().Name, Is.EqualTo("Test Image")); - } - } - - [Test] - public void Can_Perform_GetPagedResultsByQuery_ForSecondPage_On_MediaRepository() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - MediaTypeRepository mediaTypeRepository; - using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository)) - { - // Act - var query = Query.Builder.Where(x => x.Level == 2); - long totalRecords; - var result = repository.GetPagedResultsByQuery(query, 1, 1, out totalRecords, "SortOrder", Direction.Ascending, orderBySystemField: true); - - // Assert - Assert.That(totalRecords, Is.GreaterThanOrEqualTo(2)); - Assert.That(result.Count(), Is.EqualTo(1)); - Assert.That(result.First().Name, Is.EqualTo("Test File")); - } - } - - [Test] - public void Can_Perform_GetPagedResultsByQuery_WithSinglePage_On_MediaRepository() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - MediaTypeRepository mediaTypeRepository; - using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository)) - { - // Act - var query = Query.Builder.Where(x => x.Level == 2); - long totalRecords; - var result = repository.GetPagedResultsByQuery(query, 0, 2, out totalRecords, "SortOrder", Direction.Ascending, orderBySystemField: true); - - // Assert - Assert.That(totalRecords, Is.GreaterThanOrEqualTo(2)); - Assert.That(result.Count(), Is.EqualTo(2)); - Assert.That(result.First().Name, Is.EqualTo("Test Image")); - } - } - - [Test] - public void Can_Perform_GetPagedResultsByQuery_WithDescendingOrder_On_MediaRepository() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - MediaTypeRepository mediaTypeRepository; - using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository)) - { - // Act - var query = Query.Builder.Where(x => x.Level == 2); - long totalRecords; - var result = repository.GetPagedResultsByQuery(query, 0, 1, out totalRecords, "SortOrder", Direction.Descending, orderBySystemField: true); - - // Assert - Assert.That(totalRecords, Is.GreaterThanOrEqualTo(2)); - Assert.That(result.Count(), Is.EqualTo(1)); - Assert.That(result.First().Name, Is.EqualTo("Test File")); - } - } - - [Test] - public void Can_Perform_GetPagedResultsByQuery_WitAlternateOrder_On_MediaRepository() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - MediaTypeRepository mediaTypeRepository; - using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository)) - { - // Act - var query = Query.Builder.Where(x => x.Level == 2); - long totalRecords; - var result = repository.GetPagedResultsByQuery(query, 0, 1, out totalRecords, "Name", Direction.Ascending, orderBySystemField: true); - - // Assert - Assert.That(totalRecords, Is.GreaterThanOrEqualTo(2)); - Assert.That(result.Count(), Is.EqualTo(1)); - Assert.That(result.First().Name, Is.EqualTo("Test File")); - } - } - - [Test] - public void Can_Perform_GetPagedResultsByQuery_WithFilterMatchingSome_On_MediaRepository() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - MediaTypeRepository mediaTypeRepository; - using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository)) - { - // Act - var query = Query.Builder.Where(x => x.Level == 2); - long totalRecords; - var result = repository.GetPagedResultsByQuery(query, 0, 1, out totalRecords, "SortOrder", Direction.Ascending, true, "File"); - - // Assert - Assert.That(totalRecords, Is.EqualTo(1)); - Assert.That(result.Count(), Is.EqualTo(1)); - Assert.That(result.First().Name, Is.EqualTo("Test File")); - } - } - - [Test] - public void Can_Perform_GetPagedResultsByQuery_WithFilterMatchingAll_On_MediaRepository() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - MediaTypeRepository mediaTypeRepository; - using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository)) - { - // Act - var query = Query.Builder.Where(x => x.Level == 2); - long totalRecords; - var result = repository.GetPagedResultsByQuery(query, 0, 1, out totalRecords, "SortOrder", Direction.Ascending, true, "Test"); - - // Assert - Assert.That(totalRecords, Is.EqualTo(2)); - Assert.That(result.Count(), Is.EqualTo(1)); - Assert.That(result.First().Name, Is.EqualTo("Test Image")); - } - } - - [Test] - public void Can_Perform_GetAll_By_Param_Ids_On_MediaRepository() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - MediaTypeRepository mediaTypeRepository; - using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository)) - { - - // Act - var medias = repository.GetAll(NodeDto.NodeIdSeed + 1, NodeDto.NodeIdSeed + 2); - - // Assert - Assert.That(medias, Is.Not.Null); - Assert.That(medias.Any(), Is.True); - Assert.That(medias.Count(), Is.EqualTo(2)); - } - } - - [Test] - public void Can_Perform_GetAll_On_MediaRepository() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - MediaTypeRepository mediaTypeRepository; - using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository)) - { - - // Act - var medias = repository.GetAll(); - - // Assert - Assert.That(medias, Is.Not.Null); - Assert.That(medias.Any(), Is.True); - Assert.That(medias.Count(), Is.GreaterThanOrEqualTo(3)); - } - } - - [Test] - public void Can_Perform_Exists_On_MediaRepository() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - MediaTypeRepository mediaTypeRepository; - using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository)) - { - - // Act - var exists = repository.Exists(NodeDto.NodeIdSeed + 1); - var existsToo = repository.Exists(NodeDto.NodeIdSeed + 1); - var doesntExists = repository.Exists(NodeDto.NodeIdSeed + 5); - - // Assert - Assert.That(exists, Is.True); - Assert.That(existsToo, Is.True); - Assert.That(doesntExists, Is.False); - } - } - - [Test] - public void Can_Perform_Count_On_MediaRepository() - { - // Arrange - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - MediaTypeRepository mediaTypeRepository; - using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository)) - { - - // Act - int level = 2; - var query = Query.Builder.Where(x => x.Level == level); - var result = repository.Count(query); - - // Assert - Assert.That(result, Is.GreaterThanOrEqualTo(2)); - } - } - - [TearDown] - public override void TearDown() - { - base.TearDown(); - } - - public void CreateTestData() - { - //Create and Save folder-Media -> (NodeDto.NodeIdSeed) - var folderMediaType = ServiceContext.ContentTypeService.GetMediaType(1031); - var folder = MockedMedia.CreateMediaFolder(folderMediaType, -1); - ServiceContext.MediaService.Save(folder, 0); - - //Create and Save image-Media -> (NodeDto.NodeIdSeed + 1) - var imageMediaType = ServiceContext.ContentTypeService.GetMediaType(1032); - var image = MockedMedia.CreateMediaImage(imageMediaType, folder.Id); - ServiceContext.MediaService.Save(image, 0); - - //Create and Save file-Media -> (NodeDto.NodeIdSeed + 2) - var fileMediaType = ServiceContext.ContentTypeService.GetMediaType(1033); - var file = MockedMedia.CreateMediaFile(fileMediaType, folder.Id); - ServiceContext.MediaService.Save(file, 0); - } - } + [DatabaseTestBehavior(DatabaseBehavior.NewDbFileAndSchemaPerTest)] + [TestFixture] + public class MediaRepositoryTest : BaseDatabaseFactoryTest + { + public MediaRepositoryTest() + { + } + + [SetUp] + public override void Initialize() + { + base.Initialize(); + + CreateTestData(); + } + + private MediaRepository CreateRepository(IDatabaseUnitOfWork unitOfWork, out MediaTypeRepository mediaTypeRepository) + { + mediaTypeRepository = new MediaTypeRepository(unitOfWork, CacheHelper, Mock.Of(), SqlSyntax); + var tagRepository = new TagRepository(unitOfWork, CacheHelper, Mock.Of(), SqlSyntax); + var repository = new MediaRepository(unitOfWork, CacheHelper, Mock.Of(), SqlSyntax, mediaTypeRepository, tagRepository, Mock.Of()); + return repository; + } + + [Test] + public void Rebuild_All_Xml_Structures() + { + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + MediaTypeRepository mediaTypeRepository; + using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository)) + { + + var mediaType = mediaTypeRepository.Get(1032); + + for (var i = 0; i < 100; i++) + { + var image = MockedMedia.CreateMediaImage(mediaType, -1); + repository.AddOrUpdate(image); + } + unitOfWork.Commit(); + + //delete all xml + unitOfWork.Database.Execute("DELETE FROM cmsContentXml"); + Assert.AreEqual(0, unitOfWork.Database.ExecuteScalar("SELECT COUNT(*) FROM cmsContentXml")); + + repository.RebuildXmlStructures(media => new XElement("test"), 10); + + Assert.AreEqual(103, unitOfWork.Database.ExecuteScalar("SELECT COUNT(*) FROM cmsContentXml")); + } + } + + [Test] + public void Rebuild_All_Xml_Structures_For_Content_Type() + { + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + MediaTypeRepository mediaTypeRepository; + using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository)) + { + + var imageMediaType = mediaTypeRepository.Get(1032); + var fileMediaType = mediaTypeRepository.Get(1033); + var folderMediaType = mediaTypeRepository.Get(1031); + + for (var i = 0; i < 30; i++) + { + var image = MockedMedia.CreateMediaImage(imageMediaType, -1); + repository.AddOrUpdate(image); + } + for (var i = 0; i < 30; i++) + { + var file = MockedMedia.CreateMediaFile(fileMediaType, -1); + repository.AddOrUpdate(file); + } + for (var i = 0; i < 30; i++) + { + var folder = MockedMedia.CreateMediaFolder(folderMediaType, -1); + repository.AddOrUpdate(folder); + } + unitOfWork.Commit(); + + //delete all xml + unitOfWork.Database.Execute("DELETE FROM cmsContentXml"); + Assert.AreEqual(0, unitOfWork.Database.ExecuteScalar("SELECT COUNT(*) FROM cmsContentXml")); + + repository.RebuildXmlStructures(media => new XElement("test"), 10, contentTypeIds: new[] { 1032, 1033 }); + + Assert.AreEqual(62, unitOfWork.Database.ExecuteScalar("SELECT COUNT(*) FROM cmsContentXml")); + } + } + + [Test] + public void Can_Perform_Add_On_MediaRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + MediaTypeRepository mediaTypeRepository; + using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository)) + { + + var mediaType = mediaTypeRepository.Get(1032); + var image = MockedMedia.CreateMediaImage(mediaType, -1); + + // Act + mediaTypeRepository.AddOrUpdate(mediaType); + repository.AddOrUpdate(image); + unitOfWork.Commit(); + + // Assert + Assert.That(mediaType.HasIdentity, Is.True); + Assert.That(image.HasIdentity, Is.True); + } + } + + [Test] + public void Can_Perform_Multiple_Adds_On_MediaRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + MediaTypeRepository mediaTypeRepository; + using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository)) + { + + var mediaType = mediaTypeRepository.Get(1032); + var file = MockedMedia.CreateMediaFile(mediaType, -1); + + // Act + repository.AddOrUpdate(file); + unitOfWork.Commit(); + + var image = MockedMedia.CreateMediaImage(mediaType, -1); + repository.AddOrUpdate(image); + unitOfWork.Commit(); + + // Assert + Assert.That(file.HasIdentity, Is.True); + Assert.That(image.HasIdentity, Is.True); + Assert.That(file.Name, Is.EqualTo("Test File")); + Assert.That(image.Name, Is.EqualTo("Test Image")); + Assert.That(file.ContentTypeId, Is.EqualTo(mediaType.Id)); + Assert.That(image.ContentTypeId, Is.EqualTo(mediaType.Id)); + } + } + + [Test] + public void Can_Perform_Multiple_Adds_On_MediaRepository_With_RepositoryResolver() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + MediaTypeRepository mediaTypeRepository; + using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository)) + { + + var mediaType = mediaTypeRepository.Get(1032); + var file = MockedMedia.CreateMediaFile(mediaType, -1); + + // Act + repository.AddOrUpdate(file); + unitOfWork.Commit(); + + var image = MockedMedia.CreateMediaImage(mediaType, -1); + repository.AddOrUpdate(image); + unitOfWork.Commit(); + + // Assert + Assert.That(file.HasIdentity, Is.True); + Assert.That(image.HasIdentity, Is.True); + Assert.That(file.Name, Is.EqualTo("Test File")); + Assert.That(image.Name, Is.EqualTo("Test Image")); + Assert.That(file.ContentTypeId, Is.EqualTo(mediaType.Id)); + Assert.That(image.ContentTypeId, Is.EqualTo(mediaType.Id)); + } + } + + [Test] + public void Can_Verify_Fresh_Entity_Is_Not_Dirty() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + MediaTypeRepository mediaTypeRepository; + using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository)) + { + + // Act + var media = repository.Get(NodeDto.NodeIdSeed + 1); + bool dirty = ((ICanBeDirty)media).IsDirty(); + + // Assert + Assert.That(dirty, Is.False); + } + } + + [Test] + public void Can_Perform_Update_On_MediaRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + MediaTypeRepository mediaTypeRepository; + using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository)) + { + + // Act + var content = repository.Get(NodeDto.NodeIdSeed + 2); + content.Name = "Test File Updated"; + repository.AddOrUpdate(content); + unitOfWork.Commit(); + + var updatedContent = repository.Get(NodeDto.NodeIdSeed + 2); + + // Assert + Assert.That(updatedContent.Id, Is.EqualTo(content.Id)); + Assert.That(updatedContent.Name, Is.EqualTo(content.Name)); + } + } + + [Test] + public void Can_Perform_Delete_On_MediaRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + MediaTypeRepository mediaTypeRepository; + using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository)) + { + + // Act + var media = repository.Get(NodeDto.NodeIdSeed + 2); + repository.Delete(media); + unitOfWork.Commit(); + + var deleted = repository.Get(NodeDto.NodeIdSeed + 2); + var exists = repository.Exists(NodeDto.NodeIdSeed + 2); + + // Assert + Assert.That(deleted, Is.Null); + Assert.That(exists, Is.False); + } + } + + [Test] + public void Can_Perform_Get_On_MediaRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + MediaTypeRepository mediaTypeRepository; + using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository)) + { + + // Act + var media = repository.Get(NodeDto.NodeIdSeed + 1); + + // Assert + Assert.That(media.Id, Is.EqualTo(NodeDto.NodeIdSeed + 1)); + Assert.That(media.CreateDate, Is.GreaterThan(DateTime.MinValue)); + Assert.That(media.UpdateDate, Is.GreaterThan(DateTime.MinValue)); + Assert.That(media.ParentId, Is.Not.EqualTo(0)); + Assert.That(media.Name, Is.EqualTo("Test Image")); + Assert.That(media.SortOrder, Is.EqualTo(0)); + Assert.That(media.Version, Is.Not.EqualTo(Guid.Empty)); + Assert.That(media.ContentTypeId, Is.EqualTo(1032)); + Assert.That(media.Path, Is.Not.Empty); + Assert.That(media.Properties.Any(), Is.True); + } + } + + [Test] + public void Can_Perform_GetByQuery_On_MediaRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + MediaTypeRepository mediaTypeRepository; + using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository)) + { + // Act + var query = Query.Builder.Where(x => x.Level == 2); + var result = repository.GetByQuery(query); + + // Assert + Assert.That(result.Count(), Is.GreaterThanOrEqualTo(2)); //There should be two entities on level 2: File and Media + } + } + + [Test] + public void Can_Perform_GetPagedResultsByQuery_ForFirstPage_On_MediaRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + MediaTypeRepository mediaTypeRepository; + using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository)) + { + // Act + var query = Query.Builder.Where(x => x.Level == 2); + long totalRecords; + var result = repository.GetPagedResultsByQuery(query, 0, 1, out totalRecords, "SortOrder", Direction.Ascending, orderBySystemField: true); + + // Assert + Assert.That(totalRecords, Is.GreaterThanOrEqualTo(2)); + Assert.That(result.Count(), Is.EqualTo(1)); + Assert.That(result.First().Name, Is.EqualTo("Test Image")); + } + } + + [Test] + public void Can_Perform_GetPagedResultsByQuery_ForSecondPage_On_MediaRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + MediaTypeRepository mediaTypeRepository; + using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository)) + { + // Act + var query = Query.Builder.Where(x => x.Level == 2); + long totalRecords; + var result = repository.GetPagedResultsByQuery(query, 1, 1, out totalRecords, "SortOrder", Direction.Ascending, orderBySystemField: true); + + // Assert + Assert.That(totalRecords, Is.GreaterThanOrEqualTo(2)); + Assert.That(result.Count(), Is.EqualTo(1)); + Assert.That(result.First().Name, Is.EqualTo("Test File")); + } + } + + [Test] + public void Can_Perform_GetPagedResultsByQuery_WithSinglePage_On_MediaRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + MediaTypeRepository mediaTypeRepository; + using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository)) + { + // Act + var query = Query.Builder.Where(x => x.Level == 2); + long totalRecords; + var result = repository.GetPagedResultsByQuery(query, 0, 2, out totalRecords, "SortOrder", Direction.Ascending, orderBySystemField: true); + + // Assert + Assert.That(totalRecords, Is.GreaterThanOrEqualTo(2)); + Assert.That(result.Count(), Is.EqualTo(2)); + Assert.That(result.First().Name, Is.EqualTo("Test Image")); + } + } + + [Test] + public void Can_Perform_GetPagedResultsByQuery_WithDescendingOrder_On_MediaRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + MediaTypeRepository mediaTypeRepository; + using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository)) + { + // Act + var query = Query.Builder.Where(x => x.Level == 2); + long totalRecords; + var result = repository.GetPagedResultsByQuery(query, 0, 1, out totalRecords, "SortOrder", Direction.Descending, orderBySystemField: true); + + // Assert + Assert.That(totalRecords, Is.GreaterThanOrEqualTo(2)); + Assert.That(result.Count(), Is.EqualTo(1)); + Assert.That(result.First().Name, Is.EqualTo("Test File")); + } + } + + [Test] + public void Can_Perform_GetPagedResultsByQuery_WitAlternateOrder_On_MediaRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + MediaTypeRepository mediaTypeRepository; + using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository)) + { + // Act + var query = Query.Builder.Where(x => x.Level == 2); + long totalRecords; + var result = repository.GetPagedResultsByQuery(query, 0, 1, out totalRecords, "Name", Direction.Ascending, orderBySystemField: true); + + // Assert + Assert.That(totalRecords, Is.GreaterThanOrEqualTo(2)); + Assert.That(result.Count(), Is.EqualTo(1)); + Assert.That(result.First().Name, Is.EqualTo("Test File")); + } + } + + [Test] + public void Can_Perform_GetPagedResultsByQuery_WithFilterMatchingSome_On_MediaRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + MediaTypeRepository mediaTypeRepository; + using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository)) + { + // Act + var query = Query.Builder.Where(x => x.Level == 2); + long totalRecords; + var result = repository.GetPagedResultsByQuery(query, 0, 1, out totalRecords, "SortOrder", Direction.Ascending, true, "File"); + + // Assert + Assert.That(totalRecords, Is.EqualTo(1)); + Assert.That(result.Count(), Is.EqualTo(1)); + Assert.That(result.First().Name, Is.EqualTo("Test File")); + } + } + + [Test] + public void Can_Perform_GetPagedResultsByQuery_WithFilterMatchingAll_On_MediaRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + MediaTypeRepository mediaTypeRepository; + using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository)) + { + // Act + var query = Query.Builder.Where(x => x.Level == 2); + long totalRecords; + var result = repository.GetPagedResultsByQuery(query, 0, 1, out totalRecords, "SortOrder", Direction.Ascending, true, "Test"); + + // Assert + Assert.That(totalRecords, Is.EqualTo(2)); + Assert.That(result.Count(), Is.EqualTo(1)); + Assert.That(result.First().Name, Is.EqualTo("Test Image")); + } + } + + [Test] + public void Can_Perform_GetAll_By_Param_Ids_On_MediaRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + MediaTypeRepository mediaTypeRepository; + using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository)) + { + + // Act + var medias = repository.GetAll(NodeDto.NodeIdSeed + 1, NodeDto.NodeIdSeed + 2); + + // Assert + Assert.That(medias, Is.Not.Null); + Assert.That(medias.Any(), Is.True); + Assert.That(medias.Count(), Is.EqualTo(2)); + } + } + + [Test] + public void Can_Perform_GetAll_On_MediaRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + MediaTypeRepository mediaTypeRepository; + using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository)) + { + + // Act + var medias = repository.GetAll(); + + // Assert + Assert.That(medias, Is.Not.Null); + Assert.That(medias.Any(), Is.True); + Assert.That(medias.Count(), Is.GreaterThanOrEqualTo(3)); + } + } + + [Test] + public void Can_Perform_Exists_On_MediaRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + MediaTypeRepository mediaTypeRepository; + using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository)) + { + + // Act + var exists = repository.Exists(NodeDto.NodeIdSeed + 1); + var existsToo = repository.Exists(NodeDto.NodeIdSeed + 1); + var doesntExists = repository.Exists(NodeDto.NodeIdSeed + 5); + + // Assert + Assert.That(exists, Is.True); + Assert.That(existsToo, Is.True); + Assert.That(doesntExists, Is.False); + } + } + + [Test] + public void Can_Perform_Count_On_MediaRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + MediaTypeRepository mediaTypeRepository; + using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository)) + { + + // Act + int level = 2; + var query = Query.Builder.Where(x => x.Level == level); + var result = repository.Count(query); + + // Assert + Assert.That(result, Is.GreaterThanOrEqualTo(2)); + } + } + + [TearDown] + public override void TearDown() + { + base.TearDown(); + } + + public void CreateTestData() + { + //Create and Save folder-Media -> (NodeDto.NodeIdSeed) + var folderMediaType = ServiceContext.ContentTypeService.GetMediaType(1031); + var folder = MockedMedia.CreateMediaFolder(folderMediaType, -1); + ServiceContext.MediaService.Save(folder, 0); + + //Create and Save image-Media -> (NodeDto.NodeIdSeed + 1) + var imageMediaType = ServiceContext.ContentTypeService.GetMediaType(1032); + var image = MockedMedia.CreateMediaImage(imageMediaType, folder.Id); + ServiceContext.MediaService.Save(image, 0); + + //Create and Save file-Media -> (NodeDto.NodeIdSeed + 2) + var fileMediaType = ServiceContext.ContentTypeService.GetMediaType(1033); + var file = MockedMedia.CreateMediaFile(fileMediaType, folder.Id); + ServiceContext.MediaService.Save(file, 0); + } + } } \ No newline at end of file diff --git a/src/Umbraco.Tests/UmbracoExamine/IndexInitializer.cs b/src/Umbraco.Tests/UmbracoExamine/IndexInitializer.cs index 2f9ecce9b2..26f29b581b 100644 --- a/src/Umbraco.Tests/UmbracoExamine/IndexInitializer.cs +++ b/src/Umbraco.Tests/UmbracoExamine/IndexInitializer.cs @@ -20,124 +20,124 @@ using Version = Lucene.Net.Util.Version; namespace Umbraco.Tests.UmbracoExamine { - /// - /// Used internally by test classes to initialize a new index from the template - /// - internal static class IndexInitializer - { - public static UmbracoContentIndexer GetUmbracoIndexer( - Directory luceneDir, - Analyzer analyzer = null, - IDataService dataService = null, - IContentService contentService = null, - IMediaService mediaService = null, - IDataTypeService dataTypeService = null, - IMemberService memberService = null, - IUserService userService = null) - { - if (dataService == null) - { - dataService = new TestDataService(); - } - if (contentService == null) - { - contentService = Mock.Of(); - } - if (userService == null) - { - userService = Mock.Of(x => x.GetProfileById(It.IsAny()) == Mock.Of(p => p.Id == (object)0 && p.Name == "admin")); - } - if (mediaService == null) - { - long totalRecs; + /// + /// Used internally by test classes to initialize a new index from the template + /// + internal static class IndexInitializer + { + public static UmbracoContentIndexer GetUmbracoIndexer( + Directory luceneDir, + Analyzer analyzer = null, + IDataService dataService = null, + IContentService contentService = null, + IMediaService mediaService = null, + IDataTypeService dataTypeService = null, + IMemberService memberService = null, + IUserService userService = null) + { + if (dataService == null) + { + dataService = new TestDataService(); + } + if (contentService == null) + { + contentService = Mock.Of(); + } + if (userService == null) + { + userService = Mock.Of(x => x.GetProfileById(It.IsAny()) == Mock.Of(p => p.Id == (object)0 && p.Name == "admin")); + } + if (mediaService == null) + { + long totalRecs; - var allRecs = dataService.MediaService.GetLatestMediaByXpath("//node") - .Root - .Elements() - .Select(x => Mock.Of( - m => - m.Id == (int)x.Attribute("id") && - m.ParentId == (int)x.Attribute("parentID") && - m.Level == (int)x.Attribute("level") && - m.CreatorId == 0 && - m.SortOrder == (int)x.Attribute("sortOrder") && - m.CreateDate == (DateTime)x.Attribute("createDate") && - m.UpdateDate == (DateTime)x.Attribute("updateDate") && - m.Name == (string)x.Attribute("nodeName") && - m.Path == (string)x.Attribute("path") && - m.Properties == new PropertyCollection() && - m.ContentType == Mock.Of(mt => - mt.Alias == (string)x.Attribute("nodeTypeAlias") && - mt.Id == (int)x.Attribute("nodeType")))) - .ToArray(); + var allRecs = dataService.MediaService.GetLatestMediaByXpath("//node") + .Root + .Elements() + .Select(x => Mock.Of( + m => + m.Id == (int)x.Attribute("id") && + m.ParentId == (int)x.Attribute("parentID") && + m.Level == (int)x.Attribute("level") && + m.CreatorId == 0 && + m.SortOrder == (int)x.Attribute("sortOrder") && + m.CreateDate == (DateTime)x.Attribute("createDate") && + m.UpdateDate == (DateTime)x.Attribute("updateDate") && + m.Name == (string)x.Attribute("nodeName") && + m.Path == (string)x.Attribute("path") && + m.Properties == new PropertyCollection() && + m.ContentType == Mock.Of(mt => + mt.Alias == (string)x.Attribute("nodeTypeAlias") && + mt.Id == (int)x.Attribute("nodeType")))) + .ToArray(); - mediaService = Mock.Of( - x => x.GetPagedDescendants( - It.IsAny(), It.IsAny(), It.IsAny(), out totalRecs, It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()) - == - allRecs); - } - if (dataTypeService == null) - { - dataTypeService = Mock.Of(); - } + mediaService = Mock.Of( + x => x.GetPagedDescendants( + It.IsAny(), It.IsAny(), It.IsAny(), out totalRecs, It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()) + == + allRecs); + } + if (dataTypeService == null) + { + dataTypeService = Mock.Of(); + } - if (memberService == null) - { - memberService = Mock.Of(); - } + if (memberService == null) + { + memberService = Mock.Of(); + } - if (analyzer == null) - { - analyzer = new StandardAnalyzer(Version.LUCENE_29); - } + if (analyzer == null) + { + analyzer = new StandardAnalyzer(Version.LUCENE_29); + } - var indexSet = new IndexSet(); - var indexCriteria = indexSet.ToIndexCriteria(dataService, UmbracoContentIndexer.IndexFieldPolicies); + var indexSet = new IndexSet(); + var indexCriteria = indexSet.ToIndexCriteria(dataService, UmbracoContentIndexer.IndexFieldPolicies); - var i = new UmbracoContentIndexer(indexCriteria, - luceneDir, //custom lucene directory - dataService, - contentService, - mediaService, - dataTypeService, - userService, - analyzer, - false); + var i = new UmbracoContentIndexer(indexCriteria, + luceneDir, //custom lucene directory + dataService, + contentService, + mediaService, + dataTypeService, + userService, + analyzer, + false); - //i.IndexSecondsInterval = 1; + //i.IndexSecondsInterval = 1; - i.IndexingError += IndexingError; + i.IndexingError += IndexingError; - return i; - } - public static UmbracoExamineSearcher GetUmbracoSearcher(Directory luceneDir, Analyzer analyzer = null) - { - if (analyzer == null) - { - analyzer = new StandardAnalyzer(Version.LUCENE_29); - } - return new UmbracoExamineSearcher(luceneDir, analyzer); - } + return i; + } + public static UmbracoExamineSearcher GetUmbracoSearcher(Directory luceneDir, Analyzer analyzer = null) + { + if (analyzer == null) + { + analyzer = new StandardAnalyzer(Version.LUCENE_29); + } + return new UmbracoExamineSearcher(luceneDir, analyzer); + } - public static LuceneSearcher GetLuceneSearcher(Directory luceneDir) - { - return new LuceneSearcher(luceneDir, new StandardAnalyzer(Version.LUCENE_29)); - } + public static LuceneSearcher GetLuceneSearcher(Directory luceneDir) + { + return new LuceneSearcher(luceneDir, new StandardAnalyzer(Version.LUCENE_29)); + } - public static MultiIndexSearcher GetMultiSearcher(Directory pdfDir, Directory simpleDir, Directory conventionDir, Directory cwsDir) - { - var i = new MultiIndexSearcher(new[] { pdfDir, simpleDir, conventionDir, cwsDir }, new StandardAnalyzer(Version.LUCENE_29)); - return i; - } + public static MultiIndexSearcher GetMultiSearcher(Directory pdfDir, Directory simpleDir, Directory conventionDir, Directory cwsDir) + { + var i = new MultiIndexSearcher(new[] { pdfDir, simpleDir, conventionDir, cwsDir }, new StandardAnalyzer(Version.LUCENE_29)); + return i; + } - internal static void IndexingError(object sender, IndexingErrorEventArgs e) - { - throw new ApplicationException(e.Message, e.InnerException); - } + internal static void IndexingError(object sender, IndexingErrorEventArgs e) + { + throw new ApplicationException(e.Message, e.InnerException); + } - } + } } \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbtable.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbtable.directive.js index 8c74abf590..bf593397b6 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbtable.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbtable.directive.js @@ -1,71 +1,71 @@ -(function () { - 'use strict'; - - function TableDirective() { - - function link(scope, el, attr, ctrl) { - - scope.clickItem = function (item, $event) { - if (scope.onClick) { - scope.onClick(item); - $event.stopPropagation(); - } - }; - - scope.selectItem = function (item, $index, $event) { - if (scope.onSelect) { - scope.onSelect(item, $index, $event); - $event.stopPropagation(); - } - }; - - scope.selectAll = function ($event) { - if (scope.onSelectAll) { - scope.onSelectAll($event); - } - }; - - scope.isSelectedAll = function () { - if (scope.onSelectedAll && scope.items && scope.items.length > 0) { - return scope.onSelectedAll(); - } - }; - - scope.isSortDirection = function (col, direction) { - if (scope.onSortingDirection) { - return scope.onSortingDirection(col, direction); - } - }; - - scope.sort = function (field, allow, isSystem) { - if (scope.onSort) { - scope.onSort(field, allow, isSystem); - } - }; - - } - - var directive = { - restrict: 'E', - replace: true, - templateUrl: 'views/components/umb-table.html', - scope: { - items: '=', - itemProperties: '=', - allowSelectAll: '=', - onSelect: '=', - onClick: '=', - onSelectAll: '=', - onSelectedAll: '=', - onSortingDirection: '=', - onSort: '=' - }, - link: link - }; - - return directive; - } - - angular.module('umbraco.directives').directive('umbTable', TableDirective); - -})(); +(function () { + 'use strict'; + + function TableDirective() { + + function link(scope, el, attr, ctrl) { + + scope.clickItem = function (item, $event) { + if (scope.onClick) { + scope.onClick(item); + $event.stopPropagation(); + } + }; + + scope.selectItem = function (item, $index, $event) { + if (scope.onSelect) { + scope.onSelect(item, $index, $event); + $event.stopPropagation(); + } + }; + + scope.selectAll = function ($event) { + if (scope.onSelectAll) { + scope.onSelectAll($event); + } + }; + + scope.isSelectedAll = function () { + if (scope.onSelectedAll && scope.items && scope.items.length > 0) { + return scope.onSelectedAll(); + } + }; + + scope.isSortDirection = function (col, direction) { + if (scope.onSortingDirection) { + return scope.onSortingDirection(col, direction); + } + }; + + scope.sort = function (field, allow, isSystem) { + if (scope.onSort) { + scope.onSort(field, allow, isSystem); + } + }; + + } + + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/umb-table.html', + scope: { + items: '=', + itemProperties: '=', + allowSelectAll: '=', + onSelect: '=', + onClick: '=', + onSelectAll: '=', + onSelectedAll: '=', + onSortingDirection: '=', + onSort: '=' + }, + link: link + }; + + return directive; + } + + angular.module('umbraco.directives').directive('umbTable', TableDirective); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js index 2688dec37a..60ecd16f19 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js @@ -25,636 +25,636 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { - /** internal method process the saving of data and post processing the result */ - function saveContentItem(content, action, files) { - return umbRequestHelper.postSaveContent({ - restApiUrl: umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "PostSave"), - content: content, - action: action, - files: files, - dataFormatter: function (c, a) { - return umbDataFormatter.formatContentPostData(c, a); - } - }); - } - - return { - - getRecycleBin: function () { - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "GetRecycleBin")), - 'Failed to retrieve data for content recycle bin'); - }, - - /** - * @ngdoc method - * @name umbraco.resources.contentResource#sort - * @methodOf umbraco.resources.contentResource - * - * @description - * Sorts all children below a given parent node id, based on a collection of node-ids - * - * ##usage - *
-	 * var ids = [123,34533,2334,23434];
-	 * contentResource.sort({ parentId: 1244, sortedIds: ids })
-	 *    .then(function() {
-	 *        $scope.complete = true;
-	 *    });
-	 * 
- * @param {Object} args arguments object - * @param {Int} args.parentId the ID of the parent node - * @param {Array} options.sortedIds array of node IDs as they should be sorted - * @returns {Promise} resourcePromise object. - * - */ - sort: function (args) { - if (!args) { - throw "args cannot be null"; - } - if (!args.parentId) { - throw "args.parentId cannot be null"; - } - if (!args.sortedIds) { - throw "args.sortedIds cannot be null"; + /** internal method process the saving of data and post processing the result */ + function saveContentItem(content, action, files) { + return umbRequestHelper.postSaveContent({ + restApiUrl: umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "PostSave"), + content: content, + action: action, + files: files, + dataFormatter: function (c, a) { + return umbDataFormatter.formatContentPostData(c, a); + } + }); } - return umbRequestHelper.resourcePromise( - $http.post(umbRequestHelper.getApiUrl("contentApiBaseUrl", "PostSort"), - { - parentId: args.parentId, - idSortOrder: args.sortedIds - }), - 'Failed to sort content'); - }, + return { - /** - * @ngdoc method - * @name umbraco.resources.contentResource#move - * @methodOf umbraco.resources.contentResource - * - * @description - * Moves a node underneath a new parentId - * - * ##usage - *
-	 * contentResource.move({ parentId: 1244, id: 123 })
-	 *    .then(function() {
-	 *        alert("node was moved");
-	 *    }, function(err){
-	 *      alert("node didnt move:" + err.data.Message); 
-	 *    });
-	 * 
- * @param {Object} args arguments object - * @param {Int} args.idd the ID of the node to move - * @param {Int} args.parentId the ID of the parent node to move to - * @returns {Promise} resourcePromise object. - * - */ - move: function (args) { - if (!args) { - throw "args cannot be null"; - } - if (!args.parentId) { - throw "args.parentId cannot be null"; - } - if (!args.id) { - throw "args.id cannot be null"; - } + getRecycleBin: function () { + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "GetRecycleBin")), + 'Failed to retrieve data for content recycle bin'); + }, - return umbRequestHelper.resourcePromise( - $http.post(umbRequestHelper.getApiUrl("contentApiBaseUrl", "PostMove"), - { - parentId: args.parentId, - id: args.id - }), - 'Failed to move content'); - }, + /** + * @ngdoc method + * @name umbraco.resources.contentResource#sort + * @methodOf umbraco.resources.contentResource + * + * @description + * Sorts all children below a given parent node id, based on a collection of node-ids + * + * ##usage + *
+        * var ids = [123,34533,2334,23434];
+        * contentResource.sort({ parentId: 1244, sortedIds: ids })
+        *    .then(function() {
+        *        $scope.complete = true;
+        *    });
+        * 
+ * @param {Object} args arguments object + * @param {Int} args.parentId the ID of the parent node + * @param {Array} options.sortedIds array of node IDs as they should be sorted + * @returns {Promise} resourcePromise object. + * + */ + sort: function (args) { + if (!args) { + throw "args cannot be null"; + } + if (!args.parentId) { + throw "args.parentId cannot be null"; + } + if (!args.sortedIds) { + throw "args.sortedIds cannot be null"; + } - /** - * @ngdoc method - * @name umbraco.resources.contentResource#copy - * @methodOf umbraco.resources.contentResource - * - * @description - * Copies a node underneath a new parentId - * - * ##usage - *
-	 * contentResource.copy({ parentId: 1244, id: 123 })
-	 *    .then(function() {
-	 *        alert("node was copied");
-	 *    }, function(err){
-	 *      alert("node wasnt copy:" + err.data.Message); 
-	 *    });
-	 * 
- * @param {Object} args arguments object - * @param {Int} args.id the ID of the node to copy - * @param {Int} args.parentId the ID of the parent node to copy to - * @param {Boolean} args.relateToOriginal if true, relates the copy to the original through the relation api - * @returns {Promise} resourcePromise object. - * - */ - copy: function (args) { - if (!args) { - throw "args cannot be null"; - } - if (!args.parentId) { - throw "args.parentId cannot be null"; - } - if (!args.id) { - throw "args.id cannot be null"; - } + return umbRequestHelper.resourcePromise( + $http.post(umbRequestHelper.getApiUrl("contentApiBaseUrl", "PostSort"), + { + parentId: args.parentId, + idSortOrder: args.sortedIds + }), + 'Failed to sort content'); + }, - return umbRequestHelper.resourcePromise( - $http.post(umbRequestHelper.getApiUrl("contentApiBaseUrl", "PostCopy"), - args), - 'Failed to copy content'); - }, + /** + * @ngdoc method + * @name umbraco.resources.contentResource#move + * @methodOf umbraco.resources.contentResource + * + * @description + * Moves a node underneath a new parentId + * + * ##usage + *
+        * contentResource.move({ parentId: 1244, id: 123 })
+        *    .then(function() {
+        *        alert("node was moved");
+        *    }, function(err){
+        *      alert("node didnt move:" + err.data.Message); 
+        *    });
+        * 
+ * @param {Object} args arguments object + * @param {Int} args.idd the ID of the node to move + * @param {Int} args.parentId the ID of the parent node to move to + * @returns {Promise} resourcePromise object. + * + */ + move: function (args) { + if (!args) { + throw "args cannot be null"; + } + if (!args.parentId) { + throw "args.parentId cannot be null"; + } + if (!args.id) { + throw "args.id cannot be null"; + } - /** - * @ngdoc method - * @name umbraco.resources.contentResource#unPublish - * @methodOf umbraco.resources.contentResource - * - * @description - * Unpublishes a content item with a given Id - * - * ##usage - *
-	 * contentResource.unPublish(1234)
-	 *    .then(function() {
-	 *        alert("node was unpulished");
-	 *    }, function(err){
-	 *      alert("node wasnt unpublished:" + err.data.Message); 
-	 *    });
-	 * 
- * @param {Int} id the ID of the node to unpublish - * @returns {Promise} resourcePromise object. - * - */ - unPublish: function (id) { - if (!id) { - throw "id cannot be null"; - } + return umbRequestHelper.resourcePromise( + $http.post(umbRequestHelper.getApiUrl("contentApiBaseUrl", "PostMove"), + { + parentId: args.parentId, + id: args.id + }), + 'Failed to move content'); + }, - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "PostUnPublish", - [{ id: id }])), - 'Failed to publish content with id ' + id); - }, - /** - * @ngdoc method - * @name umbraco.resources.contentResource#emptyRecycleBin - * @methodOf umbraco.resources.contentResource - * - * @description - * Empties the content recycle bin - * - * ##usage - *
-	 * contentResource.emptyRecycleBin()
-	 *    .then(function() {
-	 *        alert('its empty!');
-	 *    });
-	 * 
- * - * @returns {Promise} resourcePromise object. - * - */ - emptyRecycleBin: function () { - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "EmptyRecycleBin")), - 'Failed to empty the recycle bin'); - }, + /** + * @ngdoc method + * @name umbraco.resources.contentResource#copy + * @methodOf umbraco.resources.contentResource + * + * @description + * Copies a node underneath a new parentId + * + * ##usage + *
+        * contentResource.copy({ parentId: 1244, id: 123 })
+        *    .then(function() {
+        *        alert("node was copied");
+        *    }, function(err){
+        *      alert("node wasnt copy:" + err.data.Message); 
+        *    });
+        * 
+ * @param {Object} args arguments object + * @param {Int} args.id the ID of the node to copy + * @param {Int} args.parentId the ID of the parent node to copy to + * @param {Boolean} args.relateToOriginal if true, relates the copy to the original through the relation api + * @returns {Promise} resourcePromise object. + * + */ + copy: function (args) { + if (!args) { + throw "args cannot be null"; + } + if (!args.parentId) { + throw "args.parentId cannot be null"; + } + if (!args.id) { + throw "args.id cannot be null"; + } - /** - * @ngdoc method - * @name umbraco.resources.contentResource#deleteById - * @methodOf umbraco.resources.contentResource - * - * @description - * Deletes a content item with a given id - * - * ##usage - *
-	 * contentResource.deleteById(1234)
-	 *    .then(function() {
-	 *        alert('its gone!');
-	 *    });
-	 * 
- * - * @param {Int} id id of content item to delete - * @returns {Promise} resourcePromise object. - * - */ - deleteById: function (id) { - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "DeleteById", - [{ id: id }])), - 'Failed to delete item ' + id); - }, + return umbRequestHelper.resourcePromise( + $http.post(umbRequestHelper.getApiUrl("contentApiBaseUrl", "PostCopy"), + args), + 'Failed to copy content'); + }, - /** - * @ngdoc method - * @name umbraco.resources.contentResource#getById - * @methodOf umbraco.resources.contentResource - * - * @description - * Gets a content item with a given id - * - * ##usage - *
-	 * contentResource.getById(1234)
-	 *    .then(function(content) {
-	 *        var myDoc = content; 
-	 *        alert('its here!');
-	 *    });
-	 * 
- * - * @param {Int} id id of content item to return - * @returns {Promise} resourcePromise object containing the content item. - * - */ - getById: function (id) { - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "GetById", - [{ id: id }])), - 'Failed to retrieve data for content id ' + id); - }, + /** + * @ngdoc method + * @name umbraco.resources.contentResource#unPublish + * @methodOf umbraco.resources.contentResource + * + * @description + * Unpublishes a content item with a given Id + * + * ##usage + *
+        * contentResource.unPublish(1234)
+        *    .then(function() {
+        *        alert("node was unpulished");
+        *    }, function(err){
+        *      alert("node wasnt unpublished:" + err.data.Message); 
+        *    });
+        * 
+ * @param {Int} id the ID of the node to unpublish + * @returns {Promise} resourcePromise object. + * + */ + unPublish: function (id) { + if (!id) { + throw "id cannot be null"; + } - /** - * @ngdoc method - * @name umbraco.resources.contentResource#getByIds - * @methodOf umbraco.resources.contentResource - * - * @description - * Gets an array of content items, given a collection of ids - * - * ##usage - *
-	 * contentResource.getByIds( [1234,2526,28262])
-	 *    .then(function(contentArray) {
-	 *        var myDoc = contentArray; 
-	 *        alert('they are here!');
-	 *    });
-	 * 
- * - * @param {Array} ids ids of content items to return as an array - * @returns {Promise} resourcePromise object containing the content items array. - * - */ - getByIds: function (ids) { - - var idQuery = ""; - _.each(ids, function (item) { - idQuery += "ids=" + item + "&"; - }); - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "GetByIds", - idQuery)), - 'Failed to retrieve data for content with multiple ids'); - }, - - - /** - * @ngdoc method - * @name umbraco.resources.contentResource#getScaffold - * @methodOf umbraco.resources.contentResource - * - * @description - * Returns a scaffold of an empty content item, given the id of the content item to place it underneath and the content type alias. - * - * - Parent Id must be provided so umbraco knows where to store the content - * - Content Type alias must be provided so umbraco knows which properties to put on the content scaffold - * - * The scaffold is used to build editors for content that has not yet been populated with data. - * - * ##usage - *
-	 * contentResource.getScaffold(1234, 'homepage')
-	 *    .then(function(scaffold) {
-	 *        var myDoc = scaffold;
-	 *        myDoc.name = "My new document"; 
-	 *
-	 *        contentResource.publish(myDoc, true)
-	 *            .then(function(content){
-	 *                alert("Retrieved, updated and published again");
-	 *            });
-	 *    });
-	 * 
- * - * @param {Int} parentId id of content item to return - * @param {String} alias contenttype alias to base the scaffold on - * @returns {Promise} resourcePromise object containing the content scaffold. - * - */ - getScaffold: function (parentId, alias) { - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "GetEmpty", - [{ contentTypeAlias: alias }, { parentId: parentId }])), - 'Failed to retrieve data for empty content item type ' + alias); - }, - - /** - * @ngdoc method - * @name umbraco.resources.contentResource#getNiceUrl - * @methodOf umbraco.resources.contentResource - * - * @description - * Returns a url, given a node ID - * - * ##usage - *
-	 * contentResource.getNiceUrl(id)
-	 *    .then(function(url) {
-	 *        alert('its here!');
-	 *    });
-	 * 
- * - * @param {Int} id Id of node to return the public url to - * @returns {Promise} resourcePromise object containing the url. - * - */ - getNiceUrl: function (id) { - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "GetNiceUrl", [{ id: id }])), - 'Failed to retrieve url for id:' + id); - }, - - /** - * @ngdoc method - * @name umbraco.resources.contentResource#getChildren - * @methodOf umbraco.resources.contentResource - * - * @description - * Gets children of a content item with a given id - * - * ##usage - *
-	 * contentResource.getChildren(1234, {pageSize: 10, pageNumber: 2})
-	 *    .then(function(contentArray) {
-	 *        var children = contentArray; 
-	 *        alert('they are here!');
-	 *    });
-	 * 
- * - * @param {Int} parentid id of content item to return children of - * @param {Object} options optional options object - * @param {Int} options.pageSize if paging data, number of nodes per page, default = 0 - * @param {Int} options.pageNumber if paging data, current page index, default = 0 - * @param {String} options.filter if provided, query will only return those with names matching the filter - * @param {String} options.orderDirection can be `Ascending` or `Descending` - Default: `Ascending` - * @param {String} options.orderBy property to order items by, default: `SortOrder` - * @returns {Promise} resourcePromise object containing an array of content items. - * - */ - getChildren: function (parentId, options) { - - var defaults = { - pageSize: 0, - pageNumber: 0, - filter: '', - orderDirection: "Ascending", - orderBy: "SortOrder", - orderBySystemField: true - }; - if (options === undefined) { - options = {}; - } - //overwrite the defaults if there are any specified - angular.extend(defaults, options); - //now copy back to the options we will use - options = defaults; - //change asc/desct - if (options.orderDirection === "asc") { - options.orderDirection = "Ascending"; - } - else if (options.orderDirection === "desc") { - options.orderDirection = "Descending"; - } - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "GetChildren", - [ - { id: parentId }, - { pageNumber: options.pageNumber }, - { pageSize: options.pageSize }, - { orderBy: options.orderBy }, - { orderDirection: options.orderDirection }, - { orderBySystemField: options.orderBySystemField }, - { filter: options.filter } - ])), - 'Failed to retrieve children for content item ' + parentId); - }, - - /** - * @ngdoc method - * @name umbraco.resources.contentResource#hasPermission - * @methodOf umbraco.resources.contentResource - * - * @description - * Returns true/false given a permission char to check against a nodeID - * for the current user - * - * ##usage - *
-	 * contentResource.hasPermission('p',1234)
-	 *    .then(function() {
-	 *        alert('You are allowed to publish this item');
-	 *    });
-	 * 
- * - * @param {String} permission char representing the permission to check - * @param {Int} id id of content item to delete - * @returns {Promise} resourcePromise object. - * - */ - checkPermission: function (permission, id) { - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "HasPermission", - [{ permissionToCheck: permission }, { nodeId: id }])), - 'Failed to check permission for item ' + id); - }, - - getPermissions: function (nodeIds) { - return umbRequestHelper.resourcePromise( + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "PostUnPublish", + [{ id: id }])), + 'Failed to publish content with id ' + id); + }, + /** + * @ngdoc method + * @name umbraco.resources.contentResource#emptyRecycleBin + * @methodOf umbraco.resources.contentResource + * + * @description + * Empties the content recycle bin + * + * ##usage + *
+        * contentResource.emptyRecycleBin()
+        *    .then(function() {
+        *        alert('its empty!');
+        *    });
+        * 
+ * + * @returns {Promise} resourcePromise object. + * + */ + emptyRecycleBin: function () { + return umbRequestHelper.resourcePromise( $http.post( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "GetPermissions"), - nodeIds), - 'Failed to get permissions'); - }, + umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "EmptyRecycleBin")), + 'Failed to empty the recycle bin'); + }, - /** - * @ngdoc method - * @name umbraco.resources.contentResource#save - * @methodOf umbraco.resources.contentResource - * - * @description - * Saves changes made to a content item to its current version, if the content item is new, the isNew paramater must be passed to force creation - * if the content item needs to have files attached, they must be provided as the files param and passed separately - * - * - * ##usage - *
-	 * contentResource.getById(1234)
-	 *    .then(function(content) {
-	 *          content.name = "I want a new name!";
-	 *          contentResource.save(content, false)
-	 *            .then(function(content){
-	 *                alert("Retrieved, updated and saved again");
-	 *            });
-	 *    });
-	 * 
- * - * @param {Object} content The content item object with changes applied - * @param {Bool} isNew set to true to create a new item or to update an existing - * @param {Array} files collection of files for the document - * @returns {Promise} resourcePromise object containing the saved content item. - * - */ - save: function (content, isNew, files) { - return saveContentItem(content, "save" + (isNew ? "New" : ""), files); - }, + /** + * @ngdoc method + * @name umbraco.resources.contentResource#deleteById + * @methodOf umbraco.resources.contentResource + * + * @description + * Deletes a content item with a given id + * + * ##usage + *
+        * contentResource.deleteById(1234)
+        *    .then(function() {
+        *        alert('its gone!');
+        *    });
+        * 
+ * + * @param {Int} id id of content item to delete + * @returns {Promise} resourcePromise object. + * + */ + deleteById: function (id) { + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "DeleteById", + [{ id: id }])), + 'Failed to delete item ' + id); + }, + + /** + * @ngdoc method + * @name umbraco.resources.contentResource#getById + * @methodOf umbraco.resources.contentResource + * + * @description + * Gets a content item with a given id + * + * ##usage + *
+        * contentResource.getById(1234)
+        *    .then(function(content) {
+        *        var myDoc = content; 
+        *        alert('its here!');
+        *    });
+        * 
+ * + * @param {Int} id id of content item to return + * @returns {Promise} resourcePromise object containing the content item. + * + */ + getById: function (id) { + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "GetById", + [{ id: id }])), + 'Failed to retrieve data for content id ' + id); + }, + + /** + * @ngdoc method + * @name umbraco.resources.contentResource#getByIds + * @methodOf umbraco.resources.contentResource + * + * @description + * Gets an array of content items, given a collection of ids + * + * ##usage + *
+        * contentResource.getByIds( [1234,2526,28262])
+        *    .then(function(contentArray) {
+        *        var myDoc = contentArray; 
+        *        alert('they are here!');
+        *    });
+        * 
+ * + * @param {Array} ids ids of content items to return as an array + * @returns {Promise} resourcePromise object containing the content items array. + * + */ + getByIds: function (ids) { + + var idQuery = ""; + _.each(ids, function (item) { + idQuery += "ids=" + item + "&"; + }); + + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "GetByIds", + idQuery)), + 'Failed to retrieve data for content with multiple ids'); + }, - /** - * @ngdoc method - * @name umbraco.resources.contentResource#publish - * @methodOf umbraco.resources.contentResource - * - * @description - * Saves and publishes changes made to a content item to a new version, if the content item is new, the isNew paramater must be passed to force creation - * if the content item needs to have files attached, they must be provided as the files param and passed separately - * - * - * ##usage - *
-	 * contentResource.getById(1234)
-	 *    .then(function(content) {
-	 *          content.name = "I want a new name, and be published!";
-	 *          contentResource.publish(content, false)
-	 *            .then(function(content){
-	 *                alert("Retrieved, updated and published again");
-	 *            });
-	 *    });
-	 * 
- * - * @param {Object} content The content item object with changes applied - * @param {Bool} isNew set to true to create a new item or to update an existing - * @param {Array} files collection of files for the document - * @returns {Promise} resourcePromise object containing the saved content item. - * - */ - publish: function (content, isNew, files) { - return saveContentItem(content, "publish" + (isNew ? "New" : ""), files); - }, + /** + * @ngdoc method + * @name umbraco.resources.contentResource#getScaffold + * @methodOf umbraco.resources.contentResource + * + * @description + * Returns a scaffold of an empty content item, given the id of the content item to place it underneath and the content type alias. + * + * - Parent Id must be provided so umbraco knows where to store the content + * - Content Type alias must be provided so umbraco knows which properties to put on the content scaffold + * + * The scaffold is used to build editors for content that has not yet been populated with data. + * + * ##usage + *
+        * contentResource.getScaffold(1234, 'homepage')
+        *    .then(function(scaffold) {
+        *        var myDoc = scaffold;
+        *        myDoc.name = "My new document"; 
+        *
+        *        contentResource.publish(myDoc, true)
+        *            .then(function(content){
+        *                alert("Retrieved, updated and published again");
+        *            });
+        *    });
+        * 
+ * + * @param {Int} parentId id of content item to return + * @param {String} alias contenttype alias to base the scaffold on + * @returns {Promise} resourcePromise object containing the content scaffold. + * + */ + getScaffold: function (parentId, alias) { + + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "GetEmpty", + [{ contentTypeAlias: alias }, { parentId: parentId }])), + 'Failed to retrieve data for empty content item type ' + alias); + }, + + /** + * @ngdoc method + * @name umbraco.resources.contentResource#getNiceUrl + * @methodOf umbraco.resources.contentResource + * + * @description + * Returns a url, given a node ID + * + * ##usage + *
+        * contentResource.getNiceUrl(id)
+        *    .then(function(url) {
+        *        alert('its here!');
+        *    });
+        * 
+ * + * @param {Int} id Id of node to return the public url to + * @returns {Promise} resourcePromise object containing the url. + * + */ + getNiceUrl: function (id) { + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "GetNiceUrl", [{ id: id }])), + 'Failed to retrieve url for id:' + id); + }, + + /** + * @ngdoc method + * @name umbraco.resources.contentResource#getChildren + * @methodOf umbraco.resources.contentResource + * + * @description + * Gets children of a content item with a given id + * + * ##usage + *
+        * contentResource.getChildren(1234, {pageSize: 10, pageNumber: 2})
+        *    .then(function(contentArray) {
+        *        var children = contentArray; 
+        *        alert('they are here!');
+        *    });
+        * 
+ * + * @param {Int} parentid id of content item to return children of + * @param {Object} options optional options object + * @param {Int} options.pageSize if paging data, number of nodes per page, default = 0 + * @param {Int} options.pageNumber if paging data, current page index, default = 0 + * @param {String} options.filter if provided, query will only return those with names matching the filter + * @param {String} options.orderDirection can be `Ascending` or `Descending` - Default: `Ascending` + * @param {String} options.orderBy property to order items by, default: `SortOrder` + * @returns {Promise} resourcePromise object containing an array of content items. + * + */ + getChildren: function (parentId, options) { + + var defaults = { + pageSize: 0, + pageNumber: 0, + filter: '', + orderDirection: "Ascending", + orderBy: "SortOrder", + orderBySystemField: true + }; + if (options === undefined) { + options = {}; + } + //overwrite the defaults if there are any specified + angular.extend(defaults, options); + //now copy back to the options we will use + options = defaults; + //change asc/desct + if (options.orderDirection === "asc") { + options.orderDirection = "Ascending"; + } + else if (options.orderDirection === "desc") { + options.orderDirection = "Descending"; + } + + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "GetChildren", + [ + { id: parentId }, + { pageNumber: options.pageNumber }, + { pageSize: options.pageSize }, + { orderBy: options.orderBy }, + { orderDirection: options.orderDirection }, + { orderBySystemField: options.orderBySystemField }, + { filter: options.filter } + ])), + 'Failed to retrieve children for content item ' + parentId); + }, + + /** + * @ngdoc method + * @name umbraco.resources.contentResource#hasPermission + * @methodOf umbraco.resources.contentResource + * + * @description + * Returns true/false given a permission char to check against a nodeID + * for the current user + * + * ##usage + *
+        * contentResource.hasPermission('p',1234)
+        *    .then(function() {
+        *        alert('You are allowed to publish this item');
+        *    });
+        * 
+ * + * @param {String} permission char representing the permission to check + * @param {Int} id id of content item to delete + * @returns {Promise} resourcePromise object. + * + */ + checkPermission: function (permission, id) { + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "HasPermission", + [{ permissionToCheck: permission }, { nodeId: id }])), + 'Failed to check permission for item ' + id); + }, + + getPermissions: function (nodeIds) { + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "GetPermissions"), + nodeIds), + 'Failed to get permissions'); + }, + + /** + * @ngdoc method + * @name umbraco.resources.contentResource#save + * @methodOf umbraco.resources.contentResource + * + * @description + * Saves changes made to a content item to its current version, if the content item is new, the isNew paramater must be passed to force creation + * if the content item needs to have files attached, they must be provided as the files param and passed separately + * + * + * ##usage + *
+        * contentResource.getById(1234)
+        *    .then(function(content) {
+        *          content.name = "I want a new name!";
+        *          contentResource.save(content, false)
+        *            .then(function(content){
+        *                alert("Retrieved, updated and saved again");
+        *            });
+        *    });
+        * 
+ * + * @param {Object} content The content item object with changes applied + * @param {Bool} isNew set to true to create a new item or to update an existing + * @param {Array} files collection of files for the document + * @returns {Promise} resourcePromise object containing the saved content item. + * + */ + save: function (content, isNew, files) { + return saveContentItem(content, "save" + (isNew ? "New" : ""), files); + }, - /** - * @ngdoc method - * @name umbraco.resources.contentResource#sendToPublish - * @methodOf umbraco.resources.contentResource - * - * @description - * Saves changes made to a content item, and notifies any subscribers about a pending publication - * - * ##usage - *
-	 * contentResource.getById(1234)
-	 *    .then(function(content) {
-	 *          content.name = "I want a new name, and be published!";
-	 *          contentResource.sendToPublish(content, false)
-	 *            .then(function(content){
-	 *                alert("Retrieved, updated and notication send off");
-	 *            });
-	 *    });
-	 * 
- * - * @param {Object} content The content item object with changes applied - * @param {Bool} isNew set to true to create a new item or to update an existing - * @param {Array} files collection of files for the document - * @returns {Promise} resourcePromise object containing the saved content item. - * - */ - sendToPublish: function (content, isNew, files) { - return saveContentItem(content, "sendPublish" + (isNew ? "New" : ""), files); - }, - - /** - * @ngdoc method - * @name umbraco.resources.contentResource#publishByid - * @methodOf umbraco.resources.contentResource - * - * @description - * Publishes a content item with a given ID - * - * ##usage - *
-	 * contentResource.publishById(1234)
-	 *    .then(function(content) {
-	 *        alert("published");
-	 *    });
-	 * 
- * - * @param {Int} id The ID of the conten to publish - * @returns {Promise} resourcePromise object containing the published content item. - * - */ - publishById: function (id) { - - if (!id) { - throw "id cannot be null"; - } - - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "PostPublishById", - [{ id: id }])), - 'Failed to publish content with id ' + id); - - } + /** + * @ngdoc method + * @name umbraco.resources.contentResource#publish + * @methodOf umbraco.resources.contentResource + * + * @description + * Saves and publishes changes made to a content item to a new version, if the content item is new, the isNew paramater must be passed to force creation + * if the content item needs to have files attached, they must be provided as the files param and passed separately + * + * + * ##usage + *
+        * contentResource.getById(1234)
+        *    .then(function(content) {
+        *          content.name = "I want a new name, and be published!";
+        *          contentResource.publish(content, false)
+        *            .then(function(content){
+        *                alert("Retrieved, updated and published again");
+        *            });
+        *    });
+        * 
+ * + * @param {Object} content The content item object with changes applied + * @param {Bool} isNew set to true to create a new item or to update an existing + * @param {Array} files collection of files for the document + * @returns {Promise} resourcePromise object containing the saved content item. + * + */ + publish: function (content, isNew, files) { + return saveContentItem(content, "publish" + (isNew ? "New" : ""), files); + }, - }; + /** + * @ngdoc method + * @name umbraco.resources.contentResource#sendToPublish + * @methodOf umbraco.resources.contentResource + * + * @description + * Saves changes made to a content item, and notifies any subscribers about a pending publication + * + * ##usage + *
+        * contentResource.getById(1234)
+        *    .then(function(content) {
+        *          content.name = "I want a new name, and be published!";
+        *          contentResource.sendToPublish(content, false)
+        *            .then(function(content){
+        *                alert("Retrieved, updated and notication send off");
+        *            });
+        *    });
+        * 
+ * + * @param {Object} content The content item object with changes applied + * @param {Bool} isNew set to true to create a new item or to update an existing + * @param {Array} files collection of files for the document + * @returns {Promise} resourcePromise object containing the saved content item. + * + */ + sendToPublish: function (content, isNew, files) { + return saveContentItem(content, "sendPublish" + (isNew ? "New" : ""), files); + }, + + /** + * @ngdoc method + * @name umbraco.resources.contentResource#publishByid + * @methodOf umbraco.resources.contentResource + * + * @description + * Publishes a content item with a given ID + * + * ##usage + *
+        * contentResource.publishById(1234)
+        *    .then(function(content) {
+        *        alert("published");
+        *    });
+        * 
+ * + * @param {Int} id The ID of the conten to publish + * @returns {Promise} resourcePromise object containing the published content item. + * + */ + publishById: function (id) { + + if (!id) { + throw "id cannot be null"; + } + + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "PostPublishById", + [{ id: id }])), + 'Failed to publish content with id ' + id); + + } + + + }; } angular.module('umbraco.resources').factory('contentResource', contentResource); diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/media.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/media.resource.js index 887ed661a7..a85b2cc104 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/media.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/media.resource.js @@ -5,471 +5,471 @@ **/ function mediaResource($q, $http, umbDataFormatter, umbRequestHelper) { - /** internal method process the saving of data and post processing the result */ - function saveMediaItem(content, action, files) { - return umbRequestHelper.postSaveContent({ - restApiUrl: umbRequestHelper.getApiUrl( - "mediaApiBaseUrl", - "PostSave"), - content: content, - action: action, - files: files, - dataFormatter: function (c, a) { - return umbDataFormatter.formatMediaPostData(c, a); - } - }); - } - - return { - - getRecycleBin: function () { - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "mediaApiBaseUrl", - "GetRecycleBin")), - 'Failed to retrieve data for media recycle bin'); - }, - - /** - * @ngdoc method - * @name umbraco.resources.mediaResource#sort - * @methodOf umbraco.resources.mediaResource - * - * @description - * Sorts all children below a given parent node id, based on a collection of node-ids - * - * ##usage - *
-	 * var ids = [123,34533,2334,23434];
-	 * mediaResource.sort({ sortedIds: ids })
-	 *    .then(function() {
-	 *        $scope.complete = true;
-	 *    });
-	 * 
- * @param {Object} args arguments object - * @param {Int} args.parentId the ID of the parent node - * @param {Array} options.sortedIds array of node IDs as they should be sorted - * @returns {Promise} resourcePromise object. - * - */ - sort: function (args) { - if (!args) { - throw "args cannot be null"; - } - if (!args.parentId) { - throw "args.parentId cannot be null"; - } - if (!args.sortedIds) { - throw "args.sortedIds cannot be null"; + /** internal method process the saving of data and post processing the result */ + function saveMediaItem(content, action, files) { + return umbRequestHelper.postSaveContent({ + restApiUrl: umbRequestHelper.getApiUrl( + "mediaApiBaseUrl", + "PostSave"), + content: content, + action: action, + files: files, + dataFormatter: function (c, a) { + return umbDataFormatter.formatMediaPostData(c, a); + } + }); } - return umbRequestHelper.resourcePromise( - $http.post(umbRequestHelper.getApiUrl("mediaApiBaseUrl", "PostSort"), - { - parentId: args.parentId, - idSortOrder: args.sortedIds - }), - 'Failed to sort media'); - }, + return { - /** - * @ngdoc method - * @name umbraco.resources.mediaResource#move - * @methodOf umbraco.resources.mediaResource - * - * @description - * Moves a node underneath a new parentId - * - * ##usage - *
-	 * mediaResource.move({ parentId: 1244, id: 123 })
-	 *    .then(function() {
-	 *        alert("node was moved");
-	 *    }, function(err){
-	 *      alert("node didnt move:" + err.data.Message); 
-	 *    });
-	 * 
- * @param {Object} args arguments object - * @param {Int} args.idd the ID of the node to move - * @param {Int} args.parentId the ID of the parent node to move to - * @returns {Promise} resourcePromise object. - * - */ - move: function (args) { - if (!args) { - throw "args cannot be null"; - } - if (!args.parentId) { - throw "args.parentId cannot be null"; - } - if (!args.id) { - throw "args.id cannot be null"; - } + getRecycleBin: function () { + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "mediaApiBaseUrl", + "GetRecycleBin")), + 'Failed to retrieve data for media recycle bin'); + }, - return umbRequestHelper.resourcePromise( - $http.post(umbRequestHelper.getApiUrl("mediaApiBaseUrl", "PostMove"), - { - parentId: args.parentId, - id: args.id - }), - 'Failed to move media'); - }, + /** + * @ngdoc method + * @name umbraco.resources.mediaResource#sort + * @methodOf umbraco.resources.mediaResource + * + * @description + * Sorts all children below a given parent node id, based on a collection of node-ids + * + * ##usage + *
+        * var ids = [123,34533,2334,23434];
+        * mediaResource.sort({ sortedIds: ids })
+        *    .then(function() {
+        *        $scope.complete = true;
+        *    });
+        * 
+ * @param {Object} args arguments object + * @param {Int} args.parentId the ID of the parent node + * @param {Array} options.sortedIds array of node IDs as they should be sorted + * @returns {Promise} resourcePromise object. + * + */ + sort: function (args) { + if (!args) { + throw "args cannot be null"; + } + if (!args.parentId) { + throw "args.parentId cannot be null"; + } + if (!args.sortedIds) { + throw "args.sortedIds cannot be null"; + } + + return umbRequestHelper.resourcePromise( + $http.post(umbRequestHelper.getApiUrl("mediaApiBaseUrl", "PostSort"), + { + parentId: args.parentId, + idSortOrder: args.sortedIds + }), + 'Failed to sort media'); + }, + + /** + * @ngdoc method + * @name umbraco.resources.mediaResource#move + * @methodOf umbraco.resources.mediaResource + * + * @description + * Moves a node underneath a new parentId + * + * ##usage + *
+        * mediaResource.move({ parentId: 1244, id: 123 })
+        *    .then(function() {
+        *        alert("node was moved");
+        *    }, function(err){
+        *      alert("node didnt move:" + err.data.Message); 
+        *    });
+        * 
+ * @param {Object} args arguments object + * @param {Int} args.idd the ID of the node to move + * @param {Int} args.parentId the ID of the parent node to move to + * @returns {Promise} resourcePromise object. + * + */ + move: function (args) { + if (!args) { + throw "args cannot be null"; + } + if (!args.parentId) { + throw "args.parentId cannot be null"; + } + if (!args.id) { + throw "args.id cannot be null"; + } + + return umbRequestHelper.resourcePromise( + $http.post(umbRequestHelper.getApiUrl("mediaApiBaseUrl", "PostMove"), + { + parentId: args.parentId, + id: args.id + }), + 'Failed to move media'); + }, - /** - * @ngdoc method - * @name umbraco.resources.mediaResource#getById - * @methodOf umbraco.resources.mediaResource - * - * @description - * Gets a media item with a given id - * - * ##usage - *
-	 * mediaResource.getById(1234)
-	 *    .then(function(media) {
-	 *        var myMedia = media; 
-	 *        alert('its here!');
-	 *    });
-	 * 
- * - * @param {Int} id id of media item to return - * @returns {Promise} resourcePromise object containing the media item. - * - */ - getById: function (id) { + /** + * @ngdoc method + * @name umbraco.resources.mediaResource#getById + * @methodOf umbraco.resources.mediaResource + * + * @description + * Gets a media item with a given id + * + * ##usage + *
+        * mediaResource.getById(1234)
+        *    .then(function(media) {
+        *        var myMedia = media; 
+        *        alert('its here!');
+        *    });
+        * 
+ * + * @param {Int} id id of media item to return + * @returns {Promise} resourcePromise object containing the media item. + * + */ + getById: function (id) { - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "mediaApiBaseUrl", - "GetById", - [{ id: id }])), - 'Failed to retrieve data for media id ' + id); - }, + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "mediaApiBaseUrl", + "GetById", + [{ id: id }])), + 'Failed to retrieve data for media id ' + id); + }, - /** - * @ngdoc method - * @name umbraco.resources.mediaResource#deleteById - * @methodOf umbraco.resources.mediaResource - * - * @description - * Deletes a media item with a given id - * - * ##usage - *
-	 * mediaResource.deleteById(1234)
-	 *    .then(function() {
-	 *        alert('its gone!');
-	 *    });
-	 * 
- * - * @param {Int} id id of media item to delete - * @returns {Promise} resourcePromise object. - * - */ - deleteById: function (id) { - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "mediaApiBaseUrl", - "DeleteById", - [{ id: id }])), - 'Failed to delete item ' + id); - }, + /** + * @ngdoc method + * @name umbraco.resources.mediaResource#deleteById + * @methodOf umbraco.resources.mediaResource + * + * @description + * Deletes a media item with a given id + * + * ##usage + *
+        * mediaResource.deleteById(1234)
+        *    .then(function() {
+        *        alert('its gone!');
+        *    });
+        * 
+ * + * @param {Int} id id of media item to delete + * @returns {Promise} resourcePromise object. + * + */ + deleteById: function (id) { + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "mediaApiBaseUrl", + "DeleteById", + [{ id: id }])), + 'Failed to delete item ' + id); + }, - /** - * @ngdoc method - * @name umbraco.resources.mediaResource#getByIds - * @methodOf umbraco.resources.mediaResource - * - * @description - * Gets an array of media items, given a collection of ids - * - * ##usage - *
-	 * mediaResource.getByIds( [1234,2526,28262])
-	 *    .then(function(mediaArray) {
-	 *        var myDoc = contentArray; 
-	 *        alert('they are here!');
-	 *    });
-	 * 
- * - * @param {Array} ids ids of media items to return as an array - * @returns {Promise} resourcePromise object containing the media items array. - * - */ - getByIds: function (ids) { + /** + * @ngdoc method + * @name umbraco.resources.mediaResource#getByIds + * @methodOf umbraco.resources.mediaResource + * + * @description + * Gets an array of media items, given a collection of ids + * + * ##usage + *
+        * mediaResource.getByIds( [1234,2526,28262])
+        *    .then(function(mediaArray) {
+        *        var myDoc = contentArray; 
+        *        alert('they are here!');
+        *    });
+        * 
+ * + * @param {Array} ids ids of media items to return as an array + * @returns {Promise} resourcePromise object containing the media items array. + * + */ + getByIds: function (ids) { - var idQuery = ""; - _.each(ids, function (item) { - idQuery += "ids=" + item + "&"; - }); + var idQuery = ""; + _.each(ids, function (item) { + idQuery += "ids=" + item + "&"; + }); - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "mediaApiBaseUrl", - "GetByIds", - idQuery)), - 'Failed to retrieve data for media ids ' + ids); - }, + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "mediaApiBaseUrl", + "GetByIds", + idQuery)), + 'Failed to retrieve data for media ids ' + ids); + }, - /** - * @ngdoc method - * @name umbraco.resources.mediaResource#getScaffold - * @methodOf umbraco.resources.mediaResource - * - * @description - * Returns a scaffold of an empty media item, given the id of the media item to place it underneath and the media type alias. - * - * - Parent Id must be provided so umbraco knows where to store the media - * - Media Type alias must be provided so umbraco knows which properties to put on the media scaffold - * - * The scaffold is used to build editors for media that has not yet been populated with data. - * - * ##usage - *
-	 * mediaResource.getScaffold(1234, 'folder')
-	 *    .then(function(scaffold) {
-	 *        var myDoc = scaffold;
-	 *        myDoc.name = "My new media item"; 
-	 *
-	 *        mediaResource.save(myDoc, true)
-	 *            .then(function(media){
-	 *                alert("Retrieved, updated and saved again");
-	 *            });
-	 *    });
-	 * 
- * - * @param {Int} parentId id of media item to return - * @param {String} alias mediatype alias to base the scaffold on - * @returns {Promise} resourcePromise object containing the media scaffold. - * - */ - getScaffold: function (parentId, alias) { + /** + * @ngdoc method + * @name umbraco.resources.mediaResource#getScaffold + * @methodOf umbraco.resources.mediaResource + * + * @description + * Returns a scaffold of an empty media item, given the id of the media item to place it underneath and the media type alias. + * + * - Parent Id must be provided so umbraco knows where to store the media + * - Media Type alias must be provided so umbraco knows which properties to put on the media scaffold + * + * The scaffold is used to build editors for media that has not yet been populated with data. + * + * ##usage + *
+        * mediaResource.getScaffold(1234, 'folder')
+        *    .then(function(scaffold) {
+        *        var myDoc = scaffold;
+        *        myDoc.name = "My new media item"; 
+        *
+        *        mediaResource.save(myDoc, true)
+        *            .then(function(media){
+        *                alert("Retrieved, updated and saved again");
+        *            });
+        *    });
+        * 
+ * + * @param {Int} parentId id of media item to return + * @param {String} alias mediatype alias to base the scaffold on + * @returns {Promise} resourcePromise object containing the media scaffold. + * + */ + getScaffold: function (parentId, alias) { - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "mediaApiBaseUrl", - "GetEmpty", - [{ contentTypeAlias: alias }, { parentId: parentId }])), - 'Failed to retrieve data for empty media item type ' + alias); + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "mediaApiBaseUrl", + "GetEmpty", + [{ contentTypeAlias: alias }, { parentId: parentId }])), + 'Failed to retrieve data for empty media item type ' + alias); - }, + }, - rootMedia: function () { + rootMedia: function () { - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "mediaApiBaseUrl", - "GetRootMedia")), - 'Failed to retrieve data for root media'); + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "mediaApiBaseUrl", + "GetRootMedia")), + 'Failed to retrieve data for root media'); - }, + }, - /** - * @ngdoc method - * @name umbraco.resources.mediaResource#getChildren - * @methodOf umbraco.resources.mediaResource - * - * @description - * Gets children of a media item with a given id - * - * ##usage - *
-	 * mediaResource.getChildren(1234, {pageSize: 10, pageNumber: 2})
-	 *    .then(function(contentArray) {
-	 *        var children = contentArray; 
-	 *        alert('they are here!');
-	 *    });
-	 * 
- * - * @param {Int} parentid id of content item to return children of - * @param {Object} options optional options object - * @param {Int} options.pageSize if paging data, number of nodes per page, default = 0 - * @param {Int} options.pageNumber if paging data, current page index, default = 0 - * @param {String} options.filter if provided, query will only return those with names matching the filter - * @param {String} options.orderDirection can be `Ascending` or `Descending` - Default: `Ascending` - * @param {String} options.orderBy property to order items by, default: `SortOrder` - * @returns {Promise} resourcePromise object containing an array of content items. - * - */ - getChildren: function (parentId, options) { + /** + * @ngdoc method + * @name umbraco.resources.mediaResource#getChildren + * @methodOf umbraco.resources.mediaResource + * + * @description + * Gets children of a media item with a given id + * + * ##usage + *
+        * mediaResource.getChildren(1234, {pageSize: 10, pageNumber: 2})
+        *    .then(function(contentArray) {
+        *        var children = contentArray; 
+        *        alert('they are here!');
+        *    });
+        * 
+ * + * @param {Int} parentid id of content item to return children of + * @param {Object} options optional options object + * @param {Int} options.pageSize if paging data, number of nodes per page, default = 0 + * @param {Int} options.pageNumber if paging data, current page index, default = 0 + * @param {String} options.filter if provided, query will only return those with names matching the filter + * @param {String} options.orderDirection can be `Ascending` or `Descending` - Default: `Ascending` + * @param {String} options.orderBy property to order items by, default: `SortOrder` + * @returns {Promise} resourcePromise object containing an array of content items. + * + */ + getChildren: function (parentId, options) { - var defaults = { - pageSize: 0, - pageNumber: 0, - filter: '', - orderDirection: "Ascending", - orderBy: "SortOrder", - orderBySystemField: true + var defaults = { + pageSize: 0, + pageNumber: 0, + filter: '', + orderDirection: "Ascending", + orderBy: "SortOrder", + orderBySystemField: true + }; + if (options === undefined) { + options = {}; + } + //overwrite the defaults if there are any specified + angular.extend(defaults, options); + //now copy back to the options we will use + options = defaults; + //change asc/desct + if (options.orderDirection === "asc") { + options.orderDirection = "Ascending"; + } + else if (options.orderDirection === "desc") { + options.orderDirection = "Descending"; + } + + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "mediaApiBaseUrl", + "GetChildren", + [ + { id: parentId }, + { pageNumber: options.pageNumber }, + { pageSize: options.pageSize }, + { orderBy: options.orderBy }, + { orderDirection: options.orderDirection }, + { orderBySystemField: options.orderBySystemField }, + { filter: options.filter } + ])), + 'Failed to retrieve children for media item ' + parentId); + }, + + /** + * @ngdoc method + * @name umbraco.resources.mediaResource#save + * @methodOf umbraco.resources.mediaResource + * + * @description + * Saves changes made to a media item, if the media item is new, the isNew paramater must be passed to force creation + * if the media item needs to have files attached, they must be provided as the files param and passed separately + * + * + * ##usage + *
+        * mediaResource.getById(1234)
+        *    .then(function(media) {
+        *          media.name = "I want a new name!";
+        *          mediaResource.save(media, false)
+        *            .then(function(media){
+        *                alert("Retrieved, updated and saved again");
+        *            });
+        *    });
+        * 
+ * + * @param {Object} media The media item object with changes applied + * @param {Bool} isNew set to true to create a new item or to update an existing + * @param {Array} files collection of files for the media item + * @returns {Promise} resourcePromise object containing the saved media item. + * + */ + save: function (media, isNew, files) { + return saveMediaItem(media, "save" + (isNew ? "New" : ""), files); + }, + + /** + * @ngdoc method + * @name umbraco.resources.mediaResource#addFolder + * @methodOf umbraco.resources.mediaResource + * + * @description + * Shorthand for adding a media item of the type "Folder" under a given parent ID + * + * ##usage + *
+        * mediaResource.addFolder("My gallery", 1234)
+        *    .then(function(folder) {
+        *        alert('New folder');
+        *    });
+        * 
+ * + * @param {string} name Name of the folder to create + * @param {int} parentId Id of the media item to create the folder underneath + * @returns {Promise} resourcePromise object. + * + */ + addFolder: function (name, parentId) { + return umbRequestHelper.resourcePromise( + $http.post(umbRequestHelper + .getApiUrl("mediaApiBaseUrl", "PostAddFolder"), + { + name: name, + parentId: parentId + }), + 'Failed to add folder'); + }, + + /** + * @ngdoc method + * @name umbraco.resources.mediaResource#getChildFolders + * @methodOf umbraco.resources.mediaResource + * + * @description + * Retrieves all media children with types used as folders. + * Uses the convention of looking for media items with mediaTypes ending in + * *Folder so will match "Folder", "bannerFolder", "secureFolder" etc, + * + * ##usage + *
+        * mediaResource.getChildFolders(1234)
+        *    .then(function(data) {
+        *        alert('folders');
+        *    });
+        * 
+ * + * @param {int} parentId Id of the media item to query for child folders + * @returns {Promise} resourcePromise object. + * + */ + getChildFolders: function (parentId) { + if (!parentId) { + parentId = -1; + } + + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "mediaApiBaseUrl", + "GetChildFolders", + [ + { id: parentId } + ])), + 'Failed to retrieve child folders for media item ' + parentId); + }, + + /** + * @ngdoc method + * @name umbraco.resources.mediaResource#emptyRecycleBin + * @methodOf umbraco.resources.mediaResource + * + * @description + * Empties the media recycle bin + * + * ##usage + *
+        * mediaResource.emptyRecycleBin()
+        *    .then(function() {
+        *        alert('its empty!');
+        *    });
+        * 
+ * + * @returns {Promise} resourcePromise object. + * + */ + emptyRecycleBin: function () { + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "mediaApiBaseUrl", + "EmptyRecycleBin")), + 'Failed to empty the recycle bin'); + } }; - if (options === undefined) { - options = {}; - } - //overwrite the defaults if there are any specified - angular.extend(defaults, options); - //now copy back to the options we will use - options = defaults; - //change asc/desct - if (options.orderDirection === "asc") { - options.orderDirection = "Ascending"; - } - else if (options.orderDirection === "desc") { - options.orderDirection = "Descending"; - } - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "mediaApiBaseUrl", - "GetChildren", - [ - { id: parentId }, - { pageNumber: options.pageNumber }, - { pageSize: options.pageSize }, - { orderBy: options.orderBy }, - { orderDirection: options.orderDirection }, - { orderBySystemField: options.orderBySystemField }, - { filter: options.filter } - ])), - 'Failed to retrieve children for media item ' + parentId); - }, - - /** - * @ngdoc method - * @name umbraco.resources.mediaResource#save - * @methodOf umbraco.resources.mediaResource - * - * @description - * Saves changes made to a media item, if the media item is new, the isNew paramater must be passed to force creation - * if the media item needs to have files attached, they must be provided as the files param and passed separately - * - * - * ##usage - *
-	 * mediaResource.getById(1234)
-	 *    .then(function(media) {
-	 *          media.name = "I want a new name!";
-	 *          mediaResource.save(media, false)
-	 *            .then(function(media){
-	 *                alert("Retrieved, updated and saved again");
-	 *            });
-	 *    });
-	 * 
- * - * @param {Object} media The media item object with changes applied - * @param {Bool} isNew set to true to create a new item or to update an existing - * @param {Array} files collection of files for the media item - * @returns {Promise} resourcePromise object containing the saved media item. - * - */ - save: function (media, isNew, files) { - return saveMediaItem(media, "save" + (isNew ? "New" : ""), files); - }, - - /** - * @ngdoc method - * @name umbraco.resources.mediaResource#addFolder - * @methodOf umbraco.resources.mediaResource - * - * @description - * Shorthand for adding a media item of the type "Folder" under a given parent ID - * - * ##usage - *
-	 * mediaResource.addFolder("My gallery", 1234)
-	 *    .then(function(folder) {
-	 *        alert('New folder');
-	 *    });
-	 * 
- * - * @param {string} name Name of the folder to create - * @param {int} parentId Id of the media item to create the folder underneath - * @returns {Promise} resourcePromise object. - * - */ - addFolder: function (name, parentId) { - return umbRequestHelper.resourcePromise( - $http.post(umbRequestHelper - .getApiUrl("mediaApiBaseUrl", "PostAddFolder"), - { - name: name, - parentId: parentId - }), - 'Failed to add folder'); - }, - - /** - * @ngdoc method - * @name umbraco.resources.mediaResource#getChildFolders - * @methodOf umbraco.resources.mediaResource - * - * @description - * Retrieves all media children with types used as folders. - * Uses the convention of looking for media items with mediaTypes ending in - * *Folder so will match "Folder", "bannerFolder", "secureFolder" etc, - * - * ##usage - *
-	 * mediaResource.getChildFolders(1234)
-	 *    .then(function(data) {
-	 *        alert('folders');
-	 *    });
-	 * 
- * - * @param {int} parentId Id of the media item to query for child folders - * @returns {Promise} resourcePromise object. - * - */ - getChildFolders: function (parentId) { - if (!parentId) { - parentId = -1; - } - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "mediaApiBaseUrl", - "GetChildFolders", - [ - { id: parentId } - ])), - 'Failed to retrieve child folders for media item ' + parentId); - }, - - /** - * @ngdoc method - * @name umbraco.resources.mediaResource#emptyRecycleBin - * @methodOf umbraco.resources.mediaResource - * - * @description - * Empties the media recycle bin - * - * ##usage - *
-	 * mediaResource.emptyRecycleBin()
-	 *    .then(function() {
-	 *        alert('its empty!');
-	 *    });
-	 * 
- * - * @returns {Promise} resourcePromise object. - * - */ - emptyRecycleBin: function () { - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "mediaApiBaseUrl", - "EmptyRecycleBin")), - 'Failed to empty the recycle bin'); - } - }; } angular.module('umbraco.resources').factory('mediaResource', mediaResource); diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/member.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/member.resource.js index c231a4944c..3ec9258f98 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/member.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/member.resource.js @@ -5,231 +5,231 @@ **/ function memberResource($q, $http, umbDataFormatter, umbRequestHelper) { - /** internal method process the saving of data and post processing the result */ - function saveMember(content, action, files) { + /** internal method process the saving of data and post processing the result */ + function saveMember(content, action, files) { - return umbRequestHelper.postSaveContent({ - restApiUrl: umbRequestHelper.getApiUrl( - "memberApiBaseUrl", - "PostSave"), - content: content, - action: action, - files: files, - dataFormatter: function (c, a) { - return umbDataFormatter.formatMemberPostData(c, a); - } - }); - } - - return { - - getPagedResults: function (memberTypeAlias, options) { - - if (memberTypeAlias === 'all-members') { - memberTypeAlias = null; + return umbRequestHelper.postSaveContent({ + restApiUrl: umbRequestHelper.getApiUrl( + "memberApiBaseUrl", + "PostSave"), + content: content, + action: action, + files: files, + dataFormatter: function (c, a) { + return umbDataFormatter.formatMemberPostData(c, a); + } + }); } - var defaults = { - pageSize: 25, - pageNumber: 1, - filter: '', - orderDirection: "Ascending", - orderBy: "LoginName", - orderBySystemField: true + return { + + getPagedResults: function (memberTypeAlias, options) { + + if (memberTypeAlias === 'all-members') { + memberTypeAlias = null; + } + + var defaults = { + pageSize: 25, + pageNumber: 1, + filter: '', + orderDirection: "Ascending", + orderBy: "LoginName", + orderBySystemField: true + }; + if (options === undefined) { + options = {}; + } + //overwrite the defaults if there are any specified + angular.extend(defaults, options); + //now copy back to the options we will use + options = defaults; + //change asc/desct + if (options.orderDirection === "asc") { + options.orderDirection = "Ascending"; + } + else if (options.orderDirection === "desc") { + options.orderDirection = "Descending"; + } + + var params = [ + { pageNumber: options.pageNumber }, + { pageSize: options.pageSize }, + { orderBy: options.orderBy }, + { orderDirection: options.orderDirection }, + { orderBySystemField: options.orderBySystemField }, + { filter: options.filter } + ]; + if (memberTypeAlias != null) { + params.push({ memberTypeAlias: memberTypeAlias }); + } + + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "memberApiBaseUrl", + "GetPagedResults", + params)), + 'Failed to retrieve member paged result'); + }, + + getListNode: function (listName) { + + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "memberApiBaseUrl", + "GetListNodeDisplay", + [{ listName: listName }])), + 'Failed to retrieve data for member list ' + listName); + }, + + /** + * @ngdoc method + * @name umbraco.resources.memberResource#getByKey + * @methodOf umbraco.resources.memberResource + * + * @description + * Gets a member item with a given key + * + * ##usage + *
+        * memberResource.getByKey("0000-0000-000-00000-000")
+        *    .then(function(member) {
+        *        var mymember = member; 
+        *        alert('its here!');
+        *    });
+        * 
+ * + * @param {Guid} key key of member item to return + * @returns {Promise} resourcePromise object containing the member item. + * + */ + getByKey: function (key) { + + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "memberApiBaseUrl", + "GetByKey", + [{ key: key }])), + 'Failed to retrieve data for member id ' + key); + }, + + /** + * @ngdoc method + * @name umbraco.resources.memberResource#deleteByKey + * @methodOf umbraco.resources.memberResource + * + * @description + * Deletes a member item with a given key + * + * ##usage + *
+        * memberResource.deleteByKey("0000-0000-000-00000-000")
+        *    .then(function() {
+        *        alert('its gone!');
+        *    });
+        * 
+ * + * @param {Guid} key id of member item to delete + * @returns {Promise} resourcePromise object. + * + */ + deleteByKey: function (key) { + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "memberApiBaseUrl", + "DeleteByKey", + [{ key: key }])), + 'Failed to delete item ' + key); + }, + + /** + * @ngdoc method + * @name umbraco.resources.memberResource#getScaffold + * @methodOf umbraco.resources.memberResource + * + * @description + * Returns a scaffold of an empty member item, given the id of the member item to place it underneath and the member type alias. + * + * - Member Type alias must be provided so umbraco knows which properties to put on the member scaffold + * + * The scaffold is used to build editors for member that has not yet been populated with data. + * + * ##usage + *
+        * memberResource.getScaffold('client')
+        *    .then(function(scaffold) {
+        *        var myDoc = scaffold;
+        *        myDoc.name = "My new member item"; 
+        *
+        *        memberResource.save(myDoc, true)
+        *            .then(function(member){
+        *                alert("Retrieved, updated and saved again");
+        *            });
+        *    });
+        * 
+ * + * @param {String} alias membertype alias to base the scaffold on + * @returns {Promise} resourcePromise object containing the member scaffold. + * + */ + getScaffold: function (alias) { + + if (alias) { + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "memberApiBaseUrl", + "GetEmpty", + [{ contentTypeAlias: alias }])), + 'Failed to retrieve data for empty member item type ' + alias); + } + else { + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "memberApiBaseUrl", + "GetEmpty")), + 'Failed to retrieve data for empty member item type ' + alias); + } + + }, + + /** + * @ngdoc method + * @name umbraco.resources.memberResource#save + * @methodOf umbraco.resources.memberResource + * + * @description + * Saves changes made to a member, if the member is new, the isNew paramater must be passed to force creation + * if the member needs to have files attached, they must be provided as the files param and passed separately + * + * + * ##usage + *
+        * memberResource.getBykey("23234-sd8djsd-3h8d3j-sdh8d")
+        *    .then(function(member) {
+        *          member.name = "Bob";
+        *          memberResource.save(member, false)
+        *            .then(function(member){
+        *                alert("Retrieved, updated and saved again");
+        *            });
+        *    });
+        * 
+ * + * @param {Object} media The member item object with changes applied + * @param {Bool} isNew set to true to create a new item or to update an existing + * @param {Array} files collection of files for the media item + * @returns {Promise} resourcePromise object containing the saved media item. + * + */ + save: function (member, isNew, files) { + return saveMember(member, "save" + (isNew ? "New" : ""), files); + } }; - if (options === undefined) { - options = {}; - } - //overwrite the defaults if there are any specified - angular.extend(defaults, options); - //now copy back to the options we will use - options = defaults; - //change asc/desct - if (options.orderDirection === "asc") { - options.orderDirection = "Ascending"; - } - else if (options.orderDirection === "desc") { - options.orderDirection = "Descending"; - } - - var params = [ - { pageNumber: options.pageNumber }, - { pageSize: options.pageSize }, - { orderBy: options.orderBy }, - { orderDirection: options.orderDirection }, - { orderBySystemField: options.orderBySystemField }, - { filter: options.filter } - ]; - if (memberTypeAlias != null) { - params.push({ memberTypeAlias: memberTypeAlias }); - } - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "memberApiBaseUrl", - "GetPagedResults", - params)), - 'Failed to retrieve member paged result'); - }, - - getListNode: function (listName) { - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "memberApiBaseUrl", - "GetListNodeDisplay", - [{ listName: listName }])), - 'Failed to retrieve data for member list ' + listName); - }, - - /** - * @ngdoc method - * @name umbraco.resources.memberResource#getByKey - * @methodOf umbraco.resources.memberResource - * - * @description - * Gets a member item with a given key - * - * ##usage - *
-	 * memberResource.getByKey("0000-0000-000-00000-000")
-	 *    .then(function(member) {
-	 *        var mymember = member; 
-	 *        alert('its here!');
-	 *    });
-	 * 
- * - * @param {Guid} key key of member item to return - * @returns {Promise} resourcePromise object containing the member item. - * - */ - getByKey: function (key) { - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "memberApiBaseUrl", - "GetByKey", - [{ key: key }])), - 'Failed to retrieve data for member id ' + key); - }, - - /** - * @ngdoc method - * @name umbraco.resources.memberResource#deleteByKey - * @methodOf umbraco.resources.memberResource - * - * @description - * Deletes a member item with a given key - * - * ##usage - *
-	 * memberResource.deleteByKey("0000-0000-000-00000-000")
-	 *    .then(function() {
-	 *        alert('its gone!');
-	 *    });
-	 * 
- * - * @param {Guid} key id of member item to delete - * @returns {Promise} resourcePromise object. - * - */ - deleteByKey: function (key) { - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "memberApiBaseUrl", - "DeleteByKey", - [{ key: key }])), - 'Failed to delete item ' + key); - }, - - /** - * @ngdoc method - * @name umbraco.resources.memberResource#getScaffold - * @methodOf umbraco.resources.memberResource - * - * @description - * Returns a scaffold of an empty member item, given the id of the member item to place it underneath and the member type alias. - * - * - Member Type alias must be provided so umbraco knows which properties to put on the member scaffold - * - * The scaffold is used to build editors for member that has not yet been populated with data. - * - * ##usage - *
-	 * memberResource.getScaffold('client')
-	 *    .then(function(scaffold) {
-	 *        var myDoc = scaffold;
-	 *        myDoc.name = "My new member item"; 
-	 *
-	 *        memberResource.save(myDoc, true)
-	 *            .then(function(member){
-	 *                alert("Retrieved, updated and saved again");
-	 *            });
-	 *    });
-	 * 
- * - * @param {String} alias membertype alias to base the scaffold on - * @returns {Promise} resourcePromise object containing the member scaffold. - * - */ - getScaffold: function (alias) { - - if (alias) { - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "memberApiBaseUrl", - "GetEmpty", - [{ contentTypeAlias: alias }])), - 'Failed to retrieve data for empty member item type ' + alias); - } - else { - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "memberApiBaseUrl", - "GetEmpty")), - 'Failed to retrieve data for empty member item type ' + alias); - } - - }, - - /** - * @ngdoc method - * @name umbraco.resources.memberResource#save - * @methodOf umbraco.resources.memberResource - * - * @description - * Saves changes made to a member, if the member is new, the isNew paramater must be passed to force creation - * if the member needs to have files attached, they must be provided as the files param and passed separately - * - * - * ##usage - *
-	 * memberResource.getBykey("23234-sd8djsd-3h8d3j-sdh8d")
-	 *    .then(function(member) {
-	 *          member.name = "Bob";
-	 *          memberResource.save(member, false)
-	 *            .then(function(member){
-	 *                alert("Retrieved, updated and saved again");
-	 *            });
-	 *    });
-	 * 
- * - * @param {Object} media The member item object with changes applied - * @param {Bool} isNew set to true to create a new item or to update an existing - * @param {Array} files collection of files for the media item - * @returns {Promise} resourcePromise object containing the saved media item. - * - */ - save: function (member, isNew, files) { - return saveMember(member, "save" + (isNew ? "New" : ""), files); - } - }; } angular.module('umbraco.resources').factory('memberResource', memberResource); diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-table.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-table.html index 1ab22eb406..8d9b45e6be 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-table.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-table.html @@ -1,60 +1,60 @@ -
-
- -
-
-
- -
- - -
-
- -
-
- -
- - -
-
- - -
-
- {{item[column.alias]}} -
-
-
-
- - - There are no items show in the list. - -
+
+
+ +
+
+
+ +
+ + +
+
+ +
+
+ +
+ + +
+
+ + +
+
+ {{item[column.alias]}} +
+
+
+
+ + + There are no items show in the list. + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/list/list.listviewlayout.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/list/list.listviewlayout.controller.js index de4902bdd1..b396b66564 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/list/list.listviewlayout.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/list/list.listviewlayout.controller.js @@ -1,76 +1,76 @@ -(function() { - "use strict"; - - function ListViewListLayoutController($scope, listViewHelper, $location, mediaHelper) { - - var vm = this; - - vm.nodeId = $scope.contentId; - //vm.acceptedFileTypes = mediaHelper.formatFileTypes(Umbraco.Sys.ServerVariables.umbracoSettings.imageFileTypes); - //instead of passing in a whitelist, we pass in a blacklist by adding ! to the ext - vm.acceptedFileTypes = mediaHelper.formatFileTypes(Umbraco.Sys.ServerVariables.umbracoSettings.disallowedUploadFiles).replace(/./g, "!."); - vm.maxFileSize = Umbraco.Sys.ServerVariables.umbracoSettings.maxFileSize + "KB"; - vm.activeDrag = false; - vm.isRecycleBin = $scope.contentId === '-21' || $scope.contentId === '-20'; - - vm.selectItem = selectItem; - vm.clickItem = clickItem; - vm.selectAll = selectAll; - vm.isSelectedAll = isSelectedAll; - vm.isSortDirection = isSortDirection; - vm.sort = sort; - vm.dragEnter = dragEnter; - vm.dragLeave = dragLeave; - vm.onFilesQueue = onFilesQueue; - vm.onUploadComplete = onUploadComplete; - - function selectAll($event) { - listViewHelper.selectAllItems($scope.items, $scope.selection, $event); - } - - function isSelectedAll() { - return listViewHelper.isSelectedAll($scope.items, $scope.selection); - } - - function selectItem(selectedItem, $index, $event) { - listViewHelper.selectHandler(selectedItem, $index, $scope.items, $scope.selection, $event); - } - - function clickItem(item) { - $location.path($scope.entityType + '/' + $scope.entityType + '/edit/' + item.id); - } - - function isSortDirection(col, direction) { - return listViewHelper.setSortingDirection(col, direction, $scope.options); - } - - function sort(field, allow, isSystem) { - if (allow) { - $scope.options.orderBySystemField = isSystem; - listViewHelper.setSorting(field, allow, $scope.options); - $scope.getContent($scope.contentId); - } - } - - // Dropzone upload functions - function dragEnter(el, event) { - vm.activeDrag = true; - } - - function dragLeave(el, event) { - vm.activeDrag = false; - } - - function onFilesQueue() { - vm.activeDrag = false; - } - - function onUploadComplete() { - $scope.getContent($scope.contentId); - } - - } - - angular.module("umbraco").controller("Umbraco.PropertyEditors.ListView.ListLayoutController", ListViewListLayoutController); - -})(); +(function () { + "use strict"; + + function ListViewListLayoutController($scope, listViewHelper, $location, mediaHelper) { + + var vm = this; + + vm.nodeId = $scope.contentId; + //vm.acceptedFileTypes = mediaHelper.formatFileTypes(Umbraco.Sys.ServerVariables.umbracoSettings.imageFileTypes); + //instead of passing in a whitelist, we pass in a blacklist by adding ! to the ext + vm.acceptedFileTypes = mediaHelper.formatFileTypes(Umbraco.Sys.ServerVariables.umbracoSettings.disallowedUploadFiles).replace(/./g, "!."); + vm.maxFileSize = Umbraco.Sys.ServerVariables.umbracoSettings.maxFileSize + "KB"; + vm.activeDrag = false; + vm.isRecycleBin = $scope.contentId === '-21' || $scope.contentId === '-20'; + + vm.selectItem = selectItem; + vm.clickItem = clickItem; + vm.selectAll = selectAll; + vm.isSelectedAll = isSelectedAll; + vm.isSortDirection = isSortDirection; + vm.sort = sort; + vm.dragEnter = dragEnter; + vm.dragLeave = dragLeave; + vm.onFilesQueue = onFilesQueue; + vm.onUploadComplete = onUploadComplete; + + function selectAll($event) { + listViewHelper.selectAllItems($scope.items, $scope.selection, $event); + } + + function isSelectedAll() { + return listViewHelper.isSelectedAll($scope.items, $scope.selection); + } + + function selectItem(selectedItem, $index, $event) { + listViewHelper.selectHandler(selectedItem, $index, $scope.items, $scope.selection, $event); + } + + function clickItem(item) { + $location.path($scope.entityType + '/' +$scope.entityType + '/edit/' +item.id); + } + + function isSortDirection(col, direction) { + return listViewHelper.setSortingDirection(col, direction, $scope.options); + } + + function sort(field, allow, isSystem) { + if (allow) { + $scope.options.orderBySystemField = isSystem; + listViewHelper.setSorting(field, allow, $scope.options); + $scope.getContent($scope.contentId); + } + } + + // Dropzone upload functions + function dragEnter(el, event) { + vm.activeDrag = true; + } + + function dragLeave(el, event) { + vm.activeDrag = false; + } + + function onFilesQueue() { + vm.activeDrag = false; + } + + function onUploadComplete() { + $scope.getContent($scope.contentId); + } + + } + +angular.module("umbraco").controller("Umbraco.PropertyEditors.ListView.ListLayoutController", ListViewListLayoutController); + +}) (); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js index fcb738a358..0d5451274c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js @@ -1,578 +1,578 @@ function listViewController($rootScope, $scope, $routeParams, $injector, $cookieStore, notificationsService, iconHelper, dialogService, editorState, localizationService, $location, appState, $timeout, $q, mediaResource, listViewHelper, userService) { - //this is a quick check to see if we're in create mode, if so just exit - we cannot show children for content - // that isn't created yet, if we continue this will use the parent id in the route params which isn't what - // we want. NOTE: This is just a safety check since when we scaffold an empty model on the server we remove - // the list view tab entirely when it's new. - if ($routeParams.create) { - $scope.isNew = true; - return; - } + //this is a quick check to see if we're in create mode, if so just exit - we cannot show children for content + // that isn't created yet, if we continue this will use the parent id in the route params which isn't what + // we want. NOTE: This is just a safety check since when we scaffold an empty model on the server we remove + // the list view tab entirely when it's new. + if ($routeParams.create) { + $scope.isNew = true; + return; + } - //Now we need to check if this is for media, members or content because that will depend on the resources we use - var contentResource, getContentTypesCallback, getListResultsCallback, deleteItemCallback, getIdCallback, createEditUrlCallback; + //Now we need to check if this is for media, members or content because that will depend on the resources we use + var contentResource, getContentTypesCallback, getListResultsCallback, deleteItemCallback, getIdCallback, createEditUrlCallback; - //check the config for the entity type, or the current section name (since the config is only set in c#, not in pre-vals) - if (($scope.model.config.entityType && $scope.model.config.entityType === "member") || (appState.getSectionState("currentSection") === "member")) { - $scope.entityType = "member"; - contentResource = $injector.get('memberResource'); - getContentTypesCallback = $injector.get('memberTypeResource').getTypes; - getListResultsCallback = contentResource.getPagedResults; - deleteItemCallback = contentResource.deleteByKey; - getIdCallback = function (selected) { - var selectedKey = getItemKey(selected.id); - return selectedKey; - }; - createEditUrlCallback = function (item) { - return "/" + $scope.entityType + "/" + $scope.entityType + "/edit/" + item.key + "?page=" + $scope.options.pageNumber + "&listName=" + $scope.contentId; - }; - } - else { - //check the config for the entity type, or the current section name (since the config is only set in c#, not in pre-vals) - if (($scope.model.config.entityType && $scope.model.config.entityType === "media") || (appState.getSectionState("currentSection") === "media")) { - $scope.entityType = "media"; - contentResource = $injector.get('mediaResource'); - getContentTypesCallback = $injector.get('mediaTypeResource').getAllowedTypes; - } - else { - $scope.entityType = "content"; - contentResource = $injector.get('contentResource'); - getContentTypesCallback = $injector.get('contentTypeResource').getAllowedTypes; - } - getListResultsCallback = contentResource.getChildren; - deleteItemCallback = contentResource.deleteById; - getIdCallback = function (selected) { - return selected.id; - }; - createEditUrlCallback = function (item) { - return "/" + $scope.entityType + "/" + $scope.entityType + "/edit/" + item.id + "?page=" + $scope.options.pageNumber; - }; - } + //check the config for the entity type, or the current section name (since the config is only set in c#, not in pre-vals) + if (($scope.model.config.entityType && $scope.model.config.entityType === "member") || (appState.getSectionState("currentSection") === "member")) { + $scope.entityType = "member"; + contentResource = $injector.get('memberResource'); + getContentTypesCallback = $injector.get('memberTypeResource').getTypes; + getListResultsCallback = contentResource.getPagedResults; + deleteItemCallback = contentResource.deleteByKey; + getIdCallback = function (selected) { + var selectedKey = getItemKey(selected.id); + return selectedKey; + }; + createEditUrlCallback = function (item) { + return "/" + $scope.entityType + "/" + $scope.entityType + "/edit/" + item.key + "?page=" + $scope.options.pageNumber + "&listName=" + $scope.contentId; + }; + } + else { + //check the config for the entity type, or the current section name (since the config is only set in c#, not in pre-vals) + if (($scope.model.config.entityType && $scope.model.config.entityType === "media") || (appState.getSectionState("currentSection") === "media")) { + $scope.entityType = "media"; + contentResource = $injector.get('mediaResource'); + getContentTypesCallback = $injector.get('mediaTypeResource').getAllowedTypes; + } + else { + $scope.entityType = "content"; + contentResource = $injector.get('contentResource'); + getContentTypesCallback = $injector.get('contentTypeResource').getAllowedTypes; + } + getListResultsCallback = contentResource.getChildren; + deleteItemCallback = contentResource.deleteById; + getIdCallback = function (selected) { + return selected.id; + }; + createEditUrlCallback = function (item) { + return "/" + $scope.entityType + "/" + $scope.entityType + "/edit/" + item.id + "?page=" + $scope.options.pageNumber; + }; + } - $scope.pagination = []; - $scope.isNew = false; - $scope.actionInProgress = false; - $scope.selection = []; - $scope.folders = []; - $scope.listViewResultSet = { - totalPages: 0, - items: [] - }; - - $scope.currentNodePermissions = {} - - //Just ensure we do have an editorState - if (editorState.current) { - //Fetch current node allowed actions for the current user - //This is the current node & not each individual child node in the list - var currentUserPermissions = editorState.current.allowedActions; - - //Create a nicer model rather than the funky & hard to remember permissions strings - $scope.currentNodePermissions = { - "canCopy": _.contains(currentUserPermissions, 'O'), //Magic Char = O - "canCreate": _.contains(currentUserPermissions, 'C'), //Magic Char = C - "canDelete": _.contains(currentUserPermissions, 'D'), //Magic Char = D - "canMove": _.contains(currentUserPermissions, 'M'), //Magic Char = M - "canPublish": _.contains(currentUserPermissions, 'U'), //Magic Char = U - "canUnpublish": _.contains(currentUserPermissions, 'U'), //Magic Char = Z (however UI says it can't be set, so if we can publish 'U' we can unpublish) - }; - } - - //when this is null, we don't check permissions - $scope.buttonPermissions = null; - - //When we are dealing with 'content', we need to deal with permissions on child nodes. - // Currently there is no real good way to - if ($scope.entityType === "content") { - - var idsWithPermissions = null; - - $scope.buttonPermissions = { - canCopy: true, - canCreate: true, - canDelete: true, - canMove: true, - canPublish: true, - canUnpublish: true - }; - - $scope.$watch(function() { - return $scope.selection.length; - }, function(newVal, oldVal) { - - if ((idsWithPermissions == null && newVal > 0) || (idsWithPermissions != null)) { - - //get all of the selected ids - var ids = _.map($scope.selection, function(i) { - return i.id.toString(); - }); - - //remove the dictionary items that don't have matching ids - var filtered = {}; - _.each(idsWithPermissions, function (value, key, list) { - if (_.contains(ids, key)) { - filtered[key] = value; - } - }); - idsWithPermissions = filtered; - - //find all ids that we haven't looked up permissions for - var existingIds = _.keys(idsWithPermissions); - var missingLookup = _.map(_.difference(ids, existingIds), function (i) { - return Number(i); - }); - - if (missingLookup.length > 0) { - contentResource.getPermissions(missingLookup).then(function(p) { - $scope.buttonPermissions = listViewHelper.getButtonPermissions(p, idsWithPermissions); - }); - } - else { - $scope.buttonPermissions = listViewHelper.getButtonPermissions({}, idsWithPermissions); - } - } - }); - - } - - $scope.options = { - displayAtTabNumber: $scope.model.config.displayAtTabNumber ? $scope.model.config.displayAtTabNumber : 1, - pageSize: $scope.model.config.pageSize ? $scope.model.config.pageSize : 10, - pageNumber: ($routeParams.page && Number($routeParams.page) != NaN && Number($routeParams.page) > 0) ? $routeParams.page : 1, - filter: '', - orderBy: ($scope.model.config.orderBy ? $scope.model.config.orderBy : 'VersionDate').trim(), - orderDirection: $scope.model.config.orderDirection ? $scope.model.config.orderDirection.trim() : "desc", - orderBySystemField: true, - includeProperties: $scope.model.config.includeProperties ? $scope.model.config.includeProperties : [ - { alias: 'updateDate', header: 'Last edited', isSystem: 1 }, - { alias: 'updater', header: 'Last edited by', isSystem: 1 } - ], - layout: { - layouts: $scope.model.config.layouts, - activeLayout: listViewHelper.getLayout($routeParams.id, $scope.model.config.layouts) - }, - allowBulkPublish: $scope.entityType === 'content' && $scope.model.config.bulkActionPermissions.allowBulkPublish, - allowBulkUnpublish: $scope.entityType === 'content' && $scope.model.config.bulkActionPermissions.allowBulkUnpublish, - allowBulkCopy: $scope.entityType === 'content' && $scope.model.config.bulkActionPermissions.allowBulkCopy, - allowBulkMove: $scope.model.config.bulkActionPermissions.allowBulkMove, - allowBulkDelete: $scope.model.config.bulkActionPermissions.allowBulkDelete - }; - - //update all of the system includeProperties to enable sorting - _.each($scope.options.includeProperties, function (e, i) { - - - //NOTE: special case for contentTypeAlias, it's a system property that cannot be sorted - // to do that, we'd need to update the base query for content to include the content type alias column - // which requires another join and would be slower. BUT We are doing this for members so not sure it makes a diff? - if (e.alias != "contentTypeAlias") { - e.allowSorting = true; - } - - // Another special case for lasted edited data/update date for media, again this field isn't available on the base table so we can't sort by it - if (e.isSystem && $scope.entityType == "media") { - e.allowSorting = e.alias != 'updateDate'; - } - - // Another special case for members, only fields on the base table (cmsMember) can be used for sorting - if (e.isSystem && $scope.entityType == "member") { - e.allowSorting = e.alias == 'username' || e.alias == 'email'; - } - if (e.isSystem) { - - //localize the header - var key = getLocalizedKey(e.alias); - localizationService.localize(key).then(function (v) { - e.header = v; - }); - } - }); - - $scope.selectLayout = function (selectedLayout) { - $scope.options.layout.activeLayout = listViewHelper.setLayout($routeParams.id, selectedLayout, $scope.model.config.layouts); - }; - - function showNotificationsAndReset(err, reload, successMsg) { - - //check if response is ysod - if (err.status && err.status >= 500) { - - // Open ysod overlay - $scope.ysodOverlay = { - view: "ysod", - error: err, - show: true + $scope.pagination = []; + $scope.isNew = false; + $scope.actionInProgress = false; + $scope.selection = []; + $scope.folders = []; + $scope.listViewResultSet = { + totalPages: 0, + items: [] }; - } - $timeout(function () { - $scope.bulkStatus = ""; - $scope.actionInProgress = false; - }, 500); + $scope.currentNodePermissions = {} - if (reload === true) { - $scope.reloadView($scope.contentId); - } + //Just ensure we do have an editorState + if (editorState.current) { + //Fetch current node allowed actions for the current user + //This is the current node & not each individual child node in the list + var currentUserPermissions = editorState.current.allowedActions; - if (err.data && angular.isArray(err.data.notifications)) { - for (var i = 0; i < err.data.notifications.length; i++) { - notificationsService.showNotification(err.data.notifications[i]); - } - } - else if (successMsg) { - notificationsService.success("Done", successMsg); - } - } - - $scope.next = function (pageNumber) { - $scope.options.pageNumber = pageNumber; - $scope.reloadView($scope.contentId); - }; - - $scope.goToPage = function (pageNumber) { - $scope.options.pageNumber = pageNumber; - $scope.reloadView($scope.contentId); - }; - - $scope.prev = function (pageNumber) { - $scope.options.pageNumber = pageNumber; - $scope.reloadView($scope.contentId); - }; - - - /*Loads the search results, based on parameters set in prev,next,sort and so on*/ - /*Pagination is done by an array of objects, due angularJS's funky way of monitoring state - with simple values */ - - $scope.reloadView = function (id) { - - $scope.viewLoaded = false; - - listViewHelper.clearSelection($scope.listViewResultSet.items, $scope.folders, $scope.selection); - - getListResultsCallback(id, $scope.options).then(function (data) { - - $scope.actionInProgress = false; - $scope.listViewResultSet = data; - - //update all values for display - if ($scope.listViewResultSet.items) { - _.each($scope.listViewResultSet.items, function (e, index) { - setPropertyValues(e); - }); + //Create a nicer model rather than the funky & hard to remember permissions strings + $scope.currentNodePermissions = { + "canCopy": _.contains(currentUserPermissions, 'O'), //Magic Char = O + "canCreate": _.contains(currentUserPermissions, 'C'), //Magic Char = C + "canDelete": _.contains(currentUserPermissions, 'D'), //Magic Char = D + "canMove": _.contains(currentUserPermissions, 'M'), //Magic Char = M + "canPublish": _.contains(currentUserPermissions, 'U'), //Magic Char = U + "canUnpublish": _.contains(currentUserPermissions, 'U'), //Magic Char = Z (however UI says it can't be set, so if we can publish 'U' we can unpublish) + }; } - if ($scope.entityType === 'media') { + //when this is null, we don't check permissions + $scope.buttonPermissions = null; - mediaResource.getChildFolders($scope.contentId) - .then(function (folders) { - $scope.folders = folders; - $scope.viewLoaded = true; - }); + //When we are dealing with 'content', we need to deal with permissions on child nodes. + // Currently there is no real good way to + if ($scope.entityType === "content") { + + var idsWithPermissions = null; + + $scope.buttonPermissions = { + canCopy: true, + canCreate: true, + canDelete: true, + canMove: true, + canPublish: true, + canUnpublish: true + }; + + $scope.$watch(function () { + return $scope.selection.length; + }, function (newVal, oldVal) { + + if ((idsWithPermissions == null && newVal > 0) || (idsWithPermissions != null)) { + + //get all of the selected ids + var ids = _.map($scope.selection, function (i) { + return i.id.toString(); + }); + + //remove the dictionary items that don't have matching ids + var filtered = {}; + _.each(idsWithPermissions, function (value, key, list) { + if (_.contains(ids, key)) { + filtered[key] = value; + } + }); + idsWithPermissions = filtered; + + //find all ids that we haven't looked up permissions for + var existingIds = _.keys(idsWithPermissions); + var missingLookup = _.map(_.difference(ids, existingIds), function (i) { + return Number(i); + }); + + if (missingLookup.length > 0) { + contentResource.getPermissions(missingLookup).then(function (p) { + $scope.buttonPermissions = listViewHelper.getButtonPermissions(p, idsWithPermissions); + }); + } + else { + $scope.buttonPermissions = listViewHelper.getButtonPermissions({}, idsWithPermissions); + } + } + }); - } else { - $scope.viewLoaded = true; } - //NOTE: This might occur if we are requesting a higher page number than what is actually available, for example - // if you have more than one page and you delete all items on the last page. In this case, we need to reset to the last - // available page and then re-load again - if ($scope.options.pageNumber > $scope.listViewResultSet.totalPages) { - $scope.options.pageNumber = $scope.listViewResultSet.totalPages; + $scope.options = { + displayAtTabNumber: $scope.model.config.displayAtTabNumber ? $scope.model.config.displayAtTabNumber : 1, + pageSize: $scope.model.config.pageSize ? $scope.model.config.pageSize : 10, + pageNumber: ($routeParams.page && Number($routeParams.page) != NaN && Number($routeParams.page) > 0) ? $routeParams.page : 1, + filter: '', + orderBy: ($scope.model.config.orderBy ? $scope.model.config.orderBy : 'VersionDate').trim(), + orderDirection: $scope.model.config.orderDirection ? $scope.model.config.orderDirection.trim() : "desc", + orderBySystemField: true, + includeProperties: $scope.model.config.includeProperties ? $scope.model.config.includeProperties : [ + { alias: 'updateDate', header: 'Last edited', isSystem: 1 }, + { alias: 'updater', header: 'Last edited by', isSystem: 1 } + ], + layout: { + layouts: $scope.model.config.layouts, + activeLayout: listViewHelper.getLayout($routeParams.id, $scope.model.config.layouts) + }, + allowBulkPublish: $scope.entityType === 'content' && $scope.model.config.bulkActionPermissions.allowBulkPublish, + allowBulkUnpublish: $scope.entityType === 'content' && $scope.model.config.bulkActionPermissions.allowBulkUnpublish, + allowBulkCopy: $scope.entityType === 'content' && $scope.model.config.bulkActionPermissions.allowBulkCopy, + allowBulkMove: $scope.model.config.bulkActionPermissions.allowBulkMove, + allowBulkDelete: $scope.model.config.bulkActionPermissions.allowBulkDelete + }; - //reload! - $scope.reloadView(id); + //update all of the system includeProperties to enable sorting + _.each($scope.options.includeProperties, function (e, i) { + + + //NOTE: special case for contentTypeAlias, it's a system property that cannot be sorted + // to do that, we'd need to update the base query for content to include the content type alias column + // which requires another join and would be slower. BUT We are doing this for members so not sure it makes a diff? + if (e.alias != "contentTypeAlias") { + e.allowSorting = true; + } + + // Another special case for lasted edited data/update date for media, again this field isn't available on the base table so we can't sort by it + if (e.isSystem && $scope.entityType == "media") { + e.allowSorting = e.alias != 'updateDate'; + } + + // Another special case for members, only fields on the base table (cmsMember) can be used for sorting + if (e.isSystem && $scope.entityType == "member") { + e.allowSorting = e.alias == 'username' || e.alias == 'email'; + } + if (e.isSystem) { + + //localize the header + var key = getLocalizedKey(e.alias); + localizationService.localize(key).then(function (v) { + e.header = v; + }); + } + }); + + $scope.selectLayout = function (selectedLayout) { + $scope.options.layout.activeLayout = listViewHelper.setLayout($routeParams.id, selectedLayout, $scope.model.config.layouts); + }; + + function showNotificationsAndReset(err, reload, successMsg) { + + //check if response is ysod + if (err.status && err.status >= 500) { + + // Open ysod overlay + $scope.ysodOverlay = { + view: "ysod", + error: err, + show: true + }; + } + + $timeout(function () { + $scope.bulkStatus = ""; + $scope.actionInProgress = false; + }, 500); + + if (reload === true) { + $scope.reloadView($scope.contentId); + } + + if (err.data && angular.isArray(err.data.notifications)) { + for (var i = 0; i < err.data.notifications.length; i++) { + notificationsService.showNotification(err.data.notifications[i]); + } + } + else if (successMsg) { + notificationsService.success("Done", successMsg); + } } - }); - }; + $scope.next = function (pageNumber) { + $scope.options.pageNumber = pageNumber; + $scope.reloadView($scope.contentId); + }; - var searchListView = _.debounce(function () { - $scope.$apply(function () { - makeSearch(); - }); - }, 500); + $scope.goToPage = function (pageNumber) { + $scope.options.pageNumber = pageNumber; + $scope.reloadView($scope.contentId); + }; - $scope.forceSearch = function (ev) { - //13: enter - switch (ev.keyCode) { - case 13: - makeSearch(); - break; - } - }; + $scope.prev = function (pageNumber) { + $scope.options.pageNumber = pageNumber; + $scope.reloadView($scope.contentId); + }; - $scope.enterSearch = function () { - $scope.viewLoaded = false; - searchListView(); - }; - function makeSearch() { - if ($scope.options.filter !== null && $scope.options.filter !== undefined) { - $scope.options.pageNumber = 1; - //$scope.actionInProgress = true; - $scope.reloadView($scope.contentId); - } - } + /*Loads the search results, based on parameters set in prev,next,sort and so on*/ + /*Pagination is done by an array of objects, due angularJS's funky way of monitoring state + with simple values */ - $scope.isAnythingSelected = function () { - if ($scope.selection.length === 0) { - return false; - } else { - return true; - } - }; + $scope.reloadView = function (id) { - $scope.selectedItemsCount = function () { - return $scope.selection.length; - }; + $scope.viewLoaded = false; - $scope.clearSelection = function () { - listViewHelper.clearSelection($scope.listViewResultSet.items, $scope.folders, $scope.selection); - }; + listViewHelper.clearSelection($scope.listViewResultSet.items, $scope.folders, $scope.selection); - $scope.getIcon = function (entry) { - return iconHelper.convertFromLegacyIcon(entry.icon); - }; + getListResultsCallback(id, $scope.options).then(function (data) { - function serial(selected, fn, getStatusMsg, index) { - return fn(selected, index).then(function (content) { - index++; - $scope.bulkStatus = getStatusMsg(index, selected.length); - return index < selected.length ? serial(selected, fn, getStatusMsg, index) : content; - }, function (err) { - var reload = index > 0; - showNotificationsAndReset(err, reload); - return err; - }); - } + $scope.actionInProgress = false; + $scope.listViewResultSet = data; - function applySelected(fn, getStatusMsg, getSuccessMsg, confirmMsg) { - var selected = $scope.selection; - if (selected.length === 0) - return; - if (confirmMsg && !confirm(confirmMsg)) - return; + //update all values for display + if ($scope.listViewResultSet.items) { + _.each($scope.listViewResultSet.items, function (e, index) { + setPropertyValues(e); + }); + } - $scope.actionInProgress = true; - $scope.bulkStatus = getStatusMsg(0, selected.length); + if ($scope.entityType === 'media') { - serial(selected, fn, getStatusMsg, 0).then(function (result) { - // executes once the whole selection has been processed - // in case of an error (caught by serial), result will be the error - if (!(result.data && angular.isArray(result.data.notifications))) - showNotificationsAndReset(result, true, getSuccessMsg(selected.length)); - }); - } + mediaResource.getChildFolders($scope.contentId) + .then(function (folders) { + $scope.folders = folders; + $scope.viewLoaded = true; + }); - $scope.delete = function () { - applySelected( - function (selected, index) { return deleteItemCallback(getIdCallback(selected[index])); }, - function (count, total) { return "Deleted " + count + " out of " + total + " item" + (total > 1 ? "s" : ""); }, - function (total) { return "Deleted " + total + " item" + (total > 1 ? "s" : ""); }, - "Sure you want to delete?"); - }; + } else { + $scope.viewLoaded = true; + } - $scope.publish = function () { - applySelected( - function (selected, index) { return contentResource.publishById(getIdCallback(selected[index])); }, - function (count, total) { return "Published " + count + " out of " + total + " item" + (total > 1 ? "s" : ""); }, - function (total) { return "Published " + total + " item" + (total > 1 ? "s" : ""); }); - }; + //NOTE: This might occur if we are requesting a higher page number than what is actually available, for example + // if you have more than one page and you delete all items on the last page. In this case, we need to reset to the last + // available page and then re-load again + if ($scope.options.pageNumber > $scope.listViewResultSet.totalPages) { + $scope.options.pageNumber = $scope.listViewResultSet.totalPages; - $scope.unpublish = function () { - applySelected( - function (selected, index) { return contentResource.unPublish(getIdCallback(selected[index])); }, - function (count, total) { return "Unpublished " + count + " out of " + total + " item" + (total > 1 ? "s" : ""); }, - function (total) { return "Unpublished " + total + " item" + (total > 1 ? "s" : ""); }); - }; + //reload! + $scope.reloadView(id); + } - $scope.move = function () { - $scope.moveDialog = {}; - $scope.moveDialog.title = "Move"; - $scope.moveDialog.section = $scope.entityType; - $scope.moveDialog.currentNode = $scope.contentId; - $scope.moveDialog.view = "move"; - $scope.moveDialog.show = true; + }); + }; - $scope.moveDialog.submit = function (model) { + var searchListView = _.debounce(function () { + $scope.$apply(function () { + makeSearch(); + }); + }, 500); - if (model.target) { - performMove(model.target); + $scope.forceSearch = function (ev) { + //13: enter + switch (ev.keyCode) { + case 13: + makeSearch(); + break; + } + }; + + $scope.enterSearch = function () { + $scope.viewLoaded = false; + searchListView(); + }; + + function makeSearch() { + if ($scope.options.filter !== null && $scope.options.filter !== undefined) { + $scope.options.pageNumber = 1; + //$scope.actionInProgress = true; + $scope.reloadView($scope.contentId); + } } - $scope.moveDialog.show = false; - $scope.moveDialog = null; - }; + $scope.isAnythingSelected = function () { + if ($scope.selection.length === 0) { + return false; + } else { + return true; + } + }; - $scope.moveDialog.close = function (oldModel) { - $scope.moveDialog.show = false; - $scope.moveDialog = null; - }; + $scope.selectedItemsCount = function () { + return $scope.selection.length; + }; - }; + $scope.clearSelection = function () { + listViewHelper.clearSelection($scope.listViewResultSet.items, $scope.folders, $scope.selection); + }; - function performMove(target) { + $scope.getIcon = function (entry) { + return iconHelper.convertFromLegacyIcon(entry.icon); + }; - applySelected( - function (selected, index) { return contentResource.move({ parentId: target.id, id: getIdCallback(selected[index]) }); }, - function (count, total) { return "Moved " + count + " out of " + total + " item" + (total > 1 ? "s" : ""); }, - function (total) { return "Moved " + total + " item" + (total > 1 ? "s" : ""); }); - } - - $scope.copy = function () { - $scope.copyDialog = {}; - $scope.copyDialog.title = "Copy"; - $scope.copyDialog.section = $scope.entityType; - $scope.copyDialog.currentNode = $scope.contentId; - $scope.copyDialog.view = "copy"; - $scope.copyDialog.show = true; - - $scope.copyDialog.submit = function (model) { - if (model.target) { - performCopy(model.target, model.relateToOriginal); + function serial(selected, fn, getStatusMsg, index) { + return fn(selected, index).then(function (content) { + index++; + $scope.bulkStatus = getStatusMsg(index, selected.length); + return index < selected.length ? serial(selected, fn, getStatusMsg, index) : content; + }, function (err) { + var reload = index > 0; + showNotificationsAndReset(err, reload); + return err; + }); } - $scope.copyDialog.show = false; - $scope.copyDialog = null; - }; + function applySelected(fn, getStatusMsg, getSuccessMsg, confirmMsg) { + var selected = $scope.selection; + if (selected.length === 0) + return; + if (confirmMsg && !confirm(confirmMsg)) + return; - $scope.copyDialog.close = function (oldModel) { - $scope.copyDialog.show = false; - $scope.copyDialog = null; - }; + $scope.actionInProgress = true; + $scope.bulkStatus = getStatusMsg(0, selected.length); - }; - - function performCopy(target, relateToOriginal) { - applySelected( - function (selected, index) { return contentResource.copy({ parentId: target.id, id: getIdCallback(selected[index]), relateToOriginal: relateToOriginal }); }, - function (count, total) { return "Copied " + count + " out of " + total + " item" + (total > 1 ? "s" : ""); }, - function (total) { return "Copied " + total + " item" + (total > 1 ? "s" : ""); }); - } - - function getCustomPropertyValue(alias, properties) { - var value = ''; - var index = 0; - var foundAlias = false; - for (var i = 0; i < properties.length; i++) { - if (properties[i].alias == alias) { - foundAlias = true; - break; - } - index++; - } - - if (foundAlias) { - value = properties[index].value; - } - - return value; - } - - /** This ensures that the correct value is set for each item in a row, we don't want to call a function during interpolation or ng-bind as performance is really bad that way */ - function setPropertyValues(result) { - - //set the edit url - result.editPath = createEditUrlCallback(result); - - _.each($scope.options.includeProperties, function (e, i) { - - var alias = e.alias; - - // First try to pull the value directly from the alias (e.g. updatedBy) - var value = result[alias]; - - // If this returns an object, look for the name property of that (e.g. owner.name) - if (value === Object(value)) { - value = value['name']; + serial(selected, fn, getStatusMsg, 0).then(function (result) { + // executes once the whole selection has been processed + // in case of an error (caught by serial), result will be the error + if (!(result.data && angular.isArray(result.data.notifications))) + showNotificationsAndReset(result, true, getSuccessMsg(selected.length)); + }); } - // If we've got nothing yet, look at a user defined property - if (typeof value === 'undefined') { - value = getCustomPropertyValue(alias, result.properties); + $scope.delete = function () { + applySelected( + function (selected, index) { return deleteItemCallback(getIdCallback(selected[index])); }, + function (count, total) { return "Deleted " + count + " out of " + total + " item" + (total > 1 ? "s" : ""); }, + function (total) { return "Deleted " + total + " item" + (total > 1 ? "s" : ""); }, + "Sure you want to delete?"); + }; + + $scope.publish = function () { + applySelected( + function (selected, index) { return contentResource.publishById(getIdCallback(selected[index])); }, + function (count, total) { return "Published " + count + " out of " + total + " item" + (total > 1 ? "s" : ""); }, + function (total) { return "Published " + total + " item" + (total > 1 ? "s" : ""); }); + }; + + $scope.unpublish = function () { + applySelected( + function (selected, index) { return contentResource.unPublish(getIdCallback(selected[index])); }, + function (count, total) { return "Unpublished " + count + " out of " + total + " item" + (total > 1 ? "s" : ""); }, + function (total) { return "Unpublished " + total + " item" + (total > 1 ? "s" : ""); }); + }; + + $scope.move = function () { + $scope.moveDialog = {}; + $scope.moveDialog.title = "Move"; + $scope.moveDialog.section = $scope.entityType; + $scope.moveDialog.currentNode = $scope.contentId; + $scope.moveDialog.view = "move"; + $scope.moveDialog.show = true; + + $scope.moveDialog.submit = function (model) { + + if (model.target) { + performMove(model.target); + } + + $scope.moveDialog.show = false; + $scope.moveDialog = null; + }; + + $scope.moveDialog.close = function (oldModel) { + $scope.moveDialog.show = false; + $scope.moveDialog = null; + }; + + }; + + function performMove(target) { + + applySelected( + function (selected, index) { return contentResource.move({ parentId: target.id, id: getIdCallback(selected[index]) }); }, + function (count, total) { return "Moved " + count + " out of " + total + " item" + (total > 1 ? "s" : ""); }, + function (total) { return "Moved " + total + " item" + (total > 1 ? "s" : ""); }); } - // If we have a date, format it - if (isDate(value)) { - value = value.substring(0, value.length - 3); + $scope.copy = function () { + $scope.copyDialog = {}; + $scope.copyDialog.title = "Copy"; + $scope.copyDialog.section = $scope.entityType; + $scope.copyDialog.currentNode = $scope.contentId; + $scope.copyDialog.view = "copy"; + $scope.copyDialog.show = true; + + $scope.copyDialog.submit = function (model) { + if (model.target) { + performCopy(model.target, model.relateToOriginal); + } + + $scope.copyDialog.show = false; + $scope.copyDialog = null; + }; + + $scope.copyDialog.close = function (oldModel) { + $scope.copyDialog.show = false; + $scope.copyDialog = null; + }; + + }; + + function performCopy(target, relateToOriginal) { + applySelected( + function (selected, index) { return contentResource.copy({ parentId: target.id, id: getIdCallback(selected[index]), relateToOriginal: relateToOriginal }); }, + function (count, total) { return "Copied " + count + " out of " + total + " item" + (total > 1 ? "s" : ""); }, + function (total) { return "Copied " + total + " item" + (total > 1 ? "s" : ""); }); } - // set what we've got on the result - result[alias] = value; - }); + function getCustomPropertyValue(alias, properties) { + var value = ''; + var index = 0; + var foundAlias = false; + for (var i = 0; i < properties.length; i++) { + if (properties[i].alias == alias) { + foundAlias = true; + break; + } + index++; + } + if (foundAlias) { + value = properties[index].value; + } - } - - function isDate(val) { - if (angular.isString(val)) { - return val.match(/^(\d{4})\-(\d{2})\-(\d{2})\ (\d{2})\:(\d{2})\:(\d{2})$/); - } - return false; - } - - function initView() { - //default to root id if the id is undefined - var id = $routeParams.id; - if (id === undefined) { - id = -1; - } - - $scope.listViewAllowedTypes = getContentTypesCallback(id); - - $scope.contentId = id; - $scope.isTrashed = id === "-20" || id === "-21"; - - $scope.options.allowBulkPublish = $scope.options.allowBulkPublish && !$scope.isTrashed; - $scope.options.allowBulkUnpublish = $scope.options.allowBulkUnpublish && !$scope.isTrashed; - - $scope.options.bulkActionsAllowed = $scope.options.allowBulkPublish || - $scope.options.allowBulkUnpublish || - $scope.options.allowBulkCopy || - $scope.options.allowBulkMove || - $scope.options.allowBulkDelete; - - $scope.reloadView($scope.contentId); - } - - function getLocalizedKey(alias) { - - switch (alias) { - case "sortOrder": - return "general_sort"; - case "updateDate": - return "content_updateDate"; - case "updater": - return "content_updatedBy"; - case "createDate": - return "content_createDate"; - case "owner": - return "content_createBy"; - case "published": - return "content_isPublished"; - case "contentTypeAlias": - //TODO: Check for members - return $scope.entityType === "content" ? "content_documentType" : "content_mediatype"; - case "email": - return "general_email"; - case "username": - return "general_username"; - } - return alias; - } - - function getItemKey(itemId) { - for (var i = 0; i < $scope.listViewResultSet.items.length; i++) { - var item = $scope.listViewResultSet.items[i]; - if (item.id === itemId) { - return item.key; + return value; } - } - } - //GO! - initView(); + /** This ensures that the correct value is set for each item in a row, we don't want to call a function during interpolation or ng-bind as performance is really bad that way */ + function setPropertyValues(result) { + + //set the edit url + result.editPath = createEditUrlCallback(result); + + _.each($scope.options.includeProperties, function (e, i) { + + var alias = e.alias; + + // First try to pull the value directly from the alias (e.g. updatedBy) + var value = result[alias]; + + // If this returns an object, look for the name property of that (e.g. owner.name) + if (value === Object(value)) { + value = value['name']; + } + + // If we've got nothing yet, look at a user defined property + if (typeof value === 'undefined') { + value = getCustomPropertyValue(alias, result.properties); + } + + // If we have a date, format it + if (isDate(value)) { + value = value.substring(0, value.length - 3); + } + + // set what we've got on the result + result[alias] = value; + }); + + + } + + function isDate(val) { + if (angular.isString(val)) { + return val.match(/^(\d{4})\-(\d{2})\-(\d{2})\ (\d{2})\:(\d{2})\:(\d{2})$/); + } + return false; + } + + function initView() { + //default to root id if the id is undefined + var id = $routeParams.id; + if (id === undefined) { + id = -1; + } + + $scope.listViewAllowedTypes = getContentTypesCallback(id); + + $scope.contentId = id; + $scope.isTrashed = id === "-20" || id === "-21"; + + $scope.options.allowBulkPublish = $scope.options.allowBulkPublish && !$scope.isTrashed; + $scope.options.allowBulkUnpublish = $scope.options.allowBulkUnpublish && !$scope.isTrashed; + + $scope.options.bulkActionsAllowed = $scope.options.allowBulkPublish || + $scope.options.allowBulkUnpublish || + $scope.options.allowBulkCopy || + $scope.options.allowBulkMove || + $scope.options.allowBulkDelete; + + $scope.reloadView($scope.contentId); + } + + function getLocalizedKey(alias) { + + switch (alias) { + case "sortOrder": + return "general_sort"; + case "updateDate": + return "content_updateDate"; + case "updater": + return "content_updatedBy"; + case "createDate": + return "content_createDate"; + case "owner": + return "content_createBy"; + case "published": + return "content_isPublished"; + case "contentTypeAlias": + //TODO: Check for members + return $scope.entityType === "content" ? "content_documentType" : "content_mediatype"; + case "email": + return "general_email"; + case "username": + return "general_username"; + } + return alias; + } + + function getItemKey(itemId) { + for (var i = 0; i < $scope.listViewResultSet.items.length; i++) { + var item = $scope.listViewResultSet.items[i]; + if (item.id === itemId) { + return item.key; + } + } + } + + //GO! + initView(); } diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index 217d3123b8..580e1a018c 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -38,769 +38,769 @@ using Notification = Umbraco.Web.Models.ContentEditing.Notification; namespace Umbraco.Web.Editors { - /// - /// The API controller used for editing content - /// - /// - /// This controller is decorated with the UmbracoApplicationAuthorizeAttribute which means that any user requesting - /// access to ALL of the methods on this controller will need access to the content application. - /// - [PluginController("UmbracoApi")] - [UmbracoApplicationAuthorizeAttribute(Constants.Applications.Content)] - public class ContentController : ContentControllerBase - { - /// - /// Constructor - /// - public ContentController() - : this(UmbracoContext.Current) - { - } - - /// - /// Constructor - /// - /// - public ContentController(UmbracoContext umbracoContext) - : base(umbracoContext) - { - } - - /// - /// Return content for the specified ids - /// - /// - /// - [FilterAllowedOutgoingContent(typeof(IEnumerable))] - public IEnumerable GetByIds([FromUri]int[] ids) - { - var foundContent = Services.ContentService.GetByIds(ids); - return foundContent.Select(Mapper.Map); - } - - /// - /// Returns an item to be used to display the recycle bin for content - /// - /// - public ContentItemDisplay GetRecycleBin() - { - var display = new ContentItemDisplay - { - Id = Constants.System.RecycleBinContent, - Alias = "recycleBin", - ParentId = -1, - Name = Services.TextService.Localize("general/recycleBin"), - ContentTypeAlias = "recycleBin", - CreateDate = DateTime.Now, - IsContainer = true, - Path = "-1," + Constants.System.RecycleBinContent - }; - - TabsAndPropertiesResolver.AddListView(display, "content", Services.DataTypeService, Services.TextService); - - return display; - } - - /// - /// Gets the content json for the content id - /// - /// - /// - [OutgoingEditorModelEvent] - [EnsureUserPermissionForContent("id")] - public ContentItemDisplay GetById(int id) - { - var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); - if (foundContent == null) - { - HandleContentNotFound(id); - } - - var content = Mapper.Map(foundContent); - return content; - } - - [EnsureUserPermissionForContent("id")] - public ContentItemDisplay GetWithTreeDefinition(int id) - { - var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); - if (foundContent == null) - { - HandleContentNotFound(id); - } - - var content = Mapper.Map(foundContent); - return content; - } - - /// - /// Gets an empty content item for the - /// - /// - /// - /// - /// If this is a container type, we'll remove the umbContainerView tab for a new item since - /// it cannot actually list children if it doesn't exist yet. - /// - [OutgoingEditorModelEvent] - public ContentItemDisplay GetEmpty(string contentTypeAlias, int parentId) - { - var contentType = Services.ContentTypeService.GetContentType(contentTypeAlias); - if (contentType == null) - { - throw new HttpResponseException(HttpStatusCode.NotFound); - } - - var emptyContent = Services.ContentService.CreateContent("", parentId, contentType.Alias, UmbracoUser.Id); - var mapped = Mapper.Map(emptyContent); - - //remove this tab if it exists: umbContainerView - var containerTab = mapped.Tabs.FirstOrDefault(x => x.Alias == Constants.Conventions.PropertyGroups.ListViewGroupName); - mapped.Tabs = mapped.Tabs.Except(new[] { containerTab }); - return mapped; - } - - /// - /// Gets the Url for a given node ID - /// - /// - /// - public HttpResponseMessage GetNiceUrl(int id) - { - var url = Umbraco.NiceUrl(id); - var response = Request.CreateResponse(HttpStatusCode.OK); - response.Content = new StringContent(url, Encoding.UTF8, "application/json"); - return response; - } - - /// - /// Gets the children for the content id passed in - /// - /// - [FilterAllowedOutgoingContent(typeof(IEnumerable>), "Items")] - public PagedResult> GetChildren( - int id, - int pageNumber = 0, //TODO: This should be '1' as it's not the index - int pageSize = 0, - string orderBy = "SortOrder", - Direction orderDirection = Direction.Ascending, - int? orderBySystemField = 2, - string filter = "") - { - var orderBySystemFieldBool = orderBySystemField == 1 || orderBySystemField == 2; - long totalChildren; - IContent[] children; - if (pageNumber > 0 && pageSize > 0) - { - children = Services.ContentService - .GetPagedChildren(id, (pageNumber - 1), pageSize, out totalChildren - , orderBy, orderDirection, orderBySystemFieldBool, filter).ToArray(); - } - else - { - children = Services.ContentService.GetChildren(id).ToArray(); - totalChildren = children.Length; - } - - if (totalChildren == 0) - { - return new PagedResult>(0, 0, 0); - } - - var pagedResult = new PagedResult>(totalChildren, pageNumber, pageSize); - pagedResult.Items = children - .Select(Mapper.Map>); - - return pagedResult; - } - - [Obsolete("Dont use this, it is incorrectly named, use HasPermission instead")] - public bool GetHasPermission(string permissionToCheck, int nodeId) - { - return HasPermission(permissionToCheck, nodeId); - } - - /// - /// Returns permissions for all nodes passed in for the current user - /// - /// - /// - [HttpPost] - public Dictionary GetPermissions(int[] nodeIds) - { - return Services.UserService - .GetPermissions(Security.CurrentUser, nodeIds) - .ToDictionary(x => x.EntityId, x => x.AssignedPermissions); - } - - [HttpGet] - public bool HasPermission(string permissionToCheck, int nodeId) - { - var p = Services.UserService.GetPermissions(Security.CurrentUser, nodeId).FirstOrDefault(); - if (p != null && p.AssignedPermissions.Contains(permissionToCheck.ToString(CultureInfo.InvariantCulture))) - { - return true; - } - - return false; - } - - /// - /// Saves content - /// - /// - [FileUploadCleanupFilter] - [ContentPostValidate] - public ContentItemDisplay PostSave( - [ModelBinder(typeof(ContentItemBinder))] - ContentItemSave contentItem) - { - //If we've reached here it means: - // * Our model has been bound - // * and validated - // * any file attachments have been saved to their temporary location for us to use - // * we have a reference to the DTO object and the persisted object - // * Permissions are valid - - MapPropertyValues(contentItem); - - //We need to manually check the validation results here because: - // * We still need to save the entity even if there are validation value errors - // * Depending on if the entity is new, and if there are non property validation errors (i.e. the name is null) - // then we cannot continue saving, we can only display errors - // * If there are validation errors and they were attempting to publish, we can only save, NOT publish and display - // a message indicating this - if (ModelState.IsValid == false) - { - if (ValidationHelper.ModelHasRequiredForPersistenceErrors(contentItem) && IsCreatingAction(contentItem.Action)) - { - //ok, so the absolute mandatory data is invalid and it's new, we cannot actually continue! - // add the modelstate to the outgoing object and throw a validation message - var forDisplay = Mapper.Map(contentItem.PersistedContent); - forDisplay.Errors = ModelState.ToErrorDictionary(); - throw new HttpResponseException(Request.CreateValidationErrorResponse(forDisplay)); - - } - - //if the model state is not valid we cannot publish so change it to save - switch (contentItem.Action) - { - case ContentSaveAction.Publish: - contentItem.Action = ContentSaveAction.Save; - break; - case ContentSaveAction.PublishNew: - contentItem.Action = ContentSaveAction.SaveNew; - break; - } - } - - //initialize this to successful - var publishStatus = Attempt.Succeed(); - var wasCancelled = false; - - if (contentItem.Action == ContentSaveAction.Save || contentItem.Action == ContentSaveAction.SaveNew) - { - //save the item - var saveResult = Services.ContentService.WithResult().Save(contentItem.PersistedContent, Security.CurrentUser.Id); - - wasCancelled = saveResult.Success == false && saveResult.Result.StatusType == OperationStatusType.FailedCancelledByEvent; - } - else if (contentItem.Action == ContentSaveAction.SendPublish || contentItem.Action == ContentSaveAction.SendPublishNew) - { - var sendResult = Services.ContentService.SendToPublication(contentItem.PersistedContent, Security.CurrentUser.Id); - wasCancelled = sendResult == false; - } - else - { - //publish the item and check if it worked, if not we will show a diff msg below - publishStatus = Services.ContentService.SaveAndPublishWithStatus(contentItem.PersistedContent, Security.CurrentUser.Id); - wasCancelled = publishStatus.Result.StatusType == PublishStatusType.FailedCancelledByEvent; - } - - //return the updated model - var display = Mapper.Map(contentItem.PersistedContent); - - //lasty, if it is not valid, add the modelstate to the outgoing object and throw a 403 - HandleInvalidModelState(display); - - //put the correct msgs in - switch (contentItem.Action) - { - case ContentSaveAction.Save: - case ContentSaveAction.SaveNew: - if (wasCancelled == false) - { - display.AddSuccessNotification( - Services.TextService.Localize("speechBubbles/editContentSavedHeader"), - Services.TextService.Localize("speechBubbles/editContentSavedText")); - } - else - { - AddCancelMessage(display); - } - break; - case ContentSaveAction.SendPublish: - case ContentSaveAction.SendPublishNew: - if (wasCancelled == false) - { - display.AddSuccessNotification( - Services.TextService.Localize("speechBubbles/editContentSendToPublish"), - Services.TextService.Localize("speechBubbles/editContentSendToPublishText")); - } - else - { - AddCancelMessage(display); - } - break; - case ContentSaveAction.Publish: - case ContentSaveAction.PublishNew: - ShowMessageForPublishStatus(publishStatus.Result, display); - break; - } - - UpdatePreviewContext(contentItem.PersistedContent.Id); - - //If the item is new and the operation was cancelled, we need to return a different - // status code so the UI can handle it since it won't be able to redirect since there - // is no Id to redirect to! - if (wasCancelled && IsCreatingAction(contentItem.Action)) - { - throw new HttpResponseException(Request.CreateValidationErrorResponse(display)); - } - - return display; - } - - /// - /// Publishes a document with a given ID - /// - /// - /// - /// - /// The CanAccessContentAuthorize attribute will deny access to this method if the current user - /// does not have Publish access to this node. - /// - /// - [EnsureUserPermissionForContent("id", 'U')] - public HttpResponseMessage PostPublishById(int id) - { - var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); - - if (foundContent == null) - { - return HandleContentNotFound(id, false); - } - - var publishResult = Services.ContentService.PublishWithStatus(foundContent, Security.GetUserId()); - if (publishResult.Success == false) - { - var notificationModel = new SimpleNotificationModel(); - ShowMessageForPublishStatus(publishResult.Result, notificationModel); - return Request.CreateValidationErrorResponse(notificationModel); - } - - //return ok - return Request.CreateResponse(HttpStatusCode.OK); - - } - - /// - /// Moves an item to the recycle bin, if it is already there then it will permanently delete it - /// - /// - /// - /// - /// The CanAccessContentAuthorize attribute will deny access to this method if the current user - /// does not have Delete access to this node. - /// - [EnsureUserPermissionForContent("id", 'D')] - [HttpDelete] - [HttpPost] - public HttpResponseMessage DeleteById(int id) - { - var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); - - if (foundContent == null) - { - return HandleContentNotFound(id, false); - } - - //if the current item is in the recycle bin - if (foundContent.IsInRecycleBin() == false) - { - var moveResult = Services.ContentService.WithResult().MoveToRecycleBin(foundContent, Security.GetUserId()); - if (moveResult == false) - { - //returning an object of INotificationModel will ensure that any pending - // notification messages are added to the response. - return Request.CreateValidationErrorResponse(new SimpleNotificationModel()); - } - } - else - { - var deleteResult = Services.ContentService.WithResult().Delete(foundContent, Security.GetUserId()); - if (deleteResult == false) - { - //returning an object of INotificationModel will ensure that any pending - // notification messages are added to the response. - return Request.CreateValidationErrorResponse(new SimpleNotificationModel()); - } - } - - return Request.CreateResponse(HttpStatusCode.OK); - } - - /// - /// Empties the recycle bin - /// - /// - /// - /// attributed with EnsureUserPermissionForContent to verify the user has access to the recycle bin - /// - [HttpDelete] - [HttpPost] - [EnsureUserPermissionForContent(Constants.System.RecycleBinContent)] - public HttpResponseMessage EmptyRecycleBin() - { - Services.ContentService.EmptyRecycleBin(); - return Request.CreateResponse(HttpStatusCode.OK); - } - - /// - /// Change the sort order for media - /// - /// - /// - [EnsureUserPermissionForContent("sorted.ParentId", 'S')] - public HttpResponseMessage PostSort(ContentSortOrder sorted) - { - if (sorted == null) - { - return Request.CreateResponse(HttpStatusCode.NotFound); - } - - //if there's nothing to sort just return ok - if (sorted.IdSortOrder.Length == 0) - { - return Request.CreateResponse(HttpStatusCode.OK); - } - - var contentService = Services.ContentService; - var sortedContent = new List(); - try - { - sortedContent.AddRange(Services.ContentService.GetByIds(sorted.IdSortOrder)); - - // Save content with new sort order and update content xml in db accordingly - if (contentService.Sort(sortedContent) == false) - { - LogHelper.Warn("Content sorting failed, this was probably caused by an event being cancelled"); - return Request.CreateValidationErrorResponse("Content sorting failed, this was probably caused by an event being cancelled"); - } - return Request.CreateResponse(HttpStatusCode.OK); - } - catch (Exception ex) - { - LogHelper.Error("Could not update content sort order", ex); - throw; - } - } - - /// - /// Change the sort order for media - /// - /// - /// - [EnsureUserPermissionForContent("move.ParentId", 'M')] - public HttpResponseMessage PostMove(MoveOrCopy move) - { - var toMove = ValidateMoveOrCopy(move); - - Services.ContentService.Move(toMove, move.ParentId); - - var response = Request.CreateResponse(HttpStatusCode.OK); - response.Content = new StringContent(toMove.Path, Encoding.UTF8, "application/json"); - return response; - } - - /// - /// Copies a content item and places the copy as a child of a given parent Id - /// - /// - /// - [EnsureUserPermissionForContent("copy.ParentId", 'C')] - public HttpResponseMessage PostCopy(MoveOrCopy copy) - { - var toCopy = ValidateMoveOrCopy(copy); - - var c = Services.ContentService.Copy(toCopy, copy.ParentId, copy.RelateToOriginal, copy.Recursive); - - var response = Request.CreateResponse(HttpStatusCode.OK); - response.Content = new StringContent(c.Path, Encoding.UTF8, "application/json"); - return response; - } - - /// - /// Unpublishes a node with a given Id and returns the unpublished entity - /// - /// - /// - [EnsureUserPermissionForContent("id", 'U')] - public ContentItemDisplay PostUnPublish(int id) - { - var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); - - if (foundContent == null) - HandleContentNotFound(id); - - var unpublishResult = Services.ContentService.WithResult().UnPublish(foundContent, Security.CurrentUser.Id); - - var content = Mapper.Map(foundContent); - - if (unpublishResult == false) - { - AddCancelMessage(content); - throw new HttpResponseException(Request.CreateValidationErrorResponse(content)); - } - else - { - content.AddSuccessNotification(Services.TextService.Localize("content/unPublish"), Services.TextService.Localize("speechBubbles/contentUnpublished")); - return content; - } - } - - /// - /// Checks if the user is currently in preview mode and if so will update the preview content for this item - /// - /// - private void UpdatePreviewContext(int contentId) - { - var previewId = Request.GetPreviewCookieValue(); - if (previewId.IsNullOrWhiteSpace()) return; - Guid id; - if (Guid.TryParse(previewId, out id)) - { - var d = new Document(contentId); - var pc = new PreviewContent(UmbracoUser, id, false); - pc.PrepareDocument(UmbracoUser, d, true); - pc.SavePreviewSet(); - } - } - - /// - /// Maps the dto property values to the persisted model - /// - /// - private void MapPropertyValues(ContentItemSave contentItem) - { - UpdateName(contentItem); - - //TODO: We need to support 'send to publish' - - contentItem.PersistedContent.ExpireDate = contentItem.ExpireDate; - contentItem.PersistedContent.ReleaseDate = contentItem.ReleaseDate; - //only set the template if it didn't change - var templateChanged = (contentItem.PersistedContent.Template == null && contentItem.TemplateAlias.IsNullOrWhiteSpace() == false) - || (contentItem.PersistedContent.Template != null && contentItem.PersistedContent.Template.Alias != contentItem.TemplateAlias) - || (contentItem.PersistedContent.Template != null && contentItem.TemplateAlias.IsNullOrWhiteSpace()); - if (templateChanged) - { - var template = Services.FileService.GetTemplate(contentItem.TemplateAlias); - if (template == null && contentItem.TemplateAlias.IsNullOrWhiteSpace() == false) - { - //ModelState.AddModelError("Template", "No template exists with the specified alias: " + contentItem.TemplateAlias); - LogHelper.Warn("No template exists with the specified alias: " + contentItem.TemplateAlias); - } - else - { - //NOTE: this could be null if there was a template and the posted template is null, this should remove the assigned template - contentItem.PersistedContent.Template = template; - } - } - - base.MapPropertyValues(contentItem); - } - - /// - /// Ensures the item can be moved/copied to the new location - /// - /// - /// - private IContent ValidateMoveOrCopy(MoveOrCopy model) - { - if (model == null) - { - throw new HttpResponseException(HttpStatusCode.NotFound); - } - - var contentService = Services.ContentService; - var toMove = contentService.GetById(model.Id); - if (toMove == null) - { - throw new HttpResponseException(HttpStatusCode.NotFound); - } - if (model.ParentId < 0) - { - //cannot move if the content item is not allowed at the root - if (toMove.ContentType.AllowedAsRoot == false) - { - throw new HttpResponseException( - Request.CreateNotificationValidationErrorResponse( - Services.TextService.Localize("moveOrCopy/notAllowedAtRoot"))); - } - } - else - { - var parent = contentService.GetById(model.ParentId); - if (parent == null) - { - throw new HttpResponseException(HttpStatusCode.NotFound); - } - - //check if the item is allowed under this one - if (parent.ContentType.AllowedContentTypes.Select(x => x.Id).ToArray() - .Any(x => x.Value == toMove.ContentType.Id) == false) - { - throw new HttpResponseException( - Request.CreateNotificationValidationErrorResponse( - Services.TextService.Localize("moveOrCopy/notAllowedByContentType"))); - } - - // Check on paths - if ((string.Format(",{0},", parent.Path)).IndexOf(string.Format(",{0},", toMove.Id), StringComparison.Ordinal) > -1) - { - throw new HttpResponseException( - Request.CreateNotificationValidationErrorResponse( - Services.TextService.Localize("moveOrCopy/notAllowedByPath"))); - } - } - - return toMove; - } - - private void ShowMessageForPublishStatus(PublishStatus status, INotificationModel display) - { - switch (status.StatusType) - { - case PublishStatusType.Success: - case PublishStatusType.SuccessAlreadyPublished: - display.AddSuccessNotification( - Services.TextService.Localize("speechBubbles/editContentPublishedHeader"), - Services.TextService.Localize("speechBubbles/editContentPublishedText")); - break; - case PublishStatusType.FailedPathNotPublished: - display.AddWarningNotification( - Services.TextService.Localize("publish"), - Services.TextService.Localize("publish/contentPublishedFailedByParent", - new[] { string.Format("{0} ({1})", status.ContentItem.Name, status.ContentItem.Id) }).Trim()); - break; - case PublishStatusType.FailedCancelledByEvent: - AddCancelMessage(display, "publish", "speechBubbles/contentPublishedFailedByEvent"); - break; - case PublishStatusType.FailedAwaitingRelease: - display.AddWarningNotification( - Services.TextService.Localize("publish"), - Services.TextService.Localize("publish/contentPublishedFailedAwaitingRelease", - new[] { string.Format("{0} ({1})", status.ContentItem.Name, status.ContentItem.Id) }).Trim()); - break; - case PublishStatusType.FailedHasExpired: - display.AddWarningNotification( - Services.TextService.Localize("publish"), - Services.TextService.Localize("publish/contentPublishedFailedExpired", - new[] - { - string.Format("{0} ({1})", status.ContentItem.Name, status.ContentItem.Id), - }).Trim()); - break; - case PublishStatusType.FailedIsTrashed: - //TODO: We should add proper error messaging for this! - break; - case PublishStatusType.FailedContentInvalid: - display.AddWarningNotification( - Services.TextService.Localize("publish"), - Services.TextService.Localize("publish/contentPublishedFailedInvalid", - new[] - { - string.Format("{0} ({1})", status.ContentItem.Name, status.ContentItem.Id), - string.Join(",", status.InvalidProperties.Select(x => x.Alias)) - }).Trim()); - break; - default: - throw new IndexOutOfRangeException(); - } - } - - - - /// - /// Performs a permissions check for the user to check if it has access to the node based on - /// start node and/or permissions for the node - /// - /// The storage to add the content item to so it can be reused - /// - /// - /// - /// The content to lookup, if the contentItem is not specified - /// - /// Specifies the already resolved content item to check against - /// - internal static bool CheckPermissions( - IDictionary storage, - IUser user, - IUserService userService, - IContentService contentService, - int nodeId, - char[] permissionsToCheck = null, - IContent contentItem = null) - { - - if (contentItem == null && nodeId != Constants.System.Root && nodeId != Constants.System.RecycleBinContent) - { - contentItem = contentService.GetById(nodeId); - //put the content item into storage so it can be retreived - // in the controller (saves a lookup) - storage[typeof(IContent).ToString()] = contentItem; - } - - if (contentItem == null && nodeId != Constants.System.Root && nodeId != Constants.System.RecycleBinContent) - { - throw new HttpResponseException(HttpStatusCode.NotFound); - } - - var hasPathAccess = (nodeId == Constants.System.Root) - ? UserExtensions.HasPathAccess( - Constants.System.Root.ToInvariantString(), - user.StartContentId, - Constants.System.RecycleBinContent) - : (nodeId == Constants.System.RecycleBinContent) - ? UserExtensions.HasPathAccess( - Constants.System.RecycleBinContent.ToInvariantString(), - user.StartContentId, - Constants.System.RecycleBinContent) - : user.HasPathAccess(contentItem); - - if (hasPathAccess == false) - { - return false; - } - - if (permissionsToCheck == null || permissionsToCheck.Any() == false) - { - return true; - } - - var permission = userService.GetPermissions(user, nodeId).FirstOrDefault(); - - var allowed = true; - foreach (var p in permissionsToCheck) - { - if (permission == null || permission.AssignedPermissions.Contains(p.ToString(CultureInfo.InvariantCulture)) == false) - { - allowed = false; - } - } - return allowed; - } - - } + /// + /// The API controller used for editing content + /// + /// + /// This controller is decorated with the UmbracoApplicationAuthorizeAttribute which means that any user requesting + /// access to ALL of the methods on this controller will need access to the content application. + /// + [PluginController("UmbracoApi")] + [UmbracoApplicationAuthorizeAttribute(Constants.Applications.Content)] + public class ContentController : ContentControllerBase + { + /// + /// Constructor + /// + public ContentController() + : this(UmbracoContext.Current) + { + } + + /// + /// Constructor + /// + /// + public ContentController(UmbracoContext umbracoContext) + : base(umbracoContext) + { + } + + /// + /// Return content for the specified ids + /// + /// + /// + [FilterAllowedOutgoingContent(typeof(IEnumerable))] + public IEnumerable GetByIds([FromUri]int[] ids) + { + var foundContent = Services.ContentService.GetByIds(ids); + return foundContent.Select(Mapper.Map); + } + + /// + /// Returns an item to be used to display the recycle bin for content + /// + /// + public ContentItemDisplay GetRecycleBin() + { + var display = new ContentItemDisplay + { + Id = Constants.System.RecycleBinContent, + Alias = "recycleBin", + ParentId = -1, + Name = Services.TextService.Localize("general/recycleBin"), + ContentTypeAlias = "recycleBin", + CreateDate = DateTime.Now, + IsContainer = true, + Path = "-1," + Constants.System.RecycleBinContent + }; + + TabsAndPropertiesResolver.AddListView(display, "content", Services.DataTypeService, Services.TextService); + + return display; + } + + /// + /// Gets the content json for the content id + /// + /// + /// + [OutgoingEditorModelEvent] + [EnsureUserPermissionForContent("id")] + public ContentItemDisplay GetById(int id) + { + var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); + if (foundContent == null) + { + HandleContentNotFound(id); + } + + var content = Mapper.Map(foundContent); + return content; + } + + [EnsureUserPermissionForContent("id")] + public ContentItemDisplay GetWithTreeDefinition(int id) + { + var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); + if (foundContent == null) + { + HandleContentNotFound(id); + } + + var content = Mapper.Map(foundContent); + return content; + } + + /// + /// Gets an empty content item for the + /// + /// + /// + /// + /// If this is a container type, we'll remove the umbContainerView tab for a new item since + /// it cannot actually list children if it doesn't exist yet. + /// + [OutgoingEditorModelEvent] + public ContentItemDisplay GetEmpty(string contentTypeAlias, int parentId) + { + var contentType = Services.ContentTypeService.GetContentType(contentTypeAlias); + if (contentType == null) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + var emptyContent = Services.ContentService.CreateContent("", parentId, contentType.Alias, UmbracoUser.Id); + var mapped = Mapper.Map(emptyContent); + + //remove this tab if it exists: umbContainerView + var containerTab = mapped.Tabs.FirstOrDefault(x => x.Alias == Constants.Conventions.PropertyGroups.ListViewGroupName); + mapped.Tabs = mapped.Tabs.Except(new[] { containerTab }); + return mapped; + } + + /// + /// Gets the Url for a given node ID + /// + /// + /// + public HttpResponseMessage GetNiceUrl(int id) + { + var url = Umbraco.NiceUrl(id); + var response = Request.CreateResponse(HttpStatusCode.OK); + response.Content = new StringContent(url, Encoding.UTF8, "application/json"); + return response; + } + + /// + /// Gets the children for the content id passed in + /// + /// + [FilterAllowedOutgoingContent(typeof(IEnumerable>), "Items")] + public PagedResult> GetChildren( + int id, + int pageNumber = 0, //TODO: This should be '1' as it's not the index + int pageSize = 0, + string orderBy = "SortOrder", + Direction orderDirection = Direction.Ascending, + int? orderBySystemField = 2, + string filter = "") + { + var orderBySystemFieldBool = orderBySystemField == 1 || orderBySystemField == 2; + long totalChildren; + IContent[] children; + if (pageNumber > 0 && pageSize > 0) + { + children = Services.ContentService + .GetPagedChildren(id, (pageNumber - 1), pageSize, out totalChildren + , orderBy, orderDirection, orderBySystemFieldBool, filter).ToArray(); + } + else + { + children = Services.ContentService.GetChildren(id).ToArray(); + totalChildren = children.Length; + } + + if (totalChildren == 0) + { + return new PagedResult>(0, 0, 0); + } + + var pagedResult = new PagedResult>(totalChildren, pageNumber, pageSize); + pagedResult.Items = children + .Select(Mapper.Map>); + + return pagedResult; + } + + [Obsolete("Dont use this, it is incorrectly named, use HasPermission instead")] + public bool GetHasPermission(string permissionToCheck, int nodeId) + { + return HasPermission(permissionToCheck, nodeId); + } + + /// + /// Returns permissions for all nodes passed in for the current user + /// + /// + /// + [HttpPost] + public Dictionary GetPermissions(int[] nodeIds) + { + return Services.UserService + .GetPermissions(Security.CurrentUser, nodeIds) + .ToDictionary(x => x.EntityId, x => x.AssignedPermissions); + } + + [HttpGet] + public bool HasPermission(string permissionToCheck, int nodeId) + { + var p = Services.UserService.GetPermissions(Security.CurrentUser, nodeId).FirstOrDefault(); + if (p != null && p.AssignedPermissions.Contains(permissionToCheck.ToString(CultureInfo.InvariantCulture))) + { + return true; + } + + return false; + } + + /// + /// Saves content + /// + /// + [FileUploadCleanupFilter] + [ContentPostValidate] + public ContentItemDisplay PostSave( + [ModelBinder(typeof(ContentItemBinder))] + ContentItemSave contentItem) + { + //If we've reached here it means: + // * Our model has been bound + // * and validated + // * any file attachments have been saved to their temporary location for us to use + // * we have a reference to the DTO object and the persisted object + // * Permissions are valid + + MapPropertyValues(contentItem); + + //We need to manually check the validation results here because: + // * We still need to save the entity even if there are validation value errors + // * Depending on if the entity is new, and if there are non property validation errors (i.e. the name is null) + // then we cannot continue saving, we can only display errors + // * If there are validation errors and they were attempting to publish, we can only save, NOT publish and display + // a message indicating this + if (ModelState.IsValid == false) + { + if (ValidationHelper.ModelHasRequiredForPersistenceErrors(contentItem) && IsCreatingAction(contentItem.Action)) + { + //ok, so the absolute mandatory data is invalid and it's new, we cannot actually continue! + // add the modelstate to the outgoing object and throw a validation message + var forDisplay = Mapper.Map(contentItem.PersistedContent); + forDisplay.Errors = ModelState.ToErrorDictionary(); + throw new HttpResponseException(Request.CreateValidationErrorResponse(forDisplay)); + + } + + //if the model state is not valid we cannot publish so change it to save + switch (contentItem.Action) + { + case ContentSaveAction.Publish: + contentItem.Action = ContentSaveAction.Save; + break; + case ContentSaveAction.PublishNew: + contentItem.Action = ContentSaveAction.SaveNew; + break; + } + } + + //initialize this to successful + var publishStatus = Attempt.Succeed(); + var wasCancelled = false; + + if (contentItem.Action == ContentSaveAction.Save || contentItem.Action == ContentSaveAction.SaveNew) + { + //save the item + var saveResult = Services.ContentService.WithResult().Save(contentItem.PersistedContent, Security.CurrentUser.Id); + + wasCancelled = saveResult.Success == false && saveResult.Result.StatusType == OperationStatusType.FailedCancelledByEvent; + } + else if (contentItem.Action == ContentSaveAction.SendPublish || contentItem.Action == ContentSaveAction.SendPublishNew) + { + var sendResult = Services.ContentService.SendToPublication(contentItem.PersistedContent, Security.CurrentUser.Id); + wasCancelled = sendResult == false; + } + else + { + //publish the item and check if it worked, if not we will show a diff msg below + publishStatus = Services.ContentService.SaveAndPublishWithStatus(contentItem.PersistedContent, Security.CurrentUser.Id); + wasCancelled = publishStatus.Result.StatusType == PublishStatusType.FailedCancelledByEvent; + } + + //return the updated model + var display = Mapper.Map(contentItem.PersistedContent); + + //lasty, if it is not valid, add the modelstate to the outgoing object and throw a 403 + HandleInvalidModelState(display); + + //put the correct msgs in + switch (contentItem.Action) + { + case ContentSaveAction.Save: + case ContentSaveAction.SaveNew: + if (wasCancelled == false) + { + display.AddSuccessNotification( + Services.TextService.Localize("speechBubbles/editContentSavedHeader"), + Services.TextService.Localize("speechBubbles/editContentSavedText")); + } + else + { + AddCancelMessage(display); + } + break; + case ContentSaveAction.SendPublish: + case ContentSaveAction.SendPublishNew: + if (wasCancelled == false) + { + display.AddSuccessNotification( + Services.TextService.Localize("speechBubbles/editContentSendToPublish"), + Services.TextService.Localize("speechBubbles/editContentSendToPublishText")); + } + else + { + AddCancelMessage(display); + } + break; + case ContentSaveAction.Publish: + case ContentSaveAction.PublishNew: + ShowMessageForPublishStatus(publishStatus.Result, display); + break; + } + + UpdatePreviewContext(contentItem.PersistedContent.Id); + + //If the item is new and the operation was cancelled, we need to return a different + // status code so the UI can handle it since it won't be able to redirect since there + // is no Id to redirect to! + if (wasCancelled && IsCreatingAction(contentItem.Action)) + { + throw new HttpResponseException(Request.CreateValidationErrorResponse(display)); + } + + return display; + } + + /// + /// Publishes a document with a given ID + /// + /// + /// + /// + /// The CanAccessContentAuthorize attribute will deny access to this method if the current user + /// does not have Publish access to this node. + /// + /// + [EnsureUserPermissionForContent("id", 'U')] + public HttpResponseMessage PostPublishById(int id) + { + var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); + + if (foundContent == null) + { + return HandleContentNotFound(id, false); + } + + var publishResult = Services.ContentService.PublishWithStatus(foundContent, Security.GetUserId()); + if (publishResult.Success == false) + { + var notificationModel = new SimpleNotificationModel(); + ShowMessageForPublishStatus(publishResult.Result, notificationModel); + return Request.CreateValidationErrorResponse(notificationModel); + } + + //return ok + return Request.CreateResponse(HttpStatusCode.OK); + + } + + /// + /// Moves an item to the recycle bin, if it is already there then it will permanently delete it + /// + /// + /// + /// + /// The CanAccessContentAuthorize attribute will deny access to this method if the current user + /// does not have Delete access to this node. + /// + [EnsureUserPermissionForContent("id", 'D')] + [HttpDelete] + [HttpPost] + public HttpResponseMessage DeleteById(int id) + { + var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); + + if (foundContent == null) + { + return HandleContentNotFound(id, false); + } + + //if the current item is in the recycle bin + if (foundContent.IsInRecycleBin() == false) + { + var moveResult = Services.ContentService.WithResult().MoveToRecycleBin(foundContent, Security.GetUserId()); + if (moveResult == false) + { + //returning an object of INotificationModel will ensure that any pending + // notification messages are added to the response. + return Request.CreateValidationErrorResponse(new SimpleNotificationModel()); + } + } + else + { + var deleteResult = Services.ContentService.WithResult().Delete(foundContent, Security.GetUserId()); + if (deleteResult == false) + { + //returning an object of INotificationModel will ensure that any pending + // notification messages are added to the response. + return Request.CreateValidationErrorResponse(new SimpleNotificationModel()); + } + } + + return Request.CreateResponse(HttpStatusCode.OK); + } + + /// + /// Empties the recycle bin + /// + /// + /// + /// attributed with EnsureUserPermissionForContent to verify the user has access to the recycle bin + /// + [HttpDelete] + [HttpPost] + [EnsureUserPermissionForContent(Constants.System.RecycleBinContent)] + public HttpResponseMessage EmptyRecycleBin() + { + Services.ContentService.EmptyRecycleBin(); + return Request.CreateResponse(HttpStatusCode.OK); + } + + /// + /// Change the sort order for media + /// + /// + /// + [EnsureUserPermissionForContent("sorted.ParentId", 'S')] + public HttpResponseMessage PostSort(ContentSortOrder sorted) + { + if (sorted == null) + { + return Request.CreateResponse(HttpStatusCode.NotFound); + } + + //if there's nothing to sort just return ok + if (sorted.IdSortOrder.Length == 0) + { + return Request.CreateResponse(HttpStatusCode.OK); + } + + var contentService = Services.ContentService; + var sortedContent = new List(); + try + { + sortedContent.AddRange(Services.ContentService.GetByIds(sorted.IdSortOrder)); + + // Save content with new sort order and update content xml in db accordingly + if (contentService.Sort(sortedContent) == false) + { + LogHelper.Warn("Content sorting failed, this was probably caused by an event being cancelled"); + return Request.CreateValidationErrorResponse("Content sorting failed, this was probably caused by an event being cancelled"); + } + return Request.CreateResponse(HttpStatusCode.OK); + } + catch (Exception ex) + { + LogHelper.Error("Could not update content sort order", ex); + throw; + } + } + + /// + /// Change the sort order for media + /// + /// + /// + [EnsureUserPermissionForContent("move.ParentId", 'M')] + public HttpResponseMessage PostMove(MoveOrCopy move) + { + var toMove = ValidateMoveOrCopy(move); + + Services.ContentService.Move(toMove, move.ParentId); + + var response = Request.CreateResponse(HttpStatusCode.OK); + response.Content = new StringContent(toMove.Path, Encoding.UTF8, "application/json"); + return response; + } + + /// + /// Copies a content item and places the copy as a child of a given parent Id + /// + /// + /// + [EnsureUserPermissionForContent("copy.ParentId", 'C')] + public HttpResponseMessage PostCopy(MoveOrCopy copy) + { + var toCopy = ValidateMoveOrCopy(copy); + + var c = Services.ContentService.Copy(toCopy, copy.ParentId, copy.RelateToOriginal, copy.Recursive); + + var response = Request.CreateResponse(HttpStatusCode.OK); + response.Content = new StringContent(c.Path, Encoding.UTF8, "application/json"); + return response; + } + + /// + /// Unpublishes a node with a given Id and returns the unpublished entity + /// + /// + /// + [EnsureUserPermissionForContent("id", 'U')] + public ContentItemDisplay PostUnPublish(int id) + { + var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); + + if (foundContent == null) + HandleContentNotFound(id); + + var unpublishResult = Services.ContentService.WithResult().UnPublish(foundContent, Security.CurrentUser.Id); + + var content = Mapper.Map(foundContent); + + if (unpublishResult == false) + { + AddCancelMessage(content); + throw new HttpResponseException(Request.CreateValidationErrorResponse(content)); + } + else + { + content.AddSuccessNotification(Services.TextService.Localize("content/unPublish"), Services.TextService.Localize("speechBubbles/contentUnpublished")); + return content; + } + } + + /// + /// Checks if the user is currently in preview mode and if so will update the preview content for this item + /// + /// + private void UpdatePreviewContext(int contentId) + { + var previewId = Request.GetPreviewCookieValue(); + if (previewId.IsNullOrWhiteSpace()) return; + Guid id; + if (Guid.TryParse(previewId, out id)) + { + var d = new Document(contentId); + var pc = new PreviewContent(UmbracoUser, id, false); + pc.PrepareDocument(UmbracoUser, d, true); + pc.SavePreviewSet(); + } + } + + /// + /// Maps the dto property values to the persisted model + /// + /// + private void MapPropertyValues(ContentItemSave contentItem) + { + UpdateName(contentItem); + + //TODO: We need to support 'send to publish' + + contentItem.PersistedContent.ExpireDate = contentItem.ExpireDate; + contentItem.PersistedContent.ReleaseDate = contentItem.ReleaseDate; + //only set the template if it didn't change + var templateChanged = (contentItem.PersistedContent.Template == null && contentItem.TemplateAlias.IsNullOrWhiteSpace() == false) + || (contentItem.PersistedContent.Template != null && contentItem.PersistedContent.Template.Alias != contentItem.TemplateAlias) + || (contentItem.PersistedContent.Template != null && contentItem.TemplateAlias.IsNullOrWhiteSpace()); + if (templateChanged) + { + var template = Services.FileService.GetTemplate(contentItem.TemplateAlias); + if (template == null && contentItem.TemplateAlias.IsNullOrWhiteSpace() == false) + { + //ModelState.AddModelError("Template", "No template exists with the specified alias: " + contentItem.TemplateAlias); + LogHelper.Warn("No template exists with the specified alias: " + contentItem.TemplateAlias); + } + else + { + //NOTE: this could be null if there was a template and the posted template is null, this should remove the assigned template + contentItem.PersistedContent.Template = template; + } + } + + base.MapPropertyValues(contentItem); + } + + /// + /// Ensures the item can be moved/copied to the new location + /// + /// + /// + private IContent ValidateMoveOrCopy(MoveOrCopy model) + { + if (model == null) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + var contentService = Services.ContentService; + var toMove = contentService.GetById(model.Id); + if (toMove == null) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } + if (model.ParentId < 0) + { + //cannot move if the content item is not allowed at the root + if (toMove.ContentType.AllowedAsRoot == false) + { + throw new HttpResponseException( + Request.CreateNotificationValidationErrorResponse( + Services.TextService.Localize("moveOrCopy/notAllowedAtRoot"))); + } + } + else + { + var parent = contentService.GetById(model.ParentId); + if (parent == null) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + //check if the item is allowed under this one + if (parent.ContentType.AllowedContentTypes.Select(x => x.Id).ToArray() + .Any(x => x.Value == toMove.ContentType.Id) == false) + { + throw new HttpResponseException( + Request.CreateNotificationValidationErrorResponse( + Services.TextService.Localize("moveOrCopy/notAllowedByContentType"))); + } + + // Check on paths + if ((string.Format(",{0},", parent.Path)).IndexOf(string.Format(",{0},", toMove.Id), StringComparison.Ordinal) > -1) + { + throw new HttpResponseException( + Request.CreateNotificationValidationErrorResponse( + Services.TextService.Localize("moveOrCopy/notAllowedByPath"))); + } + } + + return toMove; + } + + private void ShowMessageForPublishStatus(PublishStatus status, INotificationModel display) + { + switch (status.StatusType) + { + case PublishStatusType.Success: + case PublishStatusType.SuccessAlreadyPublished: + display.AddSuccessNotification( + Services.TextService.Localize("speechBubbles/editContentPublishedHeader"), + Services.TextService.Localize("speechBubbles/editContentPublishedText")); + break; + case PublishStatusType.FailedPathNotPublished: + display.AddWarningNotification( + Services.TextService.Localize("publish"), + Services.TextService.Localize("publish/contentPublishedFailedByParent", + new[] { string.Format("{0} ({1})", status.ContentItem.Name, status.ContentItem.Id) }).Trim()); + break; + case PublishStatusType.FailedCancelledByEvent: + AddCancelMessage(display, "publish", "speechBubbles/contentPublishedFailedByEvent"); + break; + case PublishStatusType.FailedAwaitingRelease: + display.AddWarningNotification( + Services.TextService.Localize("publish"), + Services.TextService.Localize("publish/contentPublishedFailedAwaitingRelease", + new[] { string.Format("{0} ({1})", status.ContentItem.Name, status.ContentItem.Id) }).Trim()); + break; + case PublishStatusType.FailedHasExpired: + display.AddWarningNotification( + Services.TextService.Localize("publish"), + Services.TextService.Localize("publish/contentPublishedFailedExpired", + new[] + { + string.Format("{0} ({1})", status.ContentItem.Name, status.ContentItem.Id), + }).Trim()); + break; + case PublishStatusType.FailedIsTrashed: + //TODO: We should add proper error messaging for this! + break; + case PublishStatusType.FailedContentInvalid: + display.AddWarningNotification( + Services.TextService.Localize("publish"), + Services.TextService.Localize("publish/contentPublishedFailedInvalid", + new[] + { + string.Format("{0} ({1})", status.ContentItem.Name, status.ContentItem.Id), + string.Join(",", status.InvalidProperties.Select(x => x.Alias)) + }).Trim()); + break; + default: + throw new IndexOutOfRangeException(); + } + } + + + + /// + /// Performs a permissions check for the user to check if it has access to the node based on + /// start node and/or permissions for the node + /// + /// The storage to add the content item to so it can be reused + /// + /// + /// + /// The content to lookup, if the contentItem is not specified + /// + /// Specifies the already resolved content item to check against + /// + internal static bool CheckPermissions( + IDictionary storage, + IUser user, + IUserService userService, + IContentService contentService, + int nodeId, + char[] permissionsToCheck = null, + IContent contentItem = null) + { + + if (contentItem == null && nodeId != Constants.System.Root && nodeId != Constants.System.RecycleBinContent) + { + contentItem = contentService.GetById(nodeId); + //put the content item into storage so it can be retreived + // in the controller (saves a lookup) + storage[typeof(IContent).ToString()] = contentItem; + } + + if (contentItem == null && nodeId != Constants.System.Root && nodeId != Constants.System.RecycleBinContent) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + var hasPathAccess = (nodeId == Constants.System.Root) + ? UserExtensions.HasPathAccess( + Constants.System.Root.ToInvariantString(), + user.StartContentId, + Constants.System.RecycleBinContent) + : (nodeId == Constants.System.RecycleBinContent) + ? UserExtensions.HasPathAccess( + Constants.System.RecycleBinContent.ToInvariantString(), + user.StartContentId, + Constants.System.RecycleBinContent) + : user.HasPathAccess(contentItem); + + if (hasPathAccess == false) + { + return false; + } + + if (permissionsToCheck == null || permissionsToCheck.Any() == false) + { + return true; + } + + var permission = userService.GetPermissions(user, nodeId).FirstOrDefault(); + + var allowed = true; + foreach (var p in permissionsToCheck) + { + if (permission == null || permission.AssignedPermissions.Contains(p.ToString(CultureInfo.InvariantCulture)) == false) + { + allowed = false; + } + } + return allowed; + } + + } } \ No newline at end of file diff --git a/src/Umbraco.Web/Editors/MediaController.cs b/src/Umbraco.Web/Editors/MediaController.cs index da7c72a289..060fb06750 100644 --- a/src/Umbraco.Web/Editors/MediaController.cs +++ b/src/Umbraco.Web/Editors/MediaController.cs @@ -39,675 +39,675 @@ using Notification = Umbraco.Web.Models.ContentEditing.Notification; namespace Umbraco.Web.Editors { - /// - /// This controller is decorated with the UmbracoApplicationAuthorizeAttribute which means that any user requesting - /// access to ALL of the methods on this controller will need access to the media application. - /// - [PluginController("UmbracoApi")] - [UmbracoApplicationAuthorizeAttribute(Constants.Applications.Media)] - public class MediaController : ContentControllerBase - { - /// - /// Constructor - /// - public MediaController() - : this(UmbracoContext.Current) - { - } - - /// - /// Constructor - /// - /// - public MediaController(UmbracoContext umbracoContext) - : base(umbracoContext) - { - } - - /// - /// Gets an empty content item for the - /// - /// - /// - /// - [OutgoingEditorModelEvent] - public MediaItemDisplay GetEmpty(string contentTypeAlias, int parentId) - { - var contentType = Services.ContentTypeService.GetMediaType(contentTypeAlias); - if (contentType == null) - { - throw new HttpResponseException(HttpStatusCode.NotFound); - } - - var emptyContent = Services.MediaService.CreateMedia("", parentId, contentType.Alias, UmbracoUser.Id); - var mapped = Mapper.Map(emptyContent); - - //remove this tab if it exists: umbContainerView - var containerTab = mapped.Tabs.FirstOrDefault(x => x.Alias == Constants.Conventions.PropertyGroups.ListViewGroupName); - mapped.Tabs = mapped.Tabs.Except(new[] { containerTab }); - return mapped; - } - - /// - /// Returns an item to be used to display the recycle bin for media - /// - /// - public ContentItemDisplay GetRecycleBin() - { - var display = new ContentItemDisplay - { - Id = Constants.System.RecycleBinMedia, - Alias = "recycleBin", - ParentId = -1, - Name = Services.TextService.Localize("general/recycleBin"), - ContentTypeAlias = "recycleBin", - CreateDate = DateTime.Now, - IsContainer = true, - Path = "-1," + Constants.System.RecycleBinMedia - }; - - TabsAndPropertiesResolver.AddListView(display, "media", Services.DataTypeService, Services.TextService); - - return display; - } - - /// - /// Gets the content json for the content id - /// - /// - /// - [OutgoingEditorModelEvent] - [EnsureUserPermissionForMedia("id")] - public MediaItemDisplay GetById(int id) - { - var foundContent = GetObjectFromRequest(() => Services.MediaService.GetById(id)); - - if (foundContent == null) - { - HandleContentNotFound(id); - //HandleContentNotFound will throw an exception - return null; - } - return Mapper.Map(foundContent); - } - - /// - /// Return media for the specified ids - /// - /// - /// - [FilterAllowedOutgoingMedia(typeof(IEnumerable))] - public IEnumerable GetByIds([FromUri]int[] ids) - { - var foundMedia = Services.MediaService.GetByIds(ids); - return foundMedia.Select(Mapper.Map); - } - - /// - /// Returns media items known to be a container of other media items - /// - /// - /// - [FilterAllowedOutgoingMedia(typeof(IEnumerable>))] - public IEnumerable> GetChildFolders(int id = -1) - { - //Suggested convention for folder mediatypes - we can make this more or less complicated as long as we document it... - //if you create a media type, which has an alias that ends with ...Folder then its a folder: ex: "secureFolder", "bannerFolder", "Folder" - var folderTypes = Services.ContentTypeService.GetAllMediaTypes().ToArray().Where(x => x.Alias.EndsWith("Folder")).Select(x => x.Id); - - var children = (id < 0) ? Services.MediaService.GetRootMedia() : Services.MediaService.GetById(id).Children(); - return children.Where(x => folderTypes.Contains(x.ContentTypeId)).Select(Mapper.Map>); - } - - /// - /// Returns the root media objects - /// - [FilterAllowedOutgoingMedia(typeof(IEnumerable>))] - public IEnumerable> GetRootMedia() - { - //TODO: Add permissions check! - - return Services.MediaService.GetRootMedia() - .Select(Mapper.Map>); - } - - /// - /// Returns the child media objects - /// - [FilterAllowedOutgoingMedia(typeof(IEnumerable>), "Items")] - public PagedResult> GetChildren(int id, - int pageNumber = 0, - int pageSize = 0, - string orderBy = "SortOrder", - Direction orderDirection = Direction.Ascending, - bool orderBySystemField = true, - string filter = "") - { - int totalChildren; - IMedia[] children; - if (pageNumber > 0 && pageSize > 0) - { - children = Services.MediaService - .GetPagedChildren(id, (pageNumber - 1), pageSize, out totalChildren - , orderBy, orderDirection, orderBySystemField, filter).ToArray(); - } - else - { - children = Services.MediaService.GetChildren(id).ToArray(); - totalChildren = children.Length; - } - - if (totalChildren == 0) - { - return new PagedResult>(0, 0, 0); - } - - var pagedResult = new PagedResult>(totalChildren, pageNumber, pageSize); - pagedResult.Items = children - .Select(Mapper.Map>); - - return pagedResult; - } - - /// - /// Moves an item to the recycle bin, if it is already there then it will permanently delete it - /// - /// - /// - [EnsureUserPermissionForMedia("id")] - [HttpPost] - public HttpResponseMessage DeleteById(int id) - { - var foundMedia = GetObjectFromRequest(() => Services.MediaService.GetById(id)); - - if (foundMedia == null) - { - return HandleContentNotFound(id, false); - } - - //if the current item is in the recycle bin - if (foundMedia.IsInRecycleBin() == false) - { - var moveResult = Services.MediaService.WithResult().MoveToRecycleBin(foundMedia, (int)Security.CurrentUser.Id); - if (moveResult == false) - { - //returning an object of INotificationModel will ensure that any pending - // notification messages are added to the response. - return Request.CreateValidationErrorResponse(new SimpleNotificationModel()); - } - } - else - { - var deleteResult = Services.MediaService.WithResult().Delete(foundMedia, (int)Security.CurrentUser.Id); - if (deleteResult == false) - { - //returning an object of INotificationModel will ensure that any pending - // notification messages are added to the response. - return Request.CreateValidationErrorResponse(new SimpleNotificationModel()); - } - } - - return Request.CreateResponse(HttpStatusCode.OK); - } - - /// - /// Change the sort order for media - /// - /// - /// - [EnsureUserPermissionForMedia("move.Id")] - public HttpResponseMessage PostMove(MoveOrCopy move) - { - var toMove = ValidateMoveOrCopy(move); - - Services.MediaService.Move(toMove, move.ParentId); - - var response = Request.CreateResponse(HttpStatusCode.OK); - response.Content = new StringContent(toMove.Path, Encoding.UTF8, "application/json"); - return response; - } - - /// - /// Saves content - /// - /// - [FileUploadCleanupFilter] - [MediaPostValidate] - public MediaItemDisplay PostSave( - [ModelBinder(typeof(MediaItemBinder))] - MediaItemSave contentItem) - { - //If we've reached here it means: - // * Our model has been bound - // * and validated - // * any file attachments have been saved to their temporary location for us to use - // * we have a reference to the DTO object and the persisted object - // * Permissions are valid - - MapPropertyValues(contentItem); - - //We need to manually check the validation results here because: - // * We still need to save the entity even if there are validation value errors - // * Depending on if the entity is new, and if there are non property validation errors (i.e. the name is null) - // then we cannot continue saving, we can only display errors - // * If there are validation errors and they were attempting to publish, we can only save, NOT publish and display - // a message indicating this - if (ModelState.IsValid == false) - { - if (ValidationHelper.ModelHasRequiredForPersistenceErrors(contentItem) - && (contentItem.Action == ContentSaveAction.SaveNew)) - { - //ok, so the absolute mandatory data is invalid and it's new, we cannot actually continue! - // add the modelstate to the outgoing object and throw validation response - var forDisplay = Mapper.Map(contentItem.PersistedContent); - forDisplay.Errors = ModelState.ToErrorDictionary(); - throw new HttpResponseException(Request.CreateValidationErrorResponse(forDisplay)); - } - } - - //save the item - var saveStatus = Services.MediaService.WithResult().Save(contentItem.PersistedContent, (int)Security.CurrentUser.Id); - - //return the updated model - var display = Mapper.Map(contentItem.PersistedContent); - - //lasty, if it is not valid, add the modelstate to the outgoing object and throw a 403 - HandleInvalidModelState(display); - - //put the correct msgs in - switch (contentItem.Action) - { - case ContentSaveAction.Save: - case ContentSaveAction.SaveNew: - if (saveStatus.Success) - { - display.AddSuccessNotification( - Services.TextService.Localize("speechBubbles/editMediaSaved"), - Services.TextService.Localize("speechBubbles/editMediaSavedText")); - } - else - { - AddCancelMessage(display); - - //If the item is new and the operation was cancelled, we need to return a different - // status code so the UI can handle it since it won't be able to redirect since there - // is no Id to redirect to! - if (saveStatus.Result.StatusType == OperationStatusType.FailedCancelledByEvent && IsCreatingAction(contentItem.Action)) - { - throw new HttpResponseException(Request.CreateValidationErrorResponse(display)); - } - } - - break; - } - - return display; - } - - /// - /// Maps the property values to the persisted entity - /// - /// - protected override void MapPropertyValues(ContentBaseItemSave contentItem) - { - UpdateName(contentItem); - - //use the base method to map the rest of the properties - base.MapPropertyValues(contentItem); - } - - /// - /// Empties the recycle bin - /// - /// - [HttpDelete] - [HttpPost] - public HttpResponseMessage EmptyRecycleBin() - { - Services.MediaService.EmptyRecycleBin(); - return Request.CreateResponse(HttpStatusCode.OK); - } - - /// - /// Change the sort order for media - /// - /// - /// - [EnsureUserPermissionForMedia("sorted.ParentId")] - public HttpResponseMessage PostSort(ContentSortOrder sorted) - { - if (sorted == null) - { - return Request.CreateResponse(HttpStatusCode.NotFound); - } - - //if there's nothing to sort just return ok - if (sorted.IdSortOrder.Length == 0) - { - return Request.CreateResponse(HttpStatusCode.OK); - } - - var mediaService = base.ApplicationContext.Services.MediaService; - var sortedMedia = new List(); - try - { - sortedMedia.AddRange(sorted.IdSortOrder.Select(mediaService.GetById)); - - // Save Media with new sort order and update content xml in db accordingly - if (mediaService.Sort(sortedMedia) == false) - { - LogHelper.Warn("Media sorting failed, this was probably caused by an event being cancelled"); - return Request.CreateValidationErrorResponse("Media sorting failed, this was probably caused by an event being cancelled"); - } - return Request.CreateResponse(HttpStatusCode.OK); - } - catch (Exception ex) - { - LogHelper.Error("Could not update media sort order", ex); - throw; - } - } - - [EnsureUserPermissionForMedia("folder.ParentId")] - public MediaItemDisplay PostAddFolder(EntityBasic folder) - { - var mediaService = ApplicationContext.Services.MediaService; - var f = mediaService.CreateMedia(folder.Name, folder.ParentId, Constants.Conventions.MediaTypes.Folder); - mediaService.Save(f, Security.CurrentUser.Id); - - return Mapper.Map(f); - } - - /// - /// Used to submit a media file - /// - /// - /// - /// We cannot validate this request with attributes (nicely) due to the nature of the multi-part for data. - /// - [FileUploadCleanupFilter(false)] - public async Task PostAddFile() - { - if (Request.Content.IsMimeMultipartContent() == false) - { - throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType); - } - - var root = IOHelper.MapPath("~/App_Data/TEMP/FileUploads"); - //ensure it exists - Directory.CreateDirectory(root); - var provider = new MultipartFormDataStreamProvider(root); - - var result = await Request.Content.ReadAsMultipartAsync(provider); - - //must have a file - if (result.FileData.Count == 0) - { - return Request.CreateResponse(HttpStatusCode.NotFound); - } - - //get the string json from the request - int parentId; - if (int.TryParse(result.FormData["currentFolder"], out parentId) == false) - { - return Request.CreateValidationErrorResponse("The request was not formatted correctly, the currentFolder is not an integer"); - } - - //ensure the user has access to this folder by parent id! - if (CheckPermissions( - new Dictionary(), - Security.CurrentUser, - Services.MediaService, parentId) == false) - { - return Request.CreateResponse( - HttpStatusCode.Unauthorized, - new SimpleNotificationModel(new Notification( - Services.TextService.Localize("speechBubbles/operationFailedHeader"), - Services.TextService.Localize("speechBubbles/invalidUserPermissionsText"), - SpeechBubbleIcon.Warning))); - } - - var tempFiles = new PostedFiles(); - var mediaService = ApplicationContext.Services.MediaService; - - - //in case we pass a path with a folder in it, we will create it and upload media to it. - if (result.FormData.ContainsKey("path")) - { - - var folders = result.FormData["path"].Split('/'); - - for (int i = 0; i < folders.Length - 1; i++) - { - var folderName = folders[i]; - IMedia folderMediaItem; - - //if uploading directly to media root and not a subfolder - if (parentId == -1) - { - //look for matching folder - folderMediaItem = - mediaService.GetRootMedia().FirstOrDefault(x => x.Name == folderName && x.ContentType.Alias == Constants.Conventions.MediaTypes.Folder); - if (folderMediaItem == null) - { - //if null, create a folder - folderMediaItem = mediaService.CreateMedia(folderName, -1, Constants.Conventions.MediaTypes.Folder); - mediaService.Save(folderMediaItem); - } - } - else - { - //get current parent - var mediaRoot = mediaService.GetById(parentId); - - //if the media root is null, something went wrong, we'll abort - if (mediaRoot == null) - return Request.CreateErrorResponse(HttpStatusCode.InternalServerError, - "The folder: " + folderName + " could not be used for storing images, its ID: " + parentId + - " returned null"); - - //look for matching folder - folderMediaItem = mediaRoot.Children().FirstOrDefault(x => x.Name == folderName && x.ContentType.Alias == Constants.Conventions.MediaTypes.Folder); - if (folderMediaItem == null) - { - //if null, create a folder - folderMediaItem = mediaService.CreateMedia(folderName, mediaRoot, Constants.Conventions.MediaTypes.Folder); - mediaService.Save(folderMediaItem); - } - } - //set the media root to the folder id so uploaded files will end there. - parentId = folderMediaItem.Id; - } - } - - //get the files - foreach (var file in result.FileData) - { - var fileName = file.Headers.ContentDisposition.FileName.Trim(new[] { '\"' }); - var ext = fileName.Substring(fileName.LastIndexOf('.') + 1).ToLower(); - - if (UmbracoConfig.For.UmbracoSettings().Content.DisallowedUploadFiles.Contains(ext) == false) - { - var mediaType = Constants.Conventions.MediaTypes.File; - - if (UmbracoConfig.For.UmbracoSettings().Content.ImageFileTypes.Contains(ext)) - mediaType = Constants.Conventions.MediaTypes.Image; - - //TODO: make the media item name "nice" since file names could be pretty ugly, we have - // string extensions to do much of this but we'll need: - // * Pascalcase the name (use string extensions) - // * strip the file extension - // * underscores to spaces - // * probably remove 'ugly' characters - let's discuss - // All of this logic should exist in a string extensions method and be unit tested - // http://issues.umbraco.org/issue/U4-5572 - var mediaItemName = fileName; - - var f = mediaService.CreateMedia(mediaItemName, parentId, mediaType, Security.CurrentUser.Id); - - var fileInfo = new FileInfo(file.LocalFileName); - var fs = fileInfo.OpenReadWithRetry(); - if (fs == null) throw new InvalidOperationException("Could not acquire file stream"); - using (fs) - { - f.SetValue(Constants.Conventions.Media.File, fileName, fs); - } - - var saveResult = mediaService.WithResult().Save(f, Security.CurrentUser.Id); - if (saveResult == false) - { - AddCancelMessage(tempFiles, - message: Services.TextService.Localize("speechBubbles/operationCancelledText") + " -- " + mediaItemName, - localizeMessage: false); - } - else - { - tempFiles.UploadedFiles.Add(new ContentItemFile - { - FileName = fileName, - PropertyAlias = Constants.Conventions.Media.File, - TempFilePath = file.LocalFileName - }); - } - } - else - { - tempFiles.Notifications.Add(new Notification( - Services.TextService.Localize("speechBubbles/operationFailedHeader"), - "Cannot upload file " + file.Headers.ContentDisposition.FileName + ", it is not an approved file type", - SpeechBubbleIcon.Warning)); - } - } - - //Different response if this is a 'blueimp' request - if (Request.GetQueryNameValuePairs().Any(x => x.Key == "origin")) - { - var origin = Request.GetQueryNameValuePairs().First(x => x.Key == "origin"); - if (origin.Value == "blueimp") - { - return Request.CreateResponse(HttpStatusCode.OK, - tempFiles, - //Don't output the angular xsrf stuff, blue imp doesn't like that - new JsonMediaTypeFormatter()); - } - } - - return Request.CreateResponse(HttpStatusCode.OK, tempFiles); - } - - /// - /// This is used for the response of PostAddFile so that we can analyze the response in a filter and remove the - /// temporary files that were created. - /// - [DataContract] - private class PostedFiles : IHaveUploadedFiles, INotificationModel - { - public PostedFiles() - { - UploadedFiles = new List(); - Notifications = new List(); - } - public List UploadedFiles { get; private set; } - - [DataMember(Name = "notifications")] - public List Notifications { get; private set; } - } - - /// - /// Ensures the item can be moved/copied to the new location - /// - /// - /// - private IMedia ValidateMoveOrCopy(MoveOrCopy model) - { - if (model == null) - { - throw new HttpResponseException(HttpStatusCode.NotFound); - } - - var mediaService = Services.MediaService; - var toMove = mediaService.GetById(model.Id); - if (toMove == null) - { - throw new HttpResponseException(HttpStatusCode.NotFound); - } - if (model.ParentId < 0) - { - //cannot move if the content item is not allowed at the root - if (toMove.ContentType.AllowedAsRoot == false) - { - var notificationModel = new SimpleNotificationModel(); - notificationModel.AddErrorNotification(Services.TextService.Localize("moveOrCopy/notAllowedAtRoot"), ""); - throw new HttpResponseException(Request.CreateValidationErrorResponse(notificationModel)); - } - } - else - { - var parent = mediaService.GetById(model.ParentId); - if (parent == null) - { - throw new HttpResponseException(HttpStatusCode.NotFound); - } - - //check if the item is allowed under this one - if (parent.ContentType.AllowedContentTypes.Select(x => x.Id).ToArray() - .Any(x => x.Value == toMove.ContentType.Id) == false) - { - var notificationModel = new SimpleNotificationModel(); - notificationModel.AddErrorNotification(Services.TextService.Localize("moveOrCopy/notAllowedByContentType"), ""); - throw new HttpResponseException(Request.CreateValidationErrorResponse(notificationModel)); - } - - // Check on paths - if ((string.Format(",{0},", parent.Path)).IndexOf(string.Format(",{0},", toMove.Id), StringComparison.Ordinal) > -1) - { - var notificationModel = new SimpleNotificationModel(); - notificationModel.AddErrorNotification(Services.TextService.Localize("moveOrCopy/notAllowedByPath"), ""); - throw new HttpResponseException(Request.CreateValidationErrorResponse(notificationModel)); - } - } - - return toMove; - } - - /// - /// Performs a permissions check for the user to check if it has access to the node based on - /// start node and/or permissions for the node - /// - /// The storage to add the content item to so it can be reused - /// - /// - /// The content to lookup, if the contentItem is not specified - /// Specifies the already resolved content item to check against, setting this ignores the nodeId - /// - internal static bool CheckPermissions(IDictionary storage, IUser user, IMediaService mediaService, int nodeId, IMedia media = null) - { - if (media == null && nodeId != Constants.System.Root && nodeId != Constants.System.RecycleBinMedia) - { - media = mediaService.GetById(nodeId); - //put the content item into storage so it can be retreived - // in the controller (saves a lookup) - storage[typeof(IMedia).ToString()] = media; - } - - if (media == null && nodeId != Constants.System.Root && nodeId != Constants.System.RecycleBinMedia) - { - throw new HttpResponseException(HttpStatusCode.NotFound); - } - - var hasPathAccess = (nodeId == Constants.System.Root) - ? UserExtensions.HasPathAccess( - Constants.System.Root.ToInvariantString(), - user.StartMediaId, - Constants.System.RecycleBinMedia) - : (nodeId == Constants.System.RecycleBinMedia) - ? UserExtensions.HasPathAccess( - Constants.System.RecycleBinMedia.ToInvariantString(), - user.StartMediaId, - Constants.System.RecycleBinMedia) - : user.HasPathAccess(media); - - return hasPathAccess; - } - } + /// + /// This controller is decorated with the UmbracoApplicationAuthorizeAttribute which means that any user requesting + /// access to ALL of the methods on this controller will need access to the media application. + /// + [PluginController("UmbracoApi")] + [UmbracoApplicationAuthorizeAttribute(Constants.Applications.Media)] + public class MediaController : ContentControllerBase + { + /// + /// Constructor + /// + public MediaController() + : this(UmbracoContext.Current) + { + } + + /// + /// Constructor + /// + /// + public MediaController(UmbracoContext umbracoContext) + : base(umbracoContext) + { + } + + /// + /// Gets an empty content item for the + /// + /// + /// + /// + [OutgoingEditorModelEvent] + public MediaItemDisplay GetEmpty(string contentTypeAlias, int parentId) + { + var contentType = Services.ContentTypeService.GetMediaType(contentTypeAlias); + if (contentType == null) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + var emptyContent = Services.MediaService.CreateMedia("", parentId, contentType.Alias, UmbracoUser.Id); + var mapped = Mapper.Map(emptyContent); + + //remove this tab if it exists: umbContainerView + var containerTab = mapped.Tabs.FirstOrDefault(x => x.Alias == Constants.Conventions.PropertyGroups.ListViewGroupName); + mapped.Tabs = mapped.Tabs.Except(new[] { containerTab }); + return mapped; + } + + /// + /// Returns an item to be used to display the recycle bin for media + /// + /// + public ContentItemDisplay GetRecycleBin() + { + var display = new ContentItemDisplay + { + Id = Constants.System.RecycleBinMedia, + Alias = "recycleBin", + ParentId = -1, + Name = Services.TextService.Localize("general/recycleBin"), + ContentTypeAlias = "recycleBin", + CreateDate = DateTime.Now, + IsContainer = true, + Path = "-1," + Constants.System.RecycleBinMedia + }; + + TabsAndPropertiesResolver.AddListView(display, "media", Services.DataTypeService, Services.TextService); + + return display; + } + + /// + /// Gets the content json for the content id + /// + /// + /// + [OutgoingEditorModelEvent] + [EnsureUserPermissionForMedia("id")] + public MediaItemDisplay GetById(int id) + { + var foundContent = GetObjectFromRequest(() => Services.MediaService.GetById(id)); + + if (foundContent == null) + { + HandleContentNotFound(id); + //HandleContentNotFound will throw an exception + return null; + } + return Mapper.Map(foundContent); + } + + /// + /// Return media for the specified ids + /// + /// + /// + [FilterAllowedOutgoingMedia(typeof(IEnumerable))] + public IEnumerable GetByIds([FromUri]int[] ids) + { + var foundMedia = Services.MediaService.GetByIds(ids); + return foundMedia.Select(Mapper.Map); + } + + /// + /// Returns media items known to be a container of other media items + /// + /// + /// + [FilterAllowedOutgoingMedia(typeof(IEnumerable>))] + public IEnumerable> GetChildFolders(int id = -1) + { + //Suggested convention for folder mediatypes - we can make this more or less complicated as long as we document it... + //if you create a media type, which has an alias that ends with ...Folder then its a folder: ex: "secureFolder", "bannerFolder", "Folder" + var folderTypes = Services.ContentTypeService.GetAllMediaTypes().ToArray().Where(x => x.Alias.EndsWith("Folder")).Select(x => x.Id); + + var children = (id < 0) ? Services.MediaService.GetRootMedia() : Services.MediaService.GetById(id).Children(); + return children.Where(x => folderTypes.Contains(x.ContentTypeId)).Select(Mapper.Map>); + } + + /// + /// Returns the root media objects + /// + [FilterAllowedOutgoingMedia(typeof(IEnumerable>))] + public IEnumerable> GetRootMedia() + { + //TODO: Add permissions check! + + return Services.MediaService.GetRootMedia() + .Select(Mapper.Map>); + } + + /// + /// Returns the child media objects + /// + [FilterAllowedOutgoingMedia(typeof(IEnumerable>), "Items")] + public PagedResult> GetChildren(int id, + int pageNumber = 0, + int pageSize = 0, + string orderBy = "SortOrder", + Direction orderDirection = Direction.Ascending, + bool orderBySystemField = true, + string filter = "") + { + int totalChildren; + IMedia[] children; + if (pageNumber > 0 && pageSize > 0) + { + children = Services.MediaService + .GetPagedChildren(id, (pageNumber - 1), pageSize, out totalChildren + , orderBy, orderDirection, orderBySystemField, filter).ToArray(); + } + else + { + children = Services.MediaService.GetChildren(id).ToArray(); + totalChildren = children.Length; + } + + if (totalChildren == 0) + { + return new PagedResult>(0, 0, 0); + } + + var pagedResult = new PagedResult>(totalChildren, pageNumber, pageSize); + pagedResult.Items = children + .Select(Mapper.Map>); + + return pagedResult; + } + + /// + /// Moves an item to the recycle bin, if it is already there then it will permanently delete it + /// + /// + /// + [EnsureUserPermissionForMedia("id")] + [HttpPost] + public HttpResponseMessage DeleteById(int id) + { + var foundMedia = GetObjectFromRequest(() => Services.MediaService.GetById(id)); + + if (foundMedia == null) + { + return HandleContentNotFound(id, false); + } + + //if the current item is in the recycle bin + if (foundMedia.IsInRecycleBin() == false) + { + var moveResult = Services.MediaService.WithResult().MoveToRecycleBin(foundMedia, (int)Security.CurrentUser.Id); + if (moveResult == false) + { + //returning an object of INotificationModel will ensure that any pending + // notification messages are added to the response. + return Request.CreateValidationErrorResponse(new SimpleNotificationModel()); + } + } + else + { + var deleteResult = Services.MediaService.WithResult().Delete(foundMedia, (int)Security.CurrentUser.Id); + if (deleteResult == false) + { + //returning an object of INotificationModel will ensure that any pending + // notification messages are added to the response. + return Request.CreateValidationErrorResponse(new SimpleNotificationModel()); + } + } + + return Request.CreateResponse(HttpStatusCode.OK); + } + + /// + /// Change the sort order for media + /// + /// + /// + [EnsureUserPermissionForMedia("move.Id")] + public HttpResponseMessage PostMove(MoveOrCopy move) + { + var toMove = ValidateMoveOrCopy(move); + + Services.MediaService.Move(toMove, move.ParentId); + + var response = Request.CreateResponse(HttpStatusCode.OK); + response.Content = new StringContent(toMove.Path, Encoding.UTF8, "application/json"); + return response; + } + + /// + /// Saves content + /// + /// + [FileUploadCleanupFilter] + [MediaPostValidate] + public MediaItemDisplay PostSave( + [ModelBinder(typeof(MediaItemBinder))] + MediaItemSave contentItem) + { + //If we've reached here it means: + // * Our model has been bound + // * and validated + // * any file attachments have been saved to their temporary location for us to use + // * we have a reference to the DTO object and the persisted object + // * Permissions are valid + + MapPropertyValues(contentItem); + + //We need to manually check the validation results here because: + // * We still need to save the entity even if there are validation value errors + // * Depending on if the entity is new, and if there are non property validation errors (i.e. the name is null) + // then we cannot continue saving, we can only display errors + // * If there are validation errors and they were attempting to publish, we can only save, NOT publish and display + // a message indicating this + if (ModelState.IsValid == false) + { + if (ValidationHelper.ModelHasRequiredForPersistenceErrors(contentItem) + && (contentItem.Action == ContentSaveAction.SaveNew)) + { + //ok, so the absolute mandatory data is invalid and it's new, we cannot actually continue! + // add the modelstate to the outgoing object and throw validation response + var forDisplay = Mapper.Map(contentItem.PersistedContent); + forDisplay.Errors = ModelState.ToErrorDictionary(); + throw new HttpResponseException(Request.CreateValidationErrorResponse(forDisplay)); + } + } + + //save the item + var saveStatus = Services.MediaService.WithResult().Save(contentItem.PersistedContent, (int)Security.CurrentUser.Id); + + //return the updated model + var display = Mapper.Map(contentItem.PersistedContent); + + //lasty, if it is not valid, add the modelstate to the outgoing object and throw a 403 + HandleInvalidModelState(display); + + //put the correct msgs in + switch (contentItem.Action) + { + case ContentSaveAction.Save: + case ContentSaveAction.SaveNew: + if (saveStatus.Success) + { + display.AddSuccessNotification( + Services.TextService.Localize("speechBubbles/editMediaSaved"), + Services.TextService.Localize("speechBubbles/editMediaSavedText")); + } + else + { + AddCancelMessage(display); + + //If the item is new and the operation was cancelled, we need to return a different + // status code so the UI can handle it since it won't be able to redirect since there + // is no Id to redirect to! + if (saveStatus.Result.StatusType == OperationStatusType.FailedCancelledByEvent && IsCreatingAction(contentItem.Action)) + { + throw new HttpResponseException(Request.CreateValidationErrorResponse(display)); + } + } + + break; + } + + return display; + } + + /// + /// Maps the property values to the persisted entity + /// + /// + protected override void MapPropertyValues(ContentBaseItemSave contentItem) + { + UpdateName(contentItem); + + //use the base method to map the rest of the properties + base.MapPropertyValues(contentItem); + } + + /// + /// Empties the recycle bin + /// + /// + [HttpDelete] + [HttpPost] + public HttpResponseMessage EmptyRecycleBin() + { + Services.MediaService.EmptyRecycleBin(); + return Request.CreateResponse(HttpStatusCode.OK); + } + + /// + /// Change the sort order for media + /// + /// + /// + [EnsureUserPermissionForMedia("sorted.ParentId")] + public HttpResponseMessage PostSort(ContentSortOrder sorted) + { + if (sorted == null) + { + return Request.CreateResponse(HttpStatusCode.NotFound); + } + + //if there's nothing to sort just return ok + if (sorted.IdSortOrder.Length == 0) + { + return Request.CreateResponse(HttpStatusCode.OK); + } + + var mediaService = base.ApplicationContext.Services.MediaService; + var sortedMedia = new List(); + try + { + sortedMedia.AddRange(sorted.IdSortOrder.Select(mediaService.GetById)); + + // Save Media with new sort order and update content xml in db accordingly + if (mediaService.Sort(sortedMedia) == false) + { + LogHelper.Warn("Media sorting failed, this was probably caused by an event being cancelled"); + return Request.CreateValidationErrorResponse("Media sorting failed, this was probably caused by an event being cancelled"); + } + return Request.CreateResponse(HttpStatusCode.OK); + } + catch (Exception ex) + { + LogHelper.Error("Could not update media sort order", ex); + throw; + } + } + + [EnsureUserPermissionForMedia("folder.ParentId")] + public MediaItemDisplay PostAddFolder(EntityBasic folder) + { + var mediaService = ApplicationContext.Services.MediaService; + var f = mediaService.CreateMedia(folder.Name, folder.ParentId, Constants.Conventions.MediaTypes.Folder); + mediaService.Save(f, Security.CurrentUser.Id); + + return Mapper.Map(f); + } + + /// + /// Used to submit a media file + /// + /// + /// + /// We cannot validate this request with attributes (nicely) due to the nature of the multi-part for data. + /// + [FileUploadCleanupFilter(false)] + public async Task PostAddFile() + { + if (Request.Content.IsMimeMultipartContent() == false) + { + throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType); + } + + var root = IOHelper.MapPath("~/App_Data/TEMP/FileUploads"); + //ensure it exists + Directory.CreateDirectory(root); + var provider = new MultipartFormDataStreamProvider(root); + + var result = await Request.Content.ReadAsMultipartAsync(provider); + + //must have a file + if (result.FileData.Count == 0) + { + return Request.CreateResponse(HttpStatusCode.NotFound); + } + + //get the string json from the request + int parentId; + if (int.TryParse(result.FormData["currentFolder"], out parentId) == false) + { + return Request.CreateValidationErrorResponse("The request was not formatted correctly, the currentFolder is not an integer"); + } + + //ensure the user has access to this folder by parent id! + if (CheckPermissions( + new Dictionary(), + Security.CurrentUser, + Services.MediaService, parentId) == false) + { + return Request.CreateResponse( + HttpStatusCode.Unauthorized, + new SimpleNotificationModel(new Notification( + Services.TextService.Localize("speechBubbles/operationFailedHeader"), + Services.TextService.Localize("speechBubbles/invalidUserPermissionsText"), + SpeechBubbleIcon.Warning))); + } + + var tempFiles = new PostedFiles(); + var mediaService = ApplicationContext.Services.MediaService; + + + //in case we pass a path with a folder in it, we will create it and upload media to it. + if (result.FormData.ContainsKey("path")) + { + + var folders = result.FormData["path"].Split('/'); + + for (int i = 0; i < folders.Length - 1; i++) + { + var folderName = folders[i]; + IMedia folderMediaItem; + + //if uploading directly to media root and not a subfolder + if (parentId == -1) + { + //look for matching folder + folderMediaItem = + mediaService.GetRootMedia().FirstOrDefault(x => x.Name == folderName && x.ContentType.Alias == Constants.Conventions.MediaTypes.Folder); + if (folderMediaItem == null) + { + //if null, create a folder + folderMediaItem = mediaService.CreateMedia(folderName, -1, Constants.Conventions.MediaTypes.Folder); + mediaService.Save(folderMediaItem); + } + } + else + { + //get current parent + var mediaRoot = mediaService.GetById(parentId); + + //if the media root is null, something went wrong, we'll abort + if (mediaRoot == null) + return Request.CreateErrorResponse(HttpStatusCode.InternalServerError, + "The folder: " + folderName + " could not be used for storing images, its ID: " + parentId + + " returned null"); + + //look for matching folder + folderMediaItem = mediaRoot.Children().FirstOrDefault(x => x.Name == folderName && x.ContentType.Alias == Constants.Conventions.MediaTypes.Folder); + if (folderMediaItem == null) + { + //if null, create a folder + folderMediaItem = mediaService.CreateMedia(folderName, mediaRoot, Constants.Conventions.MediaTypes.Folder); + mediaService.Save(folderMediaItem); + } + } + //set the media root to the folder id so uploaded files will end there. + parentId = folderMediaItem.Id; + } + } + + //get the files + foreach (var file in result.FileData) + { + var fileName = file.Headers.ContentDisposition.FileName.Trim(new[] { '\"' }); + var ext = fileName.Substring(fileName.LastIndexOf('.') + 1).ToLower(); + + if (UmbracoConfig.For.UmbracoSettings().Content.DisallowedUploadFiles.Contains(ext) == false) + { + var mediaType = Constants.Conventions.MediaTypes.File; + + if (UmbracoConfig.For.UmbracoSettings().Content.ImageFileTypes.Contains(ext)) + mediaType = Constants.Conventions.MediaTypes.Image; + + //TODO: make the media item name "nice" since file names could be pretty ugly, we have + // string extensions to do much of this but we'll need: + // * Pascalcase the name (use string extensions) + // * strip the file extension + // * underscores to spaces + // * probably remove 'ugly' characters - let's discuss + // All of this logic should exist in a string extensions method and be unit tested + // http://issues.umbraco.org/issue/U4-5572 + var mediaItemName = fileName; + + var f = mediaService.CreateMedia(mediaItemName, parentId, mediaType, Security.CurrentUser.Id); + + var fileInfo = new FileInfo(file.LocalFileName); + var fs = fileInfo.OpenReadWithRetry(); + if (fs == null) throw new InvalidOperationException("Could not acquire file stream"); + using (fs) + { + f.SetValue(Constants.Conventions.Media.File, fileName, fs); + } + + var saveResult = mediaService.WithResult().Save(f, Security.CurrentUser.Id); + if (saveResult == false) + { + AddCancelMessage(tempFiles, + message: Services.TextService.Localize("speechBubbles/operationCancelledText") + " -- " + mediaItemName, + localizeMessage: false); + } + else + { + tempFiles.UploadedFiles.Add(new ContentItemFile + { + FileName = fileName, + PropertyAlias = Constants.Conventions.Media.File, + TempFilePath = file.LocalFileName + }); + } + } + else + { + tempFiles.Notifications.Add(new Notification( + Services.TextService.Localize("speechBubbles/operationFailedHeader"), + "Cannot upload file " + file.Headers.ContentDisposition.FileName + ", it is not an approved file type", + SpeechBubbleIcon.Warning)); + } + } + + //Different response if this is a 'blueimp' request + if (Request.GetQueryNameValuePairs().Any(x => x.Key == "origin")) + { + var origin = Request.GetQueryNameValuePairs().First(x => x.Key == "origin"); + if (origin.Value == "blueimp") + { + return Request.CreateResponse(HttpStatusCode.OK, + tempFiles, + //Don't output the angular xsrf stuff, blue imp doesn't like that + new JsonMediaTypeFormatter()); + } + } + + return Request.CreateResponse(HttpStatusCode.OK, tempFiles); + } + + /// + /// This is used for the response of PostAddFile so that we can analyze the response in a filter and remove the + /// temporary files that were created. + /// + [DataContract] + private class PostedFiles : IHaveUploadedFiles, INotificationModel + { + public PostedFiles() + { + UploadedFiles = new List(); + Notifications = new List(); + } + public List UploadedFiles { get; private set; } + + [DataMember(Name = "notifications")] + public List Notifications { get; private set; } + } + + /// + /// Ensures the item can be moved/copied to the new location + /// + /// + /// + private IMedia ValidateMoveOrCopy(MoveOrCopy model) + { + if (model == null) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + var mediaService = Services.MediaService; + var toMove = mediaService.GetById(model.Id); + if (toMove == null) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } + if (model.ParentId < 0) + { + //cannot move if the content item is not allowed at the root + if (toMove.ContentType.AllowedAsRoot == false) + { + var notificationModel = new SimpleNotificationModel(); + notificationModel.AddErrorNotification(Services.TextService.Localize("moveOrCopy/notAllowedAtRoot"), ""); + throw new HttpResponseException(Request.CreateValidationErrorResponse(notificationModel)); + } + } + else + { + var parent = mediaService.GetById(model.ParentId); + if (parent == null) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + //check if the item is allowed under this one + if (parent.ContentType.AllowedContentTypes.Select(x => x.Id).ToArray() + .Any(x => x.Value == toMove.ContentType.Id) == false) + { + var notificationModel = new SimpleNotificationModel(); + notificationModel.AddErrorNotification(Services.TextService.Localize("moveOrCopy/notAllowedByContentType"), ""); + throw new HttpResponseException(Request.CreateValidationErrorResponse(notificationModel)); + } + + // Check on paths + if ((string.Format(",{0},", parent.Path)).IndexOf(string.Format(",{0},", toMove.Id), StringComparison.Ordinal) > -1) + { + var notificationModel = new SimpleNotificationModel(); + notificationModel.AddErrorNotification(Services.TextService.Localize("moveOrCopy/notAllowedByPath"), ""); + throw new HttpResponseException(Request.CreateValidationErrorResponse(notificationModel)); + } + } + + return toMove; + } + + /// + /// Performs a permissions check for the user to check if it has access to the node based on + /// start node and/or permissions for the node + /// + /// The storage to add the content item to so it can be reused + /// + /// + /// The content to lookup, if the contentItem is not specified + /// Specifies the already resolved content item to check against, setting this ignores the nodeId + /// + internal static bool CheckPermissions(IDictionary storage, IUser user, IMediaService mediaService, int nodeId, IMedia media = null) + { + if (media == null && nodeId != Constants.System.Root && nodeId != Constants.System.RecycleBinMedia) + { + media = mediaService.GetById(nodeId); + //put the content item into storage so it can be retreived + // in the controller (saves a lookup) + storage[typeof(IMedia).ToString()] = media; + } + + if (media == null && nodeId != Constants.System.Root && nodeId != Constants.System.RecycleBinMedia) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + var hasPathAccess = (nodeId == Constants.System.Root) + ? UserExtensions.HasPathAccess( + Constants.System.Root.ToInvariantString(), + user.StartMediaId, + Constants.System.RecycleBinMedia) + : (nodeId == Constants.System.RecycleBinMedia) + ? UserExtensions.HasPathAccess( + Constants.System.RecycleBinMedia.ToInvariantString(), + user.StartMediaId, + Constants.System.RecycleBinMedia) + : user.HasPathAccess(media); + + return hasPathAccess; + } + } } diff --git a/src/Umbraco.Web/Editors/MemberController.cs b/src/Umbraco.Web/Editors/MemberController.cs index a2f1090a63..2a3bb4399b 100644 --- a/src/Umbraco.Web/Editors/MemberController.cs +++ b/src/Umbraco.Web/Editors/MemberController.cs @@ -34,250 +34,250 @@ using Examine; namespace Umbraco.Web.Editors { - /// - /// This controller is decorated with the UmbracoApplicationAuthorizeAttribute which means that any user requesting - /// access to ALL of the methods on this controller will need access to the member application. - /// - [PluginController("UmbracoApi")] - [UmbracoApplicationAuthorizeAttribute(Constants.Applications.Members)] - [OutgoingNoHyphenGuidFormat] - public class MemberController : ContentControllerBase - { - /// - /// Constructor - /// - public MemberController() - : this(UmbracoContext.Current) - { - _provider = Core.Security.MembershipProviderExtensions.GetMembersMembershipProvider(); - } + /// + /// This controller is decorated with the UmbracoApplicationAuthorizeAttribute which means that any user requesting + /// access to ALL of the methods on this controller will need access to the member application. + /// + [PluginController("UmbracoApi")] + [UmbracoApplicationAuthorizeAttribute(Constants.Applications.Members)] + [OutgoingNoHyphenGuidFormat] + public class MemberController : ContentControllerBase + { + /// + /// Constructor + /// + public MemberController() + : this(UmbracoContext.Current) + { + _provider = Core.Security.MembershipProviderExtensions.GetMembersMembershipProvider(); + } - /// - /// Constructor - /// - /// - public MemberController(UmbracoContext umbracoContext) - : base(umbracoContext) - { - _provider = Core.Security.MembershipProviderExtensions.GetMembersMembershipProvider(); - } + /// + /// Constructor + /// + /// + public MemberController(UmbracoContext umbracoContext) + : base(umbracoContext) + { + _provider = Core.Security.MembershipProviderExtensions.GetMembersMembershipProvider(); + } - private readonly MembershipProvider _provider; + private readonly MembershipProvider _provider; - /// - /// Returns the currently configured membership scenario for members in umbraco - /// - /// - protected MembershipScenario MembershipScenario - { - get { return Services.MemberService.GetMembershipScenario(); } - } + /// + /// Returns the currently configured membership scenario for members in umbraco + /// + /// + protected MembershipScenario MembershipScenario + { + get { return Services.MemberService.GetMembershipScenario(); } + } - public PagedResult GetPagedResults( - int pageNumber = 1, - int pageSize = 100, - string orderBy = "Name", - Direction orderDirection = Direction.Ascending, - bool orderBySystemField = true, - string filter = "", - string memberTypeAlias = null) - { + public PagedResult GetPagedResults( + int pageNumber = 1, + int pageSize = 100, + string orderBy = "Name", + Direction orderDirection = Direction.Ascending, + bool orderBySystemField = true, + string filter = "", + string memberTypeAlias = null) + { - if (pageNumber <= 0 || pageSize <= 0) - { - throw new NotSupportedException("Both pageNumber and pageSize must be greater than zero"); - } + if (pageNumber <= 0 || pageSize <= 0) + { + throw new NotSupportedException("Both pageNumber and pageSize must be greater than zero"); + } - if (MembershipScenario == MembershipScenario.NativeUmbraco) - { - long totalRecords; - var members = Services.MemberService -.GetAll((pageNumber - 1), pageSize, out totalRecords, orderBy, orderDirection, orderBySystemField -, memberTypeAlias, filter).ToArray(); - if (totalRecords == 0) - { - return new PagedResult(0, 0, 0); - } - var pagedResult = new PagedResult(totalRecords, pageNumber, pageSize); - pagedResult.Items = members - .Select(Mapper.Map); - return pagedResult; - } - else - { - int totalRecords; - var members = _provider.GetAllUsers((pageNumber - 1), pageSize, out totalRecords); - if (totalRecords == 0) - { - return new PagedResult(0, 0, 0); - } - var pagedResult = new PagedResult(totalRecords, pageNumber, pageSize); - pagedResult.Items = members - .Cast() - .Select(Mapper.Map); - return pagedResult; - } + if (MembershipScenario == MembershipScenario.NativeUmbraco) + { + long totalRecords; + var members = Services.MemberService + .GetAll((pageNumber - 1), pageSize, out totalRecords, orderBy, orderDirection, orderBySystemField + , memberTypeAlias, filter).ToArray(); + if (totalRecords == 0) + { + return new PagedResult(0, 0, 0); + } + var pagedResult = new PagedResult(totalRecords, pageNumber, pageSize); + pagedResult.Items = members + .Select(Mapper.Map); + return pagedResult; + } + else + { + int totalRecords; + var members = _provider.GetAllUsers((pageNumber - 1), pageSize, out totalRecords); + if (totalRecords == 0) + { + return new PagedResult(0, 0, 0); + } + var pagedResult = new PagedResult(totalRecords, pageNumber, pageSize); + pagedResult.Items = members + .Cast() + .Select(Mapper.Map); + return pagedResult; + } - } + } - /// - /// Returns a display node with a list view to render members - /// - /// - /// - public MemberListDisplay GetListNodeDisplay(string listName) - { - var display = new MemberListDisplay - { - ContentTypeAlias = listName, - ContentTypeName = listName, - Id = listName, - IsContainer = true, - Name = listName == Constants.Conventions.MemberTypes.AllMembersListId ? "All Members" : listName, - Path = "-1," + listName, - ParentId = -1 - }; + /// + /// Returns a display node with a list view to render members + /// + /// + /// + public MemberListDisplay GetListNodeDisplay(string listName) + { + var display = new MemberListDisplay + { + ContentTypeAlias = listName, + ContentTypeName = listName, + Id = listName, + IsContainer = true, + Name = listName == Constants.Conventions.MemberTypes.AllMembersListId ? "All Members" : listName, + Path = "-1," + listName, + ParentId = -1 + }; - TabsAndPropertiesResolver.AddListView(display, "member", Services.DataTypeService, Services.TextService); + TabsAndPropertiesResolver.AddListView(display, "member", Services.DataTypeService, Services.TextService); - return display; - } + return display; + } - /// - /// Gets the content json for the member - /// - /// - /// - [OutgoingEditorModelEvent] - public MemberDisplay GetByKey(Guid key) - { - MembershipUser foundMembershipMember; - MemberDisplay display; - IMember foundMember; - switch (MembershipScenario) - { - case MembershipScenario.NativeUmbraco: - foundMember = Services.MemberService.GetByKey(key); - if (foundMember == null) - { - HandleContentNotFound(key); - } - return Mapper.Map(foundMember); - case MembershipScenario.CustomProviderWithUmbracoLink: + /// + /// Gets the content json for the member + /// + /// + /// + [OutgoingEditorModelEvent] + public MemberDisplay GetByKey(Guid key) + { + MembershipUser foundMembershipMember; + MemberDisplay display; + IMember foundMember; + switch (MembershipScenario) + { + case MembershipScenario.NativeUmbraco: + foundMember = Services.MemberService.GetByKey(key); + if (foundMember == null) + { + HandleContentNotFound(key); + } + return Mapper.Map(foundMember); + case MembershipScenario.CustomProviderWithUmbracoLink: - //TODO: Support editing custom properties for members with a custom membership provider here. + //TODO: Support editing custom properties for members with a custom membership provider here. - //foundMember = Services.MemberService.GetByKey(key); - //if (foundMember == null) - //{ - // HandleContentNotFound(key); - //} - //foundMembershipMember = Membership.GetUser(key, false); - //if (foundMembershipMember == null) - //{ - // HandleContentNotFound(key); - //} + //foundMember = Services.MemberService.GetByKey(key); + //if (foundMember == null) + //{ + // HandleContentNotFound(key); + //} + //foundMembershipMember = Membership.GetUser(key, false); + //if (foundMembershipMember == null) + //{ + // HandleContentNotFound(key); + //} - //display = Mapper.Map(foundMembershipMember); - ////map the name over - //display.Name = foundMember.Name; - //return display; + //display = Mapper.Map(foundMembershipMember); + ////map the name over + //display.Name = foundMember.Name; + //return display; - case MembershipScenario.StandaloneCustomProvider: - default: - foundMembershipMember = _provider.GetUser(key, false); - if (foundMembershipMember == null) - { - HandleContentNotFound(key); - } - display = Mapper.Map(foundMembershipMember); - return display; - } - } + case MembershipScenario.StandaloneCustomProvider: + default: + foundMembershipMember = _provider.GetUser(key, false); + if (foundMembershipMember == null) + { + HandleContentNotFound(key); + } + display = Mapper.Map(foundMembershipMember); + return display; + } + } - /// - /// Gets an empty content item for the - /// - /// - /// - [OutgoingEditorModelEvent] - public MemberDisplay GetEmpty(string contentTypeAlias = null) - { - IMember emptyContent; - switch (MembershipScenario) - { - case MembershipScenario.NativeUmbraco: - if (contentTypeAlias == null) - { - throw new HttpResponseException(HttpStatusCode.NotFound); - } + /// + /// Gets an empty content item for the + /// + /// + /// + [OutgoingEditorModelEvent] + public MemberDisplay GetEmpty(string contentTypeAlias = null) + { + IMember emptyContent; + switch (MembershipScenario) + { + case MembershipScenario.NativeUmbraco: + if (contentTypeAlias == null) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } - var contentType = Services.MemberTypeService.Get(contentTypeAlias); - if (contentType == null) - { - throw new HttpResponseException(HttpStatusCode.NotFound); - } + var contentType = Services.MemberTypeService.Get(contentTypeAlias); + if (contentType == null) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } - emptyContent = new Member(contentType); - emptyContent.AdditionalData["NewPassword"] = Membership.GeneratePassword(Membership.MinRequiredPasswordLength, Membership.MinRequiredNonAlphanumericCharacters); - return Mapper.Map(emptyContent); - case MembershipScenario.CustomProviderWithUmbracoLink: - //TODO: Support editing custom properties for members with a custom membership provider here. + emptyContent = new Member(contentType); + emptyContent.AdditionalData["NewPassword"] = Membership.GeneratePassword(Membership.MinRequiredPasswordLength, Membership.MinRequiredNonAlphanumericCharacters); + return Mapper.Map(emptyContent); + case MembershipScenario.CustomProviderWithUmbracoLink: + //TODO: Support editing custom properties for members with a custom membership provider here. - case MembershipScenario.StandaloneCustomProvider: - default: - //we need to return a scaffold of a 'simple' member - basically just what a membership provider can edit - emptyContent = MemberService.CreateGenericMembershipProviderMember("", "", "", ""); - emptyContent.AdditionalData["NewPassword"] = Membership.GeneratePassword(Membership.MinRequiredPasswordLength, Membership.MinRequiredNonAlphanumericCharacters); - return Mapper.Map(emptyContent); - } - } + case MembershipScenario.StandaloneCustomProvider: + default: + //we need to return a scaffold of a 'simple' member - basically just what a membership provider can edit + emptyContent = MemberService.CreateGenericMembershipProviderMember("", "", "", ""); + emptyContent.AdditionalData["NewPassword"] = Membership.GeneratePassword(Membership.MinRequiredPasswordLength, Membership.MinRequiredNonAlphanumericCharacters); + return Mapper.Map(emptyContent); + } + } - /// - /// Saves member - /// - /// - [FileUploadCleanupFilter] - public MemberDisplay PostSave( - [ModelBinder(typeof(MemberBinder))] - MemberSave contentItem) - { + /// + /// Saves member + /// + /// + [FileUploadCleanupFilter] + public MemberDisplay PostSave( + [ModelBinder(typeof(MemberBinder))] + MemberSave contentItem) + { - //If we've reached here it means: - // * Our model has been bound - // * and validated - // * any file attachments have been saved to their temporary location for us to use - // * we have a reference to the DTO object and the persisted object - // * Permissions are valid + //If we've reached here it means: + // * Our model has been bound + // * and validated + // * any file attachments have been saved to their temporary location for us to use + // * we have a reference to the DTO object and the persisted object + // * Permissions are valid - //This is a special case for when we're not using the umbraco membership provider - when this is the case - // we will not have a ContentTypeAlias set which means the model state will be invalid but we don't care about that - // so we'll remove that model state value - if (MembershipScenario != MembershipScenario.NativeUmbraco) - { - ModelState.Remove("ContentTypeAlias"); + //This is a special case for when we're not using the umbraco membership provider - when this is the case + // we will not have a ContentTypeAlias set which means the model state will be invalid but we don't care about that + // so we'll remove that model state value + if (MembershipScenario != MembershipScenario.NativeUmbraco) + { + ModelState.Remove("ContentTypeAlias"); - //TODO: We're removing this because we are not displaying it but when we support the CustomProviderWithUmbracoLink scenario - // we will be able to have a real name associated so do not remove this state once that is implemented! - ModelState.Remove("Name"); - } + //TODO: We're removing this because we are not displaying it but when we support the CustomProviderWithUmbracoLink scenario + // we will be able to have a real name associated so do not remove this state once that is implemented! + ModelState.Remove("Name"); + } - //map the properties to the persisted entity - MapPropertyValues(contentItem); + //map the properties to the persisted entity + MapPropertyValues(contentItem); - //Unlike content/media - if there are errors for a member, we do NOT proceed to save them, we cannot so return the errors - if (ModelState.IsValid == false) - { - var forDisplay = Mapper.Map(contentItem.PersistedContent); - forDisplay.Errors = ModelState.ToErrorDictionary(); - throw new HttpResponseException(Request.CreateValidationErrorResponse(forDisplay)); - } + //Unlike content/media - if there are errors for a member, we do NOT proceed to save them, we cannot so return the errors + if (ModelState.IsValid == false) + { + var forDisplay = Mapper.Map(contentItem.PersistedContent); + forDisplay.Errors = ModelState.ToErrorDictionary(); + throw new HttpResponseException(Request.CreateValidationErrorResponse(forDisplay)); + } - //TODO: WE need to support this! - requires UI updates, etc... - if (_provider.RequiresQuestionAndAnswer) - { - throw new NotSupportedException("Currently the member editor does not support providers that have RequiresQuestionAndAnswer specified"); - } + //TODO: WE need to support this! - requires UI updates, etc... + if (_provider.RequiresQuestionAndAnswer) + { + throw new NotSupportedException("Currently the member editor does not support providers that have RequiresQuestionAndAnswer specified"); + } //We're gonna look up the current roles now because the below code can cause // events to be raised and developers could be manually adding roles to members in @@ -287,473 +287,473 @@ namespace Umbraco.Web.Editors //find the ones to remove and remove them var rolesToRemove = currRoles.Except(contentItem.Groups).ToArray(); - string generatedPassword = null; - //Depending on the action we need to first do a create or update using the membership provider - // this ensures that passwords are formatted correclty and also performs the validation on the provider itself. - switch (contentItem.Action) - { - case ContentSaveAction.Save: - generatedPassword = UpdateWithMembershipProvider(contentItem); - break; - case ContentSaveAction.SaveNew: - MembershipCreateStatus status; - CreateWithMembershipProvider(contentItem, out status); + string generatedPassword = null; + //Depending on the action we need to first do a create or update using the membership provider + // this ensures that passwords are formatted correclty and also performs the validation on the provider itself. + switch (contentItem.Action) + { + case ContentSaveAction.Save: + generatedPassword = UpdateWithMembershipProvider(contentItem); + break; + case ContentSaveAction.SaveNew: + MembershipCreateStatus status; + CreateWithMembershipProvider(contentItem, out status); - // save the ID of the creator - contentItem.PersistedContent.CreatorId = Security.CurrentUser.Id; - break; - default: - //we don't support anything else for members - throw new HttpResponseException(HttpStatusCode.NotFound); - } + // save the ID of the creator + contentItem.PersistedContent.CreatorId = Security.CurrentUser.Id; + break; + default: + //we don't support anything else for members + throw new HttpResponseException(HttpStatusCode.NotFound); + } - //If we've had problems creating/updating the user with the provider then return the error - if (ModelState.IsValid == false) - { - var forDisplay = Mapper.Map(contentItem.PersistedContent); - forDisplay.Errors = ModelState.ToErrorDictionary(); - throw new HttpResponseException(Request.CreateValidationErrorResponse(forDisplay)); - } + //If we've had problems creating/updating the user with the provider then return the error + if (ModelState.IsValid == false) + { + var forDisplay = Mapper.Map(contentItem.PersistedContent); + forDisplay.Errors = ModelState.ToErrorDictionary(); + throw new HttpResponseException(Request.CreateValidationErrorResponse(forDisplay)); + } - //save the IMember - - //TODO: When we support the CustomProviderWithUmbracoLink scenario, we'll need to save the custom properties for that here too - if (MembershipScenario == MembershipScenario.NativeUmbraco) - { - //save the item - //NOTE: We are setting the password to NULL - this indicates to the system to not actually save the password - // so it will not get overwritten! - contentItem.PersistedContent.RawPasswordValue = null; + //save the IMember - + //TODO: When we support the CustomProviderWithUmbracoLink scenario, we'll need to save the custom properties for that here too + if (MembershipScenario == MembershipScenario.NativeUmbraco) + { + //save the item + //NOTE: We are setting the password to NULL - this indicates to the system to not actually save the password + // so it will not get overwritten! + contentItem.PersistedContent.RawPasswordValue = null; - //create/save the IMember - Services.MemberService.Save(contentItem.PersistedContent); - } + //create/save the IMember + Services.MemberService.Save(contentItem.PersistedContent); + } - //Now let's do the role provider stuff - now that we've saved the content item (that is important since - // if we are changing the username, it must be persisted before looking up the member roles). + //Now let's do the role provider stuff - now that we've saved the content item (that is important since + // if we are changing the username, it must be persisted before looking up the member roles). if (rolesToRemove.Any()) - { + { Roles.RemoveUserFromRoles(contentItem.PersistedContent.Username, rolesToRemove); - } - //find the ones to add and add them + } + //find the ones to add and add them var toAdd = contentItem.Groups.Except(currRoles).ToArray(); - if (toAdd.Any()) - { - //add the ones submitted - Roles.AddUserToRoles(contentItem.PersistedContent.Username, toAdd); - } + if (toAdd.Any()) + { + //add the ones submitted + Roles.AddUserToRoles(contentItem.PersistedContent.Username, toAdd); + } - //set the generated password (if there was one) - in order to do this we'll chuck the gen'd password into the - // additional data of the IUmbracoEntity of the persisted item - then we can retrieve this in the model mapper and set - // the value to be given to the UI. Hooray for AdditionalData :) - contentItem.PersistedContent.AdditionalData["GeneratedPassword"] = generatedPassword; + //set the generated password (if there was one) - in order to do this we'll chuck the gen'd password into the + // additional data of the IUmbracoEntity of the persisted item - then we can retrieve this in the model mapper and set + // the value to be given to the UI. Hooray for AdditionalData :) + contentItem.PersistedContent.AdditionalData["GeneratedPassword"] = generatedPassword; - //return the updated model - var display = Mapper.Map(contentItem.PersistedContent); + //return the updated model + var display = Mapper.Map(contentItem.PersistedContent); - //lasty, if it is not valid, add the modelstate to the outgoing object and throw a 403 - HandleInvalidModelState(display); + //lasty, if it is not valid, add the modelstate to the outgoing object and throw a 403 + HandleInvalidModelState(display); - var localizedTextService = Services.TextService; - //put the correct msgs in - switch (contentItem.Action) - { - case ContentSaveAction.Save: - case ContentSaveAction.SaveNew: - display.AddSuccessNotification(localizedTextService.Localize("speechBubbles/editMemberSaved"), localizedTextService.Localize("speechBubbles/editMemberSaved")); - break; - } + var localizedTextService = Services.TextService; + //put the correct msgs in + switch (contentItem.Action) + { + case ContentSaveAction.Save: + case ContentSaveAction.SaveNew: + display.AddSuccessNotification(localizedTextService.Localize("speechBubbles/editMemberSaved"), localizedTextService.Localize("speechBubbles/editMemberSaved")); + break; + } - return display; - } + return display; + } - /// - /// Maps the property values to the persisted entity - /// - /// - private void MapPropertyValues(MemberSave contentItem) - { - UpdateName(contentItem); + /// + /// Maps the property values to the persisted entity + /// + /// + private void MapPropertyValues(MemberSave contentItem) + { + UpdateName(contentItem); - //map the custom properties - this will already be set for new entities in our member binder - contentItem.PersistedContent.Email = contentItem.Email; - contentItem.PersistedContent.Username = contentItem.Username; + //map the custom properties - this will already be set for new entities in our member binder + contentItem.PersistedContent.Email = contentItem.Email; + contentItem.PersistedContent.Username = contentItem.Username; - //use the base method to map the rest of the properties - base.MapPropertyValues(contentItem); - } + //use the base method to map the rest of the properties + base.MapPropertyValues(contentItem); + } - /// - /// Update the membership user using the membership provider (for things like email, etc...) - /// If a password change is detected then we'll try that too. - /// - /// - /// - /// If the password has been reset then this method will return the reset/generated password, otherwise will return null. - /// - private string UpdateWithMembershipProvider(MemberSave contentItem) - { - //Get the member from the provider + /// + /// Update the membership user using the membership provider (for things like email, etc...) + /// If a password change is detected then we'll try that too. + /// + /// + /// + /// If the password has been reset then this method will return the reset/generated password, otherwise will return null. + /// + private string UpdateWithMembershipProvider(MemberSave contentItem) + { + //Get the member from the provider - var membershipUser = _provider.GetUser(contentItem.PersistedContent.Key, false); - if (membershipUser == null) - { - //This should never happen! so we'll let it YSOD if it does. - throw new InvalidOperationException("Could not get member from membership provider " + _provider.Name + " with key " + contentItem.PersistedContent.Key); - } + var membershipUser = _provider.GetUser(contentItem.PersistedContent.Key, false); + if (membershipUser == null) + { + //This should never happen! so we'll let it YSOD if it does. + throw new InvalidOperationException("Could not get member from membership provider " + _provider.Name + " with key " + contentItem.PersistedContent.Key); + } - var shouldReFetchMember = false; - var providedUserName = contentItem.PersistedContent.Username; + var shouldReFetchMember = false; + var providedUserName = contentItem.PersistedContent.Username; - //Update the membership user if it has changed - try - { - var requiredUpdating = Members.UpdateMember(membershipUser, _provider, - contentItem.Email.Trim(), - contentItem.IsApproved, - comment: contentItem.Comments); + //Update the membership user if it has changed + try + { + var requiredUpdating = Members.UpdateMember(membershipUser, _provider, + contentItem.Email.Trim(), + contentItem.IsApproved, + comment: contentItem.Comments); - if (requiredUpdating.Success) - { - //re-map these values - shouldReFetchMember = true; - } - } - catch (Exception ex) - { - LogHelper.WarnWithException("Could not update member, the provider returned an error", ex); - ModelState.AddPropertyError( - //specify 'default' just so that it shows up as a notification - is not assigned to a property - new ValidationResult("Could not update member, the provider returned an error: " + ex.Message + " (see log for full details)"), "default"); - } + if (requiredUpdating.Success) + { + //re-map these values + shouldReFetchMember = true; + } + } + catch (Exception ex) + { + LogHelper.WarnWithException("Could not update member, the provider returned an error", ex); + ModelState.AddPropertyError( + //specify 'default' just so that it shows up as a notification - is not assigned to a property + new ValidationResult("Could not update member, the provider returned an error: " + ex.Message + " (see log for full details)"), "default"); + } - //if they were locked but now they are trying to be unlocked - if (membershipUser.IsLockedOut && contentItem.IsLockedOut == false) - { - try - { - var result = _provider.UnlockUser(membershipUser.UserName); - if (result == false) - { - //it wasn't successful - but it won't really tell us why. - ModelState.AddModelError("custom", "Could not unlock the user"); - } - else - { - shouldReFetchMember = true; - } - } - catch (Exception ex) - { - ModelState.AddModelError("custom", ex); - } - } - else if (membershipUser.IsLockedOut == false && contentItem.IsLockedOut) - { - //NOTE: This should not ever happen unless someone is mucking around with the request data. - //An admin cannot simply lock a user, they get locked out by password attempts, but an admin can un-approve them - ModelState.AddModelError("custom", "An admin cannot lock a user"); - } + //if they were locked but now they are trying to be unlocked + if (membershipUser.IsLockedOut && contentItem.IsLockedOut == false) + { + try + { + var result = _provider.UnlockUser(membershipUser.UserName); + if (result == false) + { + //it wasn't successful - but it won't really tell us why. + ModelState.AddModelError("custom", "Could not unlock the user"); + } + else + { + shouldReFetchMember = true; + } + } + catch (Exception ex) + { + ModelState.AddModelError("custom", ex); + } + } + else if (membershipUser.IsLockedOut == false && contentItem.IsLockedOut) + { + //NOTE: This should not ever happen unless someone is mucking around with the request data. + //An admin cannot simply lock a user, they get locked out by password attempts, but an admin can un-approve them + ModelState.AddModelError("custom", "An admin cannot lock a user"); + } - //password changes ? - if (contentItem.Password == null) - { - //If the provider has changed some values, these values need to be reflected in the member object - //that will get mapped to the display object - if (shouldReFetchMember) - { - RefetchMemberData(contentItem, LookupType.ByKey); - RestoreProvidedUserName(contentItem, providedUserName); - } + //password changes ? + if (contentItem.Password == null) + { + //If the provider has changed some values, these values need to be reflected in the member object + //that will get mapped to the display object + if (shouldReFetchMember) + { + RefetchMemberData(contentItem, LookupType.ByKey); + RestoreProvidedUserName(contentItem, providedUserName); + } - return null; - } + return null; + } - var passwordChangeResult = Members.ChangePassword(membershipUser.UserName, contentItem.Password, _provider); - if (passwordChangeResult.Success) - { - //If the provider has changed some values, these values need to be reflected in the member object - //that will get mapped to the display object - if (shouldReFetchMember) - { - RefetchMemberData(contentItem, LookupType.ByKey); - RestoreProvidedUserName(contentItem, providedUserName); - } + var passwordChangeResult = Members.ChangePassword(membershipUser.UserName, contentItem.Password, _provider); + if (passwordChangeResult.Success) + { + //If the provider has changed some values, these values need to be reflected in the member object + //that will get mapped to the display object + if (shouldReFetchMember) + { + RefetchMemberData(contentItem, LookupType.ByKey); + RestoreProvidedUserName(contentItem, providedUserName); + } - //even if we weren't resetting this, it is the correct value (null), otherwise if we were resetting then it will contain the new pword - return passwordChangeResult.Result.ResetPassword; - } + //even if we weren't resetting this, it is the correct value (null), otherwise if we were resetting then it will contain the new pword + return passwordChangeResult.Result.ResetPassword; + } - //it wasn't successful, so add the change error to the model state - ModelState.AddPropertyError( - passwordChangeResult.Result.ChangeError, - string.Format("{0}password", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); + //it wasn't successful, so add the change error to the model state + ModelState.AddPropertyError( + passwordChangeResult.Result.ChangeError, + string.Format("{0}password", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); - return null; - } + return null; + } - private enum LookupType - { - ByKey, - ByUserName - } + private enum LookupType + { + ByKey, + ByUserName + } - /// - /// Re-fetches the database data to map to the PersistedContent object and re-assigns the already mapped the posted properties so that the display object is up-to-date - /// - /// - /// - /// This is done during an update if the membership provider has changed some underlying data - we need to ensure that our model is consistent with that data - /// - private void RefetchMemberData(MemberSave contentItem, LookupType lookup) - { - var currProps = contentItem.PersistedContent.Properties.ToArray(); + /// + /// Re-fetches the database data to map to the PersistedContent object and re-assigns the already mapped the posted properties so that the display object is up-to-date + /// + /// + /// + /// This is done during an update if the membership provider has changed some underlying data - we need to ensure that our model is consistent with that data + /// + private void RefetchMemberData(MemberSave contentItem, LookupType lookup) + { + var currProps = contentItem.PersistedContent.Properties.ToArray(); - switch (MembershipScenario) - { - case MembershipScenario.NativeUmbraco: - switch (lookup) - { - case LookupType.ByKey: - //Go and re-fetch the persisted item - contentItem.PersistedContent = Services.MemberService.GetByKey(contentItem.Key); - break; - case LookupType.ByUserName: - contentItem.PersistedContent = Services.MemberService.GetByUsername(contentItem.Username.Trim()); - break; - } - break; - case MembershipScenario.CustomProviderWithUmbracoLink: - case MembershipScenario.StandaloneCustomProvider: - default: - var membershipUser = _provider.GetUser(contentItem.Key, false); - //Go and re-fetch the persisted item - contentItem.PersistedContent = Mapper.Map(membershipUser); - break; - } + switch (MembershipScenario) + { + case MembershipScenario.NativeUmbraco: + switch (lookup) + { + case LookupType.ByKey: + //Go and re-fetch the persisted item + contentItem.PersistedContent = Services.MemberService.GetByKey(contentItem.Key); + break; + case LookupType.ByUserName: + contentItem.PersistedContent = Services.MemberService.GetByUsername(contentItem.Username.Trim()); + break; + } + break; + case MembershipScenario.CustomProviderWithUmbracoLink: + case MembershipScenario.StandaloneCustomProvider: + default: + var membershipUser = _provider.GetUser(contentItem.Key, false); + //Go and re-fetch the persisted item + contentItem.PersistedContent = Mapper.Map(membershipUser); + break; + } - UpdateName(contentItem); + UpdateName(contentItem); - //re-assign the mapped values that are not part of the membership provider properties. - var builtInAliases = Constants.Conventions.Member.GetStandardPropertyTypeStubs().Select(x => x.Key).ToArray(); - foreach (var p in contentItem.PersistedContent.Properties) - { - var valueMapped = currProps.SingleOrDefault(x => x.Alias == p.Alias); - if (builtInAliases.Contains(p.Alias) == false && valueMapped != null) - { - p.Value = valueMapped.Value; - p.TagSupport.Behavior = valueMapped.TagSupport.Behavior; - p.TagSupport.Enable = valueMapped.TagSupport.Enable; - p.TagSupport.Tags = valueMapped.TagSupport.Tags; - } - } - } + //re-assign the mapped values that are not part of the membership provider properties. + var builtInAliases = Constants.Conventions.Member.GetStandardPropertyTypeStubs().Select(x => x.Key).ToArray(); + foreach (var p in contentItem.PersistedContent.Properties) + { + var valueMapped = currProps.SingleOrDefault(x => x.Alias == p.Alias); + if (builtInAliases.Contains(p.Alias) == false && valueMapped != null) + { + p.Value = valueMapped.Value; + p.TagSupport.Behavior = valueMapped.TagSupport.Behavior; + p.TagSupport.Enable = valueMapped.TagSupport.Enable; + p.TagSupport.Tags = valueMapped.TagSupport.Tags; + } + } + } - /// - /// Following a refresh of member data called during an update if the membership provider has changed some underlying data, - /// we don't want to lose the provided, and potentiallly changed, username - /// - /// - /// - private static void RestoreProvidedUserName(MemberSave contentItem, string providedUserName) - { - contentItem.PersistedContent.Username = providedUserName; - } + /// + /// Following a refresh of member data called during an update if the membership provider has changed some underlying data, + /// we don't want to lose the provided, and potentiallly changed, username + /// + /// + /// + private static void RestoreProvidedUserName(MemberSave contentItem, string providedUserName) + { + contentItem.PersistedContent.Username = providedUserName; + } - /// - /// This is going to create the user with the membership provider and check for validation - /// - /// - /// - /// - /// - /// Depending on if the Umbraco membership provider is active or not, the process differs slightly: - /// - /// * If the umbraco membership provider is used - we create the membership user first with the membership provider, since - /// it's the umbraco membership provider, this writes to the umbraco tables. When that is complete we re-fetch the IMember - /// model data from the db. In this case we don't care what the provider user key is. - /// * If we're using a non-umbraco membership provider - we check if there is a 'Member' member type - if so - /// we create an empty IMember instance first (of type 'Member'), this gives us a unique ID (GUID) - /// that we then use to create the member in the custom membership provider. This acts as the link between Umbraco data and - /// the custom membership provider data. This gives us the ability to eventually have custom membership properties but still use - /// a custom memberhip provider. If there is no 'Member' member type, then we will simply just create the membership provider member - /// with no link to our data. - /// - /// If this is successful, it will go and re-fetch the IMember from the db because it will now have an ID because the Umbraco provider - /// uses the umbraco data store - then of course we need to re-map it to the saved property values. - /// - private MembershipUser CreateWithMembershipProvider(MemberSave contentItem, out MembershipCreateStatus status) - { - MembershipUser membershipUser; + /// + /// This is going to create the user with the membership provider and check for validation + /// + /// + /// + /// + /// + /// Depending on if the Umbraco membership provider is active or not, the process differs slightly: + /// + /// * If the umbraco membership provider is used - we create the membership user first with the membership provider, since + /// it's the umbraco membership provider, this writes to the umbraco tables. When that is complete we re-fetch the IMember + /// model data from the db. In this case we don't care what the provider user key is. + /// * If we're using a non-umbraco membership provider - we check if there is a 'Member' member type - if so + /// we create an empty IMember instance first (of type 'Member'), this gives us a unique ID (GUID) + /// that we then use to create the member in the custom membership provider. This acts as the link between Umbraco data and + /// the custom membership provider data. This gives us the ability to eventually have custom membership properties but still use + /// a custom memberhip provider. If there is no 'Member' member type, then we will simply just create the membership provider member + /// with no link to our data. + /// + /// If this is successful, it will go and re-fetch the IMember from the db because it will now have an ID because the Umbraco provider + /// uses the umbraco data store - then of course we need to re-map it to the saved property values. + /// + private MembershipUser CreateWithMembershipProvider(MemberSave contentItem, out MembershipCreateStatus status) + { + MembershipUser membershipUser; - switch (MembershipScenario) - { - case MembershipScenario.NativeUmbraco: - //We are using the umbraco membership provider, create the member using the membership provider first. - var umbracoMembershipProvider = (UmbracoMembershipProviderBase)_provider; - //TODO: We are not supporting q/a - passing in empty here - membershipUser = umbracoMembershipProvider.CreateUser( - contentItem.ContentTypeAlias, contentItem.Username, - contentItem.Password.NewPassword, - contentItem.Email, "", "", - contentItem.IsApproved, - Guid.NewGuid(), //since it's the umbraco provider, the user key here doesn't make any difference - out status); + switch (MembershipScenario) + { + case MembershipScenario.NativeUmbraco: + //We are using the umbraco membership provider, create the member using the membership provider first. + var umbracoMembershipProvider = (UmbracoMembershipProviderBase)_provider; + //TODO: We are not supporting q/a - passing in empty here + membershipUser = umbracoMembershipProvider.CreateUser( + contentItem.ContentTypeAlias, contentItem.Username, + contentItem.Password.NewPassword, + contentItem.Email, "", "", + contentItem.IsApproved, + Guid.NewGuid(), //since it's the umbraco provider, the user key here doesn't make any difference + out status); - break; - case MembershipScenario.CustomProviderWithUmbracoLink: - //We are using a custom membership provider, we'll create an empty IMember first to get the unique id to use - // as the provider user key. - //create it - this persisted item has already been set in the MemberBinder based on the 'Member' member type: - Services.MemberService.Save(contentItem.PersistedContent); + break; + case MembershipScenario.CustomProviderWithUmbracoLink: + //We are using a custom membership provider, we'll create an empty IMember first to get the unique id to use + // as the provider user key. + //create it - this persisted item has already been set in the MemberBinder based on the 'Member' member type: + Services.MemberService.Save(contentItem.PersistedContent); - //TODO: We are not supporting q/a - passing in empty here - membershipUser = _provider.CreateUser( - contentItem.Username, - contentItem.Password.NewPassword, - contentItem.Email, - "TEMP", //some membership provider's require something here even if q/a is disabled! - "TEMP", //some membership provider's require something here even if q/a is disabled! - contentItem.IsApproved, - contentItem.PersistedContent.Key, //custom membership provider, we'll link that based on the IMember unique id (GUID) - out status); + //TODO: We are not supporting q/a - passing in empty here + membershipUser = _provider.CreateUser( + contentItem.Username, + contentItem.Password.NewPassword, + contentItem.Email, + "TEMP", //some membership provider's require something here even if q/a is disabled! + "TEMP", //some membership provider's require something here even if q/a is disabled! + contentItem.IsApproved, + contentItem.PersistedContent.Key, //custom membership provider, we'll link that based on the IMember unique id (GUID) + out status); - break; - case MembershipScenario.StandaloneCustomProvider: - // we don't have a member type to use so we will just create the basic membership user with the provider with no - // link back to the umbraco data + break; + case MembershipScenario.StandaloneCustomProvider: + // we don't have a member type to use so we will just create the basic membership user with the provider with no + // link back to the umbraco data - var newKey = Guid.NewGuid(); - //TODO: We are not supporting q/a - passing in empty here - membershipUser = _provider.CreateUser( - contentItem.Username, - contentItem.Password.NewPassword, - contentItem.Email, - "TEMP", //some membership provider's require something here even if q/a is disabled! - "TEMP", //some membership provider's require something here even if q/a is disabled! - contentItem.IsApproved, - newKey, - out status); + var newKey = Guid.NewGuid(); + //TODO: We are not supporting q/a - passing in empty here + membershipUser = _provider.CreateUser( + contentItem.Username, + contentItem.Password.NewPassword, + contentItem.Email, + "TEMP", //some membership provider's require something here even if q/a is disabled! + "TEMP", //some membership provider's require something here even if q/a is disabled! + contentItem.IsApproved, + newKey, + out status); - break; - default: - throw new ArgumentOutOfRangeException(); - } + break; + default: + throw new ArgumentOutOfRangeException(); + } - //TODO: Localize these! - switch (status) - { - case MembershipCreateStatus.Success: + //TODO: Localize these! + switch (status) + { + case MembershipCreateStatus.Success: - //map the key back - contentItem.Key = membershipUser.ProviderUserKey.TryConvertTo().Result; - contentItem.PersistedContent.Key = contentItem.Key; + //map the key back + contentItem.Key = membershipUser.ProviderUserKey.TryConvertTo().Result; + contentItem.PersistedContent.Key = contentItem.Key; - //if the comments are there then we need to save them - if (contentItem.Comments.IsNullOrWhiteSpace() == false) - { - membershipUser.Comment = contentItem.Comments; - _provider.UpdateUser(membershipUser); - } + //if the comments are there then we need to save them + if (contentItem.Comments.IsNullOrWhiteSpace() == false) + { + membershipUser.Comment = contentItem.Comments; + _provider.UpdateUser(membershipUser); + } - RefetchMemberData(contentItem, LookupType.ByUserName); + RefetchMemberData(contentItem, LookupType.ByUserName); - break; - case MembershipCreateStatus.InvalidUserName: - ModelState.AddPropertyError( - new ValidationResult("Invalid user name", new[] { "value" }), - string.Format("{0}login", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); - break; - case MembershipCreateStatus.InvalidPassword: - ModelState.AddPropertyError( - new ValidationResult("Invalid password", new[] { "value" }), - string.Format("{0}password", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); - break; - case MembershipCreateStatus.InvalidQuestion: - case MembershipCreateStatus.InvalidAnswer: - throw new NotSupportedException("Currently the member editor does not support providers that have RequiresQuestionAndAnswer specified"); - case MembershipCreateStatus.InvalidEmail: - ModelState.AddPropertyError( - new ValidationResult("Invalid email", new[] { "value" }), - string.Format("{0}email", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); - break; - case MembershipCreateStatus.DuplicateUserName: - ModelState.AddPropertyError( - new ValidationResult("Username is already in use", new[] { "value" }), - string.Format("{0}login", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); - break; - case MembershipCreateStatus.DuplicateEmail: - ModelState.AddPropertyError( - new ValidationResult("Email address is already in use", new[] { "value" }), - string.Format("{0}email", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); - break; - case MembershipCreateStatus.InvalidProviderUserKey: - ModelState.AddPropertyError( - //specify 'default' just so that it shows up as a notification - is not assigned to a property - new ValidationResult("Invalid provider user key"), "default"); - break; - case MembershipCreateStatus.DuplicateProviderUserKey: - ModelState.AddPropertyError( - //specify 'default' just so that it shows up as a notification - is not assigned to a property - new ValidationResult("Duplicate provider user key"), "default"); - break; - case MembershipCreateStatus.ProviderError: - case MembershipCreateStatus.UserRejected: - ModelState.AddPropertyError( - //specify 'default' just so that it shows up as a notification - is not assigned to a property - new ValidationResult("User could not be created (rejected by provider)"), "default"); - break; - default: - throw new ArgumentOutOfRangeException(); - } + break; + case MembershipCreateStatus.InvalidUserName: + ModelState.AddPropertyError( + new ValidationResult("Invalid user name", new[] { "value" }), + string.Format("{0}login", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); + break; + case MembershipCreateStatus.InvalidPassword: + ModelState.AddPropertyError( + new ValidationResult("Invalid password", new[] { "value" }), + string.Format("{0}password", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); + break; + case MembershipCreateStatus.InvalidQuestion: + case MembershipCreateStatus.InvalidAnswer: + throw new NotSupportedException("Currently the member editor does not support providers that have RequiresQuestionAndAnswer specified"); + case MembershipCreateStatus.InvalidEmail: + ModelState.AddPropertyError( + new ValidationResult("Invalid email", new[] { "value" }), + string.Format("{0}email", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); + break; + case MembershipCreateStatus.DuplicateUserName: + ModelState.AddPropertyError( + new ValidationResult("Username is already in use", new[] { "value" }), + string.Format("{0}login", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); + break; + case MembershipCreateStatus.DuplicateEmail: + ModelState.AddPropertyError( + new ValidationResult("Email address is already in use", new[] { "value" }), + string.Format("{0}email", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); + break; + case MembershipCreateStatus.InvalidProviderUserKey: + ModelState.AddPropertyError( + //specify 'default' just so that it shows up as a notification - is not assigned to a property + new ValidationResult("Invalid provider user key"), "default"); + break; + case MembershipCreateStatus.DuplicateProviderUserKey: + ModelState.AddPropertyError( + //specify 'default' just so that it shows up as a notification - is not assigned to a property + new ValidationResult("Duplicate provider user key"), "default"); + break; + case MembershipCreateStatus.ProviderError: + case MembershipCreateStatus.UserRejected: + ModelState.AddPropertyError( + //specify 'default' just so that it shows up as a notification - is not assigned to a property + new ValidationResult("User could not be created (rejected by provider)"), "default"); + break; + default: + throw new ArgumentOutOfRangeException(); + } - return membershipUser; - } + return membershipUser; + } - /// - /// Permanently deletes a member - /// - /// - /// - /// - [HttpPost] - public HttpResponseMessage DeleteByKey(Guid key) - { - IMember foundMember; - MembershipUser foundMembershipUser; - switch (MembershipScenario) - { - case MembershipScenario.NativeUmbraco: - foundMember = Services.MemberService.GetByKey(key); - if (foundMember == null) - { - return HandleContentNotFound(key, false); - } - Services.MemberService.Delete(foundMember); - break; - case MembershipScenario.CustomProviderWithUmbracoLink: - foundMember = Services.MemberService.GetByKey(key); - if (foundMember != null) - { - Services.MemberService.Delete(foundMember); - } - foundMembershipUser = _provider.GetUser(key, false); - if (foundMembershipUser != null) - { - _provider.DeleteUser(foundMembershipUser.UserName, true); - } - break; - case MembershipScenario.StandaloneCustomProvider: - foundMembershipUser = _provider.GetUser(key, false); - if (foundMembershipUser != null) - { - _provider.DeleteUser(foundMembershipUser.UserName, true); - } - break; - default: - throw new ArgumentOutOfRangeException(); - } + /// + /// Permanently deletes a member + /// + /// + /// + /// + [HttpPost] + public HttpResponseMessage DeleteByKey(Guid key) + { + IMember foundMember; + MembershipUser foundMembershipUser; + switch (MembershipScenario) + { + case MembershipScenario.NativeUmbraco: + foundMember = Services.MemberService.GetByKey(key); + if (foundMember == null) + { + return HandleContentNotFound(key, false); + } + Services.MemberService.Delete(foundMember); + break; + case MembershipScenario.CustomProviderWithUmbracoLink: + foundMember = Services.MemberService.GetByKey(key); + if (foundMember != null) + { + Services.MemberService.Delete(foundMember); + } + foundMembershipUser = _provider.GetUser(key, false); + if (foundMembershipUser != null) + { + _provider.DeleteUser(foundMembershipUser.UserName, true); + } + break; + case MembershipScenario.StandaloneCustomProvider: + foundMembershipUser = _provider.GetUser(key, false); + if (foundMembershipUser != null) + { + _provider.DeleteUser(foundMembershipUser.UserName, true); + } + break; + default: + throw new ArgumentOutOfRangeException(); + } - return Request.CreateResponse(HttpStatusCode.OK); - } - } + return Request.CreateResponse(HttpStatusCode.OK); + } + } } diff --git a/src/Umbraco.Web/WebApi/Filters/DisableBrowserCacheAttribute.cs b/src/Umbraco.Web/WebApi/Filters/DisableBrowserCacheAttribute.cs index 92709916ac..edd1792e0e 100644 --- a/src/Umbraco.Web/WebApi/Filters/DisableBrowserCacheAttribute.cs +++ b/src/Umbraco.Web/WebApi/Filters/DisableBrowserCacheAttribute.cs @@ -1,54 +1,54 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using System.Web; -using System.Web.Http.Controllers; -using System.Web.Http.Filters; -using Umbraco.Core; - -namespace Umbraco.Web.WebApi.Filters -{ - /// - /// Ensures that the request is not cached by the browser - /// - public class DisableBrowserCacheAttribute : ActionFilterAttribute - { - public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext) - { - //See: http://stackoverflow.com/questions/17755239/how-to-stop-chrome-from-caching-rest-response-from-webapi - - base.OnActionExecuted(actionExecutedContext); - if (actionExecutedContext == null || actionExecutedContext.Response == null || - actionExecutedContext.Response.Headers == null) - { - return; - } - //NOTE: Until we upgraded to WebApi 2, this didn't work correctly and we had to revert to using - // HttpContext.Current responses. I've changed this back to what it should be now since it works - // and now with WebApi2, the HttpContext.Current responses dont! Anyways, all good now. - actionExecutedContext.Response.Headers.CacheControl = new CacheControlHeaderValue() - { - NoCache = true, - NoStore = true, - MaxAge = new TimeSpan(0), - MustRevalidate = true - }; - - actionExecutedContext.Response.Headers.Pragma.Add(new NameValueHeaderValue("no-cache")); - if (actionExecutedContext.Response.Content != null) - { - actionExecutedContext.Response.Content.Headers.Expires = - //Mon, 01 Jan 1990 00:00:00 GMT - new DateTimeOffset(1990, 1, 1, 0, 0, 0, TimeSpan.Zero); - } - - - - } - } -} +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Web; +using System.Web.Http.Controllers; +using System.Web.Http.Filters; +using Umbraco.Core; + +namespace Umbraco.Web.WebApi.Filters +{ + /// + /// Ensures that the request is not cached by the browser + /// + public class DisableBrowserCacheAttribute : ActionFilterAttribute + { + public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext) + { + //See: http://stackoverflow.com/questions/17755239/how-to-stop-chrome-from-caching-rest-response-from-webapi + + base.OnActionExecuted(actionExecutedContext); + if (actionExecutedContext == null || actionExecutedContext.Response == null || + actionExecutedContext.Response.Headers == null) + { + return; + } + //NOTE: Until we upgraded to WebApi 2, this didn't work correctly and we had to revert to using + // HttpContext.Current responses. I've changed this back to what it should be now since it works + // and now with WebApi2, the HttpContext.Current responses dont! Anyways, all good now. + actionExecutedContext.Response.Headers.CacheControl = new CacheControlHeaderValue() + { + NoCache = true, + NoStore = true, + MaxAge = new TimeSpan(0), + MustRevalidate = true + }; + + actionExecutedContext.Response.Headers.Pragma.Add(new NameValueHeaderValue("no-cache")); + if (actionExecutedContext.Response.Content != null) + { + actionExecutedContext.Response.Content.Headers.Expires = + //Mon, 01 Jan 1990 00:00:00 GMT + new DateTimeOffset(1990, 1, 1, 0, 0, 0, TimeSpan.Zero); + } + + + + } + } +} diff --git a/src/UmbracoExamine/UmbracoMemberIndexer.cs b/src/UmbracoExamine/UmbracoMemberIndexer.cs index 8edc470bfa..4a460e0f38 100644 --- a/src/UmbracoExamine/UmbracoMemberIndexer.cs +++ b/src/UmbracoExamine/UmbracoMemberIndexer.cs @@ -17,207 +17,207 @@ using Lucene.Net.Analysis; namespace UmbracoExamine { - /// - /// Custom indexer for members - /// - public class UmbracoMemberIndexer : UmbracoContentIndexer - { + /// + /// Custom indexer for members + /// + public class UmbracoMemberIndexer : UmbracoContentIndexer + { - private readonly IMemberService _memberService; - private readonly IDataTypeService _dataTypeService; + private readonly IMemberService _memberService; + private readonly IDataTypeService _dataTypeService; - /// - /// Default constructor - /// - public UmbracoMemberIndexer() : base() - { - _dataTypeService = ApplicationContext.Current.Services.DataTypeService; - _memberService = ApplicationContext.Current.Services.MemberService; - } + /// + /// Default constructor + /// + public UmbracoMemberIndexer() : base() + { + _dataTypeService = ApplicationContext.Current.Services.DataTypeService; + _memberService = ApplicationContext.Current.Services.MemberService; + } - /// - /// Constructor to allow for creating an indexer at runtime - /// - /// - /// - /// - /// - [Obsolete("Use the overload that specifies the Umbraco services")] - public UmbracoMemberIndexer(IIndexCriteria indexerData, DirectoryInfo indexPath, IDataService dataService, Analyzer analyzer, bool async) - : base(indexerData, indexPath, dataService, analyzer, async) - { - _dataTypeService = ApplicationContext.Current.Services.DataTypeService; - _memberService = ApplicationContext.Current.Services.MemberService; - } + /// + /// Constructor to allow for creating an indexer at runtime + /// + /// + /// + /// + /// + [Obsolete("Use the overload that specifies the Umbraco services")] + public UmbracoMemberIndexer(IIndexCriteria indexerData, DirectoryInfo indexPath, IDataService dataService, Analyzer analyzer, bool async) + : base(indexerData, indexPath, dataService, analyzer, async) + { + _dataTypeService = ApplicationContext.Current.Services.DataTypeService; + _memberService = ApplicationContext.Current.Services.MemberService; + } - /// - /// Constructor to allow for creating an indexer at runtime - /// - /// - /// - /// - /// - /// - /// - /// - public UmbracoMemberIndexer(IIndexCriteria indexerData, DirectoryInfo indexPath, IDataService dataService, - IDataTypeService dataTypeService, - IMemberService memberService, - Analyzer analyzer, bool async) - : base(indexerData, indexPath, dataService, analyzer, async) - { - _dataTypeService = dataTypeService; - _memberService = memberService; - } + /// + /// Constructor to allow for creating an indexer at runtime + /// + /// + /// + /// + /// + /// + /// + /// + public UmbracoMemberIndexer(IIndexCriteria indexerData, DirectoryInfo indexPath, IDataService dataService, + IDataTypeService dataTypeService, + IMemberService memberService, + Analyzer analyzer, bool async) + : base(indexerData, indexPath, dataService, analyzer, async) + { + _dataTypeService = dataTypeService; + _memberService = memberService; + } - /// - /// Ensures that the'_searchEmail' is added to the user fields so that it is indexed - without having to modify the config - /// - /// - /// - protected override IIndexCriteria GetIndexerData(IndexSet indexSet) - { - var indexerData = base.GetIndexerData(indexSet); + /// + /// Ensures that the'_searchEmail' is added to the user fields so that it is indexed - without having to modify the config + /// + /// + /// + protected override IIndexCriteria GetIndexerData(IndexSet indexSet) + { + var indexerData = base.GetIndexerData(indexSet); - if (CanInitialize()) - { - //If the fields are missing a custom _searchEmail, then add it + if (CanInitialize()) + { + //If the fields are missing a custom _searchEmail, then add it - if (indexerData.UserFields.Any(x => x.Name == "_searchEmail") == false) - { - var field = new IndexField { Name = "_searchEmail" }; - var policy = IndexFieldPolicies.FirstOrDefault(x => x.Name == "_searchEmail"); - if (policy != null) - { - field.Type = policy.Type; - field.EnableSorting = policy.EnableSorting; - } + if (indexerData.UserFields.Any(x => x.Name == "_searchEmail") == false) + { + var field = new IndexField { Name = "_searchEmail" }; + var policy = IndexFieldPolicies.FirstOrDefault(x => x.Name == "_searchEmail"); + if (policy != null) + { + field.Type = policy.Type; + field.EnableSorting = policy.EnableSorting; + } - return new IndexCriteria( - indexerData.StandardFields, - indexerData.UserFields.Concat(new[] { field }), - indexerData.IncludeNodeTypes, - indexerData.ExcludeNodeTypes, - indexerData.ParentNodeId - ); - } - } + return new IndexCriteria( + indexerData.StandardFields, + indexerData.UserFields.Concat(new[] { field }), + indexerData.IncludeNodeTypes, + indexerData.ExcludeNodeTypes, + indexerData.ParentNodeId + ); + } + } - return indexerData; - } + return indexerData; + } - /// - /// The supported types for this indexer - /// - protected override IEnumerable SupportedTypes - { - get - { - return new string[] { IndexTypes.Member }; - } - } + /// + /// The supported types for this indexer + /// + protected override IEnumerable SupportedTypes + { + get + { + return new string[] { IndexTypes.Member }; + } + } - /// - /// Reindex all members - /// - /// - protected override void PerformIndexAll(string type) - { - //This only supports members - if (SupportedTypes.Contains(type) == false) - return; + /// + /// Reindex all members + /// + /// + protected override void PerformIndexAll(string type) + { + //This only supports members + if (SupportedTypes.Contains(type) == false) + return; - const int pageSize = 1000; - var pageIndex = 0; + const int pageSize = 1000; + var pageIndex = 0; - IMember[] members; + IMember[] members; - if (IndexerData.IncludeNodeTypes.Any()) - { - //if there are specific node types then just index those - foreach (var nodeType in IndexerData.IncludeNodeTypes) - { - do - { - long total; - members = _memberService.GetAll(pageIndex, pageSize, out total, "LoginName" - , Direction.Ascending, true, nodeType).ToArray(); + if (IndexerData.IncludeNodeTypes.Any()) + { + //if there are specific node types then just index those + foreach (var nodeType in IndexerData.IncludeNodeTypes) + { + do + { + long total; + members = _memberService.GetAll(pageIndex, pageSize, out total, "LoginName" + , Direction.Ascending, true, nodeType).ToArray(); - AddNodesToIndex(GetSerializedMembers(members), type); + AddNodesToIndex(GetSerializedMembers(members), type); - pageIndex++; - } while (members.Length == pageSize); - } - } - else - { - //no node types specified, do all members - do - { - int total; - members = _memberService.GetAll(pageIndex, pageSize, out total).ToArray(); + pageIndex++; + } while (members.Length == pageSize); + } + } + else + { + //no node types specified, do all members + do + { + int total; + members = _memberService.GetAll(pageIndex, pageSize, out total).ToArray(); - AddNodesToIndex(GetSerializedMembers(members), type); + AddNodesToIndex(GetSerializedMembers(members), type); - pageIndex++; - } while (members.Length == pageSize); - } - } + pageIndex++; + } while (members.Length == pageSize); + } + } - private IEnumerable GetSerializedMembers(IEnumerable members) - { - var serializer = new EntityXmlSerializer(); - return members.Select(member => serializer.Serialize(_dataTypeService, member)); - } + private IEnumerable GetSerializedMembers(IEnumerable members) + { + var serializer = new EntityXmlSerializer(); + return members.Select(member => serializer.Serialize(_dataTypeService, member)); + } - protected override XDocument GetXDocument(string xPath, string type) - { - throw new NotSupportedException(); - } + protected override XDocument GetXDocument(string xPath, string type) + { + throw new NotSupportedException(); + } - protected override Dictionary GetSpecialFieldsToIndex(Dictionary allValuesForIndexing) - { - var fields = base.GetSpecialFieldsToIndex(allValuesForIndexing); + protected override Dictionary GetSpecialFieldsToIndex(Dictionary allValuesForIndexing) + { + var fields = base.GetSpecialFieldsToIndex(allValuesForIndexing); - //adds the special path property to the index - fields.Add("__key", allValuesForIndexing["__key"]); + //adds the special path property to the index + fields.Add("__key", allValuesForIndexing["__key"]); - return fields; + return fields; - } + } - /// - /// Add the special __key and _searchEmail fields - /// - /// - protected override void OnGatheringNodeData(IndexingNodeDataEventArgs e) - { - base.OnGatheringNodeData(e); + /// + /// Add the special __key and _searchEmail fields + /// + /// + protected override void OnGatheringNodeData(IndexingNodeDataEventArgs e) + { + base.OnGatheringNodeData(e); - if (e.Node.Attribute("key") != null) - { - if (e.Fields.ContainsKey("__key") == false) - e.Fields.Add("__key", e.Node.Attribute("key").Value); - } + if (e.Node.Attribute("key") != null) + { + if (e.Fields.ContainsKey("__key") == false) + e.Fields.Add("__key", e.Node.Attribute("key").Value); + } - if (e.Node.Attribute("email") != null) - { - //NOTE: the single underscore = it's not a 'special' field which means it will be indexed normally - if (e.Fields.ContainsKey("_searchEmail") == false) - e.Fields.Add("_searchEmail", e.Node.Attribute("email").Value.Replace(".", " ").Replace("@", " ")); - } + if (e.Node.Attribute("email") != null) + { + //NOTE: the single underscore = it's not a 'special' field which means it will be indexed normally + if (e.Fields.ContainsKey("_searchEmail") == false) + e.Fields.Add("_searchEmail", e.Node.Attribute("email").Value.Replace(".", " ").Replace("@", " ")); + } - if (e.Fields.ContainsKey(IconFieldName) == false) - e.Fields.Add(IconFieldName, (string)e.Node.Attribute("icon")); - } + if (e.Fields.ContainsKey(IconFieldName) == false) + e.Fields.Add(IconFieldName, (string)e.Node.Attribute("icon")); + } - private static XElement GetMemberItem(int nodeId) - { - //TODO: Change this so that it is not using the LegacyLibrary, just serialize manually! - var nodes = LegacyLibrary.GetMember(nodeId); - return XElement.Parse(nodes.Current.OuterXml); - } - } + private static XElement GetMemberItem(int nodeId) + { + //TODO: Change this so that it is not using the LegacyLibrary, just serialize manually! + var nodes = LegacyLibrary.GetMember(nodeId); + return XElement.Parse(nodes.Current.OuterXml); + } + } } From bd2a40d214a8ccb04e6f13b4b58d6f8e2ded4f2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Ferreira?= Date: Fri, 26 Feb 2016 14:30:32 +0000 Subject: [PATCH 12/46] U4-6003 List View - Order By Custom Property Fix Original work done on https://github.com/umbraco/Umbraco-CMS/pull/711 but ported to the latest version Content below for reference With the current implementation of the list view you can only sort by system columns (Name, SortOrder etc.) and not custom columns you have added to your document types. This PR allows that. The crux of it is a sub-query added to the ORDER BY clause when we are ordering by a custom field. This looks up the field's value from the most recent content version. Provided here and not in the previous pull request is: MySQL support Have done some performance testing. On a local laptop with 1000 nodes in a list view, it's sorting in around 220-250ms. It's a little slower that sorting on native properties like node name, but still perfectly usable - there's no significant delay you see in use. Please note also: GetPagedResultsByQuery() in VersionableRepositoryBase was previously doing an ORDER BY in SQL and then repeating this via LINQ to Objects. I couldn't see that this second ordering was necessary so removed it, but wanted to flag here in case I've missed something around why this was necessary. The PR also includes small amends to fix or hide sorting on a couple of the default columns for the member amd media list views. --- .../Repositories/ContentRepository.cs | 9 +- .../Interfaces/IContentRepository.cs | 3 +- .../Interfaces/IMediaRepository.cs | 5 +- .../Interfaces/IMemberRepository.cs | 17 +- .../Repositories/MediaRepository.cs | 9 +- .../Repositories/MemberRepository.cs | 53 +- .../Repositories/VersionableRepositoryBase.cs | 110 +- .../SqlSyntax/ISqlSyntaxProvider.cs | 4 + .../SqlSyntax/MySqlSyntaxProvider.cs | 17 +- .../SqlSyntax/SqlSyntaxProviderBase.cs | 12 +- src/Umbraco.Core/Services/ContentService.cs | 40 +- src/Umbraco.Core/Services/IContentService.cs | 16 +- src/Umbraco.Core/Services/IMediaService.cs | 12 +- src/Umbraco.Core/Services/IMemberService.cs | 19 +- src/Umbraco.Core/Services/MediaService.cs | 34 +- src/Umbraco.Core/Services/MemberService.cs | 102 +- .../Repositories/ContentRepositoryTest.cs | 22 +- .../Repositories/MediaRepositoryTest.cs | 28 +- .../UmbracoExamine/IndexInitializer.cs | 134 +- .../components/umbtable.directive.js | 142 +- .../src/common/resources/content.resource.js | 1160 +++++++++-------- .../src/common/resources/media.resource.js | 842 ++++++------ .../src/common/resources/member.resource.js | 400 +++--- .../src/views/components/umb-table.html | 136 +- .../list/list.listviewlayout.controller.js | 151 +-- .../listview/listview.controller.js | 911 ++++++------- src/Umbraco.Web/Editors/ContentController.cs | 184 +-- src/Umbraco.Web/Editors/MediaController.cs | 121 +- src/Umbraco.Web/Editors/MemberController.cs | 71 +- .../Filters/DisableBrowserCacheAttribute.cs | 104 +- src/UmbracoExamine/UmbracoMemberIndexer.cs | 115 +- 31 files changed, 2526 insertions(+), 2457 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs index 7d3557a81d..3ef1df3bc5 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs @@ -241,8 +241,8 @@ namespace Umbraco.Core.Persistence.Repositories // see: http://issues.umbraco.org/issue/U4-6322 & http://issues.umbraco.org/issue/U4-5982 var descendants = GetPagedResultsByQuery(query, pageIndex, pageSize, out total, new Tuple("cmsDocument", "nodeId"), - ProcessQuery, "Path", Direction.Ascending); - + ProcessQuery, "Path", Direction.Ascending, orderBySystemField: true); + var xmlItems = (from descendant in descendants let xml = serializer(descendant) select new ContentXmlDto { NodeId = descendant.Id, Xml = xml.ToDataString() }).ToArray(); @@ -766,10 +766,11 @@ namespace Umbraco.Core.Persistence.Repositories /// Total records query would return without paging /// Field to order by /// Direction to order by + /// Flag to indicate when ordering by system field /// Search text filter /// An Enumerable list of objects public IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, - string orderBy, Direction orderDirection, string filter = "") + string orderBy, Direction orderDirection, bool orderBySystemField, string filter = "") { //NOTE: This uses the GetBaseQuery method but that does not take into account the required 'newest' field which is @@ -788,7 +789,7 @@ namespace Umbraco.Core.Persistence.Repositories return GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, new Tuple("cmsDocument", "nodeId"), - ProcessQuery, orderBy, orderDirection, + ProcessQuery, orderBy, orderDirection, orderBySystemField, filterCallback); } diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs index 9d3fcbb40b..ab176df6d0 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs @@ -84,9 +84,10 @@ namespace Umbraco.Core.Persistence.Repositories /// Total records query would return without paging /// Field to order by /// Direction to order by + /// Flag to indicate when ordering by system field /// Search text filter /// An Enumerable list of objects IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, - string orderBy, Direction orderDirection, string filter = ""); + string orderBy, Direction orderDirection, bool orderBySystemField, string filter = ""); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMediaRepository.cs index 46bce6a03c..907f9b62c5 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMediaRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMediaRepository.cs @@ -9,7 +9,7 @@ namespace Umbraco.Core.Persistence.Repositories { public interface IMediaRepository : IRepositoryVersionable, IRecycleBinRepository, IDeleteMediaFilesRepository { - + /// /// Used to add/update published xml for the media item /// @@ -33,9 +33,10 @@ namespace Umbraco.Core.Persistence.Repositories /// Total records query would return without paging /// Field to order by /// Direction to order by + /// Flag to indicate when ordering by system field /// Search text filter /// An Enumerable list of objects IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, - string orderBy, Direction orderDirection, string filter = ""); + string orderBy, Direction orderDirection, bool orderBySystemField, string filter = ""); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMemberRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMemberRepository.cs index 9cb74d1806..a24116f0e2 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMemberRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMemberRepository.cs @@ -44,16 +44,17 @@ namespace Umbraco.Core.Persistence.Repositories /// /// Gets paged member results /// - /// - /// - /// - /// - /// - /// - /// + /// The query. + /// Index of the page. + /// Size of the page. + /// The total records. + /// The order by column + /// The order direction. + /// Flag to indicate when ordering by system field + /// Search query /// IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, - string orderBy, Direction orderDirection, string filter = ""); + string orderBy, Direction orderDirection, bool orderBySystemField, string filter = ""); //IEnumerable GetPagedResultsByQuery( // Sql sql, int pageIndex, int pageSize, out int totalRecords, diff --git a/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs index 162ad88ca0..bb54eebeec 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs @@ -232,7 +232,7 @@ namespace Umbraco.Core.Persistence.Repositories var processed = 0; do { - var descendants = GetPagedResultsByQuery(query, pageIndex, pageSize, out total, "Path", Direction.Ascending); + var descendants = GetPagedResultsByQuery(query, pageIndex, pageSize, out total, "Path", Direction.Ascending, orderBySystemField: true); var xmlItems = (from descendant in descendants let xml = serializer(descendant) @@ -442,10 +442,11 @@ namespace Umbraco.Core.Persistence.Repositories /// Total records query would return without paging /// Field to order by /// Direction to order by + /// Flag to indicate when ordering by system field /// Search text filter /// An Enumerable list of objects public IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, - string orderBy, Direction orderDirection, string filter = "") + string orderBy, Direction orderDirection, bool orderBySystemField, string filter = "") { var args = new List(); var sbWhere = new StringBuilder(); @@ -459,7 +460,7 @@ namespace Umbraco.Core.Persistence.Repositories return GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, new Tuple("cmsContentVersion", "contentId"), - ProcessQuery, orderBy, orderDirection, + ProcessQuery, orderBy, orderDirection, orderBySystemField, filterCallback); } @@ -468,7 +469,7 @@ namespace Umbraco.Core.Persistence.Repositories { //NOTE: This doesn't allow properties to be part of the query var dtos = Database.Fetch(sql); - + var ids = dtos.Select(x => x.ContentDto.ContentTypeId).ToArray(); //content types diff --git a/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs index ac5529b523..e0718d0356 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs @@ -74,7 +74,7 @@ namespace Umbraco.Core.Persistence.Repositories } return ProcessQuery(sql); - + } protected override IEnumerable PerformGetByQuery(IQuery query) @@ -95,7 +95,7 @@ namespace Umbraco.Core.Persistence.Repositories baseQuery.Append(new Sql("WHERE umbracoNode.id IN (" + sql.SQL + ")", sql.Arguments)) .OrderBy(x => x.SortOrder); - return ProcessQuery(baseQuery); + return ProcessQuery(baseQuery); } else { @@ -103,7 +103,7 @@ namespace Umbraco.Core.Persistence.Repositories var sql = translator.Translate() .OrderBy(x => x.SortOrder); - return ProcessQuery(sql); + return ProcessQuery(sql); } } @@ -118,7 +118,7 @@ namespace Umbraco.Core.Persistence.Repositories sql.Select(isCount ? "COUNT(*)" : "*") .From() .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) + .On(left => left.NodeId, right => right.NodeId) .InnerJoin() .On(left => left.NodeId, right => right.NodeId) //We're joining the type so we can do a query against the member type - not sure if this adds much overhead or not? @@ -269,7 +269,7 @@ namespace Umbraco.Core.Persistence.Repositories //Ensure that strings don't contain characters that are invalid in XML entity.SanitizeEntityPropertiesForXmlStorage(); - var dirtyEntity = (ICanBeDirty) entity; + var dirtyEntity = (ICanBeDirty)entity; //Look up parent to get and set the correct Path and update SortOrder if ParentId has changed if (dirtyEntity.IsPropertyDirty("ParentId")) @@ -308,9 +308,9 @@ namespace Umbraco.Core.Persistence.Repositories //Updates the current version - cmsContentVersion //Assumes a Version guid exists and Version date (modified date) has been set/updated Database.Update(dto.ContentVersionDto); - + //Updates the cmsMember entry if it has changed - + //NOTE: these cols are the REAL column names in the db var changedCols = new List(); @@ -330,13 +330,13 @@ namespace Umbraco.Core.Persistence.Repositories //only update the changed cols if (changedCols.Count > 0) { - Database.Update(dto, changedCols); + Database.Update(dto, changedCols); } //TODO ContentType for the Member entity //Create the PropertyData for this version - cmsPropertyData - var propertyFactory = new PropertyFactory(entity.ContentType.CompositionPropertyTypes.ToArray(), entity.Version, entity.Id); + var propertyFactory = new PropertyFactory(entity.ContentType.CompositionPropertyTypes.ToArray(), entity.Version, entity.Id); var keyDictionary = new Dictionary(); //Add Properties @@ -447,7 +447,7 @@ namespace Umbraco.Core.Persistence.Repositories var processed = 0; do { - var descendants = GetPagedResultsByQuery(query, pageIndex, pageSize, out total, "Path", Direction.Ascending); + var descendants = GetPagedResultsByQuery(query, pageIndex, pageSize, out total, "Path", Direction.Ascending, orderBySystemField: true); var xmlItems = (from descendant in descendants let xml = serializer(descendant) @@ -559,7 +559,7 @@ namespace Umbraco.Core.Persistence.Repositories var grpQry = new Query().Where(group => group.Name.Equals(groupName)); var memberGroup = _memberGroupRepository.GetByQuery(grpQry).FirstOrDefault(); if (memberGroup == null) return Enumerable.Empty(); - + var subQuery = new Sql().Select("Member").From().Where(dto => dto.MemberGroup == memberGroup.Id); var sql = GetBaseQuery(false) @@ -568,7 +568,7 @@ namespace Umbraco.Core.Persistence.Repositories .Append(new Sql("WHERE umbracoNode.id IN (" + subQuery.SQL + ")", subQuery.Arguments)) .OrderByDescending(x => x.VersionDate) .OrderBy(x => x.SortOrder); - + return ProcessQuery(sql); } @@ -593,7 +593,7 @@ namespace Umbraco.Core.Persistence.Repositories //get the COUNT base query var fullSql = GetBaseQuery(true) .Append(new Sql("WHERE umbracoNode.id IN (" + sql.SQL + ")", sql.Arguments)); - + return Database.ExecuteScalar(fullSql); } @@ -603,18 +603,19 @@ namespace Umbraco.Core.Persistence.Repositories /// /// The where clause, if this is null all records are queried /// - /// - /// - /// - /// - /// - /// + /// Index of the page. + /// Size of the page. + /// The total records. + /// The order by column + /// The order direction. + /// Flag to indicate when ordering by system field + /// Search query /// /// /// The query supplied will ONLY work with data specifically on the cmsMember table because we are using PetaPoco paging (SQL paging) /// public IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, - string orderBy, Direction orderDirection, string filter = "") + string orderBy, Direction orderDirection, bool orderBySystemField, string filter = "") { var args = new List(); var sbWhere = new StringBuilder(); @@ -625,14 +626,14 @@ namespace Umbraco.Core.Persistence.Repositories "OR (cmsMember.LoginName LIKE @0" + args.Count + "))"); args.Add("%" + filter + "%"); filterCallback = () => new Tuple(sbWhere.ToString().Trim(), args.ToArray()); - } + } return GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, new Tuple("cmsMember", "nodeId"), - ProcessQuery, orderBy, orderDirection, + ProcessQuery, orderBy, orderDirection, orderBySystemField, filterCallback); } - + public void AddOrUpdateContentXml(IMember content, Func xml) { _contentXmlRepository.AddOrUpdate(new ContentXmlEntity(content, xml)); @@ -682,7 +683,7 @@ namespace Umbraco.Core.Persistence.Repositories var dtosWithContentTypes = dtos //This select into and null check are required because we don't have a foreign damn key on the contentType column // http://issues.umbraco.org/issue/U4-5503 - .Select(x => new {dto = x, contentType = contentTypes.FirstOrDefault(ct => ct.Id == x.ContentVersionDto.ContentDto.ContentTypeId)}) + .Select(x => new { dto = x, contentType = contentTypes.FirstOrDefault(ct => ct.Id == x.ContentVersionDto.ContentDto.ContentTypeId) }) .Where(x => x.contentType != null) .ToArray(); @@ -694,12 +695,12 @@ namespace Umbraco.Core.Persistence.Repositories d.dto.ContentVersionDto.ContentDto.NodeDto.CreateDate, d.contentType)); - var propertyData = GetPropertyCollection(sql, docDefs); + var propertyData = GetPropertyCollection(sql, docDefs); return dtosWithContentTypes.Select(d => CreateMemberFromDto( d.dto, contentTypes.First(ct => ct.Id == d.dto.ContentVersionDto.ContentDto.ContentTypeId), - propertyData[d.dto.NodeId])); + propertyData[d.dto.NodeId])); } /// diff --git a/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs index 56b0b63ad5..acff02c892 100644 --- a/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs @@ -25,6 +25,7 @@ using Umbraco.Core.IO; namespace Umbraco.Core.Persistence.Repositories { + using SqlSyntax; internal abstract class VersionableRepositoryBase : PetaPocoRepositoryBase where TEntity : class, IAggregateRoot { @@ -61,11 +62,11 @@ namespace Umbraco.Core.Persistence.Repositories public virtual void DeleteVersion(Guid versionId) { var dto = Database.FirstOrDefault("WHERE versionId = @VersionId", new { VersionId = versionId }); - if(dto == null) return; + if (dto == null) return; //Ensure that the lastest version is not deleted var latestVersionDto = Database.FirstOrDefault("WHERE ContentId = @Id ORDER BY VersionDate DESC", new { Id = dto.NodeId }); - if(latestVersionDto.VersionId == dto.VersionId) + if (latestVersionDto.VersionId == dto.VersionId) return; using (var transaction = Database.GetTransaction()) @@ -83,7 +84,7 @@ namespace Umbraco.Core.Persistence.Repositories var list = Database.Fetch( "WHERE versionId <> @VersionId AND (ContentId = @Id AND VersionDate < @VersionDate)", - new {VersionId = latestVersionDto.VersionId, Id = id, VersionDate = versionDate}); + new { VersionId = latestVersionDto.VersionId, Id = id, VersionDate = versionDate }); if (list.Any() == false) return; using (var transaction = Database.GetTransaction()) @@ -244,23 +245,57 @@ namespace Umbraco.Core.Persistence.Repositories return filteredSql; } - private Sql GetSortedSqlForPagedResults(Sql sql, Direction orderDirection, string orderBy) + private Sql GetSortedSqlForPagedResults(Sql sql, Direction orderDirection, string orderBy, bool orderBySystemField) { //copy to var so that the original isn't changed var sortedSql = new Sql(sql.SQL, sql.Arguments); - // Apply order according to parameters - if (string.IsNullOrEmpty(orderBy) == false) + + if (orderBySystemField) { - var orderByParams = new[] { GetDatabaseFieldNameForOrderBy(orderBy) }; - if (orderDirection == Direction.Ascending) + // Apply order according to parameters + if (string.IsNullOrEmpty(orderBy) == false) { - sortedSql.OrderBy(orderByParams); + var orderByParams = new[] { GetDatabaseFieldNameForOrderBy(orderBy) }; + if (orderDirection == Direction.Ascending) + { + sortedSql.OrderBy(orderByParams); + } + else + { + sortedSql.OrderByDescending(orderByParams); + } } - else + } + else + { + // Sorting by a custom field, so set-up sub-query for ORDER BY clause to pull through valie + // from most recent content version for the given order by field + var sortedInt = string.Format(SqlSyntaxContext.SqlSyntaxProvider.ConvertIntegerToOrderableString, "dataInt"); + var sortedDate = string.Format(SqlSyntaxContext.SqlSyntaxProvider.ConvertDateToOrderableString, "dataDate"); + var sortedString = string.Format(SqlSyntaxContext.SqlSyntaxProvider.IsNull, "dataNvarchar", "''"); + + var orderBySql = string.Format(@"ORDER BY ( + SELECT CASE + WHEN dataInt Is Not Null THEN {0} + WHEN dataDate Is Not Null THEN {1} + ELSE {2} + END + FROM cmsContent c + INNER JOIN cmsContentVersion cv ON cv.ContentId = c.nodeId AND VersionDate = ( + SELECT Max(VersionDate) + FROM cmsContentVersion + WHERE ContentId = c.nodeId + ) + INNER JOIN cmsPropertyData cpd ON cpd.contentNodeId = c.nodeId + AND cpd.versionId = cv.VersionId + INNER JOIN cmsPropertyType cpt ON cpt.Id = cpd.propertytypeId + WHERE c.nodeId = umbracoNode.Id and cpt.Alias = @0)", sortedInt, sortedDate, sortedString); + + sortedSql.Append(orderBySql, orderBy); + if (orderDirection == Direction.Descending) { - sortedSql.OrderByDescending(orderByParams); + sortedSql.Append(" DESC"); } - return sortedSql; } return sortedSql; } @@ -279,13 +314,15 @@ namespace Umbraco.Core.Persistence.Repositories /// A callback to process the query result /// The order by column /// The order direction. + /// Flag to indicate when ordering by system field /// /// orderBy protected IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, Tuple nodeIdSelect, Func> processQuery, - string orderBy, + string orderBy, Direction orderDirection, + bool orderBySystemField, Func> defaultFilter = null) where TContentBase : class, IAggregateRoot, TEntity { @@ -297,18 +334,18 @@ namespace Umbraco.Core.Persistence.Repositories if (query == null) query = new Query(); var translator = new SqlTranslator(sqlBase, query); var sqlQuery = translator.Translate(); - + // Note we can't do multi-page for several DTOs like we can multi-fetch and are doing in PerformGetByQuery, // but actually given we are doing a Get on each one (again as in PerformGetByQuery), we only need the node Id. // So we'll modify the SQL. var sqlNodeIds = new Sql( - sqlQuery.SQL.Replace("SELECT *", string.Format("SELECT {0}.{1}",nodeIdSelect.Item1, nodeIdSelect.Item2)), + sqlQuery.SQL.Replace("SELECT *", string.Format("SELECT {0}.{1}", nodeIdSelect.Item1, nodeIdSelect.Item2)), sqlQuery.Arguments); - + //get sorted and filtered sql var sqlNodeIdsWithSort = GetSortedSqlForPagedResults( GetFilteredSqlForPagedResults(sqlNodeIds, defaultFilter), - orderDirection, orderBy); + orderDirection, orderBy, orderBySystemField); // Get page of results and total count IEnumerable result; @@ -324,7 +361,7 @@ namespace Umbraco.Core.Persistence.Repositories var args = sqlNodeIdsWithSort.Arguments; string sqlStringCount, sqlStringPage; Database.BuildPageQueries(pageIndex * pageSize, pageSize, sqlNodeIdsWithSort.SQL, ref args, out sqlStringCount, out sqlStringPage); - + //if this is for sql server, the sqlPage will start with a SELECT * but we don't want that, we only want to return the nodeId sqlStringPage = sqlStringPage .Replace("SELECT *", @@ -333,7 +370,7 @@ namespace Umbraco.Core.Persistence.Repositories "SELECT " + nodeIdSelect.Item2); //We need to make this an inner join on the paged query - var splitQuery = sqlQuery.SQL.Split(new[] {"WHERE "}, StringSplitOptions.None); + var splitQuery = sqlQuery.SQL.Split(new[] { "WHERE " }, StringSplitOptions.None); var withInnerJoinSql = new Sql(splitQuery[0]) .Append("INNER JOIN (") //join the paged query with the paged query arguments @@ -345,23 +382,9 @@ namespace Umbraco.Core.Persistence.Repositories //get sorted and filtered sql var fullQuery = GetSortedSqlForPagedResults( - GetFilteredSqlForPagedResults(withInnerJoinSql, defaultFilter), - orderDirection, orderBy); - - var content = processQuery(fullQuery) - .Cast() - .AsQueryable(); - - // Now we need to ensure this result is also ordered by the same order by clause - var orderByProperty = GetEntityPropertyNameForOrderBy(orderBy); - if (orderDirection == Direction.Ascending) - { - result = content.OrderBy(orderByProperty); - } - else - { - result = content.OrderByDescending(orderByProperty); - } + GetFilteredSqlForPagedResults(withInnerJoinSql, defaultFilter), + orderDirection, orderBy, orderBySystemField); + return processQuery(fullQuery); } else { @@ -394,9 +417,9 @@ INNER JOIN (" + string.Format(parsedOriginalSql, "cmsContent.nodeId, cmsContentVersion.VersionId") + @") as docData ON cmsPropertyData.versionId = docData.VersionId AND cmsPropertyData.contentNodeId = docData.nodeId LEFT OUTER JOIN cmsDataTypePreValues -ON cmsPropertyType.dataTypeId = cmsDataTypePreValues.datatypeNodeId", docSql.Arguments); +ON cmsPropertyType.dataTypeId = cmsDataTypePreValues.datatypeNodeId", docSql.Arguments); - var allPropertyData = Database.Fetch(propSql); + var allPropertyData = Database.Fetch(propSql); //This is a lazy access call to get all prevalue data for the data types that make up all of these properties which we use // below if any property requires tag support @@ -414,7 +437,7 @@ WHERE EXISTS( ON cmsPropertyType.contentTypeId = docData.contentType WHERE a.id = b.id)", docSql.Arguments); - return Database.Fetch(preValsSql); + return Database.Fetch(preValsSql); }); var result = new Dictionary(); @@ -432,8 +455,8 @@ WHERE EXISTS( var propertyDataDtos = allPropertyData.Where(x => x.NodeId == def.Id).Distinct(); var propertyFactory = new PropertyFactory(compositionProperties, def.Version, def.Id, def.CreateDate, def.VersionDate); - var properties = propertyFactory.BuildEntity(propertyDataDtos.ToArray()).ToArray(); - + var properties = propertyFactory.BuildEntity(propertyDataDtos.ToArray()).ToArray(); + var newProperties = properties.Where(x => x.HasIdentity == false && x.PropertyType.HasIdentity); foreach (var property in newProperties) @@ -481,7 +504,7 @@ WHERE EXISTS( Logger.Warn>("The query returned multiple property sets for document definition " + def.Id + ", " + def.Composition.Name); } result[def.Id] = new PropertyCollection(properties); - } + } } return result; @@ -521,6 +544,9 @@ WHERE EXISTS( case "OWNER": //TODO: This isn't going to work very nicely because it's going to order by ID, not by letter return "umbracoNode.nodeUser"; + // Members only + case "USERNAME": + return "cmsMember.LoginName"; default: //ensure invalid SQL cannot be submitted return Regex.Replace(orderBy, @"[^\w\.,`\[\]@-]", ""); diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/ISqlSyntaxProvider.cs b/src/Umbraco.Core/Persistence/SqlSyntax/ISqlSyntaxProvider.cs index 056d9c1d9a..e663c8e64b 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/ISqlSyntaxProvider.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/ISqlSyntaxProvider.cs @@ -70,6 +70,10 @@ namespace Umbraco.Core.Persistence.SqlSyntax bool SupportsIdentityInsert(); bool? SupportsCaseInsensitiveQueries(Database db); + string IsNull { get; } + string ConvertIntegerToOrderableString { get; } + string ConvertDateToOrderableString { get; } + IEnumerable GetTablesInSchema(Database db); IEnumerable GetColumnsInSchema(Database db); IEnumerable> GetConstraintsPerTable(Database db); diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/MySqlSyntaxProvider.cs b/src/Umbraco.Core/Persistence/SqlSyntax/MySqlSyntaxProvider.cs index c18a5f8477..c45aade707 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/MySqlSyntaxProvider.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/MySqlSyntaxProvider.cs @@ -18,7 +18,7 @@ namespace Umbraco.Core.Persistence.SqlSyntax public MySqlSyntaxProvider(ILogger logger) { _logger = logger; - + AutoIncrementDefinition = "AUTO_INCREMENT"; IntColumnDefinition = "int(11)"; BoolColumnDefinition = "tinyint(1)"; @@ -26,7 +26,7 @@ namespace Umbraco.Core.Persistence.SqlSyntax TimeColumnDefinition = "time"; DecimalColumnDefinition = "decimal(38,6)"; GuidColumnDefinition = "char(36)"; - + DefaultValueFormat = "DEFAULT {0}"; InitColumnTypeMap(); @@ -326,13 +326,13 @@ ORDER BY TABLE_NAME, INDEX_NAME", { case SystemMethods.NewGuid: return null; // NOT SUPPORTED! - //return "NEWID()"; + //return "NEWID()"; case SystemMethods.CurrentDateTime: return "CURRENT_TIMESTAMP"; - //case SystemMethods.NewSequentialId: - // return "NEWSEQUENTIALID()"; - //case SystemMethods.CurrentUTCDateTime: - // return "GETUTCDATE()"; + //case SystemMethods.NewSequentialId: + // return "NEWSEQUENTIALID()"; + //case SystemMethods.CurrentUTCDateTime: + // return "GETUTCDATE()"; } return null; @@ -360,6 +360,9 @@ ORDER BY TABLE_NAME, INDEX_NAME", public override string DropIndex { get { return "DROP INDEX {0} ON {1}"; } } public override string RenameColumn { get { return "ALTER TABLE {0} CHANGE {1} {2}"; } } + public override string IsNull { get { return "IFNULL({0},{1})"; } } + public override string ConvertIntegerToOrderableString { get { return "LPAD({0}, 8, '0')"; } } + public override string ConvertDateToOrderableString { get { return "DATE_FORMAT({0}, '%Y%m%d')"; } } public override bool? SupportsCaseInsensitiveQueries(Database db) { diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs b/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs index c2b81aa753..35c133ce6e 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs @@ -322,7 +322,7 @@ namespace Umbraco.Core.Persistence.SqlSyntax GetQuotedColumnName(foreignKey.ForeignColumns.First()), GetQuotedTableName(foreignKey.PrimaryTable), GetQuotedColumnName(foreignKey.PrimaryColumns.First()), - FormatCascade("DELETE", foreignKey.OnDelete), + FormatCascade("DELETE", foreignKey.OnDelete), FormatCascade("UPDATE", foreignKey.OnUpdate)); } @@ -331,7 +331,7 @@ namespace Umbraco.Core.Persistence.SqlSyntax var sb = new StringBuilder(); foreach (var column in columns) { - sb.Append(Format(column) +",\n"); + sb.Append(Format(column) + ",\n"); } return sb.ToString().TrimEnd(",\n"); } @@ -431,11 +431,11 @@ namespace Umbraco.Core.Persistence.SqlSyntax return GetSpecialDbType(column.DbType); } - Type type = column.Type.HasValue + Type type = column.Type.HasValue ? DbTypeMap.ColumnDbTypeMap.First(x => x.Value == column.Type.Value).Key : column.PropertyType; - if (type == typeof (string)) + if (type == typeof(string)) { var valueOrDefault = column.Size != default(int) ? column.Size : DefaultStringLength; return string.Format(StringLengthColumnDefinitionFormat, valueOrDefault); @@ -536,5 +536,9 @@ namespace Umbraco.Core.Persistence.SqlSyntax public virtual string CreateConstraint { get { return "ALTER TABLE {0} ADD CONSTRAINT {1} {2} ({3})"; } } public virtual string DeleteConstraint { get { return "ALTER TABLE {0} DROP CONSTRAINT {1}"; } } public virtual string CreateForeignKeyConstraint { get { return "ALTER TABLE {0} ADD CONSTRAINT {1} FOREIGN KEY ({2}) REFERENCES {3} ({4}){5}{6}"; } } + + public virtual string IsNull { get { return "ISNULL({0},{1})"; } } + public virtual string ConvertIntegerToOrderableString { get { return "RIGHT('00000000' + CAST({0} AS varchar(8)),8)"; } } + public virtual string ConvertDateToOrderableString { get { return "CONVERT(varchar, {0}, 102)"; } } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index e7298a5c6e..773d26a455 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -231,7 +231,7 @@ namespace Umbraco.Core.Services public IContent CreateContentWithIdentity(string name, int parentId, string contentTypeAlias, int userId = 0) { var contentType = FindContentTypeByAlias(contentTypeAlias); - var content = new Content(name, parentId, contentType); + var content = new Content(name, parentId, contentType); //NOTE: I really hate the notion of these Creating/Created events - they are so inconsistent, I've only just found // out that in these 'WithIdentity' methods, the Saving/Saved events were not fired, wtf. Anyways, they're added now. @@ -485,7 +485,7 @@ namespace Umbraco.Core.Services [Obsolete("Use the overload with 'long' parameter types instead")] [EditorBrowsable(EditorBrowsableState.Never)] public IEnumerable GetPagedChildren(int id, int pageIndex, int pageSize, out int totalChildren, - string orderBy, Direction orderDirection, string filter = "") + string orderBy, Direction orderDirection, bool orderBySystemField = true, string filter = "") { Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); Mandate.ParameterCondition(pageSize > 0, "pageSize"); @@ -499,7 +499,7 @@ namespace Umbraco.Core.Services query.Where(x => x.ParentId == id); } long total; - var contents = repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out total, orderBy, orderDirection, filter); + var contents = repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out total, orderBy, orderDirection, orderBySystemField, filter); totalChildren = Convert.ToInt32(total); return contents; } @@ -514,10 +514,11 @@ namespace Umbraco.Core.Services /// Total records query would return without paging /// Field to order by /// Direction to order by + /// Flag to indicate when ordering by system field /// Search text filter /// An Enumerable list of objects public IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize, out long totalChildren, - string orderBy, Direction orderDirection, string filter = "") + string orderBy, Direction orderDirection, bool orderBySystemField = true, string filter = "") { Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); Mandate.ParameterCondition(pageSize > 0, "pageSize"); @@ -530,7 +531,7 @@ namespace Umbraco.Core.Services { query.Where(x => x.ParentId == id); } - var contents = repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, filter); + var contents = repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, orderBySystemField, filter); return contents; } @@ -538,7 +539,7 @@ namespace Umbraco.Core.Services [Obsolete("Use the overload with 'long' parameter types instead")] [EditorBrowsable(EditorBrowsableState.Never)] - public IEnumerable GetPagedDescendants(int id, int pageIndex, int pageSize, out int totalChildren, string orderBy = "Path", Direction orderDirection = Direction.Ascending, string filter = "") + public IEnumerable GetPagedDescendants(int id, int pageIndex, int pageSize, out int totalChildren, string orderBy = "Path", Direction orderDirection = Direction.Ascending, bool orderBySystemField = true, string filter = "") { Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); Mandate.ParameterCondition(pageSize > 0, "pageSize"); @@ -552,7 +553,7 @@ namespace Umbraco.Core.Services query.Where(x => x.Path.SqlContains(string.Format(",{0},", id), TextColumnType.NVarchar)); } long total; - var contents = repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out total, orderBy, orderDirection, filter); + var contents = repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out total, orderBy, orderDirection, orderBySystemField, filter); totalChildren = Convert.ToInt32(total); return contents; } @@ -567,9 +568,10 @@ namespace Umbraco.Core.Services /// Total records query would return without paging /// Field to order by /// Direction to order by + /// Flag to indicate when ordering by system field /// Search text filter /// An Enumerable list of objects - public IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalChildren, string orderBy = "Path", Direction orderDirection = Direction.Ascending, string filter = "") + public IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalChildren, string orderBy = "Path", Direction orderDirection = Direction.Ascending, bool orderBySystemField = true, string filter = "") { Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); Mandate.ParameterCondition(pageSize > 0, "pageSize"); @@ -582,7 +584,7 @@ namespace Umbraco.Core.Services { query.Where(x => x.Path.SqlContains(string.Format(",{0},", id), TextColumnType.NVarchar)); } - var contents = repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, filter); + var contents = repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, orderBySystemField, filter); return contents; } @@ -906,7 +908,7 @@ namespace Umbraco.Core.Services { var originalPath = content.Path; - if (Trashing.IsRaisedEventCancelled( + if (Trashing.IsRaisedEventCancelled( new MoveEventArgs(evtMsgs, new MoveEventInfo(content, originalPath, Constants.System.RecycleBinContent)), this)) { @@ -1022,7 +1024,7 @@ namespace Umbraco.Core.Services /// True if unpublishing succeeded, otherwise False public bool UnPublish(IContent content, int userId = 0) { - return ((IContentServiceOperations) this).UnPublish(content, userId).Success; + return ((IContentServiceOperations)this).UnPublish(content, userId).Success; } /// @@ -1143,7 +1145,7 @@ namespace Umbraco.Core.Services using (new WriteLock(Locker)) { - if (Deleting.IsRaisedEventCancelled( + if (Deleting.IsRaisedEventCancelled( new DeleteEventArgs(content, evtMsgs), this)) { @@ -1168,10 +1170,10 @@ namespace Umbraco.Core.Services { repository.Delete(content); uow.Commit(); - + var args = new DeleteEventArgs(content, false, evtMsgs); Deleted.RaiseEvent(args, this); - + //remove any flagged media files repository.DeleteMediaFiles(args.MediaFilesToDelete); } @@ -1350,7 +1352,7 @@ namespace Umbraco.Core.Services /// Optional Id of the User deleting the Content public void MoveToRecycleBin(IContent content, int userId = 0) { - ((IContentServiceOperations) this).MoveToRecycleBin(content, userId); + ((IContentServiceOperations)this).MoveToRecycleBin(content, userId); } /// @@ -1654,7 +1656,7 @@ namespace Umbraco.Core.Services //TODO: This should not be an inner operation, but if we do this, it cannot raise events and cannot be cancellable! _publishingStrategy.PublishingFinalized(shouldBePublished, false); } - + Audit(AuditType.Sort, "Sorting content performed by user", userId, 0); @@ -1902,13 +1904,13 @@ namespace Umbraco.Core.Services content = newest; var evtMsgs = EventMessagesFactory.Get(); - + var published = content.Published ? content : GetPublishedVersion(content.Id); // get the published version if (published == null) { return Attempt.Succeed(new UnPublishStatus(content, UnPublishedStatusType.SuccessAlreadyUnPublished, evtMsgs)); // already unpublished } - + var unpublished = _publishingStrategy.UnPublish(content, userId); if (unpublished == false) return Attempt.Fail(new UnPublishStatus(content, UnPublishedStatusType.FailedCancelledByEvent, evtMsgs)); @@ -2046,7 +2048,7 @@ namespace Umbraco.Core.Services if (raiseEvents) { - if (Saving.IsRaisedEventCancelled( + if (Saving.IsRaisedEventCancelled( new SaveEventArgs(content, evtMsgs), this)) { diff --git a/src/Umbraco.Core/Services/IContentService.cs b/src/Umbraco.Core/Services/IContentService.cs index c686cf4891..95333bee85 100644 --- a/src/Umbraco.Core/Services/IContentService.cs +++ b/src/Umbraco.Core/Services/IContentService.cs @@ -203,7 +203,7 @@ namespace Umbraco.Core.Services [Obsolete("Use the overload with 'long' parameter types instead")] [EditorBrowsable(EditorBrowsableState.Never)] IEnumerable GetPagedChildren(int id, int pageIndex, int pageSize, out int totalRecords, - string orderBy = "SortOrder", Direction orderDirection = Direction.Ascending, string filter = ""); + string orderBy = "SortOrder", Direction orderDirection = Direction.Ascending, bool orderBySystemField = true, string filter = ""); /// /// Gets a collection of objects by Parent Id @@ -214,15 +214,16 @@ namespace Umbraco.Core.Services /// Total records query would return without paging /// Field to order by /// Direction to order by + /// Flag to indicate when ordering by system field /// Search text filter /// An Enumerable list of objects IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize, out long totalRecords, - string orderBy = "SortOrder", Direction orderDirection = Direction.Ascending, string filter = ""); + string orderBy = "SortOrder", Direction orderDirection = Direction.Ascending, bool orderBySystemField = true, string filter = ""); [Obsolete("Use the overload with 'long' parameter types instead")] [EditorBrowsable(EditorBrowsableState.Never)] IEnumerable GetPagedDescendants(int id, int pageIndex, int pageSize, out int totalRecords, - string orderBy = "Path", Direction orderDirection = Direction.Ascending, string filter = ""); + string orderBy = "Path", Direction orderDirection = Direction.Ascending, bool orderBySystemField = true, string filter = ""); /// /// Gets a collection of objects by Parent Id @@ -233,11 +234,12 @@ namespace Umbraco.Core.Services /// Total records query would return without paging /// Field to order by /// Direction to order by + /// Flag to indicate when ordering by system field /// Search text filter /// An Enumerable list of objects IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalRecords, - string orderBy = "Path", Direction orderDirection = Direction.Ascending, string filter = ""); - + string orderBy = "Path", Direction orderDirection = Direction.Ascending, bool orderBySystemField = true, string filter = ""); + /// /// Gets a collection of an objects versions by its Id /// @@ -268,7 +270,7 @@ namespace Umbraco.Core.Services /// /// An Enumerable list of objects IEnumerable GetContentInRecycleBin(); - + /// /// Saves a single object /// @@ -467,7 +469,7 @@ namespace Umbraco.Core.Services /// Optional boolean indicating whether or not to raise save events. /// True if publishing succeeded, otherwise False Attempt SaveAndPublishWithStatus(IContent content, int userId = 0, bool raiseEvents = true); - + /// /// Permanently deletes an object. /// diff --git a/src/Umbraco.Core/Services/IMediaService.cs b/src/Umbraco.Core/Services/IMediaService.cs index d104b95ddc..51c01703d5 100644 --- a/src/Umbraco.Core/Services/IMediaService.cs +++ b/src/Umbraco.Core/Services/IMediaService.cs @@ -121,7 +121,7 @@ namespace Umbraco.Core.Services [Obsolete("Use the overload with 'long' parameter types instead")] [EditorBrowsable(EditorBrowsableState.Never)] IEnumerable GetPagedChildren(int id, int pageIndex, int pageSize, out int totalRecords, - string orderBy = "SortOrder", Direction orderDirection = Direction.Ascending, string filter = ""); + string orderBy = "SortOrder", Direction orderDirection = Direction.Ascending, bool orderBySystemField = true, string filter = ""); /// /// Gets a collection of objects by Parent Id @@ -132,15 +132,16 @@ namespace Umbraco.Core.Services /// Total records query would return without paging /// Field to order by /// Direction to order by + /// Flag to indicate when ordering by system field /// Search text filter /// An Enumerable list of objects IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize, out long totalRecords, - string orderBy = "SortOrder", Direction orderDirection = Direction.Ascending, string filter = ""); + string orderBy = "SortOrder", Direction orderDirection = Direction.Ascending, bool orderBySystemField = true, string filter = ""); [Obsolete("Use the overload with 'long' parameter types instead")] [EditorBrowsable(EditorBrowsableState.Never)] IEnumerable GetPagedDescendants(int id, int pageIndex, int pageSize, out int totalRecords, - string orderBy = "Path", Direction orderDirection = Direction.Ascending, string filter = ""); + string orderBy = "Path", Direction orderDirection = Direction.Ascending, bool orderBySystemField = true, string filter = ""); /// /// Gets a collection of objects by Parent Id @@ -151,10 +152,11 @@ namespace Umbraco.Core.Services /// Total records query would return without paging /// Field to order by /// Direction to order by + /// Flag to indicate when ordering by system field /// Search text filter /// An Enumerable list of objects IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalRecords, - string orderBy = "Path", Direction orderDirection = Direction.Ascending, string filter = ""); + string orderBy = "Path", Direction orderDirection = Direction.Ascending, bool orderBySystemField = true, string filter = ""); /// /// Gets descendants of a object by its Id @@ -220,7 +222,7 @@ namespace Umbraco.Core.Services /// The to delete /// Id of the User deleting the Media void Delete(IMedia media, int userId = 0); - + /// /// Saves a single object /// diff --git a/src/Umbraco.Core/Services/IMemberService.cs b/src/Umbraco.Core/Services/IMemberService.cs index 20eef54b3c..3756ca246d 100644 --- a/src/Umbraco.Core/Services/IMemberService.cs +++ b/src/Umbraco.Core/Services/IMemberService.cs @@ -25,7 +25,7 @@ namespace Umbraco.Core.Services [Obsolete("Use the overload with 'long' parameter types instead")] [EditorBrowsable(EditorBrowsableState.Never)] IEnumerable GetAll(int pageIndex, int pageSize, out int totalRecords, - string orderBy, Direction orderDirection, string memberTypeAlias = null, string filter = ""); + string orderBy, Direction orderDirection, bool orderBySystemField = true, string memberTypeAlias = null, string filter = ""); /// /// Gets a list of paged objects @@ -34,14 +34,15 @@ namespace Umbraco.Core.Services /// Current page index /// Size of the page /// Total number of records found (out) - /// - /// + /// Field to order by + /// Direction to order by + /// Flag to indicate when ordering by system field /// - /// + /// Search text filter /// IEnumerable GetAll(long pageIndex, int pageSize, out long totalRecords, - string orderBy, Direction orderDirection, string memberTypeAlias = null, string filter = ""); - + string orderBy, Direction orderDirection, bool orderBySystemField = true, string memberTypeAlias = null, string filter = ""); + /// /// Creates an object without persisting it /// @@ -91,7 +92,7 @@ namespace Umbraco.Core.Services /// MemberType the Member should be based on /// IMember CreateMemberWithIdentity(string username, string email, string name, IMemberType memberType); - + /// /// This is simply a helper method which essentially just wraps the MembershipProvider's ChangePassword method /// @@ -115,7 +116,7 @@ namespace Umbraco.Core.Services /// Id of the Member /// True if the Member exists otherwise False bool Exists(int id); - + /// /// Gets a Member by the unique key /// @@ -160,7 +161,7 @@ namespace Umbraco.Core.Services /// Optional list of Member Ids /// IEnumerable GetAllMembers(params int[] ids); - + /// /// Delete Members of the specified MemberType id /// diff --git a/src/Umbraco.Core/Services/MediaService.cs b/src/Umbraco.Core/Services/MediaService.cs index accbebfe0b..3bbf8860ea 100644 --- a/src/Umbraco.Core/Services/MediaService.cs +++ b/src/Umbraco.Core/Services/MediaService.cs @@ -35,7 +35,7 @@ namespace Umbraco.Core.Services private readonly EntityXmlSerializer _entitySerializer = new EntityXmlSerializer(); private readonly IDataTypeService _dataTypeService; private readonly IUserService _userService; - + public MediaService(IDatabaseUnitOfWorkProvider provider, RepositoryFactory repositoryFactory, ILogger logger, IEventMessagesFactory eventMessagesFactory, IDataTypeService dataTypeService, IUserService userService) : base(provider, repositoryFactory, logger, eventMessagesFactory) { @@ -395,7 +395,7 @@ namespace Umbraco.Core.Services [Obsolete("Use the overload with 'long' parameter types instead")] [EditorBrowsable(EditorBrowsableState.Never)] public IEnumerable GetPagedChildren(int id, int pageIndex, int pageSize, out int totalChildren, - string orderBy, Direction orderDirection, string filter = "") + string orderBy, Direction orderDirection, bool orderBySystemField = true, string filter = "") { Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); Mandate.ParameterCondition(pageSize > 0, "pageSize"); @@ -403,9 +403,9 @@ namespace Umbraco.Core.Services { var query = Query.Builder; query.Where(x => x.ParentId == id); - + long total; - var medias = repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out total, orderBy, orderDirection, filter); + var medias = repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out total, orderBy, orderDirection, orderBySystemField, filter); totalChildren = Convert.ToInt32(total); return medias; @@ -421,10 +421,11 @@ namespace Umbraco.Core.Services /// Total records query would return without paging /// Field to order by /// Direction to order by + /// Flag to indicate when ordering by system field /// Search text filter /// An Enumerable list of objects public IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize, out long totalChildren, - string orderBy, Direction orderDirection, string filter = "") + string orderBy, Direction orderDirection, bool orderBySystemField = true, string filter = "") { Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); Mandate.ParameterCondition(pageSize > 0, "pageSize"); @@ -432,8 +433,8 @@ namespace Umbraco.Core.Services { var query = Query.Builder; query.Where(x => x.ParentId == id); - - var medias = repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, filter); + + var medias = repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, orderBySystemField, filter); return medias; } @@ -441,7 +442,7 @@ namespace Umbraco.Core.Services [Obsolete("Use the overload with 'long' parameter types instead")] [EditorBrowsable(EditorBrowsableState.Never)] - public IEnumerable GetPagedDescendants(int id, int pageIndex, int pageSize, out int totalChildren, string orderBy = "Path", Direction orderDirection = Direction.Ascending, string filter = "") + public IEnumerable GetPagedDescendants(int id, int pageIndex, int pageSize, out int totalChildren, string orderBy = "Path", Direction orderDirection = Direction.Ascending, bool orderBySystemField = true, string filter = "") { Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); Mandate.ParameterCondition(pageSize > 0, "pageSize"); @@ -455,7 +456,7 @@ namespace Umbraco.Core.Services query.Where(x => x.Path.SqlContains(string.Format(",{0},", id), TextColumnType.NVarchar)); } long total; - var contents = repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out total, orderBy, orderDirection, filter); + var contents = repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out total, orderBy, orderDirection, orderBySystemField, filter); totalChildren = Convert.ToInt32(total); return contents; } @@ -470,9 +471,10 @@ namespace Umbraco.Core.Services /// Total records query would return without paging /// Field to order by /// Direction to order by + /// Flag to indicate when ordering by system field /// Search text filter /// An Enumerable list of objects - public IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalChildren, string orderBy = "Path", Direction orderDirection = Direction.Ascending, string filter = "") + public IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalChildren, string orderBy = "Path", Direction orderDirection = Direction.Ascending, bool orderBySystemField = true, string filter = "") { Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); Mandate.ParameterCondition(pageSize > 0, "pageSize"); @@ -485,7 +487,7 @@ namespace Umbraco.Core.Services { query.Where(x => x.Path.SqlContains(string.Format(",{0},", id), TextColumnType.NVarchar)); } - var contents = repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, filter); + var contents = repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, orderBySystemField, filter); return contents; } @@ -727,7 +729,7 @@ namespace Umbraco.Core.Services /// Id of the User deleting the Media public void MoveToRecycleBin(IMedia media, int userId = 0) { - ((IMediaServiceOperations) this).MoveToRecycleBin(media, userId); + ((IMediaServiceOperations)this).MoveToRecycleBin(media, userId); } /// @@ -744,7 +746,7 @@ namespace Umbraco.Core.Services //TODO: IT would be much nicer to mass delete all in one trans in the repo level! var evtMsgs = EventMessagesFactory.Get(); - if (Deleting.IsRaisedEventCancelled( + if (Deleting.IsRaisedEventCancelled( new DeleteEventArgs(media, evtMsgs), this)) { return OperationStatus.Cancelled(evtMsgs); @@ -1025,7 +1027,7 @@ namespace Umbraco.Core.Services ((IMediaServiceOperations)this).Delete(media, userId); } - + /// /// Permanently deletes versions from an object prior to a specific date. @@ -1081,7 +1083,7 @@ namespace Umbraco.Core.Services Audit(AuditType.Delete, "Delete Media by version performed by user", userId, -1); } - + /// /// Saves a single object /// @@ -1090,7 +1092,7 @@ namespace Umbraco.Core.Services /// Optional boolean indicating whether or not to raise events. public void Save(IMedia media, int userId = 0, bool raiseEvents = true) { - ((IMediaServiceOperations)this).Save (media, userId, raiseEvents); + ((IMediaServiceOperations)this).Save(media, userId, raiseEvents); } /// diff --git a/src/Umbraco.Core/Services/MemberService.cs b/src/Umbraco.Core/Services/MemberService.cs index 4f0647dbf7..9991583719 100644 --- a/src/Umbraco.Core/Services/MemberService.cs +++ b/src/Umbraco.Core/Services/MemberService.cs @@ -30,7 +30,7 @@ namespace Umbraco.Core.Services private readonly EntityXmlSerializer _entitySerializer = new EntityXmlSerializer(); private readonly IDataTypeService _dataTypeService; private static readonly ReaderWriterLockSlim Locker = new ReaderWriterLockSlim(); - + public MemberService(IDatabaseUnitOfWorkProvider provider, RepositoryFactory repositoryFactory, ILogger logger, IEventMessagesFactory eventMessagesFactory, IMemberGroupService memberGroupService, IDataTypeService dataTypeService) : base(provider, repositoryFactory, logger, eventMessagesFactory) { @@ -52,7 +52,7 @@ namespace Umbraco.Core.Services { using (var repository = RepositoryFactory.CreateMemberTypeRepository(UowProvider.GetUnitOfWork())) { - var types = repository.GetAll(new int[]{}).Select(x => x.Alias).ToArray(); + var types = repository.GetAll(new int[] { }).Select(x => x.Alias).ToArray(); if (types.Any() == false) { @@ -289,7 +289,7 @@ namespace Umbraco.Core.Services throw new ArgumentOutOfRangeException("matchType"); } - return repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, "Name", Direction.Ascending); + return repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, "Name", Direction.Ascending, orderBySystemField: true); } } @@ -340,7 +340,7 @@ namespace Umbraco.Core.Services throw new ArgumentOutOfRangeException("matchType"); } - return repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, "Email", Direction.Ascending); + return repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, "Email", Direction.Ascending, orderBySystemField: true); } } @@ -391,7 +391,7 @@ namespace Umbraco.Core.Services throw new ArgumentOutOfRangeException("matchType"); } - return repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, "LoginName", Direction.Ascending); + return repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, "LoginName", Direction.Ascending, orderBySystemField: true); } } @@ -688,33 +688,33 @@ namespace Umbraco.Core.Services var uow = UowProvider.GetUnitOfWork(); using (var repository = RepositoryFactory.CreateMemberRepository(uow)) { - return repository.GetPagedResultsByQuery(null, pageIndex, pageSize, out totalRecords, "LoginName", Direction.Ascending); + return repository.GetPagedResultsByQuery(null, pageIndex, pageSize, out totalRecords, "LoginName", Direction.Ascending, orderBySystemField: true); } } [Obsolete("Use the overload with 'long' parameter types instead")] [EditorBrowsable(EditorBrowsableState.Never)] public IEnumerable GetAll(int pageIndex, int pageSize, out int totalRecords, - string orderBy, Direction orderDirection, string memberTypeAlias = null, string filter = "") + string orderBy, Direction orderDirection, bool orderBySystemField = true, string memberTypeAlias = null, string filter = "") { long total; - var result = GetAll(Convert.ToInt64(pageIndex), pageSize, out total, orderBy, orderDirection, memberTypeAlias, filter); + var result = GetAll(Convert.ToInt64(pageIndex), pageSize, out total, orderBy, orderDirection, orderBySystemField, memberTypeAlias, filter); totalRecords = Convert.ToInt32(total); return result; } public IEnumerable GetAll(long pageIndex, int pageSize, out long totalRecords, - string orderBy, Direction orderDirection, string memberTypeAlias = null, string filter = "") + string orderBy, Direction orderDirection, bool orderBySystemField = true, string memberTypeAlias = null, string filter = "") { var uow = UowProvider.GetUnitOfWork(); using (var repository = RepositoryFactory.CreateMemberRepository(uow)) { if (memberTypeAlias == null) { - return repository.GetPagedResultsByQuery(null, pageIndex, pageSize, out totalRecords, orderBy, orderDirection, filter); + return repository.GetPagedResultsByQuery(null, pageIndex, pageSize, out totalRecords, orderBy, orderDirection, orderBySystemField, filter); } var query = new Query().Where(x => x.ContentTypeAlias == memberTypeAlias); - return repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, orderBy, orderDirection, filter); + return repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, orderBy, orderDirection, orderBySystemField, filter); } } @@ -1253,52 +1253,52 @@ namespace Umbraco.Core.Services var memType = new MemberType(-1); var propGroup = new PropertyGroup - { - Name = "Membership", - Id = --identity - }; + { + Name = "Membership", + Id = --identity + }; propGroup.PropertyTypes.Add(new PropertyType(Constants.PropertyEditors.TextboxAlias, DataTypeDatabaseType.Ntext, Constants.Conventions.Member.Comments) - { - Name = Constants.Conventions.Member.CommentsLabel, - SortOrder = 0, - Id = --identity, - Key = identity.ToGuid() - }); + { + Name = Constants.Conventions.Member.CommentsLabel, + SortOrder = 0, + Id = --identity, + Key = identity.ToGuid() + }); propGroup.PropertyTypes.Add(new PropertyType(Constants.PropertyEditors.TrueFalseAlias, DataTypeDatabaseType.Integer, Constants.Conventions.Member.IsApproved) - { - Name = Constants.Conventions.Member.IsApprovedLabel, - SortOrder = 3, - Id = --identity, - Key = identity.ToGuid() - }); + { + Name = Constants.Conventions.Member.IsApprovedLabel, + SortOrder = 3, + Id = --identity, + Key = identity.ToGuid() + }); propGroup.PropertyTypes.Add(new PropertyType(Constants.PropertyEditors.TrueFalseAlias, DataTypeDatabaseType.Integer, Constants.Conventions.Member.IsLockedOut) - { - Name = Constants.Conventions.Member.IsLockedOutLabel, - SortOrder = 4, - Id = --identity, - Key = identity.ToGuid() - }); + { + Name = Constants.Conventions.Member.IsLockedOutLabel, + SortOrder = 4, + Id = --identity, + Key = identity.ToGuid() + }); propGroup.PropertyTypes.Add(new PropertyType(Constants.PropertyEditors.NoEditAlias, DataTypeDatabaseType.Date, Constants.Conventions.Member.LastLockoutDate) - { - Name = Constants.Conventions.Member.LastLockoutDateLabel, - SortOrder = 5, - Id = --identity, - Key = identity.ToGuid() - }); + { + Name = Constants.Conventions.Member.LastLockoutDateLabel, + SortOrder = 5, + Id = --identity, + Key = identity.ToGuid() + }); propGroup.PropertyTypes.Add(new PropertyType(Constants.PropertyEditors.NoEditAlias, DataTypeDatabaseType.Date, Constants.Conventions.Member.LastLoginDate) - { - Name = Constants.Conventions.Member.LastLoginDateLabel, - SortOrder = 6, - Id = --identity, - Key = identity.ToGuid() - }); + { + Name = Constants.Conventions.Member.LastLoginDateLabel, + SortOrder = 6, + Id = --identity, + Key = identity.ToGuid() + }); propGroup.PropertyTypes.Add(new PropertyType(Constants.PropertyEditors.NoEditAlias, DataTypeDatabaseType.Date, Constants.Conventions.Member.LastPasswordChangeDate) - { - Name = Constants.Conventions.Member.LastPasswordChangeDateLabel, - SortOrder = 7, - Id = --identity, - Key = identity.ToGuid() - }); + { + Name = Constants.Conventions.Member.LastPasswordChangeDateLabel, + SortOrder = 7, + Id = --identity, + Key = identity.ToGuid() + }); memType.PropertyGroups.Add(propGroup); diff --git a/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs index c8ccecdb95..e256e4082a 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs @@ -72,7 +72,7 @@ namespace Umbraco.Tests.Persistence.Repositories //create 100 non published for (var i = 0; i < 100; i++) - { + { var c1 = MockedContent.CreateSimpleContent(contentType1); repository.AddOrUpdate(c1); allCreated.Add(c1); @@ -176,7 +176,7 @@ namespace Umbraco.Tests.Persistence.Repositories for (var i = 0; i < 30; i++) { //These will be non-published so shouldn't show up - var c1 = MockedContent.CreateSimpleContent(contentType1); + var c1 = MockedContent.CreateSimpleContent(contentType1); repository.AddOrUpdate(c1); allCreated.Add(c1); } @@ -275,7 +275,7 @@ namespace Umbraco.Tests.Persistence.Repositories // Assert Assert.That(contentType.HasIdentity, Is.True); Assert.That(textpage.HasIdentity, Is.True); - + } } @@ -299,7 +299,7 @@ namespace Umbraco.Tests.Persistence.Repositories Content textpage = MockedContent.CreateSimpleContent(contentType); // Act - + contentTypeRepository.AddOrUpdate(contentType); repository.AddOrUpdate(textpage); unitOfWork.Commit(); @@ -520,7 +520,7 @@ namespace Umbraco.Tests.Persistence.Repositories var unitOfWork = provider.GetUnitOfWork(); ContentTypeRepository contentTypeRepository; using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) - { + { var result = repository.GetAll().ToArray(); foreach (var content in result) { @@ -555,7 +555,7 @@ namespace Umbraco.Tests.Persistence.Repositories // Act var query = Query.Builder.Where(x => x.Level == 2); long totalRecords; - var result = repository.GetPagedResultsByQuery(query, 0, 1, out totalRecords, "Name", Direction.Ascending); + var result = repository.GetPagedResultsByQuery(query, 0, 1, out totalRecords, "Name", Direction.Ascending, orderBySystemField: true); // Assert Assert.That(totalRecords, Is.GreaterThanOrEqualTo(2)); @@ -576,7 +576,7 @@ namespace Umbraco.Tests.Persistence.Repositories // Act var query = Query.Builder.Where(x => x.Level == 2); long totalRecords; - var result = repository.GetPagedResultsByQuery(query, 1, 1, out totalRecords, "Name", Direction.Ascending); + var result = repository.GetPagedResultsByQuery(query, 1, 1, out totalRecords, "Name", Direction.Ascending, orderBySystemField: true); // Assert Assert.That(totalRecords, Is.GreaterThanOrEqualTo(2)); @@ -597,7 +597,7 @@ namespace Umbraco.Tests.Persistence.Repositories // Act var query = Query.Builder.Where(x => x.Level == 2); long totalRecords; - var result = repository.GetPagedResultsByQuery(query, 0, 2, out totalRecords, "Name", Direction.Ascending); + var result = repository.GetPagedResultsByQuery(query, 0, 2, out totalRecords, "Name", Direction.Ascending, orderBySystemField: true); // Assert Assert.That(totalRecords, Is.GreaterThanOrEqualTo(2)); @@ -618,7 +618,7 @@ namespace Umbraco.Tests.Persistence.Repositories // Act var query = Query.Builder.Where(x => x.Level == 2); long totalRecords; - var result = repository.GetPagedResultsByQuery(query, 0, 1, out totalRecords, "Name", Direction.Descending); + var result = repository.GetPagedResultsByQuery(query, 0, 1, out totalRecords, "Name", Direction.Descending, orderBySystemField: true); // Assert Assert.That(totalRecords, Is.GreaterThanOrEqualTo(2)); @@ -639,7 +639,7 @@ namespace Umbraco.Tests.Persistence.Repositories // Act var query = Query.Builder.Where(x => x.Level == 2); long totalRecords; - var result = repository.GetPagedResultsByQuery(query, 0, 1, out totalRecords, "Name", Direction.Ascending, "Page 2"); + var result = repository.GetPagedResultsByQuery(query, 0, 1, out totalRecords, "Name", Direction.Ascending, true, "Page 2"); // Assert Assert.That(totalRecords, Is.EqualTo(1)); @@ -660,7 +660,7 @@ namespace Umbraco.Tests.Persistence.Repositories // Act var query = Query.Builder.Where(x => x.Level == 2); long totalRecords; - var result = repository.GetPagedResultsByQuery(query, 0, 1, out totalRecords, "Name", Direction.Ascending, "Page"); + var result = repository.GetPagedResultsByQuery(query, 0, 1, out totalRecords, "Name", Direction.Ascending, true, "Page"); // Assert Assert.That(totalRecords, Is.EqualTo(2)); diff --git a/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs index 60c49f89cd..32258a9961 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs @@ -27,7 +27,7 @@ namespace Umbraco.Tests.Persistence.Repositories [SetUp] public override void Initialize() - { + { base.Initialize(); CreateTestData(); @@ -51,12 +51,12 @@ namespace Umbraco.Tests.Persistence.Repositories { var mediaType = mediaTypeRepository.Get(1032); - + for (var i = 0; i < 100; i++) { var image = MockedMedia.CreateMediaImage(mediaType, -1); repository.AddOrUpdate(image); - } + } unitOfWork.Commit(); //delete all xml @@ -81,7 +81,7 @@ namespace Umbraco.Tests.Persistence.Repositories var imageMediaType = mediaTypeRepository.Get(1032); var fileMediaType = mediaTypeRepository.Get(1033); var folderMediaType = mediaTypeRepository.Get(1031); - + for (var i = 0; i < 30; i++) { var image = MockedMedia.CreateMediaImage(imageMediaType, -1); @@ -103,12 +103,12 @@ namespace Umbraco.Tests.Persistence.Repositories unitOfWork.Database.Execute("DELETE FROM cmsContentXml"); Assert.AreEqual(0, unitOfWork.Database.ExecuteScalar("SELECT COUNT(*) FROM cmsContentXml")); - repository.RebuildXmlStructures(media => new XElement("test"), 10, contentTypeIds: new[] {1032, 1033}); + repository.RebuildXmlStructures(media => new XElement("test"), 10, contentTypeIds: new[] { 1032, 1033 }); Assert.AreEqual(62, unitOfWork.Database.ExecuteScalar("SELECT COUNT(*) FROM cmsContentXml")); } } - + [Test] public void Can_Perform_Add_On_MediaRepository() { @@ -207,7 +207,7 @@ namespace Umbraco.Tests.Persistence.Repositories // Act var media = repository.Get(NodeDto.NodeIdSeed + 1); - bool dirty = ((ICanBeDirty) media).IsDirty(); + bool dirty = ((ICanBeDirty)media).IsDirty(); // Assert Assert.That(dirty, Is.False); @@ -319,7 +319,7 @@ namespace Umbraco.Tests.Persistence.Repositories // Act var query = Query.Builder.Where(x => x.Level == 2); long totalRecords; - var result = repository.GetPagedResultsByQuery(query, 0, 1, out totalRecords, "SortOrder", Direction.Ascending); + var result = repository.GetPagedResultsByQuery(query, 0, 1, out totalRecords, "SortOrder", Direction.Ascending, orderBySystemField: true); // Assert Assert.That(totalRecords, Is.GreaterThanOrEqualTo(2)); @@ -340,7 +340,7 @@ namespace Umbraco.Tests.Persistence.Repositories // Act var query = Query.Builder.Where(x => x.Level == 2); long totalRecords; - var result = repository.GetPagedResultsByQuery(query, 1, 1, out totalRecords, "SortOrder", Direction.Ascending); + var result = repository.GetPagedResultsByQuery(query, 1, 1, out totalRecords, "SortOrder", Direction.Ascending, orderBySystemField: true); // Assert Assert.That(totalRecords, Is.GreaterThanOrEqualTo(2)); @@ -361,7 +361,7 @@ namespace Umbraco.Tests.Persistence.Repositories // Act var query = Query.Builder.Where(x => x.Level == 2); long totalRecords; - var result = repository.GetPagedResultsByQuery(query, 0, 2, out totalRecords, "SortOrder", Direction.Ascending); + var result = repository.GetPagedResultsByQuery(query, 0, 2, out totalRecords, "SortOrder", Direction.Ascending, orderBySystemField: true); // Assert Assert.That(totalRecords, Is.GreaterThanOrEqualTo(2)); @@ -382,7 +382,7 @@ namespace Umbraco.Tests.Persistence.Repositories // Act var query = Query.Builder.Where(x => x.Level == 2); long totalRecords; - var result = repository.GetPagedResultsByQuery(query, 0, 1, out totalRecords, "SortOrder", Direction.Descending); + var result = repository.GetPagedResultsByQuery(query, 0, 1, out totalRecords, "SortOrder", Direction.Descending, orderBySystemField: true); // Assert Assert.That(totalRecords, Is.GreaterThanOrEqualTo(2)); @@ -403,7 +403,7 @@ namespace Umbraco.Tests.Persistence.Repositories // Act var query = Query.Builder.Where(x => x.Level == 2); long totalRecords; - var result = repository.GetPagedResultsByQuery(query, 0, 1, out totalRecords, "Name", Direction.Ascending); + var result = repository.GetPagedResultsByQuery(query, 0, 1, out totalRecords, "Name", Direction.Ascending, orderBySystemField: true); // Assert Assert.That(totalRecords, Is.GreaterThanOrEqualTo(2)); @@ -424,7 +424,7 @@ namespace Umbraco.Tests.Persistence.Repositories // Act var query = Query.Builder.Where(x => x.Level == 2); long totalRecords; - var result = repository.GetPagedResultsByQuery(query, 0, 1, out totalRecords, "SortOrder", Direction.Ascending, "File"); + var result = repository.GetPagedResultsByQuery(query, 0, 1, out totalRecords, "SortOrder", Direction.Ascending, true, "File"); // Assert Assert.That(totalRecords, Is.EqualTo(1)); @@ -445,7 +445,7 @@ namespace Umbraco.Tests.Persistence.Repositories // Act var query = Query.Builder.Where(x => x.Level == 2); long totalRecords; - var result = repository.GetPagedResultsByQuery(query, 0, 1, out totalRecords, "SortOrder", Direction.Ascending, "Test"); + var result = repository.GetPagedResultsByQuery(query, 0, 1, out totalRecords, "SortOrder", Direction.Ascending, true, "Test"); // Assert Assert.That(totalRecords, Is.EqualTo(2)); diff --git a/src/Umbraco.Tests/UmbracoExamine/IndexInitializer.cs b/src/Umbraco.Tests/UmbracoExamine/IndexInitializer.cs index 6c56c76ef7..26f29b581b 100644 --- a/src/Umbraco.Tests/UmbracoExamine/IndexInitializer.cs +++ b/src/Umbraco.Tests/UmbracoExamine/IndexInitializer.cs @@ -20,33 +20,33 @@ using Version = Lucene.Net.Util.Version; namespace Umbraco.Tests.UmbracoExamine { - /// - /// Used internally by test classes to initialize a new index from the template - /// - internal static class IndexInitializer - { - public static UmbracoContentIndexer GetUmbracoIndexer( - Directory luceneDir, - Analyzer analyzer = null, - IDataService dataService = null, - IContentService contentService = null, - IMediaService mediaService = null, - IDataTypeService dataTypeService = null, - IMemberService memberService = null, - IUserService userService = null) - { + /// + /// Used internally by test classes to initialize a new index from the template + /// + internal static class IndexInitializer + { + public static UmbracoContentIndexer GetUmbracoIndexer( + Directory luceneDir, + Analyzer analyzer = null, + IDataService dataService = null, + IContentService contentService = null, + IMediaService mediaService = null, + IDataTypeService dataTypeService = null, + IMemberService memberService = null, + IUserService userService = null) + { if (dataService == null) { dataService = new TestDataService(); } - if (contentService == null) - { + if (contentService == null) + { contentService = Mock.Of(); - } - if (userService == null) - { - userService = Mock.Of(x => x.GetProfileById(It.IsAny()) == Mock.Of(p => p.Id == (object)0 && p.Name == "admin")); - } + } + if (userService == null) + { + userService = Mock.Of(x => x.GetProfileById(It.IsAny()) == Mock.Of(p => p.Id == (object)0 && p.Name == "admin")); + } if (mediaService == null) { long totalRecs; @@ -56,25 +56,25 @@ namespace Umbraco.Tests.UmbracoExamine .Elements() .Select(x => Mock.Of( m => - m.Id == (int) x.Attribute("id") && - m.ParentId == (int) x.Attribute("parentID") && - m.Level == (int) x.Attribute("level") && + m.Id == (int)x.Attribute("id") && + m.ParentId == (int)x.Attribute("parentID") && + m.Level == (int)x.Attribute("level") && m.CreatorId == 0 && - m.SortOrder == (int) x.Attribute("sortOrder") && - m.CreateDate == (DateTime) x.Attribute("createDate") && - m.UpdateDate == (DateTime) x.Attribute("updateDate") && - m.Name == (string) x.Attribute("nodeName") && - m.Path == (string) x.Attribute("path") && + m.SortOrder == (int)x.Attribute("sortOrder") && + m.CreateDate == (DateTime)x.Attribute("createDate") && + m.UpdateDate == (DateTime)x.Attribute("updateDate") && + m.Name == (string)x.Attribute("nodeName") && + m.Path == (string)x.Attribute("path") && m.Properties == new PropertyCollection() && m.ContentType == Mock.Of(mt => - mt.Alias == (string) x.Attribute("nodeTypeAlias") && - mt.Id == (int) x.Attribute("nodeType")))) + mt.Alias == (string)x.Attribute("nodeTypeAlias") && + mt.Id == (int)x.Attribute("nodeType")))) .ToArray(); - + mediaService = Mock.Of( x => x.GetPagedDescendants( - It.IsAny(), It.IsAny(), It.IsAny(), out totalRecs, It.IsAny(), It.IsAny(), It.IsAny()) + It.IsAny(), It.IsAny(), It.IsAny(), out totalRecs, It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()) == allRecs); } @@ -82,7 +82,7 @@ namespace Umbraco.Tests.UmbracoExamine { dataTypeService = Mock.Of(); } - + if (memberService == null) { memberService = Mock.Of(); @@ -93,51 +93,51 @@ namespace Umbraco.Tests.UmbracoExamine analyzer = new StandardAnalyzer(Version.LUCENE_29); } - var indexSet = new IndexSet(); + var indexSet = new IndexSet(); var indexCriteria = indexSet.ToIndexCriteria(dataService, UmbracoContentIndexer.IndexFieldPolicies); - var i = new UmbracoContentIndexer(indexCriteria, - luceneDir, //custom lucene directory - dataService, - contentService, - mediaService, - dataTypeService, - userService, - analyzer, - false); + var i = new UmbracoContentIndexer(indexCriteria, + luceneDir, //custom lucene directory + dataService, + contentService, + mediaService, + dataTypeService, + userService, + analyzer, + false); - //i.IndexSecondsInterval = 1; + //i.IndexSecondsInterval = 1; - i.IndexingError += IndexingError; + i.IndexingError += IndexingError; - return i; - } + return i; + } public static UmbracoExamineSearcher GetUmbracoSearcher(Directory luceneDir, Analyzer analyzer = null) - { + { if (analyzer == null) { analyzer = new StandardAnalyzer(Version.LUCENE_29); } return new UmbracoExamineSearcher(luceneDir, analyzer); - } - - public static LuceneSearcher GetLuceneSearcher(Directory luceneDir) - { - return new LuceneSearcher(luceneDir, new StandardAnalyzer(Version.LUCENE_29)); - } - - public static MultiIndexSearcher GetMultiSearcher(Directory pdfDir, Directory simpleDir, Directory conventionDir, Directory cwsDir) - { - var i = new MultiIndexSearcher(new[] { pdfDir, simpleDir, conventionDir, cwsDir }, new StandardAnalyzer(Version.LUCENE_29)); - return i; - } + } + + public static LuceneSearcher GetLuceneSearcher(Directory luceneDir) + { + return new LuceneSearcher(luceneDir, new StandardAnalyzer(Version.LUCENE_29)); + } + + public static MultiIndexSearcher GetMultiSearcher(Directory pdfDir, Directory simpleDir, Directory conventionDir, Directory cwsDir) + { + var i = new MultiIndexSearcher(new[] { pdfDir, simpleDir, conventionDir, cwsDir }, new StandardAnalyzer(Version.LUCENE_29)); + return i; + } - internal static void IndexingError(object sender, IndexingErrorEventArgs e) - { - throw new ApplicationException(e.Message, e.InnerException); - } + internal static void IndexingError(object sender, IndexingErrorEventArgs e) + { + throw new ApplicationException(e.Message, e.InnerException); + } - } + } } \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbtable.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbtable.directive.js index fe80d915da..bf593397b6 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbtable.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbtable.directive.js @@ -1,71 +1,71 @@ -(function() { - 'use strict'; - - function TableDirective() { - - function link(scope, el, attr, ctrl) { - - scope.clickItem = function(item, $event) { - if(scope.onClick) { - scope.onClick(item); - $event.stopPropagation(); - } - }; - - scope.selectItem = function(item, $index, $event) { - if(scope.onSelect) { - scope.onSelect(item, $index, $event); - $event.stopPropagation(); - } - }; - - scope.selectAll = function($event) { - if(scope.onSelectAll) { - scope.onSelectAll($event); - } - }; - - scope.isSelectedAll = function() { - if(scope.onSelectedAll && scope.items && scope.items.length > 0) { - return scope.onSelectedAll(); - } - }; - - scope.isSortDirection = function (col, direction) { - if (scope.onSortingDirection) { - return scope.onSortingDirection(col, direction); - } - }; - - scope.sort = function(field, allow) { - if(scope.onSort) { - scope.onSort(field, allow); - } - }; - - } - - var directive = { - restrict: 'E', - replace: true, - templateUrl: 'views/components/umb-table.html', - scope: { - items: '=', - itemProperties: '=', - allowSelectAll: '=', - onSelect: '=', - onClick: '=', - onSelectAll: '=', - onSelectedAll: '=', - onSortingDirection: '=', - onSort: '=' - }, - link: link - }; - - return directive; - } - - angular.module('umbraco.directives').directive('umbTable', TableDirective); - -})(); +(function () { + 'use strict'; + + function TableDirective() { + + function link(scope, el, attr, ctrl) { + + scope.clickItem = function (item, $event) { + if (scope.onClick) { + scope.onClick(item); + $event.stopPropagation(); + } + }; + + scope.selectItem = function (item, $index, $event) { + if (scope.onSelect) { + scope.onSelect(item, $index, $event); + $event.stopPropagation(); + } + }; + + scope.selectAll = function ($event) { + if (scope.onSelectAll) { + scope.onSelectAll($event); + } + }; + + scope.isSelectedAll = function () { + if (scope.onSelectedAll && scope.items && scope.items.length > 0) { + return scope.onSelectedAll(); + } + }; + + scope.isSortDirection = function (col, direction) { + if (scope.onSortingDirection) { + return scope.onSortingDirection(col, direction); + } + }; + + scope.sort = function (field, allow, isSystem) { + if (scope.onSort) { + scope.onSort(field, allow, isSystem); + } + }; + + } + + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/umb-table.html', + scope: { + items: '=', + itemProperties: '=', + allowSelectAll: '=', + onSelect: '=', + onClick: '=', + onSelectAll: '=', + onSelectedAll: '=', + onSortingDirection: '=', + onSort: '=' + }, + link: link + }; + + return directive; + } + + angular.module('umbraco.directives').directive('umbTable', TableDirective); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js index 2cf7127707..60ecd16f19 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js @@ -25,634 +25,636 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { - /** internal method process the saving of data and post processing the result */ - function saveContentItem(content, action, files) { - return umbRequestHelper.postSaveContent({ - restApiUrl: umbRequestHelper.getApiUrl( + /** internal method process the saving of data and post processing the result */ + function saveContentItem(content, action, files) { + return umbRequestHelper.postSaveContent({ + restApiUrl: umbRequestHelper.getApiUrl( "contentApiBaseUrl", "PostSave"), - content: content, - action: action, - files: files, - dataFormatter: function (c, a) { - return umbDataFormatter.formatContentPostData(c, a); - } - }); - } + content: content, + action: action, + files: files, + dataFormatter: function (c, a) { + return umbDataFormatter.formatContentPostData(c, a); + } + }); + } - return { - - getRecycleBin: function() { - return umbRequestHelper.resourcePromise( + return { + + getRecycleBin: function () { + return umbRequestHelper.resourcePromise( $http.get( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "GetRecycleBin")), + umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "GetRecycleBin")), 'Failed to retrieve data for content recycle bin'); - }, + }, - /** - * @ngdoc method - * @name umbraco.resources.contentResource#sort - * @methodOf umbraco.resources.contentResource - * - * @description - * Sorts all children below a given parent node id, based on a collection of node-ids - * - * ##usage - *
-         * var ids = [123,34533,2334,23434];
-         * contentResource.sort({ parentId: 1244, sortedIds: ids })
-         *    .then(function() {
-         *        $scope.complete = true;
-         *    });
-         * 
- * @param {Object} args arguments object - * @param {Int} args.parentId the ID of the parent node - * @param {Array} options.sortedIds array of node IDs as they should be sorted - * @returns {Promise} resourcePromise object. - * - */ - sort: function (args) { - if (!args) { - throw "args cannot be null"; - } - if (!args.parentId) { - throw "args.parentId cannot be null"; - } - if (!args.sortedIds) { - throw "args.sortedIds cannot be null"; - } + /** + * @ngdoc method + * @name umbraco.resources.contentResource#sort + * @methodOf umbraco.resources.contentResource + * + * @description + * Sorts all children below a given parent node id, based on a collection of node-ids + * + * ##usage + *
+        * var ids = [123,34533,2334,23434];
+        * contentResource.sort({ parentId: 1244, sortedIds: ids })
+        *    .then(function() {
+        *        $scope.complete = true;
+        *    });
+        * 
+ * @param {Object} args arguments object + * @param {Int} args.parentId the ID of the parent node + * @param {Array} options.sortedIds array of node IDs as they should be sorted + * @returns {Promise} resourcePromise object. + * + */ + sort: function (args) { + if (!args) { + throw "args cannot be null"; + } + if (!args.parentId) { + throw "args.parentId cannot be null"; + } + if (!args.sortedIds) { + throw "args.sortedIds cannot be null"; + } - return umbRequestHelper.resourcePromise( + return umbRequestHelper.resourcePromise( $http.post(umbRequestHelper.getApiUrl("contentApiBaseUrl", "PostSort"), - { - parentId: args.parentId, - idSortOrder: args.sortedIds - }), + { + parentId: args.parentId, + idSortOrder: args.sortedIds + }), 'Failed to sort content'); - }, + }, - /** - * @ngdoc method - * @name umbraco.resources.contentResource#move - * @methodOf umbraco.resources.contentResource - * - * @description - * Moves a node underneath a new parentId - * - * ##usage - *
-         * contentResource.move({ parentId: 1244, id: 123 })
-         *    .then(function() {
-         *        alert("node was moved");
-         *    }, function(err){
-         *      alert("node didnt move:" + err.data.Message); 
-         *    });
-         * 
- * @param {Object} args arguments object - * @param {Int} args.idd the ID of the node to move - * @param {Int} args.parentId the ID of the parent node to move to - * @returns {Promise} resourcePromise object. - * - */ - move: function (args) { - if (!args) { - throw "args cannot be null"; - } - if (!args.parentId) { - throw "args.parentId cannot be null"; - } - if (!args.id) { - throw "args.id cannot be null"; - } + /** + * @ngdoc method + * @name umbraco.resources.contentResource#move + * @methodOf umbraco.resources.contentResource + * + * @description + * Moves a node underneath a new parentId + * + * ##usage + *
+        * contentResource.move({ parentId: 1244, id: 123 })
+        *    .then(function() {
+        *        alert("node was moved");
+        *    }, function(err){
+        *      alert("node didnt move:" + err.data.Message); 
+        *    });
+        * 
+ * @param {Object} args arguments object + * @param {Int} args.idd the ID of the node to move + * @param {Int} args.parentId the ID of the parent node to move to + * @returns {Promise} resourcePromise object. + * + */ + move: function (args) { + if (!args) { + throw "args cannot be null"; + } + if (!args.parentId) { + throw "args.parentId cannot be null"; + } + if (!args.id) { + throw "args.id cannot be null"; + } - return umbRequestHelper.resourcePromise( + return umbRequestHelper.resourcePromise( $http.post(umbRequestHelper.getApiUrl("contentApiBaseUrl", "PostMove"), - { - parentId: args.parentId, - id: args.id - }), + { + parentId: args.parentId, + id: args.id + }), 'Failed to move content'); - }, + }, - /** - * @ngdoc method - * @name umbraco.resources.contentResource#copy - * @methodOf umbraco.resources.contentResource - * - * @description - * Copies a node underneath a new parentId - * - * ##usage - *
-         * contentResource.copy({ parentId: 1244, id: 123 })
-         *    .then(function() {
-         *        alert("node was copied");
-         *    }, function(err){
-         *      alert("node wasnt copy:" + err.data.Message); 
-         *    });
-         * 
- * @param {Object} args arguments object - * @param {Int} args.id the ID of the node to copy - * @param {Int} args.parentId the ID of the parent node to copy to - * @param {Boolean} args.relateToOriginal if true, relates the copy to the original through the relation api - * @returns {Promise} resourcePromise object. - * - */ - copy: function (args) { - if (!args) { - throw "args cannot be null"; - } - if (!args.parentId) { - throw "args.parentId cannot be null"; - } - if (!args.id) { - throw "args.id cannot be null"; - } + /** + * @ngdoc method + * @name umbraco.resources.contentResource#copy + * @methodOf umbraco.resources.contentResource + * + * @description + * Copies a node underneath a new parentId + * + * ##usage + *
+        * contentResource.copy({ parentId: 1244, id: 123 })
+        *    .then(function() {
+        *        alert("node was copied");
+        *    }, function(err){
+        *      alert("node wasnt copy:" + err.data.Message); 
+        *    });
+        * 
+ * @param {Object} args arguments object + * @param {Int} args.id the ID of the node to copy + * @param {Int} args.parentId the ID of the parent node to copy to + * @param {Boolean} args.relateToOriginal if true, relates the copy to the original through the relation api + * @returns {Promise} resourcePromise object. + * + */ + copy: function (args) { + if (!args) { + throw "args cannot be null"; + } + if (!args.parentId) { + throw "args.parentId cannot be null"; + } + if (!args.id) { + throw "args.id cannot be null"; + } - return umbRequestHelper.resourcePromise( + return umbRequestHelper.resourcePromise( $http.post(umbRequestHelper.getApiUrl("contentApiBaseUrl", "PostCopy"), - args), + args), 'Failed to copy content'); - }, + }, - /** - * @ngdoc method - * @name umbraco.resources.contentResource#unPublish - * @methodOf umbraco.resources.contentResource - * - * @description - * Unpublishes a content item with a given Id - * - * ##usage - *
-         * contentResource.unPublish(1234)
-         *    .then(function() {
-         *        alert("node was unpulished");
-         *    }, function(err){
-         *      alert("node wasnt unpublished:" + err.data.Message); 
-         *    });
-         * 
- * @param {Int} id the ID of the node to unpublish - * @returns {Promise} resourcePromise object. - * - */ - unPublish: function (id) { - if (!id) { - throw "id cannot be null"; - } - - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "PostUnPublish", - [{ id: id }])), - 'Failed to publish content with id ' + id); - }, - /** - * @ngdoc method - * @name umbraco.resources.contentResource#emptyRecycleBin - * @methodOf umbraco.resources.contentResource - * - * @description - * Empties the content recycle bin - * - * ##usage - *
-         * contentResource.emptyRecycleBin()
-         *    .then(function() {
-         *        alert('its empty!');
-         *    });
-         * 
- * - * @returns {Promise} resourcePromise object. - * - */ - emptyRecycleBin: function() { - return umbRequestHelper.resourcePromise( + /** + * @ngdoc method + * @name umbraco.resources.contentResource#unPublish + * @methodOf umbraco.resources.contentResource + * + * @description + * Unpublishes a content item with a given Id + * + * ##usage + *
+        * contentResource.unPublish(1234)
+        *    .then(function() {
+        *        alert("node was unpulished");
+        *    }, function(err){
+        *      alert("node wasnt unpublished:" + err.data.Message); 
+        *    });
+        * 
+ * @param {Int} id the ID of the node to unpublish + * @returns {Promise} resourcePromise object. + * + */ + unPublish: function (id) { + if (!id) { + throw "id cannot be null"; + } + + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "PostUnPublish", + [{ id: id }])), + 'Failed to publish content with id ' + id); + }, + /** + * @ngdoc method + * @name umbraco.resources.contentResource#emptyRecycleBin + * @methodOf umbraco.resources.contentResource + * + * @description + * Empties the content recycle bin + * + * ##usage + *
+        * contentResource.emptyRecycleBin()
+        *    .then(function() {
+        *        alert('its empty!');
+        *    });
+        * 
+ * + * @returns {Promise} resourcePromise object. + * + */ + emptyRecycleBin: function () { + return umbRequestHelper.resourcePromise( $http.post( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "EmptyRecycleBin")), + umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "EmptyRecycleBin")), 'Failed to empty the recycle bin'); - }, + }, - /** - * @ngdoc method - * @name umbraco.resources.contentResource#deleteById - * @methodOf umbraco.resources.contentResource - * - * @description - * Deletes a content item with a given id - * - * ##usage - *
-         * contentResource.deleteById(1234)
-         *    .then(function() {
-         *        alert('its gone!');
-         *    });
-         * 
- * - * @param {Int} id id of content item to delete - * @returns {Promise} resourcePromise object. - * - */ - deleteById: function(id) { - return umbRequestHelper.resourcePromise( + /** + * @ngdoc method + * @name umbraco.resources.contentResource#deleteById + * @methodOf umbraco.resources.contentResource + * + * @description + * Deletes a content item with a given id + * + * ##usage + *
+        * contentResource.deleteById(1234)
+        *    .then(function() {
+        *        alert('its gone!');
+        *    });
+        * 
+ * + * @param {Int} id id of content item to delete + * @returns {Promise} resourcePromise object. + * + */ + deleteById: function (id) { + return umbRequestHelper.resourcePromise( $http.post( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "DeleteById", - [{ id: id }])), + umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "DeleteById", + [{ id: id }])), 'Failed to delete item ' + id); - }, + }, - /** - * @ngdoc method - * @name umbraco.resources.contentResource#getById - * @methodOf umbraco.resources.contentResource - * - * @description - * Gets a content item with a given id - * - * ##usage - *
-         * contentResource.getById(1234)
-         *    .then(function(content) {
-         *        var myDoc = content; 
-         *        alert('its here!');
-         *    });
-         * 
- * - * @param {Int} id id of content item to return - * @returns {Promise} resourcePromise object containing the content item. - * - */ - getById: function (id) { - return umbRequestHelper.resourcePromise( + /** + * @ngdoc method + * @name umbraco.resources.contentResource#getById + * @methodOf umbraco.resources.contentResource + * + * @description + * Gets a content item with a given id + * + * ##usage + *
+        * contentResource.getById(1234)
+        *    .then(function(content) {
+        *        var myDoc = content; 
+        *        alert('its here!');
+        *    });
+        * 
+ * + * @param {Int} id id of content item to return + * @returns {Promise} resourcePromise object containing the content item. + * + */ + getById: function (id) { + return umbRequestHelper.resourcePromise( $http.get( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "GetById", - [{ id: id }])), + umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "GetById", + [{ id: id }])), 'Failed to retrieve data for content id ' + id); - }, - - /** - * @ngdoc method - * @name umbraco.resources.contentResource#getByIds - * @methodOf umbraco.resources.contentResource - * - * @description - * Gets an array of content items, given a collection of ids - * - * ##usage - *
-         * contentResource.getByIds( [1234,2526,28262])
-         *    .then(function(contentArray) {
-         *        var myDoc = contentArray; 
-         *        alert('they are here!');
-         *    });
-         * 
- * - * @param {Array} ids ids of content items to return as an array - * @returns {Promise} resourcePromise object containing the content items array. - * - */ - getByIds: function (ids) { - - var idQuery = ""; - _.each(ids, function(item) { - idQuery += "ids=" + item + "&"; - }); + }, - return umbRequestHelper.resourcePromise( + /** + * @ngdoc method + * @name umbraco.resources.contentResource#getByIds + * @methodOf umbraco.resources.contentResource + * + * @description + * Gets an array of content items, given a collection of ids + * + * ##usage + *
+        * contentResource.getByIds( [1234,2526,28262])
+        *    .then(function(contentArray) {
+        *        var myDoc = contentArray; 
+        *        alert('they are here!');
+        *    });
+        * 
+ * + * @param {Array} ids ids of content items to return as an array + * @returns {Promise} resourcePromise object containing the content items array. + * + */ + getByIds: function (ids) { + + var idQuery = ""; + _.each(ids, function (item) { + idQuery += "ids=" + item + "&"; + }); + + return umbRequestHelper.resourcePromise( $http.get( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "GetByIds", - idQuery)), + umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "GetByIds", + idQuery)), 'Failed to retrieve data for content with multiple ids'); - }, + }, - - /** - * @ngdoc method - * @name umbraco.resources.contentResource#getScaffold - * @methodOf umbraco.resources.contentResource - * - * @description - * Returns a scaffold of an empty content item, given the id of the content item to place it underneath and the content type alias. - * - * - Parent Id must be provided so umbraco knows where to store the content - * - Content Type alias must be provided so umbraco knows which properties to put on the content scaffold - * - * The scaffold is used to build editors for content that has not yet been populated with data. - * - * ##usage - *
-         * contentResource.getScaffold(1234, 'homepage')
-         *    .then(function(scaffold) {
-         *        var myDoc = scaffold;
-         *        myDoc.name = "My new document"; 
-         *
-         *        contentResource.publish(myDoc, true)
-         *            .then(function(content){
-         *                alert("Retrieved, updated and published again");
-         *            });
-         *    });
-         * 
- * - * @param {Int} parentId id of content item to return - * @param {String} alias contenttype alias to base the scaffold on - * @returns {Promise} resourcePromise object containing the content scaffold. - * - */ - getScaffold: function (parentId, alias) { - - return umbRequestHelper.resourcePromise( + + /** + * @ngdoc method + * @name umbraco.resources.contentResource#getScaffold + * @methodOf umbraco.resources.contentResource + * + * @description + * Returns a scaffold of an empty content item, given the id of the content item to place it underneath and the content type alias. + * + * - Parent Id must be provided so umbraco knows where to store the content + * - Content Type alias must be provided so umbraco knows which properties to put on the content scaffold + * + * The scaffold is used to build editors for content that has not yet been populated with data. + * + * ##usage + *
+        * contentResource.getScaffold(1234, 'homepage')
+        *    .then(function(scaffold) {
+        *        var myDoc = scaffold;
+        *        myDoc.name = "My new document"; 
+        *
+        *        contentResource.publish(myDoc, true)
+        *            .then(function(content){
+        *                alert("Retrieved, updated and published again");
+        *            });
+        *    });
+        * 
+ * + * @param {Int} parentId id of content item to return + * @param {String} alias contenttype alias to base the scaffold on + * @returns {Promise} resourcePromise object containing the content scaffold. + * + */ + getScaffold: function (parentId, alias) { + + return umbRequestHelper.resourcePromise( $http.get( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "GetEmpty", - [{ contentTypeAlias: alias }, { parentId: parentId }])), + umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "GetEmpty", + [{ contentTypeAlias: alias }, { parentId: parentId }])), 'Failed to retrieve data for empty content item type ' + alias); - }, + }, - /** - * @ngdoc method - * @name umbraco.resources.contentResource#getNiceUrl - * @methodOf umbraco.resources.contentResource - * - * @description - * Returns a url, given a node ID - * - * ##usage - *
-         * contentResource.getNiceUrl(id)
-         *    .then(function(url) {
-         *        alert('its here!');
-         *    });
-         * 
- * - * @param {Int} id Id of node to return the public url to - * @returns {Promise} resourcePromise object containing the url. - * - */ - getNiceUrl: function (id) { - return umbRequestHelper.resourcePromise( + /** + * @ngdoc method + * @name umbraco.resources.contentResource#getNiceUrl + * @methodOf umbraco.resources.contentResource + * + * @description + * Returns a url, given a node ID + * + * ##usage + *
+        * contentResource.getNiceUrl(id)
+        *    .then(function(url) {
+        *        alert('its here!');
+        *    });
+        * 
+ * + * @param {Int} id Id of node to return the public url to + * @returns {Promise} resourcePromise object containing the url. + * + */ + getNiceUrl: function (id) { + return umbRequestHelper.resourcePromise( $http.get( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "GetNiceUrl",[{id: id}])), + umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "GetNiceUrl", [{ id: id }])), 'Failed to retrieve url for id:' + id); - }, + }, - /** - * @ngdoc method - * @name umbraco.resources.contentResource#getChildren - * @methodOf umbraco.resources.contentResource - * - * @description - * Gets children of a content item with a given id - * - * ##usage - *
-         * contentResource.getChildren(1234, {pageSize: 10, pageNumber: 2})
-         *    .then(function(contentArray) {
-         *        var children = contentArray; 
-         *        alert('they are here!');
-         *    });
-         * 
- * - * @param {Int} parentid id of content item to return children of - * @param {Object} options optional options object - * @param {Int} options.pageSize if paging data, number of nodes per page, default = 0 - * @param {Int} options.pageNumber if paging data, current page index, default = 0 - * @param {String} options.filter if provided, query will only return those with names matching the filter - * @param {String} options.orderDirection can be `Ascending` or `Descending` - Default: `Ascending` - * @param {String} options.orderBy property to order items by, default: `SortOrder` - * @returns {Promise} resourcePromise object containing an array of content items. - * - */ - getChildren: function (parentId, options) { + /** + * @ngdoc method + * @name umbraco.resources.contentResource#getChildren + * @methodOf umbraco.resources.contentResource + * + * @description + * Gets children of a content item with a given id + * + * ##usage + *
+        * contentResource.getChildren(1234, {pageSize: 10, pageNumber: 2})
+        *    .then(function(contentArray) {
+        *        var children = contentArray; 
+        *        alert('they are here!');
+        *    });
+        * 
+ * + * @param {Int} parentid id of content item to return children of + * @param {Object} options optional options object + * @param {Int} options.pageSize if paging data, number of nodes per page, default = 0 + * @param {Int} options.pageNumber if paging data, current page index, default = 0 + * @param {String} options.filter if provided, query will only return those with names matching the filter + * @param {String} options.orderDirection can be `Ascending` or `Descending` - Default: `Ascending` + * @param {String} options.orderBy property to order items by, default: `SortOrder` + * @returns {Promise} resourcePromise object containing an array of content items. + * + */ + getChildren: function (parentId, options) { - var defaults = { - pageSize: 0, - pageNumber: 0, - filter: '', - orderDirection: "Ascending", - orderBy: "SortOrder" - }; - if (options === undefined) { - options = {}; - } - //overwrite the defaults if there are any specified - angular.extend(defaults, options); - //now copy back to the options we will use - options = defaults; - //change asc/desct - if (options.orderDirection === "asc") { - options.orderDirection = "Ascending"; - } - else if (options.orderDirection === "desc") { - options.orderDirection = "Descending"; - } + var defaults = { + pageSize: 0, + pageNumber: 0, + filter: '', + orderDirection: "Ascending", + orderBy: "SortOrder", + orderBySystemField: true + }; + if (options === undefined) { + options = {}; + } + //overwrite the defaults if there are any specified + angular.extend(defaults, options); + //now copy back to the options we will use + options = defaults; + //change asc/desct + if (options.orderDirection === "asc") { + options.orderDirection = "Ascending"; + } + else if (options.orderDirection === "desc") { + options.orderDirection = "Descending"; + } - return umbRequestHelper.resourcePromise( + return umbRequestHelper.resourcePromise( $http.get( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "GetChildren", - [ - { id: parentId }, - { pageNumber: options.pageNumber }, - { pageSize: options.pageSize }, - { orderBy: options.orderBy }, - { orderDirection: options.orderDirection }, - { filter: options.filter } - ])), + umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "GetChildren", + [ + { id: parentId }, + { pageNumber: options.pageNumber }, + { pageSize: options.pageSize }, + { orderBy: options.orderBy }, + { orderDirection: options.orderDirection }, + { orderBySystemField: options.orderBySystemField }, + { filter: options.filter } + ])), 'Failed to retrieve children for content item ' + parentId); - }, + }, - /** - * @ngdoc method - * @name umbraco.resources.contentResource#hasPermission - * @methodOf umbraco.resources.contentResource - * - * @description - * Returns true/false given a permission char to check against a nodeID - * for the current user - * - * ##usage - *
-         * contentResource.hasPermission('p',1234)
-         *    .then(function() {
-         *        alert('You are allowed to publish this item');
-         *    });
-         * 
- * - * @param {String} permission char representing the permission to check - * @param {Int} id id of content item to delete - * @returns {Promise} resourcePromise object. - * - */ - checkPermission: function(permission, id) { - return umbRequestHelper.resourcePromise( + /** + * @ngdoc method + * @name umbraco.resources.contentResource#hasPermission + * @methodOf umbraco.resources.contentResource + * + * @description + * Returns true/false given a permission char to check against a nodeID + * for the current user + * + * ##usage + *
+        * contentResource.hasPermission('p',1234)
+        *    .then(function() {
+        *        alert('You are allowed to publish this item');
+        *    });
+        * 
+ * + * @param {String} permission char representing the permission to check + * @param {Int} id id of content item to delete + * @returns {Promise} resourcePromise object. + * + */ + checkPermission: function (permission, id) { + return umbRequestHelper.resourcePromise( $http.get( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "HasPermission", - [{ permissionToCheck: permission },{ nodeId: id }])), + umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "HasPermission", + [{ permissionToCheck: permission }, { nodeId: id }])), 'Failed to check permission for item ' + id); - }, + }, - getPermissions: function (nodeIds) { - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "GetPermissions"), - nodeIds), - 'Failed to get permissions'); - }, + getPermissions: function (nodeIds) { + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "GetPermissions"), + nodeIds), + 'Failed to get permissions'); + }, - /** - * @ngdoc method - * @name umbraco.resources.contentResource#save - * @methodOf umbraco.resources.contentResource - * - * @description - * Saves changes made to a content item to its current version, if the content item is new, the isNew paramater must be passed to force creation - * if the content item needs to have files attached, they must be provided as the files param and passed separately - * - * - * ##usage - *
-         * contentResource.getById(1234)
-         *    .then(function(content) {
-         *          content.name = "I want a new name!";
-         *          contentResource.save(content, false)
-         *            .then(function(content){
-         *                alert("Retrieved, updated and saved again");
-         *            });
-         *    });
-         * 
- * - * @param {Object} content The content item object with changes applied - * @param {Bool} isNew set to true to create a new item or to update an existing - * @param {Array} files collection of files for the document - * @returns {Promise} resourcePromise object containing the saved content item. - * - */ - save: function (content, isNew, files) { - return saveContentItem(content, "save" + (isNew ? "New" : ""), files); - }, + /** + * @ngdoc method + * @name umbraco.resources.contentResource#save + * @methodOf umbraco.resources.contentResource + * + * @description + * Saves changes made to a content item to its current version, if the content item is new, the isNew paramater must be passed to force creation + * if the content item needs to have files attached, they must be provided as the files param and passed separately + * + * + * ##usage + *
+        * contentResource.getById(1234)
+        *    .then(function(content) {
+        *          content.name = "I want a new name!";
+        *          contentResource.save(content, false)
+        *            .then(function(content){
+        *                alert("Retrieved, updated and saved again");
+        *            });
+        *    });
+        * 
+ * + * @param {Object} content The content item object with changes applied + * @param {Bool} isNew set to true to create a new item or to update an existing + * @param {Array} files collection of files for the document + * @returns {Promise} resourcePromise object containing the saved content item. + * + */ + save: function (content, isNew, files) { + return saveContentItem(content, "save" + (isNew ? "New" : ""), files); + }, - /** - * @ngdoc method - * @name umbraco.resources.contentResource#publish - * @methodOf umbraco.resources.contentResource - * - * @description - * Saves and publishes changes made to a content item to a new version, if the content item is new, the isNew paramater must be passed to force creation - * if the content item needs to have files attached, they must be provided as the files param and passed separately - * - * - * ##usage - *
-         * contentResource.getById(1234)
-         *    .then(function(content) {
-         *          content.name = "I want a new name, and be published!";
-         *          contentResource.publish(content, false)
-         *            .then(function(content){
-         *                alert("Retrieved, updated and published again");
-         *            });
-         *    });
-         * 
- * - * @param {Object} content The content item object with changes applied - * @param {Bool} isNew set to true to create a new item or to update an existing - * @param {Array} files collection of files for the document - * @returns {Promise} resourcePromise object containing the saved content item. - * - */ - publish: function (content, isNew, files) { - return saveContentItem(content, "publish" + (isNew ? "New" : ""), files); - }, - - - /** - * @ngdoc method - * @name umbraco.resources.contentResource#sendToPublish - * @methodOf umbraco.resources.contentResource - * - * @description - * Saves changes made to a content item, and notifies any subscribers about a pending publication - * - * ##usage - *
-         * contentResource.getById(1234)
-         *    .then(function(content) {
-         *          content.name = "I want a new name, and be published!";
-         *          contentResource.sendToPublish(content, false)
-         *            .then(function(content){
-         *                alert("Retrieved, updated and notication send off");
-         *            });
-         *    });
-         * 
- * - * @param {Object} content The content item object with changes applied - * @param {Bool} isNew set to true to create a new item or to update an existing - * @param {Array} files collection of files for the document - * @returns {Promise} resourcePromise object containing the saved content item. - * - */ - sendToPublish: function (content, isNew, files) { - return saveContentItem(content, "sendPublish" + (isNew ? "New" : ""), files); - }, - - /** - * @ngdoc method - * @name umbraco.resources.contentResource#publishByid - * @methodOf umbraco.resources.contentResource - * - * @description - * Publishes a content item with a given ID - * - * ##usage - *
-         * contentResource.publishById(1234)
-         *    .then(function(content) {
-         *        alert("published");
-         *    });
-         * 
- * - * @param {Int} id The ID of the conten to publish - * @returns {Promise} resourcePromise object containing the published content item. - * - */ - publishById: function(id){ - - if (!id) { - throw "id cannot be null"; - } - - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "PostPublishById", - [{ id: id }])), - 'Failed to publish content with id ' + id); - - } + /** + * @ngdoc method + * @name umbraco.resources.contentResource#publish + * @methodOf umbraco.resources.contentResource + * + * @description + * Saves and publishes changes made to a content item to a new version, if the content item is new, the isNew paramater must be passed to force creation + * if the content item needs to have files attached, they must be provided as the files param and passed separately + * + * + * ##usage + *
+        * contentResource.getById(1234)
+        *    .then(function(content) {
+        *          content.name = "I want a new name, and be published!";
+        *          contentResource.publish(content, false)
+        *            .then(function(content){
+        *                alert("Retrieved, updated and published again");
+        *            });
+        *    });
+        * 
+ * + * @param {Object} content The content item object with changes applied + * @param {Bool} isNew set to true to create a new item or to update an existing + * @param {Array} files collection of files for the document + * @returns {Promise} resourcePromise object containing the saved content item. + * + */ + publish: function (content, isNew, files) { + return saveContentItem(content, "publish" + (isNew ? "New" : ""), files); + }, - }; + /** + * @ngdoc method + * @name umbraco.resources.contentResource#sendToPublish + * @methodOf umbraco.resources.contentResource + * + * @description + * Saves changes made to a content item, and notifies any subscribers about a pending publication + * + * ##usage + *
+        * contentResource.getById(1234)
+        *    .then(function(content) {
+        *          content.name = "I want a new name, and be published!";
+        *          contentResource.sendToPublish(content, false)
+        *            .then(function(content){
+        *                alert("Retrieved, updated and notication send off");
+        *            });
+        *    });
+        * 
+ * + * @param {Object} content The content item object with changes applied + * @param {Bool} isNew set to true to create a new item or to update an existing + * @param {Array} files collection of files for the document + * @returns {Promise} resourcePromise object containing the saved content item. + * + */ + sendToPublish: function (content, isNew, files) { + return saveContentItem(content, "sendPublish" + (isNew ? "New" : ""), files); + }, + + /** + * @ngdoc method + * @name umbraco.resources.contentResource#publishByid + * @methodOf umbraco.resources.contentResource + * + * @description + * Publishes a content item with a given ID + * + * ##usage + *
+        * contentResource.publishById(1234)
+        *    .then(function(content) {
+        *        alert("published");
+        *    });
+        * 
+ * + * @param {Int} id The ID of the conten to publish + * @returns {Promise} resourcePromise object containing the published content item. + * + */ + publishById: function (id) { + + if (!id) { + throw "id cannot be null"; + } + + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "PostPublishById", + [{ id: id }])), + 'Failed to publish content with id ' + id); + + } + + + }; } angular.module('umbraco.resources').factory('contentResource', contentResource); diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/media.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/media.resource.js index 9a2310299f..a85b2cc104 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/media.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/media.resource.js @@ -4,470 +4,472 @@ * @description Loads in data for media **/ function mediaResource($q, $http, umbDataFormatter, umbRequestHelper) { - - /** internal method process the saving of data and post processing the result */ - function saveMediaItem(content, action, files) { - return umbRequestHelper.postSaveContent({ - restApiUrl: umbRequestHelper.getApiUrl( + + /** internal method process the saving of data and post processing the result */ + function saveMediaItem(content, action, files) { + return umbRequestHelper.postSaveContent({ + restApiUrl: umbRequestHelper.getApiUrl( "mediaApiBaseUrl", "PostSave"), - content: content, - action: action, - files: files, - dataFormatter: function (c, a) { - return umbDataFormatter.formatMediaPostData(c, a); - } - }); - } + content: content, + action: action, + files: files, + dataFormatter: function (c, a) { + return umbDataFormatter.formatMediaPostData(c, a); + } + }); + } - return { - - getRecycleBin: function () { - return umbRequestHelper.resourcePromise( + return { + + getRecycleBin: function () { + return umbRequestHelper.resourcePromise( $http.get( - umbRequestHelper.getApiUrl( - "mediaApiBaseUrl", - "GetRecycleBin")), + umbRequestHelper.getApiUrl( + "mediaApiBaseUrl", + "GetRecycleBin")), 'Failed to retrieve data for media recycle bin'); - }, + }, - /** - * @ngdoc method - * @name umbraco.resources.mediaResource#sort - * @methodOf umbraco.resources.mediaResource - * - * @description - * Sorts all children below a given parent node id, based on a collection of node-ids - * - * ##usage - *
-         * var ids = [123,34533,2334,23434];
-         * mediaResource.sort({ sortedIds: ids })
-         *    .then(function() {
-         *        $scope.complete = true;
-         *    });
-         * 
- * @param {Object} args arguments object - * @param {Int} args.parentId the ID of the parent node - * @param {Array} options.sortedIds array of node IDs as they should be sorted - * @returns {Promise} resourcePromise object. - * - */ - sort: function (args) { - if (!args) { - throw "args cannot be null"; - } - if (!args.parentId) { - throw "args.parentId cannot be null"; - } - if (!args.sortedIds) { - throw "args.sortedIds cannot be null"; - } + /** + * @ngdoc method + * @name umbraco.resources.mediaResource#sort + * @methodOf umbraco.resources.mediaResource + * + * @description + * Sorts all children below a given parent node id, based on a collection of node-ids + * + * ##usage + *
+        * var ids = [123,34533,2334,23434];
+        * mediaResource.sort({ sortedIds: ids })
+        *    .then(function() {
+        *        $scope.complete = true;
+        *    });
+        * 
+ * @param {Object} args arguments object + * @param {Int} args.parentId the ID of the parent node + * @param {Array} options.sortedIds array of node IDs as they should be sorted + * @returns {Promise} resourcePromise object. + * + */ + sort: function (args) { + if (!args) { + throw "args cannot be null"; + } + if (!args.parentId) { + throw "args.parentId cannot be null"; + } + if (!args.sortedIds) { + throw "args.sortedIds cannot be null"; + } - return umbRequestHelper.resourcePromise( + return umbRequestHelper.resourcePromise( $http.post(umbRequestHelper.getApiUrl("mediaApiBaseUrl", "PostSort"), - { - parentId: args.parentId, - idSortOrder: args.sortedIds - }), + { + parentId: args.parentId, + idSortOrder: args.sortedIds + }), 'Failed to sort media'); - }, + }, - /** - * @ngdoc method - * @name umbraco.resources.mediaResource#move - * @methodOf umbraco.resources.mediaResource - * - * @description - * Moves a node underneath a new parentId - * - * ##usage - *
-         * mediaResource.move({ parentId: 1244, id: 123 })
-         *    .then(function() {
-         *        alert("node was moved");
-         *    }, function(err){
-         *      alert("node didnt move:" + err.data.Message); 
-         *    });
-         * 
- * @param {Object} args arguments object - * @param {Int} args.idd the ID of the node to move - * @param {Int} args.parentId the ID of the parent node to move to - * @returns {Promise} resourcePromise object. - * - */ - move: function (args) { - if (!args) { - throw "args cannot be null"; - } - if (!args.parentId) { - throw "args.parentId cannot be null"; - } - if (!args.id) { - throw "args.id cannot be null"; - } + /** + * @ngdoc method + * @name umbraco.resources.mediaResource#move + * @methodOf umbraco.resources.mediaResource + * + * @description + * Moves a node underneath a new parentId + * + * ##usage + *
+        * mediaResource.move({ parentId: 1244, id: 123 })
+        *    .then(function() {
+        *        alert("node was moved");
+        *    }, function(err){
+        *      alert("node didnt move:" + err.data.Message); 
+        *    });
+        * 
+ * @param {Object} args arguments object + * @param {Int} args.idd the ID of the node to move + * @param {Int} args.parentId the ID of the parent node to move to + * @returns {Promise} resourcePromise object. + * + */ + move: function (args) { + if (!args) { + throw "args cannot be null"; + } + if (!args.parentId) { + throw "args.parentId cannot be null"; + } + if (!args.id) { + throw "args.id cannot be null"; + } - return umbRequestHelper.resourcePromise( + return umbRequestHelper.resourcePromise( $http.post(umbRequestHelper.getApiUrl("mediaApiBaseUrl", "PostMove"), - { - parentId: args.parentId, - id: args.id - }), + { + parentId: args.parentId, + id: args.id + }), 'Failed to move media'); - }, + }, - /** - * @ngdoc method - * @name umbraco.resources.mediaResource#getById - * @methodOf umbraco.resources.mediaResource - * - * @description - * Gets a media item with a given id - * - * ##usage - *
-         * mediaResource.getById(1234)
-         *    .then(function(media) {
-         *        var myMedia = media; 
-         *        alert('its here!');
-         *    });
-         * 
- * - * @param {Int} id id of media item to return - * @returns {Promise} resourcePromise object containing the media item. - * - */ - getById: function (id) { - - return umbRequestHelper.resourcePromise( + /** + * @ngdoc method + * @name umbraco.resources.mediaResource#getById + * @methodOf umbraco.resources.mediaResource + * + * @description + * Gets a media item with a given id + * + * ##usage + *
+        * mediaResource.getById(1234)
+        *    .then(function(media) {
+        *        var myMedia = media; 
+        *        alert('its here!');
+        *    });
+        * 
+ * + * @param {Int} id id of media item to return + * @returns {Promise} resourcePromise object containing the media item. + * + */ + getById: function (id) { + + return umbRequestHelper.resourcePromise( $http.get( - umbRequestHelper.getApiUrl( - "mediaApiBaseUrl", - "GetById", - [{ id: id }])), + umbRequestHelper.getApiUrl( + "mediaApiBaseUrl", + "GetById", + [{ id: id }])), 'Failed to retrieve data for media id ' + id); - }, + }, - /** - * @ngdoc method - * @name umbraco.resources.mediaResource#deleteById - * @methodOf umbraco.resources.mediaResource - * - * @description - * Deletes a media item with a given id - * - * ##usage - *
-         * mediaResource.deleteById(1234)
-         *    .then(function() {
-         *        alert('its gone!');
-         *    });
-         * 
- * - * @param {Int} id id of media item to delete - * @returns {Promise} resourcePromise object. - * - */ - deleteById: function(id) { - return umbRequestHelper.resourcePromise( + /** + * @ngdoc method + * @name umbraco.resources.mediaResource#deleteById + * @methodOf umbraco.resources.mediaResource + * + * @description + * Deletes a media item with a given id + * + * ##usage + *
+        * mediaResource.deleteById(1234)
+        *    .then(function() {
+        *        alert('its gone!');
+        *    });
+        * 
+ * + * @param {Int} id id of media item to delete + * @returns {Promise} resourcePromise object. + * + */ + deleteById: function (id) { + return umbRequestHelper.resourcePromise( $http.post( - umbRequestHelper.getApiUrl( - "mediaApiBaseUrl", - "DeleteById", - [{ id: id }])), + umbRequestHelper.getApiUrl( + "mediaApiBaseUrl", + "DeleteById", + [{ id: id }])), 'Failed to delete item ' + id); - }, + }, - /** - * @ngdoc method - * @name umbraco.resources.mediaResource#getByIds - * @methodOf umbraco.resources.mediaResource - * - * @description - * Gets an array of media items, given a collection of ids - * - * ##usage - *
-         * mediaResource.getByIds( [1234,2526,28262])
-         *    .then(function(mediaArray) {
-         *        var myDoc = contentArray; 
-         *        alert('they are here!');
-         *    });
-         * 
- * - * @param {Array} ids ids of media items to return as an array - * @returns {Promise} resourcePromise object containing the media items array. - * - */ - getByIds: function (ids) { - - var idQuery = ""; - _.each(ids, function(item) { - idQuery += "ids=" + item + "&"; - }); + /** + * @ngdoc method + * @name umbraco.resources.mediaResource#getByIds + * @methodOf umbraco.resources.mediaResource + * + * @description + * Gets an array of media items, given a collection of ids + * + * ##usage + *
+        * mediaResource.getByIds( [1234,2526,28262])
+        *    .then(function(mediaArray) {
+        *        var myDoc = contentArray; 
+        *        alert('they are here!');
+        *    });
+        * 
+ * + * @param {Array} ids ids of media items to return as an array + * @returns {Promise} resourcePromise object containing the media items array. + * + */ + getByIds: function (ids) { - return umbRequestHelper.resourcePromise( + var idQuery = ""; + _.each(ids, function (item) { + idQuery += "ids=" + item + "&"; + }); + + return umbRequestHelper.resourcePromise( $http.get( - umbRequestHelper.getApiUrl( - "mediaApiBaseUrl", - "GetByIds", - idQuery)), + umbRequestHelper.getApiUrl( + "mediaApiBaseUrl", + "GetByIds", + idQuery)), 'Failed to retrieve data for media ids ' + ids); - }, + }, - /** - * @ngdoc method - * @name umbraco.resources.mediaResource#getScaffold - * @methodOf umbraco.resources.mediaResource - * - * @description - * Returns a scaffold of an empty media item, given the id of the media item to place it underneath and the media type alias. - * - * - Parent Id must be provided so umbraco knows where to store the media - * - Media Type alias must be provided so umbraco knows which properties to put on the media scaffold - * - * The scaffold is used to build editors for media that has not yet been populated with data. - * - * ##usage - *
-         * mediaResource.getScaffold(1234, 'folder')
-         *    .then(function(scaffold) {
-         *        var myDoc = scaffold;
-         *        myDoc.name = "My new media item"; 
-         *
-         *        mediaResource.save(myDoc, true)
-         *            .then(function(media){
-         *                alert("Retrieved, updated and saved again");
-         *            });
-         *    });
-         * 
- * - * @param {Int} parentId id of media item to return - * @param {String} alias mediatype alias to base the scaffold on - * @returns {Promise} resourcePromise object containing the media scaffold. - * - */ - getScaffold: function (parentId, alias) { - - return umbRequestHelper.resourcePromise( + /** + * @ngdoc method + * @name umbraco.resources.mediaResource#getScaffold + * @methodOf umbraco.resources.mediaResource + * + * @description + * Returns a scaffold of an empty media item, given the id of the media item to place it underneath and the media type alias. + * + * - Parent Id must be provided so umbraco knows where to store the media + * - Media Type alias must be provided so umbraco knows which properties to put on the media scaffold + * + * The scaffold is used to build editors for media that has not yet been populated with data. + * + * ##usage + *
+        * mediaResource.getScaffold(1234, 'folder')
+        *    .then(function(scaffold) {
+        *        var myDoc = scaffold;
+        *        myDoc.name = "My new media item"; 
+        *
+        *        mediaResource.save(myDoc, true)
+        *            .then(function(media){
+        *                alert("Retrieved, updated and saved again");
+        *            });
+        *    });
+        * 
+ * + * @param {Int} parentId id of media item to return + * @param {String} alias mediatype alias to base the scaffold on + * @returns {Promise} resourcePromise object containing the media scaffold. + * + */ + getScaffold: function (parentId, alias) { + + return umbRequestHelper.resourcePromise( $http.get( - umbRequestHelper.getApiUrl( - "mediaApiBaseUrl", - "GetEmpty", - [{ contentTypeAlias: alias }, { parentId: parentId }])), + umbRequestHelper.getApiUrl( + "mediaApiBaseUrl", + "GetEmpty", + [{ contentTypeAlias: alias }, { parentId: parentId }])), 'Failed to retrieve data for empty media item type ' + alias); - }, + }, - rootMedia: function () { - - return umbRequestHelper.resourcePromise( + rootMedia: function () { + + return umbRequestHelper.resourcePromise( $http.get( - umbRequestHelper.getApiUrl( - "mediaApiBaseUrl", - "GetRootMedia")), + umbRequestHelper.getApiUrl( + "mediaApiBaseUrl", + "GetRootMedia")), 'Failed to retrieve data for root media'); - }, + }, - /** - * @ngdoc method - * @name umbraco.resources.mediaResource#getChildren - * @methodOf umbraco.resources.mediaResource - * - * @description - * Gets children of a media item with a given id - * - * ##usage - *
-         * mediaResource.getChildren(1234, {pageSize: 10, pageNumber: 2})
-         *    .then(function(contentArray) {
-         *        var children = contentArray; 
-         *        alert('they are here!');
-         *    });
-         * 
- * - * @param {Int} parentid id of content item to return children of - * @param {Object} options optional options object - * @param {Int} options.pageSize if paging data, number of nodes per page, default = 0 - * @param {Int} options.pageNumber if paging data, current page index, default = 0 - * @param {String} options.filter if provided, query will only return those with names matching the filter - * @param {String} options.orderDirection can be `Ascending` or `Descending` - Default: `Ascending` - * @param {String} options.orderBy property to order items by, default: `SortOrder` - * @returns {Promise} resourcePromise object containing an array of content items. - * - */ - getChildren: function (parentId, options) { + /** + * @ngdoc method + * @name umbraco.resources.mediaResource#getChildren + * @methodOf umbraco.resources.mediaResource + * + * @description + * Gets children of a media item with a given id + * + * ##usage + *
+        * mediaResource.getChildren(1234, {pageSize: 10, pageNumber: 2})
+        *    .then(function(contentArray) {
+        *        var children = contentArray; 
+        *        alert('they are here!');
+        *    });
+        * 
+ * + * @param {Int} parentid id of content item to return children of + * @param {Object} options optional options object + * @param {Int} options.pageSize if paging data, number of nodes per page, default = 0 + * @param {Int} options.pageNumber if paging data, current page index, default = 0 + * @param {String} options.filter if provided, query will only return those with names matching the filter + * @param {String} options.orderDirection can be `Ascending` or `Descending` - Default: `Ascending` + * @param {String} options.orderBy property to order items by, default: `SortOrder` + * @returns {Promise} resourcePromise object containing an array of content items. + * + */ + getChildren: function (parentId, options) { - var defaults = { - pageSize: 0, - pageNumber: 0, - filter: '', - orderDirection: "Ascending", - orderBy: "SortOrder" - }; - if (options === undefined) { - options = {}; - } - //overwrite the defaults if there are any specified - angular.extend(defaults, options); - //now copy back to the options we will use - options = defaults; - //change asc/desct - if (options.orderDirection === "asc") { - options.orderDirection = "Ascending"; - } - else if (options.orderDirection === "desc") { - options.orderDirection = "Descending"; - } + var defaults = { + pageSize: 0, + pageNumber: 0, + filter: '', + orderDirection: "Ascending", + orderBy: "SortOrder", + orderBySystemField: true + }; + if (options === undefined) { + options = {}; + } + //overwrite the defaults if there are any specified + angular.extend(defaults, options); + //now copy back to the options we will use + options = defaults; + //change asc/desct + if (options.orderDirection === "asc") { + options.orderDirection = "Ascending"; + } + else if (options.orderDirection === "desc") { + options.orderDirection = "Descending"; + } - return umbRequestHelper.resourcePromise( + return umbRequestHelper.resourcePromise( $http.get( - umbRequestHelper.getApiUrl( - "mediaApiBaseUrl", - "GetChildren", - [ - { id: parentId }, - { pageNumber: options.pageNumber }, - { pageSize: options.pageSize }, - { orderBy: options.orderBy }, - { orderDirection: options.orderDirection }, - { filter: options.filter } - ])), + umbRequestHelper.getApiUrl( + "mediaApiBaseUrl", + "GetChildren", + [ + { id: parentId }, + { pageNumber: options.pageNumber }, + { pageSize: options.pageSize }, + { orderBy: options.orderBy }, + { orderDirection: options.orderDirection }, + { orderBySystemField: options.orderBySystemField }, + { filter: options.filter } + ])), 'Failed to retrieve children for media item ' + parentId); - }, - - /** - * @ngdoc method - * @name umbraco.resources.mediaResource#save - * @methodOf umbraco.resources.mediaResource - * - * @description - * Saves changes made to a media item, if the media item is new, the isNew paramater must be passed to force creation - * if the media item needs to have files attached, they must be provided as the files param and passed separately - * - * - * ##usage - *
-         * mediaResource.getById(1234)
-         *    .then(function(media) {
-         *          media.name = "I want a new name!";
-         *          mediaResource.save(media, false)
-         *            .then(function(media){
-         *                alert("Retrieved, updated and saved again");
-         *            });
-         *    });
-         * 
- * - * @param {Object} media The media item object with changes applied - * @param {Bool} isNew set to true to create a new item or to update an existing - * @param {Array} files collection of files for the media item - * @returns {Promise} resourcePromise object containing the saved media item. - * - */ - save: function (media, isNew, files) { - return saveMediaItem(media, "save" + (isNew ? "New" : ""), files); - }, + }, - /** - * @ngdoc method - * @name umbraco.resources.mediaResource#addFolder - * @methodOf umbraco.resources.mediaResource - * - * @description - * Shorthand for adding a media item of the type "Folder" under a given parent ID - * - * ##usage - *
-         * mediaResource.addFolder("My gallery", 1234)
-         *    .then(function(folder) {
-         *        alert('New folder');
-         *    });
-         * 
- * - * @param {string} name Name of the folder to create - * @param {int} parentId Id of the media item to create the folder underneath - * @returns {Promise} resourcePromise object. - * - */ - addFolder: function(name, parentId){ - return umbRequestHelper.resourcePromise( + /** + * @ngdoc method + * @name umbraco.resources.mediaResource#save + * @methodOf umbraco.resources.mediaResource + * + * @description + * Saves changes made to a media item, if the media item is new, the isNew paramater must be passed to force creation + * if the media item needs to have files attached, they must be provided as the files param and passed separately + * + * + * ##usage + *
+        * mediaResource.getById(1234)
+        *    .then(function(media) {
+        *          media.name = "I want a new name!";
+        *          mediaResource.save(media, false)
+        *            .then(function(media){
+        *                alert("Retrieved, updated and saved again");
+        *            });
+        *    });
+        * 
+ * + * @param {Object} media The media item object with changes applied + * @param {Bool} isNew set to true to create a new item or to update an existing + * @param {Array} files collection of files for the media item + * @returns {Promise} resourcePromise object containing the saved media item. + * + */ + save: function (media, isNew, files) { + return saveMediaItem(media, "save" + (isNew ? "New" : ""), files); + }, + + /** + * @ngdoc method + * @name umbraco.resources.mediaResource#addFolder + * @methodOf umbraco.resources.mediaResource + * + * @description + * Shorthand for adding a media item of the type "Folder" under a given parent ID + * + * ##usage + *
+        * mediaResource.addFolder("My gallery", 1234)
+        *    .then(function(folder) {
+        *        alert('New folder');
+        *    });
+        * 
+ * + * @param {string} name Name of the folder to create + * @param {int} parentId Id of the media item to create the folder underneath + * @returns {Promise} resourcePromise object. + * + */ + addFolder: function (name, parentId) { + return umbRequestHelper.resourcePromise( $http.post(umbRequestHelper - .getApiUrl("mediaApiBaseUrl", "PostAddFolder"), - { - name: name, - parentId: parentId - }), + .getApiUrl("mediaApiBaseUrl", "PostAddFolder"), + { + name: name, + parentId: parentId + }), 'Failed to add folder'); - }, - - /** - * @ngdoc method - * @name umbraco.resources.mediaResource#getChildFolders - * @methodOf umbraco.resources.mediaResource - * - * @description - * Retrieves all media children with types used as folders. - * Uses the convention of looking for media items with mediaTypes ending in - * *Folder so will match "Folder", "bannerFolder", "secureFolder" etc, - * - * ##usage - *
-         * mediaResource.getChildFolders(1234)
-         *    .then(function(data) {
-         *        alert('folders');
-         *    });
-         * 
- * - * @param {int} parentId Id of the media item to query for child folders - * @returns {Promise} resourcePromise object. - * - */ - getChildFolders: function(parentId){ - if(!parentId){ - parentId = -1; - } - - return umbRequestHelper.resourcePromise( + }, + + /** + * @ngdoc method + * @name umbraco.resources.mediaResource#getChildFolders + * @methodOf umbraco.resources.mediaResource + * + * @description + * Retrieves all media children with types used as folders. + * Uses the convention of looking for media items with mediaTypes ending in + * *Folder so will match "Folder", "bannerFolder", "secureFolder" etc, + * + * ##usage + *
+        * mediaResource.getChildFolders(1234)
+        *    .then(function(data) {
+        *        alert('folders');
+        *    });
+        * 
+ * + * @param {int} parentId Id of the media item to query for child folders + * @returns {Promise} resourcePromise object. + * + */ + getChildFolders: function (parentId) { + if (!parentId) { + parentId = -1; + } + + return umbRequestHelper.resourcePromise( $http.get( - umbRequestHelper.getApiUrl( - "mediaApiBaseUrl", - "GetChildFolders", - [ - { id: parentId } - ])), + umbRequestHelper.getApiUrl( + "mediaApiBaseUrl", + "GetChildFolders", + [ + { id: parentId } + ])), 'Failed to retrieve child folders for media item ' + parentId); - }, - - /** - * @ngdoc method - * @name umbraco.resources.mediaResource#emptyRecycleBin - * @methodOf umbraco.resources.mediaResource - * - * @description - * Empties the media recycle bin - * - * ##usage - *
-         * mediaResource.emptyRecycleBin()
-         *    .then(function() {
-         *        alert('its empty!');
-         *    });
-         * 
- * - * @returns {Promise} resourcePromise object. - * - */ - emptyRecycleBin: function() { - return umbRequestHelper.resourcePromise( + }, + + /** + * @ngdoc method + * @name umbraco.resources.mediaResource#emptyRecycleBin + * @methodOf umbraco.resources.mediaResource + * + * @description + * Empties the media recycle bin + * + * ##usage + *
+        * mediaResource.emptyRecycleBin()
+        *    .then(function() {
+        *        alert('its empty!');
+        *    });
+        * 
+ * + * @returns {Promise} resourcePromise object. + * + */ + emptyRecycleBin: function () { + return umbRequestHelper.resourcePromise( $http.post( - umbRequestHelper.getApiUrl( - "mediaApiBaseUrl", - "EmptyRecycleBin")), + umbRequestHelper.getApiUrl( + "mediaApiBaseUrl", + "EmptyRecycleBin")), 'Failed to empty the recycle bin'); - } - }; + } + }; } angular.module('umbraco.resources').factory('mediaResource', mediaResource); diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/member.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/member.resource.js index 42db4f6366..3ec9258f98 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/member.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/member.resource.js @@ -4,230 +4,232 @@ * @description Loads in data for members **/ function memberResource($q, $http, umbDataFormatter, umbRequestHelper) { - - /** internal method process the saving of data and post processing the result */ - function saveMember(content, action, files) { - - return umbRequestHelper.postSaveContent({ - restApiUrl: umbRequestHelper.getApiUrl( + + /** internal method process the saving of data and post processing the result */ + function saveMember(content, action, files) { + + return umbRequestHelper.postSaveContent({ + restApiUrl: umbRequestHelper.getApiUrl( "memberApiBaseUrl", "PostSave"), - content: content, - action: action, - files: files, - dataFormatter: function(c, a) { - return umbDataFormatter.formatMemberPostData(c, a); - } - }); - } + content: content, + action: action, + files: files, + dataFormatter: function (c, a) { + return umbDataFormatter.formatMemberPostData(c, a); + } + }); + } - return { - - getPagedResults: function (memberTypeAlias, options) { + return { - if (memberTypeAlias === 'all-members') { - memberTypeAlias = null; - } + getPagedResults: function (memberTypeAlias, options) { - var defaults = { - pageSize: 25, - pageNumber: 1, - filter: '', - orderDirection: "Ascending", - orderBy: "LoginName" - }; - if (options === undefined) { - options = {}; - } - //overwrite the defaults if there are any specified - angular.extend(defaults, options); - //now copy back to the options we will use - options = defaults; - //change asc/desct - if (options.orderDirection === "asc") { - options.orderDirection = "Ascending"; - } - else if (options.orderDirection === "desc") { - options.orderDirection = "Descending"; - } + if (memberTypeAlias === 'all-members') { + memberTypeAlias = null; + } - var params = [ + var defaults = { + pageSize: 25, + pageNumber: 1, + filter: '', + orderDirection: "Ascending", + orderBy: "LoginName", + orderBySystemField: true + }; + if (options === undefined) { + options = {}; + } + //overwrite the defaults if there are any specified + angular.extend(defaults, options); + //now copy back to the options we will use + options = defaults; + //change asc/desct + if (options.orderDirection === "asc") { + options.orderDirection = "Ascending"; + } + else if (options.orderDirection === "desc") { + options.orderDirection = "Descending"; + } + + var params = [ { pageNumber: options.pageNumber }, { pageSize: options.pageSize }, { orderBy: options.orderBy }, { orderDirection: options.orderDirection }, + { orderBySystemField: options.orderBySystemField }, { filter: options.filter } - ]; - if (memberTypeAlias != null) { - params.push({ memberTypeAlias: memberTypeAlias }); - } + ]; + if (memberTypeAlias != null) { + params.push({ memberTypeAlias: memberTypeAlias }); + } - return umbRequestHelper.resourcePromise( + return umbRequestHelper.resourcePromise( $http.get( - umbRequestHelper.getApiUrl( - "memberApiBaseUrl", - "GetPagedResults", - params)), + umbRequestHelper.getApiUrl( + "memberApiBaseUrl", + "GetPagedResults", + params)), 'Failed to retrieve member paged result'); - }, - - getListNode: function (listName) { + }, - return umbRequestHelper.resourcePromise( + getListNode: function (listName) { + + return umbRequestHelper.resourcePromise( $http.get( - umbRequestHelper.getApiUrl( - "memberApiBaseUrl", - "GetListNodeDisplay", - [{ listName: listName }])), + umbRequestHelper.getApiUrl( + "memberApiBaseUrl", + "GetListNodeDisplay", + [{ listName: listName }])), 'Failed to retrieve data for member list ' + listName); - }, + }, - /** - * @ngdoc method - * @name umbraco.resources.memberResource#getByKey - * @methodOf umbraco.resources.memberResource - * - * @description - * Gets a member item with a given key - * - * ##usage - *
-         * memberResource.getByKey("0000-0000-000-00000-000")
-         *    .then(function(member) {
-         *        var mymember = member; 
-         *        alert('its here!');
-         *    });
-         * 
- * - * @param {Guid} key key of member item to return - * @returns {Promise} resourcePromise object containing the member item. - * - */ - getByKey: function (key) { - - return umbRequestHelper.resourcePromise( + /** + * @ngdoc method + * @name umbraco.resources.memberResource#getByKey + * @methodOf umbraco.resources.memberResource + * + * @description + * Gets a member item with a given key + * + * ##usage + *
+        * memberResource.getByKey("0000-0000-000-00000-000")
+        *    .then(function(member) {
+        *        var mymember = member; 
+        *        alert('its here!');
+        *    });
+        * 
+ * + * @param {Guid} key key of member item to return + * @returns {Promise} resourcePromise object containing the member item. + * + */ + getByKey: function (key) { + + return umbRequestHelper.resourcePromise( $http.get( - umbRequestHelper.getApiUrl( - "memberApiBaseUrl", - "GetByKey", - [{ key: key }])), + umbRequestHelper.getApiUrl( + "memberApiBaseUrl", + "GetByKey", + [{ key: key }])), 'Failed to retrieve data for member id ' + key); - }, + }, - /** - * @ngdoc method - * @name umbraco.resources.memberResource#deleteByKey - * @methodOf umbraco.resources.memberResource - * - * @description - * Deletes a member item with a given key - * - * ##usage - *
-         * memberResource.deleteByKey("0000-0000-000-00000-000")
-         *    .then(function() {
-         *        alert('its gone!');
-         *    });
-         * 
- * - * @param {Guid} key id of member item to delete - * @returns {Promise} resourcePromise object. - * - */ - deleteByKey: function (key) { - return umbRequestHelper.resourcePromise( + /** + * @ngdoc method + * @name umbraco.resources.memberResource#deleteByKey + * @methodOf umbraco.resources.memberResource + * + * @description + * Deletes a member item with a given key + * + * ##usage + *
+        * memberResource.deleteByKey("0000-0000-000-00000-000")
+        *    .then(function() {
+        *        alert('its gone!');
+        *    });
+        * 
+ * + * @param {Guid} key id of member item to delete + * @returns {Promise} resourcePromise object. + * + */ + deleteByKey: function (key) { + return umbRequestHelper.resourcePromise( $http.post( - umbRequestHelper.getApiUrl( - "memberApiBaseUrl", - "DeleteByKey", - [{ key: key }])), + umbRequestHelper.getApiUrl( + "memberApiBaseUrl", + "DeleteByKey", + [{ key: key }])), 'Failed to delete item ' + key); - }, + }, - /** - * @ngdoc method - * @name umbraco.resources.memberResource#getScaffold - * @methodOf umbraco.resources.memberResource - * - * @description - * Returns a scaffold of an empty member item, given the id of the member item to place it underneath and the member type alias. - * - * - Member Type alias must be provided so umbraco knows which properties to put on the member scaffold - * - * The scaffold is used to build editors for member that has not yet been populated with data. - * - * ##usage - *
-         * memberResource.getScaffold('client')
-         *    .then(function(scaffold) {
-         *        var myDoc = scaffold;
-         *        myDoc.name = "My new member item"; 
-         *
-         *        memberResource.save(myDoc, true)
-         *            .then(function(member){
-         *                alert("Retrieved, updated and saved again");
-         *            });
-         *    });
-         * 
- * - * @param {String} alias membertype alias to base the scaffold on - * @returns {Promise} resourcePromise object containing the member scaffold. - * - */ - getScaffold: function (alias) { - - if (alias) { - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "memberApiBaseUrl", - "GetEmpty", - [{ contentTypeAlias: alias }])), - 'Failed to retrieve data for empty member item type ' + alias); - } - else { - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "memberApiBaseUrl", - "GetEmpty")), - 'Failed to retrieve data for empty member item type ' + alias); - } + /** + * @ngdoc method + * @name umbraco.resources.memberResource#getScaffold + * @methodOf umbraco.resources.memberResource + * + * @description + * Returns a scaffold of an empty member item, given the id of the member item to place it underneath and the member type alias. + * + * - Member Type alias must be provided so umbraco knows which properties to put on the member scaffold + * + * The scaffold is used to build editors for member that has not yet been populated with data. + * + * ##usage + *
+        * memberResource.getScaffold('client')
+        *    .then(function(scaffold) {
+        *        var myDoc = scaffold;
+        *        myDoc.name = "My new member item"; 
+        *
+        *        memberResource.save(myDoc, true)
+        *            .then(function(member){
+        *                alert("Retrieved, updated and saved again");
+        *            });
+        *    });
+        * 
+ * + * @param {String} alias membertype alias to base the scaffold on + * @returns {Promise} resourcePromise object containing the member scaffold. + * + */ + getScaffold: function (alias) { - }, - - /** - * @ngdoc method - * @name umbraco.resources.memberResource#save - * @methodOf umbraco.resources.memberResource - * - * @description - * Saves changes made to a member, if the member is new, the isNew paramater must be passed to force creation - * if the member needs to have files attached, they must be provided as the files param and passed separately - * - * - * ##usage - *
-         * memberResource.getBykey("23234-sd8djsd-3h8d3j-sdh8d")
-         *    .then(function(member) {
-         *          member.name = "Bob";
-         *          memberResource.save(member, false)
-         *            .then(function(member){
-         *                alert("Retrieved, updated and saved again");
-         *            });
-         *    });
-         * 
- * - * @param {Object} media The member item object with changes applied - * @param {Bool} isNew set to true to create a new item or to update an existing - * @param {Array} files collection of files for the media item - * @returns {Promise} resourcePromise object containing the saved media item. - * - */ - save: function (member, isNew, files) { - return saveMember(member, "save" + (isNew ? "New" : ""), files); - } - }; + if (alias) { + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "memberApiBaseUrl", + "GetEmpty", + [{ contentTypeAlias: alias }])), + 'Failed to retrieve data for empty member item type ' + alias); + } + else { + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "memberApiBaseUrl", + "GetEmpty")), + 'Failed to retrieve data for empty member item type ' + alias); + } + + }, + + /** + * @ngdoc method + * @name umbraco.resources.memberResource#save + * @methodOf umbraco.resources.memberResource + * + * @description + * Saves changes made to a member, if the member is new, the isNew paramater must be passed to force creation + * if the member needs to have files attached, they must be provided as the files param and passed separately + * + * + * ##usage + *
+        * memberResource.getBykey("23234-sd8djsd-3h8d3j-sdh8d")
+        *    .then(function(member) {
+        *          member.name = "Bob";
+        *          memberResource.save(member, false)
+        *            .then(function(member){
+        *                alert("Retrieved, updated and saved again");
+        *            });
+        *    });
+        * 
+ * + * @param {Object} media The member item object with changes applied + * @param {Bool} isNew set to true to create a new item or to update an existing + * @param {Array} files collection of files for the media item + * @returns {Promise} resourcePromise object containing the saved media item. + * + */ + save: function (member, isNew, files) { + return saveMember(member, "save" + (isNew ? "New" : ""), files); + } + }; } angular.module('umbraco.resources').factory('memberResource', memberResource); diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-table.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-table.html index fdd7dd23c9..8d9b45e6be 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-table.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-table.html @@ -1,76 +1,60 @@ -
- -
- - -
-
- -
- -
- - - - - -
-
- - -
-
- - -
- - -
- -
- - -
- -
- {{item[column.alias]}} -
- -
-
- -
- - - - There are no items show in the list. - - -
+
+
+ +
+
+
+ +
+ + +
+
+ +
+
+ +
+ + +
+
+ + +
+
+ {{item[column.alias]}} +
+
+
+
+ + + There are no items show in the list. + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/list/list.listviewlayout.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/list/list.listviewlayout.controller.js index 1d2d361f28..b396b66564 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/list/list.listviewlayout.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/list/list.listviewlayout.controller.js @@ -1,75 +1,76 @@ -(function() { - "use strict"; - - function ListViewListLayoutController($scope, listViewHelper, $location, mediaHelper) { - - var vm = this; - - vm.nodeId = $scope.contentId; - //vm.acceptedFileTypes = mediaHelper.formatFileTypes(Umbraco.Sys.ServerVariables.umbracoSettings.imageFileTypes); - //instead of passing in a whitelist, we pass in a blacklist by adding ! to the ext - vm.acceptedFileTypes = mediaHelper.formatFileTypes(Umbraco.Sys.ServerVariables.umbracoSettings.disallowedUploadFiles).replace(/./g, "!."); - vm.maxFileSize = Umbraco.Sys.ServerVariables.umbracoSettings.maxFileSize + "KB"; - vm.activeDrag = false; - vm.isRecycleBin = $scope.contentId === '-21' || $scope.contentId === '-20'; - - vm.selectItem = selectItem; - vm.clickItem = clickItem; - vm.selectAll = selectAll; - vm.isSelectedAll = isSelectedAll; - vm.isSortDirection = isSortDirection; - vm.sort = sort; - vm.dragEnter = dragEnter; - vm.dragLeave = dragLeave; - vm.onFilesQueue = onFilesQueue; - vm.onUploadComplete = onUploadComplete; - - function selectAll($event) { - listViewHelper.selectAllItems($scope.items, $scope.selection, $event); - } - - function isSelectedAll() { - return listViewHelper.isSelectedAll($scope.items, $scope.selection); - } - - function selectItem(selectedItem, $index, $event) { - listViewHelper.selectHandler(selectedItem, $index, $scope.items, $scope.selection, $event); - } - - function clickItem(item) { - $location.path($scope.entityType + '/' + $scope.entityType + '/edit/' + item.id); - } - - function isSortDirection(col, direction) { - return listViewHelper.setSortingDirection(col, direction, $scope.options); - } - - function sort(field, allow) { - if (allow) { - listViewHelper.setSorting(field, allow, $scope.options); - $scope.getContent($scope.contentId); - } - } - - // Dropzone upload functions - function dragEnter(el, event) { - vm.activeDrag = true; - } - - function dragLeave(el, event) { - vm.activeDrag = false; - } - - function onFilesQueue() { - vm.activeDrag = false; - } - - function onUploadComplete() { - $scope.getContent($scope.contentId); - } - - } - - angular.module("umbraco").controller("Umbraco.PropertyEditors.ListView.ListLayoutController", ListViewListLayoutController); - -})(); +(function () { + "use strict"; + + function ListViewListLayoutController($scope, listViewHelper, $location, mediaHelper) { + + var vm = this; + + vm.nodeId = $scope.contentId; + //vm.acceptedFileTypes = mediaHelper.formatFileTypes(Umbraco.Sys.ServerVariables.umbracoSettings.imageFileTypes); + //instead of passing in a whitelist, we pass in a blacklist by adding ! to the ext + vm.acceptedFileTypes = mediaHelper.formatFileTypes(Umbraco.Sys.ServerVariables.umbracoSettings.disallowedUploadFiles).replace(/./g, "!."); + vm.maxFileSize = Umbraco.Sys.ServerVariables.umbracoSettings.maxFileSize + "KB"; + vm.activeDrag = false; + vm.isRecycleBin = $scope.contentId === '-21' || $scope.contentId === '-20'; + + vm.selectItem = selectItem; + vm.clickItem = clickItem; + vm.selectAll = selectAll; + vm.isSelectedAll = isSelectedAll; + vm.isSortDirection = isSortDirection; + vm.sort = sort; + vm.dragEnter = dragEnter; + vm.dragLeave = dragLeave; + vm.onFilesQueue = onFilesQueue; + vm.onUploadComplete = onUploadComplete; + + function selectAll($event) { + listViewHelper.selectAllItems($scope.items, $scope.selection, $event); + } + + function isSelectedAll() { + return listViewHelper.isSelectedAll($scope.items, $scope.selection); + } + + function selectItem(selectedItem, $index, $event) { + listViewHelper.selectHandler(selectedItem, $index, $scope.items, $scope.selection, $event); + } + + function clickItem(item) { + $location.path($scope.entityType + '/' +$scope.entityType + '/edit/' +item.id); + } + + function isSortDirection(col, direction) { + return listViewHelper.setSortingDirection(col, direction, $scope.options); + } + + function sort(field, allow, isSystem) { + if (allow) { + $scope.options.orderBySystemField = isSystem; + listViewHelper.setSorting(field, allow, $scope.options); + $scope.getContent($scope.contentId); + } + } + + // Dropzone upload functions + function dragEnter(el, event) { + vm.activeDrag = true; + } + + function dragLeave(el, event) { + vm.activeDrag = false; + } + + function onFilesQueue() { + vm.activeDrag = false; + } + + function onUploadComplete() { + $scope.getContent($scope.contentId); + } + + } + +angular.module("umbraco").controller("Umbraco.PropertyEditors.ListView.ListLayoutController", ListViewListLayoutController); + +}) (); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js index 59ea03aa91..0d5451274c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js @@ -1,382 +1,393 @@ function listViewController($rootScope, $scope, $routeParams, $injector, $cookieStore, notificationsService, iconHelper, dialogService, editorState, localizationService, $location, appState, $timeout, $q, mediaResource, listViewHelper, userService) { - //this is a quick check to see if we're in create mode, if so just exit - we cannot show children for content - // that isn't created yet, if we continue this will use the parent id in the route params which isn't what - // we want. NOTE: This is just a safety check since when we scaffold an empty model on the server we remove - // the list view tab entirely when it's new. - if ($routeParams.create) { - $scope.isNew = true; - return; - } + //this is a quick check to see if we're in create mode, if so just exit - we cannot show children for content + // that isn't created yet, if we continue this will use the parent id in the route params which isn't what + // we want. NOTE: This is just a safety check since when we scaffold an empty model on the server we remove + // the list view tab entirely when it's new. + if ($routeParams.create) { + $scope.isNew = true; + return; + } - //Now we need to check if this is for media, members or content because that will depend on the resources we use - var contentResource, getContentTypesCallback, getListResultsCallback, deleteItemCallback, getIdCallback, createEditUrlCallback; + //Now we need to check if this is for media, members or content because that will depend on the resources we use + var contentResource, getContentTypesCallback, getListResultsCallback, deleteItemCallback, getIdCallback, createEditUrlCallback; - //check the config for the entity type, or the current section name (since the config is only set in c#, not in pre-vals) - if (($scope.model.config.entityType && $scope.model.config.entityType === "member") || (appState.getSectionState("currentSection") === "member")) { - $scope.entityType = "member"; - contentResource = $injector.get('memberResource'); - getContentTypesCallback = $injector.get('memberTypeResource').getTypes; - getListResultsCallback = contentResource.getPagedResults; - deleteItemCallback = contentResource.deleteByKey; - getIdCallback = function(selected) { - var selectedKey = getItemKey(selected.id); - return selectedKey; - }; - createEditUrlCallback = function(item) { - return "/" + $scope.entityType + "/" + $scope.entityType + "/edit/" + item.key + "?page=" + $scope.options.pageNumber + "&listName=" + $scope.contentId; - }; - } - else { - //check the config for the entity type, or the current section name (since the config is only set in c#, not in pre-vals) - if (($scope.model.config.entityType && $scope.model.config.entityType === "media") || (appState.getSectionState("currentSection") === "media")) { - $scope.entityType = "media"; - contentResource = $injector.get('mediaResource'); - getContentTypesCallback = $injector.get('mediaTypeResource').getAllowedTypes; - } - else { - $scope.entityType = "content"; - contentResource = $injector.get('contentResource'); - getContentTypesCallback = $injector.get('contentTypeResource').getAllowedTypes; - } - getListResultsCallback = contentResource.getChildren; - deleteItemCallback = contentResource.deleteById; - getIdCallback = function(selected) { - return selected.id; - }; - createEditUrlCallback = function(item) { - return "/" + $scope.entityType + "/" + $scope.entityType + "/edit/" + item.id + "?page=" + $scope.options.pageNumber; - }; - } + //check the config for the entity type, or the current section name (since the config is only set in c#, not in pre-vals) + if (($scope.model.config.entityType && $scope.model.config.entityType === "member") || (appState.getSectionState("currentSection") === "member")) { + $scope.entityType = "member"; + contentResource = $injector.get('memberResource'); + getContentTypesCallback = $injector.get('memberTypeResource').getTypes; + getListResultsCallback = contentResource.getPagedResults; + deleteItemCallback = contentResource.deleteByKey; + getIdCallback = function (selected) { + var selectedKey = getItemKey(selected.id); + return selectedKey; + }; + createEditUrlCallback = function (item) { + return "/" + $scope.entityType + "/" + $scope.entityType + "/edit/" + item.key + "?page=" + $scope.options.pageNumber + "&listName=" + $scope.contentId; + }; + } + else { + //check the config for the entity type, or the current section name (since the config is only set in c#, not in pre-vals) + if (($scope.model.config.entityType && $scope.model.config.entityType === "media") || (appState.getSectionState("currentSection") === "media")) { + $scope.entityType = "media"; + contentResource = $injector.get('mediaResource'); + getContentTypesCallback = $injector.get('mediaTypeResource').getAllowedTypes; + } + else { + $scope.entityType = "content"; + contentResource = $injector.get('contentResource'); + getContentTypesCallback = $injector.get('contentTypeResource').getAllowedTypes; + } + getListResultsCallback = contentResource.getChildren; + deleteItemCallback = contentResource.deleteById; + getIdCallback = function (selected) { + return selected.id; + }; + createEditUrlCallback = function (item) { + return "/" + $scope.entityType + "/" + $scope.entityType + "/edit/" + item.id + "?page=" + $scope.options.pageNumber; + }; + } - $scope.pagination = []; - $scope.isNew = false; - $scope.actionInProgress = false; - $scope.selection = []; - $scope.folders = []; - $scope.listViewResultSet = { - totalPages: 0, - items: [] - }; - - $scope.currentNodePermissions = {} + $scope.pagination = []; + $scope.isNew = false; + $scope.actionInProgress = false; + $scope.selection = []; + $scope.folders = []; + $scope.listViewResultSet = { + totalPages: 0, + items: [] + }; - //Just ensure we do have an editorState - if (editorState.current) { - //Fetch current node allowed actions for the current user - //This is the current node & not each individual child node in the list - var currentUserPermissions = editorState.current.allowedActions; + $scope.currentNodePermissions = {} - //Create a nicer model rather than the funky & hard to remember permissions strings - $scope.currentNodePermissions = { - "canCopy": _.contains(currentUserPermissions, 'O'), //Magic Char = O - "canCreate": _.contains(currentUserPermissions, 'C'), //Magic Char = C - "canDelete": _.contains(currentUserPermissions, 'D'), //Magic Char = D - "canMove": _.contains(currentUserPermissions, 'M'), //Magic Char = M - "canPublish": _.contains(currentUserPermissions, 'U'), //Magic Char = U - "canUnpublish": _.contains(currentUserPermissions, 'U'), //Magic Char = Z (however UI says it can't be set, so if we can publish 'U' we can unpublish) - }; - } + //Just ensure we do have an editorState + if (editorState.current) { + //Fetch current node allowed actions for the current user + //This is the current node & not each individual child node in the list + var currentUserPermissions = editorState.current.allowedActions; - //when this is null, we don't check permissions - $scope.buttonPermissions = null; + //Create a nicer model rather than the funky & hard to remember permissions strings + $scope.currentNodePermissions = { + "canCopy": _.contains(currentUserPermissions, 'O'), //Magic Char = O + "canCreate": _.contains(currentUserPermissions, 'C'), //Magic Char = C + "canDelete": _.contains(currentUserPermissions, 'D'), //Magic Char = D + "canMove": _.contains(currentUserPermissions, 'M'), //Magic Char = M + "canPublish": _.contains(currentUserPermissions, 'U'), //Magic Char = U + "canUnpublish": _.contains(currentUserPermissions, 'U'), //Magic Char = Z (however UI says it can't be set, so if we can publish 'U' we can unpublish) + }; + } - //When we are dealing with 'content', we need to deal with permissions on child nodes. - // Currently there is no real good way to - if ($scope.entityType === "content") { + //when this is null, we don't check permissions + $scope.buttonPermissions = null; - var idsWithPermissions = null; + //When we are dealing with 'content', we need to deal with permissions on child nodes. + // Currently there is no real good way to + if ($scope.entityType === "content") { - $scope.buttonPermissions = { - canCopy: true, - canCreate: true, - canDelete: true, - canMove: true, - canPublish: true, - canUnpublish: true - }; + var idsWithPermissions = null; - $scope.$watch(function() { - return $scope.selection.length; - }, function(newVal, oldVal) { + $scope.buttonPermissions = { + canCopy: true, + canCreate: true, + canDelete: true, + canMove: true, + canPublish: true, + canUnpublish: true + }; - if ((idsWithPermissions == null && newVal > 0) || (idsWithPermissions != null)) { - - //get all of the selected ids - var ids = _.map($scope.selection, function(i) { - return i.id.toString(); - }); + $scope.$watch(function () { + return $scope.selection.length; + }, function (newVal, oldVal) { - //remove the dictionary items that don't have matching ids - var filtered = {}; - _.each(idsWithPermissions, function (value, key, list) { - if (_.contains(ids, key)) { - filtered[key] = value; - } - }); - idsWithPermissions = filtered; + if ((idsWithPermissions == null && newVal > 0) || (idsWithPermissions != null)) { - //find all ids that we haven't looked up permissions for - var existingIds = _.keys(idsWithPermissions); - var missingLookup = _.map(_.difference(ids, existingIds), function (i) { - return Number(i); - }); - - if (missingLookup.length > 0) { - contentResource.getPermissions(missingLookup).then(function(p) { - $scope.buttonPermissions = listViewHelper.getButtonPermissions(p, idsWithPermissions); - }); - } - else { - $scope.buttonPermissions = listViewHelper.getButtonPermissions({}, idsWithPermissions); - } - } - }); - - } - - $scope.options = { - displayAtTabNumber: $scope.model.config.displayAtTabNumber ? $scope.model.config.displayAtTabNumber : 1, - pageSize: $scope.model.config.pageSize ? $scope.model.config.pageSize : 10, - pageNumber: ($routeParams.page && Number($routeParams.page) != NaN && Number($routeParams.page) > 0) ? $routeParams.page : 1, - filter: '', - orderBy: ($scope.model.config.orderBy ? $scope.model.config.orderBy : 'VersionDate').trim(), - orderDirection: $scope.model.config.orderDirection ? $scope.model.config.orderDirection.trim() : "desc", - includeProperties: $scope.model.config.includeProperties ? $scope.model.config.includeProperties : [ - { alias: 'updateDate', header: 'Last edited', isSystem: 1 }, - { alias: 'updater', header: 'Last edited by', isSystem: 1 } - ], - layout: { - layouts: $scope.model.config.layouts, - activeLayout: listViewHelper.getLayout($routeParams.id, $scope.model.config.layouts) - }, - allowBulkPublish: $scope.entityType === 'content' && $scope.model.config.bulkActionPermissions.allowBulkPublish, - allowBulkUnpublish: $scope.entityType === 'content' && $scope.model.config.bulkActionPermissions.allowBulkUnpublish, - allowBulkCopy: $scope.entityType === 'content' && $scope.model.config.bulkActionPermissions.allowBulkCopy, - allowBulkMove: $scope.model.config.bulkActionPermissions.allowBulkMove, - allowBulkDelete: $scope.model.config.bulkActionPermissions.allowBulkDelete - }; - - //update all of the system includeProperties to enable sorting - _.each($scope.options.includeProperties, function(e, i) { - - if (e.isSystem) { - - //NOTE: special case for contentTypeAlias, it's a system property that cannot be sorted - // to do that, we'd need to update the base query for content to include the content type alias column - // which requires another join and would be slower. BUT We are doing this for members so not sure it makes a diff? - if (e.alias != "contentTypeAlias") { - e.allowSorting = true; - } - - //localize the header - var key = getLocalizedKey(e.alias); - localizationService.localize(key).then(function (v) { - e.header = v; + //get all of the selected ids + var ids = _.map($scope.selection, function (i) { + return i.id.toString(); }); - } - }); - $scope.selectLayout = function(selectedLayout) { - $scope.options.layout.activeLayout = listViewHelper.setLayout($routeParams.id, selectedLayout, $scope.model.config.layouts); - }; + //remove the dictionary items that don't have matching ids + var filtered = {}; + _.each(idsWithPermissions, function (value, key, list) { + if (_.contains(ids, key)) { + filtered[key] = value; + } + }); + idsWithPermissions = filtered; - function showNotificationsAndReset(err, reload, successMsg) { + //find all ids that we haven't looked up permissions for + var existingIds = _.keys(idsWithPermissions); + var missingLookup = _.map(_.difference(ids, existingIds), function (i) { + return Number(i); + }); - //check if response is ysod - if(err.status && err.status >= 500) { - - // Open ysod overlay - $scope.ysodOverlay = { - view : "ysod", - error : err, - show : true - }; - } - - $timeout(function() { - $scope.bulkStatus = ""; - $scope.actionInProgress = false; - }, 500); - - if (reload === true) { - $scope.reloadView($scope.contentId); - } - - if (err.data && angular.isArray(err.data.notifications)) { - for (var i = 0; i < err.data.notifications.length; i++) { - notificationsService.showNotification(err.data.notifications[i]); + if (missingLookup.length > 0) { + contentResource.getPermissions(missingLookup).then(function (p) { + $scope.buttonPermissions = listViewHelper.getButtonPermissions(p, idsWithPermissions); + }); } - } - else if (successMsg) { - notificationsService.success("Done", successMsg); - } - } - - $scope.next = function(pageNumber) { - $scope.options.pageNumber = pageNumber; - $scope.reloadView($scope.contentId); - }; - - $scope.goToPage = function(pageNumber) { - $scope.options.pageNumber = pageNumber; - $scope.reloadView($scope.contentId); - }; - - $scope.prev = function(pageNumber) { - $scope.options.pageNumber = pageNumber; - $scope.reloadView($scope.contentId); - }; - - - /*Loads the search results, based on parameters set in prev,next,sort and so on*/ - /*Pagination is done by an array of objects, due angularJS's funky way of monitoring state - with simple values */ - - $scope.reloadView = function(id) { - - $scope.viewLoaded = false; - - listViewHelper.clearSelection($scope.listViewResultSet.items, $scope.folders, $scope.selection); - - getListResultsCallback(id, $scope.options).then(function(data) { - - $scope.actionInProgress = false; - $scope.listViewResultSet = data; - - //update all values for display - if ($scope.listViewResultSet.items) { - _.each($scope.listViewResultSet.items, function(e, index) { - setPropertyValues(e); - }); + else { + $scope.buttonPermissions = listViewHelper.getButtonPermissions({}, idsWithPermissions); } + } + }); - if ($scope.entityType === 'media') { + } - mediaResource.getChildFolders($scope.contentId) - .then(function(folders) { - $scope.folders = folders; - $scope.viewLoaded = true; + $scope.options = { + displayAtTabNumber: $scope.model.config.displayAtTabNumber ? $scope.model.config.displayAtTabNumber : 1, + pageSize: $scope.model.config.pageSize ? $scope.model.config.pageSize : 10, + pageNumber: ($routeParams.page && Number($routeParams.page) != NaN && Number($routeParams.page) > 0) ? $routeParams.page : 1, + filter: '', + orderBy: ($scope.model.config.orderBy ? $scope.model.config.orderBy : 'VersionDate').trim(), + orderDirection: $scope.model.config.orderDirection ? $scope.model.config.orderDirection.trim() : "desc", + orderBySystemField: true, + includeProperties: $scope.model.config.includeProperties ? $scope.model.config.includeProperties : [ + { alias: 'updateDate', header: 'Last edited', isSystem: 1 }, + { alias: 'updater', header: 'Last edited by', isSystem: 1 } + ], + layout: { + layouts: $scope.model.config.layouts, + activeLayout: listViewHelper.getLayout($routeParams.id, $scope.model.config.layouts) + }, + allowBulkPublish: $scope.entityType === 'content' && $scope.model.config.bulkActionPermissions.allowBulkPublish, + allowBulkUnpublish: $scope.entityType === 'content' && $scope.model.config.bulkActionPermissions.allowBulkUnpublish, + allowBulkCopy: $scope.entityType === 'content' && $scope.model.config.bulkActionPermissions.allowBulkCopy, + allowBulkMove: $scope.model.config.bulkActionPermissions.allowBulkMove, + allowBulkDelete: $scope.model.config.bulkActionPermissions.allowBulkDelete + }; + + //update all of the system includeProperties to enable sorting + _.each($scope.options.includeProperties, function (e, i) { + + + //NOTE: special case for contentTypeAlias, it's a system property that cannot be sorted + // to do that, we'd need to update the base query for content to include the content type alias column + // which requires another join and would be slower. BUT We are doing this for members so not sure it makes a diff? + if (e.alias != "contentTypeAlias") { + e.allowSorting = true; + } + + // Another special case for lasted edited data/update date for media, again this field isn't available on the base table so we can't sort by it + if (e.isSystem && $scope.entityType == "media") { + e.allowSorting = e.alias != 'updateDate'; + } + + // Another special case for members, only fields on the base table (cmsMember) can be used for sorting + if (e.isSystem && $scope.entityType == "member") { + e.allowSorting = e.alias == 'username' || e.alias == 'email'; + } + if (e.isSystem) { + + //localize the header + var key = getLocalizedKey(e.alias); + localizationService.localize(key).then(function (v) { + e.header = v; + }); + } + }); + + $scope.selectLayout = function (selectedLayout) { + $scope.options.layout.activeLayout = listViewHelper.setLayout($routeParams.id, selectedLayout, $scope.model.config.layouts); + }; + + function showNotificationsAndReset(err, reload, successMsg) { + + //check if response is ysod + if (err.status && err.status >= 500) { + + // Open ysod overlay + $scope.ysodOverlay = { + view: "ysod", + error: err, + show: true + }; + } + + $timeout(function () { + $scope.bulkStatus = ""; + $scope.actionInProgress = false; + }, 500); + + if (reload === true) { + $scope.reloadView($scope.contentId); + } + + if (err.data && angular.isArray(err.data.notifications)) { + for (var i = 0; i < err.data.notifications.length; i++) { + notificationsService.showNotification(err.data.notifications[i]); + } + } + else if (successMsg) { + notificationsService.success("Done", successMsg); + } + } + + $scope.next = function (pageNumber) { + $scope.options.pageNumber = pageNumber; + $scope.reloadView($scope.contentId); + }; + + $scope.goToPage = function (pageNumber) { + $scope.options.pageNumber = pageNumber; + $scope.reloadView($scope.contentId); + }; + + $scope.prev = function (pageNumber) { + $scope.options.pageNumber = pageNumber; + $scope.reloadView($scope.contentId); + }; + + + /*Loads the search results, based on parameters set in prev,next,sort and so on*/ + /*Pagination is done by an array of objects, due angularJS's funky way of monitoring state + with simple values */ + + $scope.reloadView = function (id) { + + $scope.viewLoaded = false; + + listViewHelper.clearSelection($scope.listViewResultSet.items, $scope.folders, $scope.selection); + + getListResultsCallback(id, $scope.options).then(function (data) { + + $scope.actionInProgress = false; + $scope.listViewResultSet = data; + + //update all values for display + if ($scope.listViewResultSet.items) { + _.each($scope.listViewResultSet.items, function (e, index) { + setPropertyValues(e); + }); + } + + if ($scope.entityType === 'media') { + + mediaResource.getChildFolders($scope.contentId) + .then(function (folders) { + $scope.folders = folders; + $scope.viewLoaded = true; }); - } else { - $scope.viewLoaded = true; - } + } else { + $scope.viewLoaded = true; + } - //NOTE: This might occur if we are requesting a higher page number than what is actually available, for example - // if you have more than one page and you delete all items on the last page. In this case, we need to reset to the last - // available page and then re-load again - if ($scope.options.pageNumber > $scope.listViewResultSet.totalPages) { - $scope.options.pageNumber = $scope.listViewResultSet.totalPages; + //NOTE: This might occur if we are requesting a higher page number than what is actually available, for example + // if you have more than one page and you delete all items on the last page. In this case, we need to reset to the last + // available page and then re-load again + if ($scope.options.pageNumber > $scope.listViewResultSet.totalPages) { + $scope.options.pageNumber = $scope.listViewResultSet.totalPages; - //reload! - $scope.reloadView(id); - } + //reload! + $scope.reloadView(id); + } - }); - }; + }); + }; - var searchListView = _.debounce(function(){ - $scope.$apply(function() { + var searchListView = _.debounce(function () { + $scope.$apply(function () { + makeSearch(); + }); + }, 500); + + $scope.forceSearch = function (ev) { + //13: enter + switch (ev.keyCode) { + case 13: makeSearch(); - }); - }, 500); + break; + } + }; - $scope.forceSearch = function (ev) { - //13: enter - switch (ev.keyCode) { - case 13: - makeSearch(); - break; - } - }; + $scope.enterSearch = function () { + $scope.viewLoaded = false; + searchListView(); + }; - $scope.enterSearch = function() { - $scope.viewLoaded = false; - searchListView(); - }; + function makeSearch() { + if ($scope.options.filter !== null && $scope.options.filter !== undefined) { + $scope.options.pageNumber = 1; + //$scope.actionInProgress = true; + $scope.reloadView($scope.contentId); + } + } - function makeSearch() { - if ($scope.options.filter !== null && $scope.options.filter !== undefined) { - $scope.options.pageNumber = 1; - //$scope.actionInProgress = true; - $scope.reloadView($scope.contentId); - } - } + $scope.isAnythingSelected = function () { + if ($scope.selection.length === 0) { + return false; + } else { + return true; + } + }; - $scope.isAnythingSelected = function() { - if ($scope.selection.length === 0) { - return false; - } else { - return true; - } - }; - - $scope.selectedItemsCount = function() { + $scope.selectedItemsCount = function () { return $scope.selection.length; - }; + }; - $scope.clearSelection = function() { + $scope.clearSelection = function () { listViewHelper.clearSelection($scope.listViewResultSet.items, $scope.folders, $scope.selection); - }; + }; - $scope.getIcon = function(entry) { - return iconHelper.convertFromLegacyIcon(entry.icon); - }; + $scope.getIcon = function (entry) { + return iconHelper.convertFromLegacyIcon(entry.icon); + }; - function serial(selected, fn, getStatusMsg, index) { - return fn(selected, index).then(function (content) { - index++; - $scope.bulkStatus = getStatusMsg(index, selected.length); - return index < selected.length ? serial(selected, fn, getStatusMsg, index) : content; - }, function (err) { - var reload = index > 0; - showNotificationsAndReset(err, reload); - return err; - }); - } + function serial(selected, fn, getStatusMsg, index) { + return fn(selected, index).then(function (content) { + index++; + $scope.bulkStatus = getStatusMsg(index, selected.length); + return index < selected.length ? serial(selected, fn, getStatusMsg, index) : content; + }, function (err) { + var reload = index > 0; + showNotificationsAndReset(err, reload); + return err; + }); + } - function applySelected(fn, getStatusMsg, getSuccessMsg, confirmMsg) { - var selected = $scope.selection; - if (selected.length === 0) - return; - if (confirmMsg && !confirm(confirmMsg)) - return; + function applySelected(fn, getStatusMsg, getSuccessMsg, confirmMsg) { + var selected = $scope.selection; + if (selected.length === 0) + return; + if (confirmMsg && !confirm(confirmMsg)) + return; - $scope.actionInProgress = true; - $scope.bulkStatus = getStatusMsg(0, selected.length); + $scope.actionInProgress = true; + $scope.bulkStatus = getStatusMsg(0, selected.length); - serial(selected, fn, getStatusMsg, 0).then(function (result) { - // executes once the whole selection has been processed - // in case of an error (caught by serial), result will be the error - if (!(result.data && angular.isArray(result.data.notifications))) - showNotificationsAndReset(result, true, getSuccessMsg(selected.length)); - }); - } + serial(selected, fn, getStatusMsg, 0).then(function (result) { + // executes once the whole selection has been processed + // in case of an error (caught by serial), result will be the error + if (!(result.data && angular.isArray(result.data.notifications))) + showNotificationsAndReset(result, true, getSuccessMsg(selected.length)); + }); + } - $scope.delete = function () { - applySelected( - function (selected, index) { return deleteItemCallback(getIdCallback(selected[index])); }, - function (count, total) { return "Deleted " + count + " out of " + total + " item" + (total > 1 ? "s" : ""); }, - function (total) { return "Deleted " + total + " item" + (total > 1 ? "s" : ""); }, - "Sure you want to delete?"); - }; + $scope.delete = function () { + applySelected( + function (selected, index) { return deleteItemCallback(getIdCallback(selected[index])); }, + function (count, total) { return "Deleted " + count + " out of " + total + " item" + (total > 1 ? "s" : ""); }, + function (total) { return "Deleted " + total + " item" + (total > 1 ? "s" : ""); }, + "Sure you want to delete?"); + }; - $scope.publish = function () { - applySelected( - function (selected, index) { return contentResource.publishById(getIdCallback(selected[index])); }, - function (count, total) { return "Published " + count + " out of " + total + " item" + (total > 1 ? "s" : ""); }, - function (total) { return "Published " + total + " item" + (total > 1 ? "s" : ""); }); - }; + $scope.publish = function () { + applySelected( + function (selected, index) { return contentResource.publishById(getIdCallback(selected[index])); }, + function (count, total) { return "Published " + count + " out of " + total + " item" + (total > 1 ? "s" : ""); }, + function (total) { return "Published " + total + " item" + (total > 1 ? "s" : ""); }); + }; - $scope.unpublish = function() { - applySelected( - function (selected, index) { return contentResource.unPublish(getIdCallback(selected[index])); }, - function (count, total) { return "Unpublished " + count + " out of " + total + " item" + (total > 1 ? "s" : ""); }, - function (total) { return "Unpublished " + total + " item" + (total > 1 ? "s" : ""); }); - }; + $scope.unpublish = function () { + applySelected( + function (selected, index) { return contentResource.unPublish(getIdCallback(selected[index])); }, + function (count, total) { return "Unpublished " + count + " out of " + total + " item" + (total > 1 ? "s" : ""); }, + function (total) { return "Unpublished " + total + " item" + (total > 1 ? "s" : ""); }); + }; - $scope.move = function() { + $scope.move = function () { $scope.moveDialog = {}; $scope.moveDialog.title = "Move"; $scope.moveDialog.section = $scope.entityType; @@ -384,9 +395,9 @@ function listViewController($rootScope, $scope, $routeParams, $injector, $cookie $scope.moveDialog.view = "move"; $scope.moveDialog.show = true; - $scope.moveDialog.submit = function(model) { + $scope.moveDialog.submit = function (model) { - if(model.target) { + if (model.target) { performMove(model.target); } @@ -394,22 +405,22 @@ function listViewController($rootScope, $scope, $routeParams, $injector, $cookie $scope.moveDialog = null; }; - $scope.moveDialog.close = function(oldModel) { + $scope.moveDialog.close = function (oldModel) { $scope.moveDialog.show = false; $scope.moveDialog = null; }; - }; + }; - function performMove(target) { + function performMove(target) { applySelected( - function(selected, index) {return contentResource.move({parentId: target.id, id: getIdCallback(selected[index])}); }, - function(count, total) {return "Moved " + count + " out of " + total + " item" + (total > 1 ? "s" : ""); }, - function(total) {return "Moved " + total + " item" + (total > 1 ? "s" : ""); }); - } + function (selected, index) { return contentResource.move({ parentId: target.id, id: getIdCallback(selected[index]) }); }, + function (count, total) { return "Moved " + count + " out of " + total + " item" + (total > 1 ? "s" : ""); }, + function (total) { return "Moved " + total + " item" + (total > 1 ? "s" : ""); }); + } - $scope.copy = function() { + $scope.copy = function () { $scope.copyDialog = {}; $scope.copyDialog.title = "Copy"; $scope.copyDialog.section = $scope.entityType; @@ -417,8 +428,8 @@ function listViewController($rootScope, $scope, $routeParams, $injector, $cookie $scope.copyDialog.view = "copy"; $scope.copyDialog.show = true; - $scope.copyDialog.submit = function(model) { - if(model.target) { + $scope.copyDialog.submit = function (model) { + if (model.target) { performCopy(model.target, model.relateToOriginal); } @@ -426,142 +437,142 @@ function listViewController($rootScope, $scope, $routeParams, $injector, $cookie $scope.copyDialog = null; }; - $scope.copyDialog.close = function(oldModel) { + $scope.copyDialog.close = function (oldModel) { $scope.copyDialog.show = false; $scope.copyDialog = null; }; - }; + }; - function performCopy(target, relateToOriginal) { + function performCopy(target, relateToOriginal) { applySelected( - function(selected, index) {return contentResource.copy({parentId: target.id, id: getIdCallback(selected[index]), relateToOriginal: relateToOriginal}); }, - function(count, total) {return "Copied " + count + " out of " + total + " item" + (total > 1 ? "s" : ""); }, - function(total) {return "Copied " + total + " item" + (total > 1 ? "s" : ""); }); - } + function (selected, index) { return contentResource.copy({ parentId: target.id, id: getIdCallback(selected[index]), relateToOriginal: relateToOriginal }); }, + function (count, total) { return "Copied " + count + " out of " + total + " item" + (total > 1 ? "s" : ""); }, + function (total) { return "Copied " + total + " item" + (total > 1 ? "s" : ""); }); + } - function getCustomPropertyValue(alias, properties) { - var value = ''; - var index = 0; - var foundAlias = false; - for (var i = 0; i < properties.length; i++) { - if (properties[i].alias == alias) { - foundAlias = true; - break; - } - index++; - } + function getCustomPropertyValue(alias, properties) { + var value = ''; + var index = 0; + var foundAlias = false; + for (var i = 0; i < properties.length; i++) { + if (properties[i].alias == alias) { + foundAlias = true; + break; + } + index++; + } - if (foundAlias) { - value = properties[index].value; - } + if (foundAlias) { + value = properties[index].value; + } - return value; - } + return value; + } - /** This ensures that the correct value is set for each item in a row, we don't want to call a function during interpolation or ng-bind as performance is really bad that way */ - function setPropertyValues(result) { + /** This ensures that the correct value is set for each item in a row, we don't want to call a function during interpolation or ng-bind as performance is really bad that way */ + function setPropertyValues(result) { - //set the edit url - result.editPath = createEditUrlCallback(result); + //set the edit url + result.editPath = createEditUrlCallback(result); - _.each($scope.options.includeProperties, function (e, i) { + _.each($scope.options.includeProperties, function (e, i) { - var alias = e.alias; + var alias = e.alias; - // First try to pull the value directly from the alias (e.g. updatedBy) - var value = result[alias]; + // First try to pull the value directly from the alias (e.g. updatedBy) + var value = result[alias]; - // If this returns an object, look for the name property of that (e.g. owner.name) - if (value === Object(value)) { - value = value['name']; - } + // If this returns an object, look for the name property of that (e.g. owner.name) + if (value === Object(value)) { + value = value['name']; + } - // If we've got nothing yet, look at a user defined property - if (typeof value === 'undefined') { - value = getCustomPropertyValue(alias, result.properties); - } + // If we've got nothing yet, look at a user defined property + if (typeof value === 'undefined') { + value = getCustomPropertyValue(alias, result.properties); + } - // If we have a date, format it - if (isDate(value)) { - value = value.substring(0, value.length - 3); - } + // If we have a date, format it + if (isDate(value)) { + value = value.substring(0, value.length - 3); + } - // set what we've got on the result - result[alias] = value; - }); + // set what we've got on the result + result[alias] = value; + }); - } + } - function isDate(val) { - if (angular.isString(val)) { - return val.match(/^(\d{4})\-(\d{2})\-(\d{2})\ (\d{2})\:(\d{2})\:(\d{2})$/); - } - return false; - } + function isDate(val) { + if (angular.isString(val)) { + return val.match(/^(\d{4})\-(\d{2})\-(\d{2})\ (\d{2})\:(\d{2})\:(\d{2})$/); + } + return false; + } - function initView() { - //default to root id if the id is undefined - var id = $routeParams.id; - if(id === undefined){ - id = -1; - } + function initView() { + //default to root id if the id is undefined + var id = $routeParams.id; + if (id === undefined) { + id = -1; + } - $scope.listViewAllowedTypes = getContentTypesCallback(id); + $scope.listViewAllowedTypes = getContentTypesCallback(id); - $scope.contentId = id; - $scope.isTrashed = id === "-20" || id === "-21"; + $scope.contentId = id; + $scope.isTrashed = id === "-20" || id === "-21"; - $scope.options.allowBulkPublish = $scope.options.allowBulkPublish && !$scope.isTrashed; - $scope.options.allowBulkUnpublish = $scope.options.allowBulkUnpublish && !$scope.isTrashed; + $scope.options.allowBulkPublish = $scope.options.allowBulkPublish && !$scope.isTrashed; + $scope.options.allowBulkUnpublish = $scope.options.allowBulkUnpublish && !$scope.isTrashed; - $scope.options.bulkActionsAllowed = $scope.options.allowBulkPublish || - $scope.options.allowBulkUnpublish || - $scope.options.allowBulkCopy || - $scope.options.allowBulkMove || - $scope.options.allowBulkDelete; + $scope.options.bulkActionsAllowed = $scope.options.allowBulkPublish || + $scope.options.allowBulkUnpublish || + $scope.options.allowBulkCopy || + $scope.options.allowBulkMove || + $scope.options.allowBulkDelete; - $scope.reloadView($scope.contentId); - } + $scope.reloadView($scope.contentId); + } - function getLocalizedKey(alias) { + function getLocalizedKey(alias) { - switch (alias) { - case "sortOrder": - return "general_sort"; - case "updateDate": - return "content_updateDate"; - case "updater": - return "content_updatedBy"; - case "createDate": - return "content_createDate"; - case "owner": - return "content_createBy"; - case "published": - return "content_isPublished"; - case "contentTypeAlias": - //TODO: Check for members - return $scope.entityType === "content" ? "content_documentType" : "content_mediatype"; - case "email": - return "general_email"; - case "username": - return "general_username"; - } - return alias; - } + switch (alias) { + case "sortOrder": + return "general_sort"; + case "updateDate": + return "content_updateDate"; + case "updater": + return "content_updatedBy"; + case "createDate": + return "content_createDate"; + case "owner": + return "content_createBy"; + case "published": + return "content_isPublished"; + case "contentTypeAlias": + //TODO: Check for members + return $scope.entityType === "content" ? "content_documentType" : "content_mediatype"; + case "email": + return "general_email"; + case "username": + return "general_username"; + } + return alias; + } - function getItemKey(itemId) { - for (var i = 0; i < $scope.listViewResultSet.items.length; i++) { - var item = $scope.listViewResultSet.items[i]; - if (item.id === itemId) { - return item.key; - } - } - } + function getItemKey(itemId) { + for (var i = 0; i < $scope.listViewResultSet.items.length; i++) { + var item = $scope.listViewResultSet.items[i]; + if (item.id === itemId) { + return item.key; + } + } + } - //GO! - initView(); + //GO! + initView(); } diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index a65821cfa0..580e1a018c 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -48,13 +48,13 @@ namespace Umbraco.Web.Editors [PluginController("UmbracoApi")] [UmbracoApplicationAuthorizeAttribute(Constants.Applications.Content)] public class ContentController : ContentControllerBase - { + { /// /// Constructor /// public ContentController() - : this(UmbracoContext.Current) - { + : this(UmbracoContext.Current) + { } /// @@ -62,8 +62,8 @@ namespace Umbraco.Web.Editors /// /// public ContentController(UmbracoContext umbracoContext) - : base(umbracoContext) - { + : base(umbracoContext) + { } /// @@ -110,15 +110,15 @@ namespace Umbraco.Web.Editors [EnsureUserPermissionForContent("id")] public ContentItemDisplay GetById(int id) { - var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); + var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); if (foundContent == null) { HandleContentNotFound(id); } - + var content = Mapper.Map(foundContent); return content; - } + } [EnsureUserPermissionForContent("id")] public ContentItemDisplay GetWithTreeDefinition(int id) @@ -156,7 +156,7 @@ namespace Umbraco.Web.Editors //remove this tab if it exists: umbContainerView var containerTab = mapped.Tabs.FirstOrDefault(x => x.Alias == Constants.Conventions.PropertyGroups.ListViewGroupName); - mapped.Tabs = mapped.Tabs.Except(new[] {containerTab}); + mapped.Tabs = mapped.Tabs.Except(new[] { containerTab }); return mapped; } @@ -169,7 +169,7 @@ namespace Umbraco.Web.Editors { var url = Umbraco.NiceUrl(id); var response = Request.CreateResponse(HttpStatusCode.OK); - response.Content = new StringContent(url, Encoding.UTF8, "application/json"); + response.Content = new StringContent(url, Encoding.UTF8, "application/json"); return response; } @@ -179,18 +179,22 @@ namespace Umbraco.Web.Editors /// [FilterAllowedOutgoingContent(typeof(IEnumerable>), "Items")] public PagedResult> GetChildren( - int id, - int pageNumber = 0, //TODO: This should be '1' as it's not the index - int pageSize = 0, - string orderBy = "SortOrder", - Direction orderDirection = Direction.Ascending, - string filter = "") + int id, + int pageNumber = 0, //TODO: This should be '1' as it's not the index + int pageSize = 0, + string orderBy = "SortOrder", + Direction orderDirection = Direction.Ascending, + int? orderBySystemField = 2, + string filter = "") { + var orderBySystemFieldBool = orderBySystemField == 1 || orderBySystemField == 2; long totalChildren; IContent[] children; if (pageNumber > 0 && pageSize > 0) { - children = Services.ContentService.GetPagedChildren(id, (pageNumber - 1), pageSize, out totalChildren, orderBy, orderDirection, filter).ToArray(); + children = Services.ContentService + .GetPagedChildren(id, (pageNumber - 1), pageSize, out totalChildren + , orderBy, orderDirection, orderBySystemFieldBool, filter).ToArray(); } else { @@ -205,7 +209,7 @@ namespace Umbraco.Web.Editors var pagedResult = new PagedResult>(totalChildren, pageNumber, pageSize); pagedResult.Items = children - .Select(Mapper.Map>); + .Select(Mapper.Map>); return pagedResult; } @@ -213,7 +217,7 @@ namespace Umbraco.Web.Editors [Obsolete("Dont use this, it is incorrectly named, use HasPermission instead")] public bool GetHasPermission(string permissionToCheck, int nodeId) { - return HasPermission(permissionToCheck, nodeId); + return HasPermission(permissionToCheck, nodeId); } /// @@ -225,8 +229,8 @@ namespace Umbraco.Web.Editors public Dictionary GetPermissions(int[] nodeIds) { return Services.UserService - .GetPermissions(Security.CurrentUser, nodeIds) - .ToDictionary(x => x.EntityId, x => x.AssignedPermissions); + .GetPermissions(Security.CurrentUser, nodeIds) + .ToDictionary(x => x.EntityId, x => x.AssignedPermissions); } [HttpGet] @@ -240,7 +244,7 @@ namespace Umbraco.Web.Editors return false; } - + /// /// Saves content /// @@ -248,16 +252,16 @@ namespace Umbraco.Web.Editors [FileUploadCleanupFilter] [ContentPostValidate] public ContentItemDisplay PostSave( - [ModelBinder(typeof(ContentItemBinder))] - ContentItemSave contentItem) - { + [ModelBinder(typeof(ContentItemBinder))] + ContentItemSave contentItem) + { //If we've reached here it means: // * Our model has been bound // * and validated // * any file attachments have been saved to their temporary location for us to use // * we have a reference to the DTO object and the persisted object // * Permissions are valid - + MapPropertyValues(contentItem); //We need to manually check the validation results here because: @@ -275,7 +279,7 @@ namespace Umbraco.Web.Editors var forDisplay = Mapper.Map(contentItem.PersistedContent); forDisplay.Errors = ModelState.ToErrorDictionary(); throw new HttpResponseException(Request.CreateValidationErrorResponse(forDisplay)); - + } //if the model state is not valid we cannot publish so change it to save @@ -292,7 +296,7 @@ namespace Umbraco.Web.Editors //initialize this to successful var publishStatus = Attempt.Succeed(); - var wasCancelled = false; + var wasCancelled = false; if (contentItem.Action == ContentSaveAction.Save || contentItem.Action == ContentSaveAction.SaveNew) { @@ -327,8 +331,8 @@ namespace Umbraco.Web.Editors if (wasCancelled == false) { display.AddSuccessNotification( - Services.TextService.Localize("speechBubbles/editContentSavedHeader"), - Services.TextService.Localize("speechBubbles/editContentSavedText")); + Services.TextService.Localize("speechBubbles/editContentSavedHeader"), + Services.TextService.Localize("speechBubbles/editContentSavedText")); } else { @@ -340,8 +344,8 @@ namespace Umbraco.Web.Editors if (wasCancelled == false) { display.AddSuccessNotification( - Services.TextService.Localize("speechBubbles/editContentSendToPublish"), - Services.TextService.Localize("speechBubbles/editContentSendToPublishText")); + Services.TextService.Localize("speechBubbles/editContentSendToPublish"), + Services.TextService.Localize("speechBubbles/editContentSendToPublishText")); } else { @@ -366,7 +370,7 @@ namespace Umbraco.Web.Editors return display; } - + /// /// Publishes a document with a given ID /// @@ -392,7 +396,7 @@ namespace Umbraco.Web.Editors { var notificationModel = new SimpleNotificationModel(); ShowMessageForPublishStatus(publishResult.Result, notificationModel); - return Request.CreateValidationErrorResponse(notificationModel); + return Request.CreateValidationErrorResponse(notificationModel); } //return ok @@ -440,7 +444,7 @@ namespace Umbraco.Web.Editors //returning an object of INotificationModel will ensure that any pending // notification messages are added to the response. return Request.CreateValidationErrorResponse(new SimpleNotificationModel()); - } + } } return Request.CreateResponse(HttpStatusCode.OK); @@ -457,7 +461,7 @@ namespace Umbraco.Web.Editors [HttpPost] [EnsureUserPermissionForContent(Constants.System.RecycleBinContent)] public HttpResponseMessage EmptyRecycleBin() - { + { Services.ContentService.EmptyRecycleBin(); return Request.CreateResponse(HttpStatusCode.OK); } @@ -516,7 +520,7 @@ namespace Umbraco.Web.Editors var response = Request.CreateResponse(HttpStatusCode.OK); response.Content = new StringContent(toMove.Path, Encoding.UTF8, "application/json"); - return response; + return response; } /// @@ -548,7 +552,7 @@ namespace Umbraco.Web.Editors if (foundContent == null) HandleContentNotFound(id); - + var unpublishResult = Services.ContentService.WithResult().UnPublish(foundContent, Security.CurrentUser.Id); var content = Mapper.Map(foundContent); @@ -559,7 +563,7 @@ namespace Umbraco.Web.Editors throw new HttpResponseException(Request.CreateValidationErrorResponse(content)); } else - { + { content.AddSuccessNotification(Services.TextService.Localize("content/unPublish"), Services.TextService.Localize("speechBubbles/contentUnpublished")); return content; } @@ -597,8 +601,8 @@ namespace Umbraco.Web.Editors contentItem.PersistedContent.ReleaseDate = contentItem.ReleaseDate; //only set the template if it didn't change var templateChanged = (contentItem.PersistedContent.Template == null && contentItem.TemplateAlias.IsNullOrWhiteSpace() == false) - || (contentItem.PersistedContent.Template != null && contentItem.PersistedContent.Template.Alias != contentItem.TemplateAlias) - || (contentItem.PersistedContent.Template != null && contentItem.TemplateAlias.IsNullOrWhiteSpace()); + || (contentItem.PersistedContent.Template != null && contentItem.PersistedContent.Template.Alias != contentItem.TemplateAlias) + || (contentItem.PersistedContent.Template != null && contentItem.TemplateAlias.IsNullOrWhiteSpace()); if (templateChanged) { var template = Services.FileService.GetTemplate(contentItem.TemplateAlias); @@ -641,8 +645,8 @@ namespace Umbraco.Web.Editors if (toMove.ContentType.AllowedAsRoot == false) { throw new HttpResponseException( - Request.CreateNotificationValidationErrorResponse( - Services.TextService.Localize("moveOrCopy/notAllowedAtRoot"))); + Request.CreateNotificationValidationErrorResponse( + Services.TextService.Localize("moveOrCopy/notAllowedAtRoot"))); } } else @@ -655,19 +659,19 @@ namespace Umbraco.Web.Editors //check if the item is allowed under this one if (parent.ContentType.AllowedContentTypes.Select(x => x.Id).ToArray() - .Any(x => x.Value == toMove.ContentType.Id) == false) + .Any(x => x.Value == toMove.ContentType.Id) == false) { throw new HttpResponseException( - Request.CreateNotificationValidationErrorResponse( - Services.TextService.Localize("moveOrCopy/notAllowedByContentType"))); + Request.CreateNotificationValidationErrorResponse( + Services.TextService.Localize("moveOrCopy/notAllowedByContentType"))); } // Check on paths if ((string.Format(",{0},", parent.Path)).IndexOf(string.Format(",{0},", toMove.Id), StringComparison.Ordinal) > -1) - { + { throw new HttpResponseException( - Request.CreateNotificationValidationErrorResponse( - Services.TextService.Localize("moveOrCopy/notAllowedByPath"))); + Request.CreateNotificationValidationErrorResponse( + Services.TextService.Localize("moveOrCopy/notAllowedByPath"))); } } @@ -681,52 +685,52 @@ namespace Umbraco.Web.Editors case PublishStatusType.Success: case PublishStatusType.SuccessAlreadyPublished: display.AddSuccessNotification( - Services.TextService.Localize("speechBubbles/editContentPublishedHeader"), - Services.TextService.Localize("speechBubbles/editContentPublishedText")); + Services.TextService.Localize("speechBubbles/editContentPublishedHeader"), + Services.TextService.Localize("speechBubbles/editContentPublishedText")); break; case PublishStatusType.FailedPathNotPublished: display.AddWarningNotification( - Services.TextService.Localize("publish"), - Services.TextService.Localize("publish/contentPublishedFailedByParent", - new[] {string.Format("{0} ({1})", status.ContentItem.Name, status.ContentItem.Id)}).Trim()); + Services.TextService.Localize("publish"), + Services.TextService.Localize("publish/contentPublishedFailedByParent", + new[] { string.Format("{0} ({1})", status.ContentItem.Name, status.ContentItem.Id) }).Trim()); break; case PublishStatusType.FailedCancelledByEvent: AddCancelMessage(display, "publish", "speechBubbles/contentPublishedFailedByEvent"); - break; + break; case PublishStatusType.FailedAwaitingRelease: display.AddWarningNotification( - Services.TextService.Localize("publish"), - Services.TextService.Localize("publish/contentPublishedFailedAwaitingRelease", - new[] {string.Format("{0} ({1})", status.ContentItem.Name, status.ContentItem.Id)}).Trim()); + Services.TextService.Localize("publish"), + Services.TextService.Localize("publish/contentPublishedFailedAwaitingRelease", + new[] { string.Format("{0} ({1})", status.ContentItem.Name, status.ContentItem.Id) }).Trim()); break; case PublishStatusType.FailedHasExpired: display.AddWarningNotification( - Services.TextService.Localize("publish"), - Services.TextService.Localize("publish/contentPublishedFailedExpired", - new[] - { - string.Format("{0} ({1})", status.ContentItem.Name, status.ContentItem.Id), - }).Trim()); + Services.TextService.Localize("publish"), + Services.TextService.Localize("publish/contentPublishedFailedExpired", + new[] + { + string.Format("{0} ({1})", status.ContentItem.Name, status.ContentItem.Id), + }).Trim()); break; case PublishStatusType.FailedIsTrashed: //TODO: We should add proper error messaging for this! break; case PublishStatusType.FailedContentInvalid: display.AddWarningNotification( - Services.TextService.Localize("publish"), - Services.TextService.Localize("publish/contentPublishedFailedInvalid", - new[] - { - string.Format("{0} ({1})", status.ContentItem.Name, status.ContentItem.Id), - string.Join(",", status.InvalidProperties.Select(x => x.Alias)) - }).Trim()); + Services.TextService.Localize("publish"), + Services.TextService.Localize("publish/contentPublishedFailedInvalid", + new[] + { + string.Format("{0} ({1})", status.ContentItem.Name, status.ContentItem.Id), + string.Join(",", status.InvalidProperties.Select(x => x.Alias)) + }).Trim()); break; default: throw new IndexOutOfRangeException(); } } - + /// /// Performs a permissions check for the user to check if it has access to the node based on @@ -741,15 +745,15 @@ namespace Umbraco.Web.Editors /// Specifies the already resolved content item to check against /// internal static bool CheckPermissions( - IDictionary storage, - IUser user, - IUserService userService, - IContentService contentService, - int nodeId, - char[] permissionsToCheck = null, - IContent contentItem = null) + IDictionary storage, + IUser user, + IUserService userService, + IContentService contentService, + int nodeId, + char[] permissionsToCheck = null, + IContent contentItem = null) { - + if (contentItem == null && nodeId != Constants.System.Root && nodeId != Constants.System.RecycleBinContent) { contentItem = contentService.GetById(nodeId); @@ -764,16 +768,16 @@ namespace Umbraco.Web.Editors } var hasPathAccess = (nodeId == Constants.System.Root) - ? UserExtensions.HasPathAccess( - Constants.System.Root.ToInvariantString(), - user.StartContentId, - Constants.System.RecycleBinContent) - : (nodeId == Constants.System.RecycleBinContent) - ? UserExtensions.HasPathAccess( - Constants.System.RecycleBinContent.ToInvariantString(), - user.StartContentId, - Constants.System.RecycleBinContent) - : user.HasPathAccess(contentItem); + ? UserExtensions.HasPathAccess( + Constants.System.Root.ToInvariantString(), + user.StartContentId, + Constants.System.RecycleBinContent) + : (nodeId == Constants.System.RecycleBinContent) + ? UserExtensions.HasPathAccess( + Constants.System.RecycleBinContent.ToInvariantString(), + user.StartContentId, + Constants.System.RecycleBinContent) + : user.HasPathAccess(contentItem); if (hasPathAccess == false) { diff --git a/src/Umbraco.Web/Editors/MediaController.cs b/src/Umbraco.Web/Editors/MediaController.cs index 66889e2206..060fb06750 100644 --- a/src/Umbraco.Web/Editors/MediaController.cs +++ b/src/Umbraco.Web/Editors/MediaController.cs @@ -52,7 +52,7 @@ namespace Umbraco.Web.Editors /// public MediaController() : this(UmbracoContext.Current) - { + { } /// @@ -156,7 +156,7 @@ namespace Umbraco.Web.Editors var folderTypes = Services.ContentTypeService.GetAllMediaTypes().ToArray().Where(x => x.Alias.EndsWith("Folder")).Select(x => x.Id); var children = (id < 0) ? Services.MediaService.GetRootMedia() : Services.MediaService.GetById(id).Children(); - return children.Where(x => folderTypes.Contains(x.ContentTypeId)).Select(Mapper.Map>); + return children.Where(x => folderTypes.Contains(x.ContentTypeId)).Select(Mapper.Map>); } /// @@ -180,13 +180,16 @@ namespace Umbraco.Web.Editors int pageSize = 0, string orderBy = "SortOrder", Direction orderDirection = Direction.Ascending, + bool orderBySystemField = true, string filter = "") { int totalChildren; IMedia[] children; if (pageNumber > 0 && pageSize > 0) { - children = Services.MediaService.GetPagedChildren(id, (pageNumber - 1), pageSize, out totalChildren, orderBy, orderDirection, filter).ToArray(); + children = Services.MediaService + .GetPagedChildren(id, (pageNumber - 1), pageSize, out totalChildren + , orderBy, orderDirection, orderBySystemField, filter).ToArray(); } else { @@ -261,7 +264,7 @@ namespace Umbraco.Web.Editors var response = Request.CreateResponse(HttpStatusCode.OK); response.Content = new StringContent(toMove.Path, Encoding.UTF8, "application/json"); - return response; + return response; } /// @@ -307,7 +310,7 @@ namespace Umbraco.Web.Editors //return the updated model var display = Mapper.Map(contentItem.PersistedContent); - + //lasty, if it is not valid, add the modelstate to the outgoing object and throw a 403 HandleInvalidModelState(display); @@ -334,8 +337,8 @@ namespace Umbraco.Web.Editors throw new HttpResponseException(Request.CreateValidationErrorResponse(display)); } } - - break; + + break; } return display; @@ -364,7 +367,7 @@ namespace Umbraco.Web.Editors Services.MediaService.EmptyRecycleBin(); return Request.CreateResponse(HttpStatusCode.OK); } - + /// /// Change the sort order for media /// @@ -383,7 +386,7 @@ namespace Umbraco.Web.Editors { return Request.CreateResponse(HttpStatusCode.OK); } - + var mediaService = base.ApplicationContext.Services.MediaService; var sortedMedia = new List(); try @@ -405,7 +408,7 @@ namespace Umbraco.Web.Editors } } - [EnsureUserPermissionForMedia("folder.ParentId")] + [EnsureUserPermissionForMedia("folder.ParentId")] public MediaItemDisplay PostAddFolder(EntityBasic folder) { var mediaService = ApplicationContext.Services.MediaService; @@ -436,7 +439,7 @@ namespace Umbraco.Web.Editors var provider = new MultipartFormDataStreamProvider(root); var result = await Request.Content.ReadAsMultipartAsync(provider); - + //must have a file if (result.FileData.Count == 0) { @@ -449,10 +452,10 @@ namespace Umbraco.Web.Editors { return Request.CreateValidationErrorResponse("The request was not formatted correctly, the currentFolder is not an integer"); } - + //ensure the user has access to this folder by parent id! if (CheckPermissions( - new Dictionary(), + new Dictionary(), Security.CurrentUser, Services.MediaService, parentId) == false) { @@ -463,65 +466,65 @@ namespace Umbraco.Web.Editors Services.TextService.Localize("speechBubbles/invalidUserPermissionsText"), SpeechBubbleIcon.Warning))); } - + var tempFiles = new PostedFiles(); var mediaService = ApplicationContext.Services.MediaService; //in case we pass a path with a folder in it, we will create it and upload media to it. - if (result.FormData.ContainsKey("path")) - { + if (result.FormData.ContainsKey("path")) + { - var folders = result.FormData["path"].Split('/'); + var folders = result.FormData["path"].Split('/'); - for (int i = 0; i < folders.Length - 1; i++) - { - var folderName = folders[i]; - IMedia folderMediaItem; + for (int i = 0; i < folders.Length - 1; i++) + { + var folderName = folders[i]; + IMedia folderMediaItem; - //if uploading directly to media root and not a subfolder - if (parentId == -1) - { - //look for matching folder - folderMediaItem = - mediaService.GetRootMedia().FirstOrDefault(x => x.Name == folderName && x.ContentType.Alias == Constants.Conventions.MediaTypes.Folder); - if (folderMediaItem == null) - { - //if null, create a folder - folderMediaItem = mediaService.CreateMedia(folderName, -1, Constants.Conventions.MediaTypes.Folder); - mediaService.Save(folderMediaItem); - } - } - else - { - //get current parent - var mediaRoot = mediaService.GetById(parentId); + //if uploading directly to media root and not a subfolder + if (parentId == -1) + { + //look for matching folder + folderMediaItem = + mediaService.GetRootMedia().FirstOrDefault(x => x.Name == folderName && x.ContentType.Alias == Constants.Conventions.MediaTypes.Folder); + if (folderMediaItem == null) + { + //if null, create a folder + folderMediaItem = mediaService.CreateMedia(folderName, -1, Constants.Conventions.MediaTypes.Folder); + mediaService.Save(folderMediaItem); + } + } + else + { + //get current parent + var mediaRoot = mediaService.GetById(parentId); - //if the media root is null, something went wrong, we'll abort - if (mediaRoot == null) - return Request.CreateErrorResponse(HttpStatusCode.InternalServerError, - "The folder: " + folderName + " could not be used for storing images, its ID: " + parentId + - " returned null"); + //if the media root is null, something went wrong, we'll abort + if (mediaRoot == null) + return Request.CreateErrorResponse(HttpStatusCode.InternalServerError, + "The folder: " + folderName + " could not be used for storing images, its ID: " + parentId + + " returned null"); - //look for matching folder - folderMediaItem = mediaRoot.Children().FirstOrDefault(x => x.Name == folderName && x.ContentType.Alias == Constants.Conventions.MediaTypes.Folder); - if (folderMediaItem == null) - { - //if null, create a folder - folderMediaItem = mediaService.CreateMedia(folderName, mediaRoot, Constants.Conventions.MediaTypes.Folder); - mediaService.Save(folderMediaItem); - } - } - //set the media root to the folder id so uploaded files will end there. - parentId = folderMediaItem.Id; - } - } + //look for matching folder + folderMediaItem = mediaRoot.Children().FirstOrDefault(x => x.Name == folderName && x.ContentType.Alias == Constants.Conventions.MediaTypes.Folder); + if (folderMediaItem == null) + { + //if null, create a folder + folderMediaItem = mediaService.CreateMedia(folderName, mediaRoot, Constants.Conventions.MediaTypes.Folder); + mediaService.Save(folderMediaItem); + } + } + //set the media root to the folder id so uploaded files will end there. + parentId = folderMediaItem.Id; + } + } - //get the files + //get the files foreach (var file in result.FileData) { var fileName = file.Headers.ContentDisposition.FileName.Trim(new[] { '\"' }); - var ext = fileName.Substring(fileName.LastIndexOf('.')+1).ToLower(); + var ext = fileName.Substring(fileName.LastIndexOf('.') + 1).ToLower(); if (UmbracoConfig.For.UmbracoSettings().Content.DisallowedUploadFiles.Contains(ext) == false) { @@ -583,7 +586,7 @@ namespace Umbraco.Web.Editors if (origin.Value == "blueimp") { return Request.CreateResponse(HttpStatusCode.OK, - tempFiles, + tempFiles, //Don't output the angular xsrf stuff, blue imp doesn't like that new JsonMediaTypeFormatter()); } diff --git a/src/Umbraco.Web/Editors/MemberController.cs b/src/Umbraco.Web/Editors/MemberController.cs index 909189b7ad..2a3bb4399b 100644 --- a/src/Umbraco.Web/Editors/MemberController.cs +++ b/src/Umbraco.Web/Editors/MemberController.cs @@ -73,15 +73,16 @@ namespace Umbraco.Web.Editors get { return Services.MemberService.GetMembershipScenario(); } } - public PagedResult GetPagedResults( + public PagedResult GetPagedResults( int pageNumber = 1, int pageSize = 100, string orderBy = "Name", Direction orderDirection = Direction.Ascending, + bool orderBySystemField = true, string filter = "", string memberTypeAlias = null) { - + if (pageNumber <= 0 || pageSize <= 0) { throw new NotSupportedException("Both pageNumber and pageSize must be greater than zero"); @@ -90,7 +91,9 @@ namespace Umbraco.Web.Editors if (MembershipScenario == MembershipScenario.NativeUmbraco) { long totalRecords; - var members = Services.MemberService.GetAll((pageNumber - 1), pageSize, out totalRecords, orderBy, orderDirection, memberTypeAlias, filter).ToArray(); + var members = Services.MemberService + .GetAll((pageNumber - 1), pageSize, out totalRecords, orderBy, orderDirection, orderBySystemField + , memberTypeAlias, filter).ToArray(); if (totalRecords == 0) { return new PagedResult(0, 0, 0); @@ -114,7 +117,7 @@ namespace Umbraco.Web.Editors .Select(Mapper.Map); return pagedResult; } - + } /// @@ -162,23 +165,23 @@ namespace Umbraco.Web.Editors return Mapper.Map(foundMember); case MembershipScenario.CustomProviderWithUmbracoLink: - //TODO: Support editing custom properties for members with a custom membership provider here. + //TODO: Support editing custom properties for members with a custom membership provider here. - //foundMember = Services.MemberService.GetByKey(key); - //if (foundMember == null) - //{ - // HandleContentNotFound(key); - //} - //foundMembershipMember = Membership.GetUser(key, false); - //if (foundMembershipMember == null) - //{ - // HandleContentNotFound(key); - //} + //foundMember = Services.MemberService.GetByKey(key); + //if (foundMember == null) + //{ + // HandleContentNotFound(key); + //} + //foundMembershipMember = Membership.GetUser(key, false); + //if (foundMembershipMember == null) + //{ + // HandleContentNotFound(key); + //} - //display = Mapper.Map(foundMembershipMember); - ////map the name over - //display.Name = foundMember.Name; - //return display; + //display = Mapper.Map(foundMembershipMember); + ////map the name over + //display.Name = foundMember.Name; + //return display; case MembershipScenario.StandaloneCustomProvider: default: @@ -219,7 +222,7 @@ namespace Umbraco.Web.Editors emptyContent.AdditionalData["NewPassword"] = Membership.GeneratePassword(Membership.MinRequiredPasswordLength, Membership.MinRequiredNonAlphanumericCharacters); return Mapper.Map(emptyContent); case MembershipScenario.CustomProviderWithUmbracoLink: - //TODO: Support editing custom properties for members with a custom membership provider here. + //TODO: Support editing custom properties for members with a custom membership provider here. case MembershipScenario.StandaloneCustomProvider: default: @@ -275,7 +278,7 @@ namespace Umbraco.Web.Editors { throw new NotSupportedException("Currently the member editor does not support providers that have RequiresQuestionAndAnswer specified"); } - + //We're gonna look up the current roles now because the below code can cause // events to be raised and developers could be manually adding roles to members in // their handlers. If we don't look this up now there's a chance we'll just end up @@ -324,7 +327,7 @@ namespace Umbraco.Web.Editors //create/save the IMember Services.MemberService.Save(contentItem.PersistedContent); } - + //Now let's do the role provider stuff - now that we've saved the content item (that is important since // if we are changing the username, it must be persisted before looking up the member roles). if (rolesToRemove.Any()) @@ -504,7 +507,7 @@ namespace Umbraco.Web.Editors private void RefetchMemberData(MemberSave contentItem, LookupType lookup) { var currProps = contentItem.PersistedContent.Properties.ToArray(); - + switch (MembershipScenario) { case MembershipScenario.NativeUmbraco: @@ -512,11 +515,11 @@ namespace Umbraco.Web.Editors { case LookupType.ByKey: //Go and re-fetch the persisted item - contentItem.PersistedContent = Services.MemberService.GetByKey(contentItem.Key); + contentItem.PersistedContent = Services.MemberService.GetByKey(contentItem.Key); break; case LookupType.ByUserName: contentItem.PersistedContent = Services.MemberService.GetByUsername(contentItem.Username.Trim()); - break; + break; } break; case MembershipScenario.CustomProviderWithUmbracoLink: @@ -524,14 +527,14 @@ namespace Umbraco.Web.Editors default: var membershipUser = _provider.GetUser(contentItem.Key, false); //Go and re-fetch the persisted item - contentItem.PersistedContent = Mapper.Map(membershipUser); + contentItem.PersistedContent = Mapper.Map(membershipUser); break; } UpdateName(contentItem); //re-assign the mapped values that are not part of the membership provider properties. - var builtInAliases = Constants.Conventions.Member.GetStandardPropertyTypeStubs().Select(x => x.Key).ToArray(); + var builtInAliases = Constants.Conventions.Member.GetStandardPropertyTypeStubs().Select(x => x.Key).ToArray(); foreach (var p in contentItem.PersistedContent.Properties) { var valueMapped = currProps.SingleOrDefault(x => x.Alias == p.Alias); @@ -542,7 +545,7 @@ namespace Umbraco.Web.Editors p.TagSupport.Enable = valueMapped.TagSupport.Enable; p.TagSupport.Tags = valueMapped.TagSupport.Tags; } - } + } } /// @@ -595,7 +598,7 @@ namespace Umbraco.Web.Editors contentItem.IsApproved, Guid.NewGuid(), //since it's the umbraco provider, the user key here doesn't make any difference out status); - + break; case MembershipScenario.CustomProviderWithUmbracoLink: //We are using a custom membership provider, we'll create an empty IMember first to get the unique id to use @@ -618,7 +621,7 @@ namespace Umbraco.Web.Editors case MembershipScenario.StandaloneCustomProvider: // we don't have a member type to use so we will just create the basic membership user with the provider with no // link back to the umbraco data - + var newKey = Guid.NewGuid(); //TODO: We are not supporting q/a - passing in empty here membershipUser = _provider.CreateUser( @@ -628,9 +631,9 @@ namespace Umbraco.Web.Editors "TEMP", //some membership provider's require something here even if q/a is disabled! "TEMP", //some membership provider's require something here even if q/a is disabled! contentItem.IsApproved, - newKey, + newKey, out status); - + break; default: throw new ArgumentOutOfRangeException(); @@ -685,12 +688,12 @@ namespace Umbraco.Web.Editors break; case MembershipCreateStatus.InvalidProviderUserKey: ModelState.AddPropertyError( - //specify 'default' just so that it shows up as a notification - is not assigned to a property + //specify 'default' just so that it shows up as a notification - is not assigned to a property new ValidationResult("Invalid provider user key"), "default"); break; case MembershipCreateStatus.DuplicateProviderUserKey: ModelState.AddPropertyError( - //specify 'default' just so that it shows up as a notification - is not assigned to a property + //specify 'default' just so that it shows up as a notification - is not assigned to a property new ValidationResult("Duplicate provider user key"), "default"); break; case MembershipCreateStatus.ProviderError: diff --git a/src/Umbraco.Web/WebApi/Filters/DisableBrowserCacheAttribute.cs b/src/Umbraco.Web/WebApi/Filters/DisableBrowserCacheAttribute.cs index e1890326fb..edd1792e0e 100644 --- a/src/Umbraco.Web/WebApi/Filters/DisableBrowserCacheAttribute.cs +++ b/src/Umbraco.Web/WebApi/Filters/DisableBrowserCacheAttribute.cs @@ -1,50 +1,54 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using System.Web; -using System.Web.Http.Controllers; -using System.Web.Http.Filters; -using Umbraco.Core; - -namespace Umbraco.Web.WebApi.Filters -{ - /// - /// Ensures that the request is not cached by the browser - /// - public class DisableBrowserCacheAttribute : ActionFilterAttribute - { - public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext) - { - //See: http://stackoverflow.com/questions/17755239/how-to-stop-chrome-from-caching-rest-response-from-webapi - - base.OnActionExecuted(actionExecutedContext); - - //NOTE: Until we upgraded to WebApi 2, this didn't work correctly and we had to revert to using - // HttpContext.Current responses. I've changed this back to what it should be now since it works - // and now with WebApi2, the HttpContext.Current responses dont! Anyways, all good now. - actionExecutedContext.Response.Headers.CacheControl = new CacheControlHeaderValue() - { - NoCache = true, - NoStore = true, - MaxAge = new TimeSpan(0), - MustRevalidate = true - }; - - actionExecutedContext.Response.Headers.Pragma.Add(new NameValueHeaderValue("no-cache")); - if (actionExecutedContext.Response.Content != null) - { - actionExecutedContext.Response.Content.Headers.Expires = - //Mon, 01 Jan 1990 00:00:00 GMT - new DateTimeOffset(1990, 1, 1, 0, 0, 0, TimeSpan.Zero); - } - - - - } - } -} +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Web; +using System.Web.Http.Controllers; +using System.Web.Http.Filters; +using Umbraco.Core; + +namespace Umbraco.Web.WebApi.Filters +{ + /// + /// Ensures that the request is not cached by the browser + /// + public class DisableBrowserCacheAttribute : ActionFilterAttribute + { + public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext) + { + //See: http://stackoverflow.com/questions/17755239/how-to-stop-chrome-from-caching-rest-response-from-webapi + + base.OnActionExecuted(actionExecutedContext); + if (actionExecutedContext == null || actionExecutedContext.Response == null || + actionExecutedContext.Response.Headers == null) + { + return; + } + //NOTE: Until we upgraded to WebApi 2, this didn't work correctly and we had to revert to using + // HttpContext.Current responses. I've changed this back to what it should be now since it works + // and now with WebApi2, the HttpContext.Current responses dont! Anyways, all good now. + actionExecutedContext.Response.Headers.CacheControl = new CacheControlHeaderValue() + { + NoCache = true, + NoStore = true, + MaxAge = new TimeSpan(0), + MustRevalidate = true + }; + + actionExecutedContext.Response.Headers.Pragma.Add(new NameValueHeaderValue("no-cache")); + if (actionExecutedContext.Response.Content != null) + { + actionExecutedContext.Response.Content.Headers.Expires = + //Mon, 01 Jan 1990 00:00:00 GMT + new DateTimeOffset(1990, 1, 1, 0, 0, 0, TimeSpan.Zero); + } + + + + } + } +} diff --git a/src/UmbracoExamine/UmbracoMemberIndexer.cs b/src/UmbracoExamine/UmbracoMemberIndexer.cs index 873391da50..4a460e0f38 100644 --- a/src/UmbracoExamine/UmbracoMemberIndexer.cs +++ b/src/UmbracoExamine/UmbracoMemberIndexer.cs @@ -16,8 +16,8 @@ using Lucene.Net.Analysis; namespace UmbracoExamine { - - /// + + /// /// Custom indexer for members /// public class UmbracoMemberIndexer : UmbracoContentIndexer @@ -26,29 +26,29 @@ namespace UmbracoExamine private readonly IMemberService _memberService; private readonly IDataTypeService _dataTypeService; - /// - /// Default constructor - /// - public UmbracoMemberIndexer() : base() - { + /// + /// Default constructor + /// + public UmbracoMemberIndexer() : base() + { _dataTypeService = ApplicationContext.Current.Services.DataTypeService; _memberService = ApplicationContext.Current.Services.MemberService; - } + } - /// - /// Constructor to allow for creating an indexer at runtime - /// - /// - /// - /// - /// - [Obsolete("Use the overload that specifies the Umbraco services")] - public UmbracoMemberIndexer(IIndexCriteria indexerData, DirectoryInfo indexPath, IDataService dataService, Analyzer analyzer, bool async) - : base(indexerData, indexPath, dataService, analyzer, async) - { + /// + /// Constructor to allow for creating an indexer at runtime + /// + /// + /// + /// + /// + [Obsolete("Use the overload that specifies the Umbraco services")] + public UmbracoMemberIndexer(IIndexCriteria indexerData, DirectoryInfo indexPath, IDataService dataService, Analyzer analyzer, bool async) + : base(indexerData, indexPath, dataService, analyzer, async) + { _dataTypeService = ApplicationContext.Current.Services.DataTypeService; _memberService = ApplicationContext.Current.Services.MemberService; - } + } /// /// Constructor to allow for creating an indexer at runtime @@ -60,19 +60,19 @@ namespace UmbracoExamine /// /// /// - public UmbracoMemberIndexer(IIndexCriteria indexerData, DirectoryInfo indexPath, IDataService dataService, - IDataTypeService dataTypeService, - IMemberService memberService, - Analyzer analyzer, bool async) - : base(indexerData, indexPath, dataService, analyzer, async) - { + public UmbracoMemberIndexer(IIndexCriteria indexerData, DirectoryInfo indexPath, IDataService dataService, + IDataTypeService dataTypeService, + IMemberService memberService, + Analyzer analyzer, bool async) + : base(indexerData, indexPath, dataService, analyzer, async) + { _dataTypeService = dataTypeService; _memberService = memberService; - } + } - - /// + + /// /// Ensures that the'_searchEmail' is added to the user fields so that it is indexed - without having to modify the config /// /// @@ -87,7 +87,7 @@ namespace UmbracoExamine if (indexerData.UserFields.Any(x => x.Name == "_searchEmail") == false) { - var field = new IndexField {Name = "_searchEmail"}; + var field = new IndexField { Name = "_searchEmail" }; var policy = IndexFieldPolicies.FirstOrDefault(x => x.Name == "_searchEmail"); if (policy != null) { @@ -97,7 +97,7 @@ namespace UmbracoExamine return new IndexCriteria( indexerData.StandardFields, - indexerData.UserFields.Concat(new[] {field}), + indexerData.UserFields.Concat(new[] { field }), indexerData.IncludeNodeTypes, indexerData.ExcludeNodeTypes, indexerData.ParentNodeId @@ -105,10 +105,10 @@ namespace UmbracoExamine } } - return indexerData; + return indexerData; } - /// + /// /// The supported types for this indexer /// protected override IEnumerable SupportedTypes @@ -119,39 +119,40 @@ namespace UmbracoExamine } } - /// - /// Reindex all members - /// - /// - protected override void PerformIndexAll(string type) - { + /// + /// Reindex all members + /// + /// + protected override void PerformIndexAll(string type) + { //This only supports members if (SupportedTypes.Contains(type) == false) return; - + const int pageSize = 1000; var pageIndex = 0; IMember[] members; if (IndexerData.IncludeNodeTypes.Any()) - { + { //if there are specific node types then just index those foreach (var nodeType in IndexerData.IncludeNodeTypes) - { + { do { long total; - members = _memberService.GetAll(pageIndex, pageSize, out total, "LoginName", Direction.Ascending, nodeType).ToArray(); + members = _memberService.GetAll(pageIndex, pageSize, out total, "LoginName" + , Direction.Ascending, true, nodeType).ToArray(); AddNodesToIndex(GetSerializedMembers(members), type); pageIndex++; } while (members.Length == pageSize); - } - } - else - { + } + } + else + { //no node types specified, do all members do { @@ -162,8 +163,8 @@ namespace UmbracoExamine pageIndex++; } while (members.Length == pageSize); - } - } + } + } private IEnumerable GetSerializedMembers(IEnumerable members) { @@ -171,18 +172,18 @@ namespace UmbracoExamine return members.Select(member => serializer.Serialize(_dataTypeService, member)); } - protected override XDocument GetXDocument(string xPath, string type) - { - throw new NotSupportedException(); - } - + protected override XDocument GetXDocument(string xPath, string type) + { + throw new NotSupportedException(); + } + protected override Dictionary GetSpecialFieldsToIndex(Dictionary allValuesForIndexing) { var fields = base.GetSpecialFieldsToIndex(allValuesForIndexing); //adds the special path property to the index fields.Add("__key", allValuesForIndexing["__key"]); - + return fields; } @@ -207,14 +208,14 @@ namespace UmbracoExamine if (e.Fields.ContainsKey("_searchEmail") == false) e.Fields.Add("_searchEmail", e.Node.Attribute("email").Value.Replace(".", " ").Replace("@", " ")); } - + if (e.Fields.ContainsKey(IconFieldName) == false) e.Fields.Add(IconFieldName, (string)e.Node.Attribute("icon")); } private static XElement GetMemberItem(int nodeId) { - //TODO: Change this so that it is not using the LegacyLibrary, just serialize manually! + //TODO: Change this so that it is not using the LegacyLibrary, just serialize manually! var nodes = LegacyLibrary.GetMember(nodeId); return XElement.Parse(nodes.Current.OuterXml); } From 80d7443bd3b255281cbdbce1ee66f590175bf504 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 5 Apr 2016 16:00:49 +0200 Subject: [PATCH 13/46] fix build --- .../Repositories/ContentRepository.cs | 3 +- src/Umbraco.Core/Services/ContentService.cs | 45 +++++-------------- src/Umbraco.Core/Services/MediaService.cs | 40 ++++------------- src/Umbraco.Core/Services/MemberService.cs | 6 +-- src/Umbraco.Web.UI/config/trees.config | 1 + src/Umbraco.Web/Editors/MediaController.cs | 2 +- src/Umbraco.Web/Editors/MemberController.cs | 3 +- src/UmbracoExamine/UmbracoMemberIndexer.cs | 3 +- 8 files changed, 27 insertions(+), 76 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs index 5c66f2a19c..e0e9f0ec52 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs @@ -234,7 +234,6 @@ namespace Umbraco.Core.Persistence.Repositories var processed = 0; do { - var descendants = GetPagedResultsByQuery(query, pageIndex, pageSize, out total, "Path", Direction.Ascending, true); //NOTE: This is an important call, we cannot simply make a call to: // GetPagedResultsByQuery(query, pageIndex, pageSize, out total, "Path", Direction.Ascending); // because that method is used to query 'latest' content items where in this case we don't necessarily @@ -242,7 +241,7 @@ namespace Umbraco.Core.Persistence.Repositories // see: http://issues.umbraco.org/issue/U4-6322 & http://issues.umbraco.org/issue/U4-5982 var descendants = GetPagedResultsByQuery(query, pageIndex, pageSize, out total, new Tuple("cmsDocument", "nodeId"), - ProcessQuery, "Path", Direction.Ascending); + ProcessQuery, "Path", Direction.Ascending, true); var xmlItems = (from descendant in descendants let xml = serializer(descendant) diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index ed9dab8e29..c56d9360b3 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -487,22 +487,10 @@ namespace Umbraco.Core.Services public IEnumerable GetPagedChildren(int id, int pageIndex, int pageSize, out int totalChildren, string orderBy, Direction orderDirection, string filter = "") { - Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); - Mandate.ParameterCondition(pageSize > 0, "pageSize"); - using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) - { - - var query = Query.Builder; - //if the id is System Root, then just get all - if (id != Constants.System.Root) - { - query.Where(x => x.ParentId == id); - } - long total; - var contents = repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out total, orderBy, orderDirection, filter); - totalChildren = Convert.ToInt32(total); - return contents; - } + long total; + var result = GetPagedChildren(id, Convert.ToInt64(pageIndex), pageSize, out total, orderBy, orderDirection, true, filter); + totalChildren = Convert.ToInt32(total); + return result; } /// @@ -541,22 +529,10 @@ namespace Umbraco.Core.Services [EditorBrowsable(EditorBrowsableState.Never)] public IEnumerable GetPagedDescendants(int id, int pageIndex, int pageSize, out int totalChildren, string orderBy = "Path", Direction orderDirection = Direction.Ascending, string filter = "") { - Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); - Mandate.ParameterCondition(pageSize > 0, "pageSize"); - using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) - { - - var query = Query.Builder; - //if the id is System Root, then just get all - if (id != Constants.System.Root) - { - query.Where(x => x.Path.SqlContains(string.Format(",{0},", id), TextColumnType.NVarchar)); - } - long total; - var contents = repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out total, orderBy, orderDirection, filter); - totalChildren = Convert.ToInt32(total); - return contents; - } + long total; + var result = GetPagedDescendants(id, Convert.ToInt64(pageIndex), pageSize, out total, orderBy, orderDirection, true, filter); + totalChildren = Convert.ToInt32(total); + return result; } /// @@ -570,9 +546,8 @@ namespace Umbraco.Core.Services /// Direction to order by /// Flag to indicate when ordering by system field /// Search text filter - /// An Enumerable list of objects - public IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalChildren, string orderBy = "Path", Direction orderDirection = Direction.Ascending, string filter = "") - public IEnumerable GetPagedDescendants(int id, int pageIndex, int pageSize, out int totalChildren, string orderBy = "Path", Direction orderDirection = Direction.Ascending, bool orderBySystemField = true, string filter = "") + /// An Enumerable list of objects + public IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalChildren, string orderBy = "Path", Direction orderDirection = Direction.Ascending, bool orderBySystemField = true, string filter = "") { Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); Mandate.ParameterCondition(pageSize > 0, "pageSize"); diff --git a/src/Umbraco.Core/Services/MediaService.cs b/src/Umbraco.Core/Services/MediaService.cs index b60a66451e..64a22df06f 100644 --- a/src/Umbraco.Core/Services/MediaService.cs +++ b/src/Umbraco.Core/Services/MediaService.cs @@ -397,19 +397,10 @@ namespace Umbraco.Core.Services public IEnumerable GetPagedChildren(int id, int pageIndex, int pageSize, out int totalChildren, string orderBy, Direction orderDirection, string filter = "") { - Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); - Mandate.ParameterCondition(pageSize > 0, "pageSize"); - using (var repository = RepositoryFactory.CreateMediaRepository(UowProvider.GetUnitOfWork())) - { - var query = Query.Builder; - query.Where(x => x.ParentId == id); - - long total; - var medias = repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out total, orderBy, orderDirection, filter); - - totalChildren = Convert.ToInt32(total); - return medias; - } + long total; + var result = GetPagedChildren(id, Convert.ToInt64(pageIndex), pageSize, out total, orderBy, orderDirection, true, filter); + totalChildren = Convert.ToInt32(total); + return result; } /// @@ -444,22 +435,10 @@ namespace Umbraco.Core.Services [EditorBrowsable(EditorBrowsableState.Never)] public IEnumerable GetPagedDescendants(int id, int pageIndex, int pageSize, out int totalChildren, string orderBy = "Path", Direction orderDirection = Direction.Ascending, string filter = "") { - Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); - Mandate.ParameterCondition(pageSize > 0, "pageSize"); - using (var repository = RepositoryFactory.CreateMediaRepository(UowProvider.GetUnitOfWork())) - { - - var query = Query.Builder; - //if the id is -1, then just get all - if (id != -1) - { - query.Where(x => x.Path.SqlContains(string.Format(",{0},", id), TextColumnType.NVarchar)); - } - long total; - var contents = repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out total, orderBy, orderDirection, filter); - totalChildren = Convert.ToInt32(total); - return contents; - } + long total; + var result = GetPagedDescendants(id, Convert.ToInt64(pageIndex), pageSize, out total, orderBy, orderDirection, true, filter); + totalChildren = Convert.ToInt32(total); + return result; } /// @@ -474,8 +453,7 @@ namespace Umbraco.Core.Services /// Flag to indicate when ordering by system field /// Search text filter /// An Enumerable list of objects - public IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalChildren, string orderBy = "Path", Direction orderDirection = Direction.Ascending, string filter = "") - public IEnumerable GetPagedDescendants(int id, int pageIndex, int pageSize, out int totalChildren, string orderBy = "Path", Direction orderDirection = Direction.Ascending, bool orderBySystemField = true, string filter = "") + public IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalChildren, string orderBy = "Path", Direction orderDirection = Direction.Ascending, bool orderBySystemField = true, string filter = "") { Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); Mandate.ParameterCondition(pageSize > 0, "pageSize"); diff --git a/src/Umbraco.Core/Services/MemberService.cs b/src/Umbraco.Core/Services/MemberService.cs index db3bfbbd61..47fe52ada9 100644 --- a/src/Umbraco.Core/Services/MemberService.cs +++ b/src/Umbraco.Core/Services/MemberService.cs @@ -695,16 +695,16 @@ namespace Umbraco.Core.Services [Obsolete("Use the overload with 'long' parameter types instead")] [EditorBrowsable(EditorBrowsableState.Never)] public IEnumerable GetAll(int pageIndex, int pageSize, out int totalRecords, - string orderBy, Direction orderDirection, bool orderBySystemField = true, string memberTypeAlias = null, string filter = "") + string orderBy, Direction orderDirection, string memberTypeAlias = null, string filter = "") { long total; - var result = GetAll(Convert.ToInt64(pageIndex), pageSize, out total, orderBy, orderDirection, memberTypeAlias, filter); + var result = GetAll(Convert.ToInt64(pageIndex), pageSize, out total, orderBy, orderDirection, true, memberTypeAlias, filter); totalRecords = Convert.ToInt32(total); return result; } public IEnumerable GetAll(long pageIndex, int pageSize, out long totalRecords, - string orderBy, Direction orderDirection, string memberTypeAlias = null, string filter = "") + string orderBy, Direction orderDirection, bool orderBySystemField = true, string memberTypeAlias = null, string filter = "") { var uow = UowProvider.GetUnitOfWork(); using (var repository = RepositoryFactory.CreateMemberRepository(uow)) diff --git a/src/Umbraco.Web.UI/config/trees.config b/src/Umbraco.Web.UI/config/trees.config index 42bc28239b..91c216c727 100644 --- a/src/Umbraco.Web.UI/config/trees.config +++ b/src/Umbraco.Web.UI/config/trees.config @@ -41,4 +41,5 @@ + \ No newline at end of file diff --git a/src/Umbraco.Web/Editors/MediaController.cs b/src/Umbraco.Web/Editors/MediaController.cs index 6ec9bcb0f8..d2963832fd 100644 --- a/src/Umbraco.Web/Editors/MediaController.cs +++ b/src/Umbraco.Web/Editors/MediaController.cs @@ -183,7 +183,7 @@ namespace Umbraco.Web.Editors bool orderBySystemField = true, string filter = "") { - int totalChildren; + long totalChildren; IMedia[] children; if (pageNumber > 0 && pageSize > 0) { diff --git a/src/Umbraco.Web/Editors/MemberController.cs b/src/Umbraco.Web/Editors/MemberController.cs index 0790997b0e..f4799f983c 100644 --- a/src/Umbraco.Web/Editors/MemberController.cs +++ b/src/Umbraco.Web/Editors/MemberController.cs @@ -91,8 +91,7 @@ namespace Umbraco.Web.Editors if (MembershipScenario == MembershipScenario.NativeUmbraco) { long totalRecords; - var members = Services.MemberService.GetAll((pageNumber - 1), pageSize, out totalRecords, orderBy, orderDirection, orderBySystemField, memberTypeAlias, filter).ToArray(); - var members = Services.MemberService.GetAll((pageNumber - 1), pageSize, out totalRecords, orderBy, orderDirection, memberTypeAlias, filter).ToArray(); + var members = Services.MemberService.GetAll((pageNumber - 1), pageSize, out totalRecords, orderBy, orderDirection, orderBySystemField, memberTypeAlias, filter).ToArray(); if (totalRecords == 0) { return new PagedResult(0, 0, 0); diff --git a/src/UmbracoExamine/UmbracoMemberIndexer.cs b/src/UmbracoExamine/UmbracoMemberIndexer.cs index 8c3aced77c..134509610a 100644 --- a/src/UmbracoExamine/UmbracoMemberIndexer.cs +++ b/src/UmbracoExamine/UmbracoMemberIndexer.cs @@ -142,8 +142,7 @@ namespace UmbracoExamine do { long total; - var members = _memberService.GetAll(pageIndex, pageSize, out total, "LoginName", Direction.Ascending, true, nodeType).ToArray(); - members = _memberService.GetAll(pageIndex, pageSize, out total, "LoginName", Direction.Ascending, nodeType).ToArray(); + members = _memberService.GetAll(pageIndex, pageSize, out total, "LoginName", Direction.Ascending, true, nodeType).ToArray(); AddNodesToIndex(GetSerializedMembers(members), type); From 90a66aeebd231027b31aa88a31c9539c819e1ba3 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 5 Apr 2016 16:32:46 +0200 Subject: [PATCH 14/46] fixes merge / build --- .../Repositories/VersionableRepositoryBase.cs | 42 +++---------------- .../SqlSyntax/MySqlSyntaxProvider.cs | 6 +-- src/Umbraco.Core/Services/IContentService.cs | 4 +- src/Umbraco.Core/Services/IMediaService.cs | 4 +- src/Umbraco.Core/Services/IMemberService.cs | 2 +- src/Umbraco.Core/Services/MediaService.cs | 4 +- src/Umbraco.Core/Services/MemberService.cs | 2 +- 7 files changed, 14 insertions(+), 50 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs index bb7780887e..2d55a3dd05 100644 --- a/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs @@ -271,9 +271,9 @@ namespace Umbraco.Core.Persistence.Repositories { // Sorting by a custom field, so set-up sub-query for ORDER BY clause to pull through valie // from most recent content version for the given order by field - var sortedInt = string.Format(SqlSyntaxContext.SqlSyntaxProvider.ConvertIntegerToOrderableString, "dataInt"); - var sortedDate = string.Format(SqlSyntaxContext.SqlSyntaxProvider.ConvertDateToOrderableString, "dataDate"); - var sortedString = string.Format(SqlSyntaxContext.SqlSyntaxProvider.IsNull, "dataNvarchar", "''"); + var sortedInt = string.Format(SqlSyntax.ConvertIntegerToOrderableString, "dataInt"); + var sortedDate = string.Format(SqlSyntax.ConvertDateToOrderableString, "dataDate"); + var sortedString = string.Format(SqlSyntax.IsNull, "dataNvarchar", "''"); var orderBySql = string.Format(@"ORDER BY ( SELECT CASE @@ -296,41 +296,9 @@ namespace Umbraco.Core.Persistence.Repositories if (orderDirection == Direction.Descending) { sortedSql.Append(" DESC"); - } - } + } } - else - { - // Sorting by a custom field, so set-up sub-query for ORDER BY clause to pull through valie - // from most recent content version for the given order by field - var sortedInt = string.Format(SqlSyntaxContext.SqlSyntaxProvider.ConvertIntegerToOrderableString, "dataInt"); - var sortedDate = string.Format(SqlSyntaxContext.SqlSyntaxProvider.ConvertDateToOrderableString, "dataDate"); - var sortedString = string.Format(SqlSyntaxContext.SqlSyntaxProvider.IsNull, "dataNvarchar", "''"); - - var orderBySql = string.Format(@"ORDER BY ( - SELECT CASE - WHEN dataInt Is Not Null THEN {0} - WHEN dataDate Is Not Null THEN {1} - ELSE {2} - END - FROM cmsContent c - INNER JOIN cmsContentVersion cv ON cv.ContentId = c.nodeId AND VersionDate = ( - SELECT Max(VersionDate) - FROM cmsContentVersion - WHERE ContentId = c.nodeId - ) - INNER JOIN cmsPropertyData cpd ON cpd.contentNodeId = c.nodeId - AND cpd.versionId = cv.VersionId - INNER JOIN cmsPropertyType cpt ON cpt.Id = cpd.propertytypeId - WHERE c.nodeId = umbracoNode.Id and cpt.Alias = @0)", sortedInt, sortedDate, sortedString); - - sortedSql.Append(orderBySql, orderBy); - if (orderDirection == Direction.Descending) - { - sortedSql.Append(" DESC"); - } - } - + return sortedSql; } diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/MySqlSyntaxProvider.cs b/src/Umbraco.Core/Persistence/SqlSyntax/MySqlSyntaxProvider.cs index 9291138bce..d02a5fb8dc 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/MySqlSyntaxProvider.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/MySqlSyntaxProvider.cs @@ -363,11 +363,7 @@ ORDER BY TABLE_NAME, INDEX_NAME", public override string IsNull { get { return "IFNULL({0},{1})"; } } public override string ConvertIntegerToOrderableString { get { return "LPAD({0}, 8, '0')"; } } public override string ConvertDateToOrderableString { get { return "DATE_FORMAT({0}, '%Y%m%d')"; } } - - public override string IsNull { get { return "IFNULL({0},{1})"; } } - public override string ConvertIntegerToOrderableString { get { return "LPAD({0}, 8, '0')"; } } - public override string ConvertDateToOrderableString { get { return "DATE_FORMAT({0}, '%Y%m%d')"; } } - + public override bool? SupportsCaseInsensitiveQueries(Database db) { bool? supportsCaseInsensitiveQueries = null; diff --git a/src/Umbraco.Core/Services/IContentService.cs b/src/Umbraco.Core/Services/IContentService.cs index 32d5fb1209..75a412e683 100644 --- a/src/Umbraco.Core/Services/IContentService.cs +++ b/src/Umbraco.Core/Services/IContentService.cs @@ -203,7 +203,7 @@ namespace Umbraco.Core.Services [Obsolete("Use the overload with 'long' parameter types instead")] [EditorBrowsable(EditorBrowsableState.Never)] IEnumerable GetPagedChildren(int id, int pageIndex, int pageSize, out int totalRecords, - string orderBy = "SortOrder", Direction orderDirection = Direction.Ascending, bool orderBySystemField = true, string filter = ""); + string orderBy = "SortOrder", Direction orderDirection = Direction.Ascending, string filter = ""); /// /// Gets a collection of objects by Parent Id @@ -223,7 +223,7 @@ namespace Umbraco.Core.Services [Obsolete("Use the overload with 'long' parameter types instead")] [EditorBrowsable(EditorBrowsableState.Never)] IEnumerable GetPagedDescendants(int id, int pageIndex, int pageSize, out int totalRecords, - string orderBy = "Path", Direction orderDirection = Direction.Ascending, bool orderBySystemField = true, string filter = ""); + string orderBy = "Path", Direction orderDirection = Direction.Ascending, string filter = ""); /// /// Gets a collection of objects by Parent Id diff --git a/src/Umbraco.Core/Services/IMediaService.cs b/src/Umbraco.Core/Services/IMediaService.cs index f950b88734..8359b487b1 100644 --- a/src/Umbraco.Core/Services/IMediaService.cs +++ b/src/Umbraco.Core/Services/IMediaService.cs @@ -121,7 +121,7 @@ namespace Umbraco.Core.Services [Obsolete("Use the overload with 'long' parameter types instead")] [EditorBrowsable(EditorBrowsableState.Never)] IEnumerable GetPagedChildren(int id, int pageIndex, int pageSize, out int totalRecords, - string orderBy = "SortOrder", Direction orderDirection = Direction.Ascending, bool orderBySystemField = true, string filter = ""); + string orderBy = "SortOrder", Direction orderDirection = Direction.Ascending, string filter = ""); /// /// Gets a collection of objects by Parent Id @@ -141,7 +141,7 @@ namespace Umbraco.Core.Services [Obsolete("Use the overload with 'long' parameter types instead")] [EditorBrowsable(EditorBrowsableState.Never)] IEnumerable GetPagedDescendants(int id, int pageIndex, int pageSize, out int totalRecords, - string orderBy = "Path", Direction orderDirection = Direction.Ascending, bool orderBySystemField = true, string filter = ""); + string orderBy = "Path", Direction orderDirection = Direction.Ascending, string filter = ""); /// /// Gets a collection of objects by Parent Id diff --git a/src/Umbraco.Core/Services/IMemberService.cs b/src/Umbraco.Core/Services/IMemberService.cs index fbad457ecc..ee37d983aa 100644 --- a/src/Umbraco.Core/Services/IMemberService.cs +++ b/src/Umbraco.Core/Services/IMemberService.cs @@ -25,7 +25,7 @@ namespace Umbraco.Core.Services [Obsolete("Use the overload with 'long' parameter types instead")] [EditorBrowsable(EditorBrowsableState.Never)] IEnumerable GetAll(int pageIndex, int pageSize, out int totalRecords, - string orderBy, Direction orderDirection, bool orderBySystemField = true, string memberTypeAlias = null, string filter = ""); + string orderBy, Direction orderDirection, string memberTypeAlias = null, string filter = ""); /// /// Gets a list of paged objects diff --git a/src/Umbraco.Core/Services/MediaService.cs b/src/Umbraco.Core/Services/MediaService.cs index 786dcaa657..d73ee52c2e 100644 --- a/src/Umbraco.Core/Services/MediaService.cs +++ b/src/Umbraco.Core/Services/MediaService.cs @@ -395,7 +395,7 @@ namespace Umbraco.Core.Services [Obsolete("Use the overload with 'long' parameter types instead")] [EditorBrowsable(EditorBrowsableState.Never)] public IEnumerable GetPagedChildren(int id, int pageIndex, int pageSize, out int totalChildren, - string orderBy, Direction orderDirection, bool orderBySystemField = true, string filter = "") + string orderBy, Direction orderDirection, string filter = "") { long total; var result = GetPagedChildren(id, Convert.ToInt64(pageIndex), pageSize, out total, orderBy, orderDirection, true, filter); @@ -433,7 +433,7 @@ namespace Umbraco.Core.Services [Obsolete("Use the overload with 'long' parameter types instead")] [EditorBrowsable(EditorBrowsableState.Never)] - public IEnumerable GetPagedDescendants(int id, int pageIndex, int pageSize, out int totalChildren, string orderBy = "Path", Direction orderDirection = Direction.Ascending, bool orderBySystemField = true, string filter = "") + public IEnumerable GetPagedDescendants(int id, int pageIndex, int pageSize, out int totalChildren, string orderBy = "Path", Direction orderDirection = Direction.Ascending, string filter = "") { long total; var result = GetPagedDescendants(id, Convert.ToInt64(pageIndex), pageSize, out total, orderBy, orderDirection, true, filter); diff --git a/src/Umbraco.Core/Services/MemberService.cs b/src/Umbraco.Core/Services/MemberService.cs index e9e066cb3c..93d7a1a89f 100644 --- a/src/Umbraco.Core/Services/MemberService.cs +++ b/src/Umbraco.Core/Services/MemberService.cs @@ -695,7 +695,7 @@ namespace Umbraco.Core.Services [Obsolete("Use the overload with 'long' parameter types instead")] [EditorBrowsable(EditorBrowsableState.Never)] public IEnumerable GetAll(int pageIndex, int pageSize, out int totalRecords, - string orderBy, Direction orderDirection, bool orderBySystemField = true, string memberTypeAlias = null, string filter = "") + string orderBy, Direction orderDirection, string memberTypeAlias = null, string filter = "") { long total; var result = GetAll(Convert.ToInt64(pageIndex), pageSize, out total, orderBy, orderDirection, true, memberTypeAlias, filter); From dac7b9320985d1f8b36bcfa88992d4b2d7b80571 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 6 Apr 2016 09:17:05 +0200 Subject: [PATCH 15/46] fixes GetChildren non bool argument to work as a bool again. --- .../src/common/resources/content.resource.js | 1238 +++++++++-------- src/Umbraco.Web/Editors/ContentController.cs | 7 +- 2 files changed, 629 insertions(+), 616 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js index 60ecd16f19..3c1c1e2252 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js @@ -25,636 +25,650 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { - /** internal method process the saving of data and post processing the result */ - function saveContentItem(content, action, files) { - return umbRequestHelper.postSaveContent({ - restApiUrl: umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "PostSave"), - content: content, - action: action, - files: files, - dataFormatter: function (c, a) { - return umbDataFormatter.formatContentPostData(c, a); - } - }); - } + /** internal method process the saving of data and post processing the result */ + function saveContentItem(content, action, files) { + return umbRequestHelper.postSaveContent({ + restApiUrl: umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "PostSave"), + content: content, + action: action, + files: files, + dataFormatter: function (c, a) { + return umbDataFormatter.formatContentPostData(c, a); + } + }); + } - return { + return { - getRecycleBin: function () { - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "GetRecycleBin")), - 'Failed to retrieve data for content recycle bin'); - }, + getRecycleBin: function () { + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "GetRecycleBin")), + 'Failed to retrieve data for content recycle bin'); + }, - /** - * @ngdoc method - * @name umbraco.resources.contentResource#sort - * @methodOf umbraco.resources.contentResource - * - * @description - * Sorts all children below a given parent node id, based on a collection of node-ids - * - * ##usage - *
-        * var ids = [123,34533,2334,23434];
-        * contentResource.sort({ parentId: 1244, sortedIds: ids })
-        *    .then(function() {
-        *        $scope.complete = true;
-        *    });
-        * 
- * @param {Object} args arguments object - * @param {Int} args.parentId the ID of the parent node - * @param {Array} options.sortedIds array of node IDs as they should be sorted - * @returns {Promise} resourcePromise object. - * - */ - sort: function (args) { - if (!args) { - throw "args cannot be null"; - } - if (!args.parentId) { - throw "args.parentId cannot be null"; - } - if (!args.sortedIds) { - throw "args.sortedIds cannot be null"; - } + /** + * @ngdoc method + * @name umbraco.resources.contentResource#sort + * @methodOf umbraco.resources.contentResource + * + * @description + * Sorts all children below a given parent node id, based on a collection of node-ids + * + * ##usage + *
+          * var ids = [123,34533,2334,23434];
+          * contentResource.sort({ parentId: 1244, sortedIds: ids })
+          *    .then(function() {
+          *        $scope.complete = true;
+          *    });
+          * 
+ * @param {Object} args arguments object + * @param {Int} args.parentId the ID of the parent node + * @param {Array} options.sortedIds array of node IDs as they should be sorted + * @returns {Promise} resourcePromise object. + * + */ + sort: function (args) { + if (!args) { + throw "args cannot be null"; + } + if (!args.parentId) { + throw "args.parentId cannot be null"; + } + if (!args.sortedIds) { + throw "args.sortedIds cannot be null"; + } - return umbRequestHelper.resourcePromise( - $http.post(umbRequestHelper.getApiUrl("contentApiBaseUrl", "PostSort"), - { - parentId: args.parentId, - idSortOrder: args.sortedIds - }), - 'Failed to sort content'); - }, + return umbRequestHelper.resourcePromise( + $http.post(umbRequestHelper.getApiUrl("contentApiBaseUrl", "PostSort"), + { + parentId: args.parentId, + idSortOrder: args.sortedIds + }), + 'Failed to sort content'); + }, - /** - * @ngdoc method - * @name umbraco.resources.contentResource#move - * @methodOf umbraco.resources.contentResource - * - * @description - * Moves a node underneath a new parentId - * - * ##usage - *
-        * contentResource.move({ parentId: 1244, id: 123 })
-        *    .then(function() {
-        *        alert("node was moved");
-        *    }, function(err){
-        *      alert("node didnt move:" + err.data.Message); 
-        *    });
-        * 
- * @param {Object} args arguments object - * @param {Int} args.idd the ID of the node to move - * @param {Int} args.parentId the ID of the parent node to move to - * @returns {Promise} resourcePromise object. - * - */ - move: function (args) { - if (!args) { - throw "args cannot be null"; - } - if (!args.parentId) { - throw "args.parentId cannot be null"; - } - if (!args.id) { - throw "args.id cannot be null"; - } + /** + * @ngdoc method + * @name umbraco.resources.contentResource#move + * @methodOf umbraco.resources.contentResource + * + * @description + * Moves a node underneath a new parentId + * + * ##usage + *
+          * contentResource.move({ parentId: 1244, id: 123 })
+          *    .then(function() {
+          *        alert("node was moved");
+          *    }, function(err){
+          *      alert("node didnt move:" + err.data.Message); 
+          *    });
+          * 
+ * @param {Object} args arguments object + * @param {Int} args.idd the ID of the node to move + * @param {Int} args.parentId the ID of the parent node to move to + * @returns {Promise} resourcePromise object. + * + */ + move: function (args) { + if (!args) { + throw "args cannot be null"; + } + if (!args.parentId) { + throw "args.parentId cannot be null"; + } + if (!args.id) { + throw "args.id cannot be null"; + } - return umbRequestHelper.resourcePromise( - $http.post(umbRequestHelper.getApiUrl("contentApiBaseUrl", "PostMove"), - { - parentId: args.parentId, - id: args.id - }), - 'Failed to move content'); - }, + return umbRequestHelper.resourcePromise( + $http.post(umbRequestHelper.getApiUrl("contentApiBaseUrl", "PostMove"), + { + parentId: args.parentId, + id: args.id + }), + 'Failed to move content'); + }, - /** - * @ngdoc method - * @name umbraco.resources.contentResource#copy - * @methodOf umbraco.resources.contentResource - * - * @description - * Copies a node underneath a new parentId - * - * ##usage - *
-        * contentResource.copy({ parentId: 1244, id: 123 })
-        *    .then(function() {
-        *        alert("node was copied");
-        *    }, function(err){
-        *      alert("node wasnt copy:" + err.data.Message); 
-        *    });
-        * 
- * @param {Object} args arguments object - * @param {Int} args.id the ID of the node to copy - * @param {Int} args.parentId the ID of the parent node to copy to - * @param {Boolean} args.relateToOriginal if true, relates the copy to the original through the relation api - * @returns {Promise} resourcePromise object. - * - */ - copy: function (args) { - if (!args) { - throw "args cannot be null"; - } - if (!args.parentId) { - throw "args.parentId cannot be null"; - } - if (!args.id) { - throw "args.id cannot be null"; - } + /** + * @ngdoc method + * @name umbraco.resources.contentResource#copy + * @methodOf umbraco.resources.contentResource + * + * @description + * Copies a node underneath a new parentId + * + * ##usage + *
+          * contentResource.copy({ parentId: 1244, id: 123 })
+          *    .then(function() {
+          *        alert("node was copied");
+          *    }, function(err){
+          *      alert("node wasnt copy:" + err.data.Message); 
+          *    });
+          * 
+ * @param {Object} args arguments object + * @param {Int} args.id the ID of the node to copy + * @param {Int} args.parentId the ID of the parent node to copy to + * @param {Boolean} args.relateToOriginal if true, relates the copy to the original through the relation api + * @returns {Promise} resourcePromise object. + * + */ + copy: function (args) { + if (!args) { + throw "args cannot be null"; + } + if (!args.parentId) { + throw "args.parentId cannot be null"; + } + if (!args.id) { + throw "args.id cannot be null"; + } - return umbRequestHelper.resourcePromise( - $http.post(umbRequestHelper.getApiUrl("contentApiBaseUrl", "PostCopy"), - args), - 'Failed to copy content'); - }, + return umbRequestHelper.resourcePromise( + $http.post(umbRequestHelper.getApiUrl("contentApiBaseUrl", "PostCopy"), + args), + 'Failed to copy content'); + }, - /** - * @ngdoc method - * @name umbraco.resources.contentResource#unPublish - * @methodOf umbraco.resources.contentResource - * - * @description - * Unpublishes a content item with a given Id - * - * ##usage - *
-        * contentResource.unPublish(1234)
-        *    .then(function() {
-        *        alert("node was unpulished");
-        *    }, function(err){
-        *      alert("node wasnt unpublished:" + err.data.Message); 
-        *    });
-        * 
- * @param {Int} id the ID of the node to unpublish - * @returns {Promise} resourcePromise object. - * - */ - unPublish: function (id) { - if (!id) { - throw "id cannot be null"; - } + /** + * @ngdoc method + * @name umbraco.resources.contentResource#unPublish + * @methodOf umbraco.resources.contentResource + * + * @description + * Unpublishes a content item with a given Id + * + * ##usage + *
+          * contentResource.unPublish(1234)
+          *    .then(function() {
+          *        alert("node was unpulished");
+          *    }, function(err){
+          *      alert("node wasnt unpublished:" + err.data.Message); 
+          *    });
+          * 
+ * @param {Int} id the ID of the node to unpublish + * @returns {Promise} resourcePromise object. + * + */ + unPublish: function (id) { + if (!id) { + throw "id cannot be null"; + } - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "PostUnPublish", - [{ id: id }])), - 'Failed to publish content with id ' + id); - }, - /** - * @ngdoc method - * @name umbraco.resources.contentResource#emptyRecycleBin - * @methodOf umbraco.resources.contentResource - * - * @description - * Empties the content recycle bin - * - * ##usage - *
-        * contentResource.emptyRecycleBin()
-        *    .then(function() {
-        *        alert('its empty!');
-        *    });
-        * 
- * - * @returns {Promise} resourcePromise object. - * - */ - emptyRecycleBin: function () { - return umbRequestHelper.resourcePromise( + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "PostUnPublish", + [{ id: id }])), + 'Failed to publish content with id ' + id); + }, + /** + * @ngdoc method + * @name umbraco.resources.contentResource#emptyRecycleBin + * @methodOf umbraco.resources.contentResource + * + * @description + * Empties the content recycle bin + * + * ##usage + *
+          * contentResource.emptyRecycleBin()
+          *    .then(function() {
+          *        alert('its empty!');
+          *    });
+          * 
+ * + * @returns {Promise} resourcePromise object. + * + */ + emptyRecycleBin: function () { + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "EmptyRecycleBin")), + 'Failed to empty the recycle bin'); + }, + + /** + * @ngdoc method + * @name umbraco.resources.contentResource#deleteById + * @methodOf umbraco.resources.contentResource + * + * @description + * Deletes a content item with a given id + * + * ##usage + *
+          * contentResource.deleteById(1234)
+          *    .then(function() {
+          *        alert('its gone!');
+          *    });
+          * 
+ * + * @param {Int} id id of content item to delete + * @returns {Promise} resourcePromise object. + * + */ + deleteById: function (id) { + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "DeleteById", + [{ id: id }])), + 'Failed to delete item ' + id); + }, + + /** + * @ngdoc method + * @name umbraco.resources.contentResource#getById + * @methodOf umbraco.resources.contentResource + * + * @description + * Gets a content item with a given id + * + * ##usage + *
+          * contentResource.getById(1234)
+          *    .then(function(content) {
+          *        var myDoc = content; 
+          *        alert('its here!');
+          *    });
+          * 
+ * + * @param {Int} id id of content item to return + * @returns {Promise} resourcePromise object containing the content item. + * + */ + getById: function (id) { + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "GetById", + [{ id: id }])), + 'Failed to retrieve data for content id ' + id); + }, + + /** + * @ngdoc method + * @name umbraco.resources.contentResource#getByIds + * @methodOf umbraco.resources.contentResource + * + * @description + * Gets an array of content items, given a collection of ids + * + * ##usage + *
+          * contentResource.getByIds( [1234,2526,28262])
+          *    .then(function(contentArray) {
+          *        var myDoc = contentArray; 
+          *        alert('they are here!');
+          *    });
+          * 
+ * + * @param {Array} ids ids of content items to return as an array + * @returns {Promise} resourcePromise object containing the content items array. + * + */ + getByIds: function (ids) { + + var idQuery = ""; + _.each(ids, function (item) { + idQuery += "ids=" + item + "&"; + }); + + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "GetByIds", + idQuery)), + 'Failed to retrieve data for content with multiple ids'); + }, + + + /** + * @ngdoc method + * @name umbraco.resources.contentResource#getScaffold + * @methodOf umbraco.resources.contentResource + * + * @description + * Returns a scaffold of an empty content item, given the id of the content item to place it underneath and the content type alias. + * + * - Parent Id must be provided so umbraco knows where to store the content + * - Content Type alias must be provided so umbraco knows which properties to put on the content scaffold + * + * The scaffold is used to build editors for content that has not yet been populated with data. + * + * ##usage + *
+          * contentResource.getScaffold(1234, 'homepage')
+          *    .then(function(scaffold) {
+          *        var myDoc = scaffold;
+          *        myDoc.name = "My new document"; 
+          *
+          *        contentResource.publish(myDoc, true)
+          *            .then(function(content){
+          *                alert("Retrieved, updated and published again");
+          *            });
+          *    });
+          * 
+ * + * @param {Int} parentId id of content item to return + * @param {String} alias contenttype alias to base the scaffold on + * @returns {Promise} resourcePromise object containing the content scaffold. + * + */ + getScaffold: function (parentId, alias) { + + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "GetEmpty", + [{ contentTypeAlias: alias }, { parentId: parentId }])), + 'Failed to retrieve data for empty content item type ' + alias); + }, + + /** + * @ngdoc method + * @name umbraco.resources.contentResource#getNiceUrl + * @methodOf umbraco.resources.contentResource + * + * @description + * Returns a url, given a node ID + * + * ##usage + *
+          * contentResource.getNiceUrl(id)
+          *    .then(function(url) {
+          *        alert('its here!');
+          *    });
+          * 
+ * + * @param {Int} id Id of node to return the public url to + * @returns {Promise} resourcePromise object containing the url. + * + */ + getNiceUrl: function (id) { + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "GetNiceUrl", [{ id: id }])), + 'Failed to retrieve url for id:' + id); + }, + + /** + * @ngdoc method + * @name umbraco.resources.contentResource#getChildren + * @methodOf umbraco.resources.contentResource + * + * @description + * Gets children of a content item with a given id + * + * ##usage + *
+          * contentResource.getChildren(1234, {pageSize: 10, pageNumber: 2})
+          *    .then(function(contentArray) {
+          *        var children = contentArray; 
+          *        alert('they are here!');
+          *    });
+          * 
+ * + * @param {Int} parentid id of content item to return children of + * @param {Object} options optional options object + * @param {Int} options.pageSize if paging data, number of nodes per page, default = 0 + * @param {Int} options.pageNumber if paging data, current page index, default = 0 + * @param {String} options.filter if provided, query will only return those with names matching the filter + * @param {String} options.orderDirection can be `Ascending` or `Descending` - Default: `Ascending` + * @param {String} options.orderBy property to order items by, default: `SortOrder` + * @returns {Promise} resourcePromise object containing an array of content items. + * + */ + getChildren: function (parentId, options) { + + var defaults = { + pageSize: 0, + pageNumber: 0, + filter: '', + orderDirection: "Ascending", + orderBy: "SortOrder", + orderBySystemField: true + }; + if (options === undefined) { + options = {}; + } + //overwrite the defaults if there are any specified + angular.extend(defaults, options); + //now copy back to the options we will use + options = defaults; + //change asc/desct + if (options.orderDirection === "asc") { + options.orderDirection = "Ascending"; + } + else if (options.orderDirection === "desc") { + options.orderDirection = "Descending"; + } + + //converts the value to a js bool + function toBool(v) { + if (angular.isNumber(v)) { + return v > 0; + } + if (angular.isString(v)) { + return v === "true"; + } + if (typeof v === "boolean") { + return v; + } + return false; + } + + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "GetChildren", + [ + { id: parentId }, + { pageNumber: options.pageNumber }, + { pageSize: options.pageSize }, + { orderBy: options.orderBy }, + { orderDirection: options.orderDirection }, + { orderBySystemField: toBool(options.orderBySystemField) }, + { filter: options.filter } + ])), + 'Failed to retrieve children for content item ' + parentId); + }, + + /** + * @ngdoc method + * @name umbraco.resources.contentResource#hasPermission + * @methodOf umbraco.resources.contentResource + * + * @description + * Returns true/false given a permission char to check against a nodeID + * for the current user + * + * ##usage + *
+          * contentResource.hasPermission('p',1234)
+          *    .then(function() {
+          *        alert('You are allowed to publish this item');
+          *    });
+          * 
+ * + * @param {String} permission char representing the permission to check + * @param {Int} id id of content item to delete + * @returns {Promise} resourcePromise object. + * + */ + checkPermission: function (permission, id) { + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "HasPermission", + [{ permissionToCheck: permission }, { nodeId: id }])), + 'Failed to check permission for item ' + id); + }, + + getPermissions: function (nodeIds) { + return umbRequestHelper.resourcePromise( $http.post( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "EmptyRecycleBin")), - 'Failed to empty the recycle bin'); - }, + umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "GetPermissions"), + nodeIds), + 'Failed to get permissions'); + }, - /** - * @ngdoc method - * @name umbraco.resources.contentResource#deleteById - * @methodOf umbraco.resources.contentResource - * - * @description - * Deletes a content item with a given id - * - * ##usage - *
-        * contentResource.deleteById(1234)
-        *    .then(function() {
-        *        alert('its gone!');
-        *    });
-        * 
- * - * @param {Int} id id of content item to delete - * @returns {Promise} resourcePromise object. - * - */ - deleteById: function (id) { - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "DeleteById", - [{ id: id }])), - 'Failed to delete item ' + id); - }, - - /** - * @ngdoc method - * @name umbraco.resources.contentResource#getById - * @methodOf umbraco.resources.contentResource - * - * @description - * Gets a content item with a given id - * - * ##usage - *
-        * contentResource.getById(1234)
-        *    .then(function(content) {
-        *        var myDoc = content; 
-        *        alert('its here!');
-        *    });
-        * 
- * - * @param {Int} id id of content item to return - * @returns {Promise} resourcePromise object containing the content item. - * - */ - getById: function (id) { - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "GetById", - [{ id: id }])), - 'Failed to retrieve data for content id ' + id); - }, - - /** - * @ngdoc method - * @name umbraco.resources.contentResource#getByIds - * @methodOf umbraco.resources.contentResource - * - * @description - * Gets an array of content items, given a collection of ids - * - * ##usage - *
-        * contentResource.getByIds( [1234,2526,28262])
-        *    .then(function(contentArray) {
-        *        var myDoc = contentArray; 
-        *        alert('they are here!');
-        *    });
-        * 
- * - * @param {Array} ids ids of content items to return as an array - * @returns {Promise} resourcePromise object containing the content items array. - * - */ - getByIds: function (ids) { - - var idQuery = ""; - _.each(ids, function (item) { - idQuery += "ids=" + item + "&"; - }); - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "GetByIds", - idQuery)), - 'Failed to retrieve data for content with multiple ids'); - }, + /** + * @ngdoc method + * @name umbraco.resources.contentResource#save + * @methodOf umbraco.resources.contentResource + * + * @description + * Saves changes made to a content item to its current version, if the content item is new, the isNew paramater must be passed to force creation + * if the content item needs to have files attached, they must be provided as the files param and passed separately + * + * + * ##usage + *
+          * contentResource.getById(1234)
+          *    .then(function(content) {
+          *          content.name = "I want a new name!";
+          *          contentResource.save(content, false)
+          *            .then(function(content){
+          *                alert("Retrieved, updated and saved again");
+          *            });
+          *    });
+          * 
+ * + * @param {Object} content The content item object with changes applied + * @param {Bool} isNew set to true to create a new item or to update an existing + * @param {Array} files collection of files for the document + * @returns {Promise} resourcePromise object containing the saved content item. + * + */ + save: function (content, isNew, files) { + return saveContentItem(content, "save" + (isNew ? "New" : ""), files); + }, - /** - * @ngdoc method - * @name umbraco.resources.contentResource#getScaffold - * @methodOf umbraco.resources.contentResource - * - * @description - * Returns a scaffold of an empty content item, given the id of the content item to place it underneath and the content type alias. - * - * - Parent Id must be provided so umbraco knows where to store the content - * - Content Type alias must be provided so umbraco knows which properties to put on the content scaffold - * - * The scaffold is used to build editors for content that has not yet been populated with data. - * - * ##usage - *
-        * contentResource.getScaffold(1234, 'homepage')
-        *    .then(function(scaffold) {
-        *        var myDoc = scaffold;
-        *        myDoc.name = "My new document"; 
-        *
-        *        contentResource.publish(myDoc, true)
-        *            .then(function(content){
-        *                alert("Retrieved, updated and published again");
-        *            });
-        *    });
-        * 
- * - * @param {Int} parentId id of content item to return - * @param {String} alias contenttype alias to base the scaffold on - * @returns {Promise} resourcePromise object containing the content scaffold. - * - */ - getScaffold: function (parentId, alias) { - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "GetEmpty", - [{ contentTypeAlias: alias }, { parentId: parentId }])), - 'Failed to retrieve data for empty content item type ' + alias); - }, - - /** - * @ngdoc method - * @name umbraco.resources.contentResource#getNiceUrl - * @methodOf umbraco.resources.contentResource - * - * @description - * Returns a url, given a node ID - * - * ##usage - *
-        * contentResource.getNiceUrl(id)
-        *    .then(function(url) {
-        *        alert('its here!');
-        *    });
-        * 
- * - * @param {Int} id Id of node to return the public url to - * @returns {Promise} resourcePromise object containing the url. - * - */ - getNiceUrl: function (id) { - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "GetNiceUrl", [{ id: id }])), - 'Failed to retrieve url for id:' + id); - }, - - /** - * @ngdoc method - * @name umbraco.resources.contentResource#getChildren - * @methodOf umbraco.resources.contentResource - * - * @description - * Gets children of a content item with a given id - * - * ##usage - *
-        * contentResource.getChildren(1234, {pageSize: 10, pageNumber: 2})
-        *    .then(function(contentArray) {
-        *        var children = contentArray; 
-        *        alert('they are here!');
-        *    });
-        * 
- * - * @param {Int} parentid id of content item to return children of - * @param {Object} options optional options object - * @param {Int} options.pageSize if paging data, number of nodes per page, default = 0 - * @param {Int} options.pageNumber if paging data, current page index, default = 0 - * @param {String} options.filter if provided, query will only return those with names matching the filter - * @param {String} options.orderDirection can be `Ascending` or `Descending` - Default: `Ascending` - * @param {String} options.orderBy property to order items by, default: `SortOrder` - * @returns {Promise} resourcePromise object containing an array of content items. - * - */ - getChildren: function (parentId, options) { - - var defaults = { - pageSize: 0, - pageNumber: 0, - filter: '', - orderDirection: "Ascending", - orderBy: "SortOrder", - orderBySystemField: true - }; - if (options === undefined) { - options = {}; - } - //overwrite the defaults if there are any specified - angular.extend(defaults, options); - //now copy back to the options we will use - options = defaults; - //change asc/desct - if (options.orderDirection === "asc") { - options.orderDirection = "Ascending"; - } - else if (options.orderDirection === "desc") { - options.orderDirection = "Descending"; - } - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "GetChildren", - [ - { id: parentId }, - { pageNumber: options.pageNumber }, - { pageSize: options.pageSize }, - { orderBy: options.orderBy }, - { orderDirection: options.orderDirection }, - { orderBySystemField: options.orderBySystemField }, - { filter: options.filter } - ])), - 'Failed to retrieve children for content item ' + parentId); - }, - - /** - * @ngdoc method - * @name umbraco.resources.contentResource#hasPermission - * @methodOf umbraco.resources.contentResource - * - * @description - * Returns true/false given a permission char to check against a nodeID - * for the current user - * - * ##usage - *
-        * contentResource.hasPermission('p',1234)
-        *    .then(function() {
-        *        alert('You are allowed to publish this item');
-        *    });
-        * 
- * - * @param {String} permission char representing the permission to check - * @param {Int} id id of content item to delete - * @returns {Promise} resourcePromise object. - * - */ - checkPermission: function (permission, id) { - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "HasPermission", - [{ permissionToCheck: permission }, { nodeId: id }])), - 'Failed to check permission for item ' + id); - }, - - getPermissions: function (nodeIds) { - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "GetPermissions"), - nodeIds), - 'Failed to get permissions'); - }, - - /** - * @ngdoc method - * @name umbraco.resources.contentResource#save - * @methodOf umbraco.resources.contentResource - * - * @description - * Saves changes made to a content item to its current version, if the content item is new, the isNew paramater must be passed to force creation - * if the content item needs to have files attached, they must be provided as the files param and passed separately - * - * - * ##usage - *
-        * contentResource.getById(1234)
-        *    .then(function(content) {
-        *          content.name = "I want a new name!";
-        *          contentResource.save(content, false)
-        *            .then(function(content){
-        *                alert("Retrieved, updated and saved again");
-        *            });
-        *    });
-        * 
- * - * @param {Object} content The content item object with changes applied - * @param {Bool} isNew set to true to create a new item or to update an existing - * @param {Array} files collection of files for the document - * @returns {Promise} resourcePromise object containing the saved content item. - * - */ - save: function (content, isNew, files) { - return saveContentItem(content, "save" + (isNew ? "New" : ""), files); - }, + /** + * @ngdoc method + * @name umbraco.resources.contentResource#publish + * @methodOf umbraco.resources.contentResource + * + * @description + * Saves and publishes changes made to a content item to a new version, if the content item is new, the isNew paramater must be passed to force creation + * if the content item needs to have files attached, they must be provided as the files param and passed separately + * + * + * ##usage + *
+          * contentResource.getById(1234)
+          *    .then(function(content) {
+          *          content.name = "I want a new name, and be published!";
+          *          contentResource.publish(content, false)
+          *            .then(function(content){
+          *                alert("Retrieved, updated and published again");
+          *            });
+          *    });
+          * 
+ * + * @param {Object} content The content item object with changes applied + * @param {Bool} isNew set to true to create a new item or to update an existing + * @param {Array} files collection of files for the document + * @returns {Promise} resourcePromise object containing the saved content item. + * + */ + publish: function (content, isNew, files) { + return saveContentItem(content, "publish" + (isNew ? "New" : ""), files); + }, - /** - * @ngdoc method - * @name umbraco.resources.contentResource#publish - * @methodOf umbraco.resources.contentResource - * - * @description - * Saves and publishes changes made to a content item to a new version, if the content item is new, the isNew paramater must be passed to force creation - * if the content item needs to have files attached, they must be provided as the files param and passed separately - * - * - * ##usage - *
-        * contentResource.getById(1234)
-        *    .then(function(content) {
-        *          content.name = "I want a new name, and be published!";
-        *          contentResource.publish(content, false)
-        *            .then(function(content){
-        *                alert("Retrieved, updated and published again");
-        *            });
-        *    });
-        * 
- * - * @param {Object} content The content item object with changes applied - * @param {Bool} isNew set to true to create a new item or to update an existing - * @param {Array} files collection of files for the document - * @returns {Promise} resourcePromise object containing the saved content item. - * - */ - publish: function (content, isNew, files) { - return saveContentItem(content, "publish" + (isNew ? "New" : ""), files); - }, + /** + * @ngdoc method + * @name umbraco.resources.contentResource#sendToPublish + * @methodOf umbraco.resources.contentResource + * + * @description + * Saves changes made to a content item, and notifies any subscribers about a pending publication + * + * ##usage + *
+          * contentResource.getById(1234)
+          *    .then(function(content) {
+          *          content.name = "I want a new name, and be published!";
+          *          contentResource.sendToPublish(content, false)
+          *            .then(function(content){
+          *                alert("Retrieved, updated and notication send off");
+          *            });
+          *    });
+          * 
+ * + * @param {Object} content The content item object with changes applied + * @param {Bool} isNew set to true to create a new item or to update an existing + * @param {Array} files collection of files for the document + * @returns {Promise} resourcePromise object containing the saved content item. + * + */ + sendToPublish: function (content, isNew, files) { + return saveContentItem(content, "sendPublish" + (isNew ? "New" : ""), files); + }, + + /** + * @ngdoc method + * @name umbraco.resources.contentResource#publishByid + * @methodOf umbraco.resources.contentResource + * + * @description + * Publishes a content item with a given ID + * + * ##usage + *
+          * contentResource.publishById(1234)
+          *    .then(function(content) {
+          *        alert("published");
+          *    });
+          * 
+ * + * @param {Int} id The ID of the conten to publish + * @returns {Promise} resourcePromise object containing the published content item. + * + */ + publishById: function (id) { + + if (!id) { + throw "id cannot be null"; + } + + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "PostPublishById", + [{ id: id }])), + 'Failed to publish content with id ' + id); + + } - /** - * @ngdoc method - * @name umbraco.resources.contentResource#sendToPublish - * @methodOf umbraco.resources.contentResource - * - * @description - * Saves changes made to a content item, and notifies any subscribers about a pending publication - * - * ##usage - *
-        * contentResource.getById(1234)
-        *    .then(function(content) {
-        *          content.name = "I want a new name, and be published!";
-        *          contentResource.sendToPublish(content, false)
-        *            .then(function(content){
-        *                alert("Retrieved, updated and notication send off");
-        *            });
-        *    });
-        * 
- * - * @param {Object} content The content item object with changes applied - * @param {Bool} isNew set to true to create a new item or to update an existing - * @param {Array} files collection of files for the document - * @returns {Promise} resourcePromise object containing the saved content item. - * - */ - sendToPublish: function (content, isNew, files) { - return saveContentItem(content, "sendPublish" + (isNew ? "New" : ""), files); - }, - - /** - * @ngdoc method - * @name umbraco.resources.contentResource#publishByid - * @methodOf umbraco.resources.contentResource - * - * @description - * Publishes a content item with a given ID - * - * ##usage - *
-        * contentResource.publishById(1234)
-        *    .then(function(content) {
-        *        alert("published");
-        *    });
-        * 
- * - * @param {Int} id The ID of the conten to publish - * @returns {Promise} resourcePromise object containing the published content item. - * - */ - publishById: function (id) { - - if (!id) { - throw "id cannot be null"; - } - - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "PostPublishById", - [{ id: id }])), - 'Failed to publish content with id ' + id); - - } - - - }; + }; } angular.module('umbraco.resources').factory('contentResource', contentResource); diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index 580e1a018c..7abdaf7e69 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -184,17 +184,16 @@ namespace Umbraco.Web.Editors int pageSize = 0, string orderBy = "SortOrder", Direction orderDirection = Direction.Ascending, - int? orderBySystemField = 2, + bool orderBySystemField = true, string filter = "") - { - var orderBySystemFieldBool = orderBySystemField == 1 || orderBySystemField == 2; + { long totalChildren; IContent[] children; if (pageNumber > 0 && pageSize > 0) { children = Services.ContentService .GetPagedChildren(id, (pageNumber - 1), pageSize, out totalChildren - , orderBy, orderDirection, orderBySystemFieldBool, filter).ToArray(); + , orderBy, orderDirection, orderBySystemField, filter).ToArray(); } else { From b9b3d6299495df157a19c9bb147257ceb492baa4 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 6 Apr 2016 09:44:33 +0200 Subject: [PATCH 16/46] fixes boolean isSystem for media --- .../src/common/resources/media.resource.js | 890 +++++++++--------- 1 file changed, 452 insertions(+), 438 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/media.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/media.resource.js index a85b2cc104..4b56e55801 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/media.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/media.resource.js @@ -5,471 +5,485 @@ **/ function mediaResource($q, $http, umbDataFormatter, umbRequestHelper) { - /** internal method process the saving of data and post processing the result */ - function saveMediaItem(content, action, files) { - return umbRequestHelper.postSaveContent({ - restApiUrl: umbRequestHelper.getApiUrl( - "mediaApiBaseUrl", - "PostSave"), - content: content, - action: action, - files: files, - dataFormatter: function (c, a) { - return umbDataFormatter.formatMediaPostData(c, a); - } - }); - } + /** internal method process the saving of data and post processing the result */ + function saveMediaItem(content, action, files) { + return umbRequestHelper.postSaveContent({ + restApiUrl: umbRequestHelper.getApiUrl( + "mediaApiBaseUrl", + "PostSave"), + content: content, + action: action, + files: files, + dataFormatter: function (c, a) { + return umbDataFormatter.formatMediaPostData(c, a); + } + }); + } - return { + return { - getRecycleBin: function () { - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "mediaApiBaseUrl", - "GetRecycleBin")), - 'Failed to retrieve data for media recycle bin'); - }, + getRecycleBin: function () { + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "mediaApiBaseUrl", + "GetRecycleBin")), + 'Failed to retrieve data for media recycle bin'); + }, - /** - * @ngdoc method - * @name umbraco.resources.mediaResource#sort - * @methodOf umbraco.resources.mediaResource - * - * @description - * Sorts all children below a given parent node id, based on a collection of node-ids - * - * ##usage - *
-        * var ids = [123,34533,2334,23434];
-        * mediaResource.sort({ sortedIds: ids })
-        *    .then(function() {
-        *        $scope.complete = true;
-        *    });
-        * 
- * @param {Object} args arguments object - * @param {Int} args.parentId the ID of the parent node - * @param {Array} options.sortedIds array of node IDs as they should be sorted - * @returns {Promise} resourcePromise object. - * - */ - sort: function (args) { - if (!args) { - throw "args cannot be null"; - } - if (!args.parentId) { - throw "args.parentId cannot be null"; - } - if (!args.sortedIds) { - throw "args.sortedIds cannot be null"; - } + /** + * @ngdoc method + * @name umbraco.resources.mediaResource#sort + * @methodOf umbraco.resources.mediaResource + * + * @description + * Sorts all children below a given parent node id, based on a collection of node-ids + * + * ##usage + *
+          * var ids = [123,34533,2334,23434];
+          * mediaResource.sort({ sortedIds: ids })
+          *    .then(function() {
+          *        $scope.complete = true;
+          *    });
+          * 
+ * @param {Object} args arguments object + * @param {Int} args.parentId the ID of the parent node + * @param {Array} options.sortedIds array of node IDs as they should be sorted + * @returns {Promise} resourcePromise object. + * + */ + sort: function (args) { + if (!args) { + throw "args cannot be null"; + } + if (!args.parentId) { + throw "args.parentId cannot be null"; + } + if (!args.sortedIds) { + throw "args.sortedIds cannot be null"; + } - return umbRequestHelper.resourcePromise( - $http.post(umbRequestHelper.getApiUrl("mediaApiBaseUrl", "PostSort"), - { - parentId: args.parentId, - idSortOrder: args.sortedIds - }), - 'Failed to sort media'); - }, + return umbRequestHelper.resourcePromise( + $http.post(umbRequestHelper.getApiUrl("mediaApiBaseUrl", "PostSort"), + { + parentId: args.parentId, + idSortOrder: args.sortedIds + }), + 'Failed to sort media'); + }, - /** - * @ngdoc method - * @name umbraco.resources.mediaResource#move - * @methodOf umbraco.resources.mediaResource - * - * @description - * Moves a node underneath a new parentId - * - * ##usage - *
-        * mediaResource.move({ parentId: 1244, id: 123 })
-        *    .then(function() {
-        *        alert("node was moved");
-        *    }, function(err){
-        *      alert("node didnt move:" + err.data.Message); 
-        *    });
-        * 
- * @param {Object} args arguments object - * @param {Int} args.idd the ID of the node to move - * @param {Int} args.parentId the ID of the parent node to move to - * @returns {Promise} resourcePromise object. - * - */ - move: function (args) { - if (!args) { - throw "args cannot be null"; - } - if (!args.parentId) { - throw "args.parentId cannot be null"; - } - if (!args.id) { - throw "args.id cannot be null"; - } + /** + * @ngdoc method + * @name umbraco.resources.mediaResource#move + * @methodOf umbraco.resources.mediaResource + * + * @description + * Moves a node underneath a new parentId + * + * ##usage + *
+          * mediaResource.move({ parentId: 1244, id: 123 })
+          *    .then(function() {
+          *        alert("node was moved");
+          *    }, function(err){
+          *      alert("node didnt move:" + err.data.Message); 
+          *    });
+          * 
+ * @param {Object} args arguments object + * @param {Int} args.idd the ID of the node to move + * @param {Int} args.parentId the ID of the parent node to move to + * @returns {Promise} resourcePromise object. + * + */ + move: function (args) { + if (!args) { + throw "args cannot be null"; + } + if (!args.parentId) { + throw "args.parentId cannot be null"; + } + if (!args.id) { + throw "args.id cannot be null"; + } - return umbRequestHelper.resourcePromise( - $http.post(umbRequestHelper.getApiUrl("mediaApiBaseUrl", "PostMove"), - { - parentId: args.parentId, - id: args.id - }), - 'Failed to move media'); - }, + return umbRequestHelper.resourcePromise( + $http.post(umbRequestHelper.getApiUrl("mediaApiBaseUrl", "PostMove"), + { + parentId: args.parentId, + id: args.id + }), + 'Failed to move media'); + }, - /** - * @ngdoc method - * @name umbraco.resources.mediaResource#getById - * @methodOf umbraco.resources.mediaResource - * - * @description - * Gets a media item with a given id - * - * ##usage - *
-        * mediaResource.getById(1234)
-        *    .then(function(media) {
-        *        var myMedia = media; 
-        *        alert('its here!');
-        *    });
-        * 
- * - * @param {Int} id id of media item to return - * @returns {Promise} resourcePromise object containing the media item. - * - */ - getById: function (id) { + /** + * @ngdoc method + * @name umbraco.resources.mediaResource#getById + * @methodOf umbraco.resources.mediaResource + * + * @description + * Gets a media item with a given id + * + * ##usage + *
+          * mediaResource.getById(1234)
+          *    .then(function(media) {
+          *        var myMedia = media; 
+          *        alert('its here!');
+          *    });
+          * 
+ * + * @param {Int} id id of media item to return + * @returns {Promise} resourcePromise object containing the media item. + * + */ + getById: function (id) { - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "mediaApiBaseUrl", - "GetById", - [{ id: id }])), - 'Failed to retrieve data for media id ' + id); - }, + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "mediaApiBaseUrl", + "GetById", + [{ id: id }])), + 'Failed to retrieve data for media id ' + id); + }, - /** - * @ngdoc method - * @name umbraco.resources.mediaResource#deleteById - * @methodOf umbraco.resources.mediaResource - * - * @description - * Deletes a media item with a given id - * - * ##usage - *
-        * mediaResource.deleteById(1234)
-        *    .then(function() {
-        *        alert('its gone!');
-        *    });
-        * 
- * - * @param {Int} id id of media item to delete - * @returns {Promise} resourcePromise object. - * - */ - deleteById: function (id) { - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "mediaApiBaseUrl", - "DeleteById", - [{ id: id }])), - 'Failed to delete item ' + id); - }, + /** + * @ngdoc method + * @name umbraco.resources.mediaResource#deleteById + * @methodOf umbraco.resources.mediaResource + * + * @description + * Deletes a media item with a given id + * + * ##usage + *
+          * mediaResource.deleteById(1234)
+          *    .then(function() {
+          *        alert('its gone!');
+          *    });
+          * 
+ * + * @param {Int} id id of media item to delete + * @returns {Promise} resourcePromise object. + * + */ + deleteById: function (id) { + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "mediaApiBaseUrl", + "DeleteById", + [{ id: id }])), + 'Failed to delete item ' + id); + }, - /** - * @ngdoc method - * @name umbraco.resources.mediaResource#getByIds - * @methodOf umbraco.resources.mediaResource - * - * @description - * Gets an array of media items, given a collection of ids - * - * ##usage - *
-        * mediaResource.getByIds( [1234,2526,28262])
-        *    .then(function(mediaArray) {
-        *        var myDoc = contentArray; 
-        *        alert('they are here!');
-        *    });
-        * 
- * - * @param {Array} ids ids of media items to return as an array - * @returns {Promise} resourcePromise object containing the media items array. - * - */ - getByIds: function (ids) { + /** + * @ngdoc method + * @name umbraco.resources.mediaResource#getByIds + * @methodOf umbraco.resources.mediaResource + * + * @description + * Gets an array of media items, given a collection of ids + * + * ##usage + *
+          * mediaResource.getByIds( [1234,2526,28262])
+          *    .then(function(mediaArray) {
+          *        var myDoc = contentArray; 
+          *        alert('they are here!');
+          *    });
+          * 
+ * + * @param {Array} ids ids of media items to return as an array + * @returns {Promise} resourcePromise object containing the media items array. + * + */ + getByIds: function (ids) { - var idQuery = ""; - _.each(ids, function (item) { - idQuery += "ids=" + item + "&"; - }); + var idQuery = ""; + _.each(ids, function (item) { + idQuery += "ids=" + item + "&"; + }); - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "mediaApiBaseUrl", - "GetByIds", - idQuery)), - 'Failed to retrieve data for media ids ' + ids); - }, + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "mediaApiBaseUrl", + "GetByIds", + idQuery)), + 'Failed to retrieve data for media ids ' + ids); + }, - /** - * @ngdoc method - * @name umbraco.resources.mediaResource#getScaffold - * @methodOf umbraco.resources.mediaResource - * - * @description - * Returns a scaffold of an empty media item, given the id of the media item to place it underneath and the media type alias. - * - * - Parent Id must be provided so umbraco knows where to store the media - * - Media Type alias must be provided so umbraco knows which properties to put on the media scaffold - * - * The scaffold is used to build editors for media that has not yet been populated with data. - * - * ##usage - *
-        * mediaResource.getScaffold(1234, 'folder')
-        *    .then(function(scaffold) {
-        *        var myDoc = scaffold;
-        *        myDoc.name = "My new media item"; 
-        *
-        *        mediaResource.save(myDoc, true)
-        *            .then(function(media){
-        *                alert("Retrieved, updated and saved again");
-        *            });
-        *    });
-        * 
- * - * @param {Int} parentId id of media item to return - * @param {String} alias mediatype alias to base the scaffold on - * @returns {Promise} resourcePromise object containing the media scaffold. - * - */ - getScaffold: function (parentId, alias) { + /** + * @ngdoc method + * @name umbraco.resources.mediaResource#getScaffold + * @methodOf umbraco.resources.mediaResource + * + * @description + * Returns a scaffold of an empty media item, given the id of the media item to place it underneath and the media type alias. + * + * - Parent Id must be provided so umbraco knows where to store the media + * - Media Type alias must be provided so umbraco knows which properties to put on the media scaffold + * + * The scaffold is used to build editors for media that has not yet been populated with data. + * + * ##usage + *
+          * mediaResource.getScaffold(1234, 'folder')
+          *    .then(function(scaffold) {
+          *        var myDoc = scaffold;
+          *        myDoc.name = "My new media item"; 
+          *
+          *        mediaResource.save(myDoc, true)
+          *            .then(function(media){
+          *                alert("Retrieved, updated and saved again");
+          *            });
+          *    });
+          * 
+ * + * @param {Int} parentId id of media item to return + * @param {String} alias mediatype alias to base the scaffold on + * @returns {Promise} resourcePromise object containing the media scaffold. + * + */ + getScaffold: function (parentId, alias) { - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "mediaApiBaseUrl", - "GetEmpty", - [{ contentTypeAlias: alias }, { parentId: parentId }])), - 'Failed to retrieve data for empty media item type ' + alias); + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "mediaApiBaseUrl", + "GetEmpty", + [{ contentTypeAlias: alias }, { parentId: parentId }])), + 'Failed to retrieve data for empty media item type ' + alias); - }, + }, - rootMedia: function () { + rootMedia: function () { - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "mediaApiBaseUrl", - "GetRootMedia")), - 'Failed to retrieve data for root media'); + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "mediaApiBaseUrl", + "GetRootMedia")), + 'Failed to retrieve data for root media'); - }, + }, - /** - * @ngdoc method - * @name umbraco.resources.mediaResource#getChildren - * @methodOf umbraco.resources.mediaResource - * - * @description - * Gets children of a media item with a given id - * - * ##usage - *
-        * mediaResource.getChildren(1234, {pageSize: 10, pageNumber: 2})
-        *    .then(function(contentArray) {
-        *        var children = contentArray; 
-        *        alert('they are here!');
-        *    });
-        * 
- * - * @param {Int} parentid id of content item to return children of - * @param {Object} options optional options object - * @param {Int} options.pageSize if paging data, number of nodes per page, default = 0 - * @param {Int} options.pageNumber if paging data, current page index, default = 0 - * @param {String} options.filter if provided, query will only return those with names matching the filter - * @param {String} options.orderDirection can be `Ascending` or `Descending` - Default: `Ascending` - * @param {String} options.orderBy property to order items by, default: `SortOrder` - * @returns {Promise} resourcePromise object containing an array of content items. - * - */ - getChildren: function (parentId, options) { + /** + * @ngdoc method + * @name umbraco.resources.mediaResource#getChildren + * @methodOf umbraco.resources.mediaResource + * + * @description + * Gets children of a media item with a given id + * + * ##usage + *
+          * mediaResource.getChildren(1234, {pageSize: 10, pageNumber: 2})
+          *    .then(function(contentArray) {
+          *        var children = contentArray; 
+          *        alert('they are here!');
+          *    });
+          * 
+ * + * @param {Int} parentid id of content item to return children of + * @param {Object} options optional options object + * @param {Int} options.pageSize if paging data, number of nodes per page, default = 0 + * @param {Int} options.pageNumber if paging data, current page index, default = 0 + * @param {String} options.filter if provided, query will only return those with names matching the filter + * @param {String} options.orderDirection can be `Ascending` or `Descending` - Default: `Ascending` + * @param {String} options.orderBy property to order items by, default: `SortOrder` + * @returns {Promise} resourcePromise object containing an array of content items. + * + */ + getChildren: function (parentId, options) { - var defaults = { - pageSize: 0, - pageNumber: 0, - filter: '', - orderDirection: "Ascending", - orderBy: "SortOrder", - orderBySystemField: true - }; - if (options === undefined) { - options = {}; - } - //overwrite the defaults if there are any specified - angular.extend(defaults, options); - //now copy back to the options we will use - options = defaults; - //change asc/desct - if (options.orderDirection === "asc") { - options.orderDirection = "Ascending"; - } - else if (options.orderDirection === "desc") { - options.orderDirection = "Descending"; - } + var defaults = { + pageSize: 0, + pageNumber: 0, + filter: '', + orderDirection: "Ascending", + orderBy: "SortOrder", + orderBySystemField: true + }; + if (options === undefined) { + options = {}; + } + //overwrite the defaults if there are any specified + angular.extend(defaults, options); + //now copy back to the options we will use + options = defaults; + //change asc/desct + if (options.orderDirection === "asc") { + options.orderDirection = "Ascending"; + } + else if (options.orderDirection === "desc") { + options.orderDirection = "Descending"; + } - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "mediaApiBaseUrl", - "GetChildren", - [ - { id: parentId }, - { pageNumber: options.pageNumber }, - { pageSize: options.pageSize }, - { orderBy: options.orderBy }, - { orderDirection: options.orderDirection }, - { orderBySystemField: options.orderBySystemField }, - { filter: options.filter } - ])), - 'Failed to retrieve children for media item ' + parentId); - }, + //converts the value to a js bool + function toBool(v) { + if (angular.isNumber(v)) { + return v > 0; + } + if (angular.isString(v)) { + return v === "true"; + } + if (typeof v === "boolean") { + return v; + } + return false; + } - /** - * @ngdoc method - * @name umbraco.resources.mediaResource#save - * @methodOf umbraco.resources.mediaResource - * - * @description - * Saves changes made to a media item, if the media item is new, the isNew paramater must be passed to force creation - * if the media item needs to have files attached, they must be provided as the files param and passed separately - * - * - * ##usage - *
-        * mediaResource.getById(1234)
-        *    .then(function(media) {
-        *          media.name = "I want a new name!";
-        *          mediaResource.save(media, false)
-        *            .then(function(media){
-        *                alert("Retrieved, updated and saved again");
-        *            });
-        *    });
-        * 
- * - * @param {Object} media The media item object with changes applied - * @param {Bool} isNew set to true to create a new item or to update an existing - * @param {Array} files collection of files for the media item - * @returns {Promise} resourcePromise object containing the saved media item. - * - */ - save: function (media, isNew, files) { - return saveMediaItem(media, "save" + (isNew ? "New" : ""), files); - }, + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "mediaApiBaseUrl", + "GetChildren", + [ + { id: parentId }, + { pageNumber: options.pageNumber }, + { pageSize: options.pageSize }, + { orderBy: options.orderBy }, + { orderDirection: options.orderDirection }, + { orderBySystemField: toBool(options.orderBySystemField) }, + { filter: options.filter } + ])), + 'Failed to retrieve children for media item ' + parentId); + }, - /** - * @ngdoc method - * @name umbraco.resources.mediaResource#addFolder - * @methodOf umbraco.resources.mediaResource - * - * @description - * Shorthand for adding a media item of the type "Folder" under a given parent ID - * - * ##usage - *
-        * mediaResource.addFolder("My gallery", 1234)
-        *    .then(function(folder) {
-        *        alert('New folder');
-        *    });
-        * 
- * - * @param {string} name Name of the folder to create - * @param {int} parentId Id of the media item to create the folder underneath - * @returns {Promise} resourcePromise object. - * - */ - addFolder: function (name, parentId) { - return umbRequestHelper.resourcePromise( - $http.post(umbRequestHelper - .getApiUrl("mediaApiBaseUrl", "PostAddFolder"), - { - name: name, - parentId: parentId - }), - 'Failed to add folder'); - }, + /** + * @ngdoc method + * @name umbraco.resources.mediaResource#save + * @methodOf umbraco.resources.mediaResource + * + * @description + * Saves changes made to a media item, if the media item is new, the isNew paramater must be passed to force creation + * if the media item needs to have files attached, they must be provided as the files param and passed separately + * + * + * ##usage + *
+          * mediaResource.getById(1234)
+          *    .then(function(media) {
+          *          media.name = "I want a new name!";
+          *          mediaResource.save(media, false)
+          *            .then(function(media){
+          *                alert("Retrieved, updated and saved again");
+          *            });
+          *    });
+          * 
+ * + * @param {Object} media The media item object with changes applied + * @param {Bool} isNew set to true to create a new item or to update an existing + * @param {Array} files collection of files for the media item + * @returns {Promise} resourcePromise object containing the saved media item. + * + */ + save: function (media, isNew, files) { + return saveMediaItem(media, "save" + (isNew ? "New" : ""), files); + }, - /** - * @ngdoc method - * @name umbraco.resources.mediaResource#getChildFolders - * @methodOf umbraco.resources.mediaResource - * - * @description - * Retrieves all media children with types used as folders. - * Uses the convention of looking for media items with mediaTypes ending in - * *Folder so will match "Folder", "bannerFolder", "secureFolder" etc, - * - * ##usage - *
-        * mediaResource.getChildFolders(1234)
-        *    .then(function(data) {
-        *        alert('folders');
-        *    });
-        * 
- * - * @param {int} parentId Id of the media item to query for child folders - * @returns {Promise} resourcePromise object. - * - */ - getChildFolders: function (parentId) { - if (!parentId) { - parentId = -1; - } + /** + * @ngdoc method + * @name umbraco.resources.mediaResource#addFolder + * @methodOf umbraco.resources.mediaResource + * + * @description + * Shorthand for adding a media item of the type "Folder" under a given parent ID + * + * ##usage + *
+          * mediaResource.addFolder("My gallery", 1234)
+          *    .then(function(folder) {
+          *        alert('New folder');
+          *    });
+          * 
+ * + * @param {string} name Name of the folder to create + * @param {int} parentId Id of the media item to create the folder underneath + * @returns {Promise} resourcePromise object. + * + */ + addFolder: function (name, parentId) { + return umbRequestHelper.resourcePromise( + $http.post(umbRequestHelper + .getApiUrl("mediaApiBaseUrl", "PostAddFolder"), + { + name: name, + parentId: parentId + }), + 'Failed to add folder'); + }, - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "mediaApiBaseUrl", - "GetChildFolders", - [ - { id: parentId } - ])), - 'Failed to retrieve child folders for media item ' + parentId); - }, + /** + * @ngdoc method + * @name umbraco.resources.mediaResource#getChildFolders + * @methodOf umbraco.resources.mediaResource + * + * @description + * Retrieves all media children with types used as folders. + * Uses the convention of looking for media items with mediaTypes ending in + * *Folder so will match "Folder", "bannerFolder", "secureFolder" etc, + * + * ##usage + *
+          * mediaResource.getChildFolders(1234)
+          *    .then(function(data) {
+          *        alert('folders');
+          *    });
+          * 
+ * + * @param {int} parentId Id of the media item to query for child folders + * @returns {Promise} resourcePromise object. + * + */ + getChildFolders: function (parentId) { + if (!parentId) { + parentId = -1; + } - /** - * @ngdoc method - * @name umbraco.resources.mediaResource#emptyRecycleBin - * @methodOf umbraco.resources.mediaResource - * - * @description - * Empties the media recycle bin - * - * ##usage - *
-        * mediaResource.emptyRecycleBin()
-        *    .then(function() {
-        *        alert('its empty!');
-        *    });
-        * 
- * - * @returns {Promise} resourcePromise object. - * - */ - emptyRecycleBin: function () { - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "mediaApiBaseUrl", - "EmptyRecycleBin")), - 'Failed to empty the recycle bin'); - } - }; + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "mediaApiBaseUrl", + "GetChildFolders", + [ + { id: parentId } + ])), + 'Failed to retrieve child folders for media item ' + parentId); + }, + + /** + * @ngdoc method + * @name umbraco.resources.mediaResource#emptyRecycleBin + * @methodOf umbraco.resources.mediaResource + * + * @description + * Empties the media recycle bin + * + * ##usage + *
+          * mediaResource.emptyRecycleBin()
+          *    .then(function() {
+          *        alert('its empty!');
+          *    });
+          * 
+ * + * @returns {Promise} resourcePromise object. + * + */ + emptyRecycleBin: function () { + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "mediaApiBaseUrl", + "EmptyRecycleBin")), + 'Failed to empty the recycle bin'); + } + }; } angular.module('umbraco.resources').factory('mediaResource', mediaResource); From 6856540a7cc6f5bd84010fb5a71b72d39a48c52a Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 6 Apr 2016 09:49:53 +0200 Subject: [PATCH 17/46] fixes bool parameter for members --- .../src/common/resources/member.resource.js | 440 +++++++++--------- 1 file changed, 227 insertions(+), 213 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/member.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/member.resource.js index 3ec9258f98..2073307db9 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/member.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/member.resource.js @@ -5,231 +5,245 @@ **/ function memberResource($q, $http, umbDataFormatter, umbRequestHelper) { - /** internal method process the saving of data and post processing the result */ - function saveMember(content, action, files) { + /** internal method process the saving of data and post processing the result */ + function saveMember(content, action, files) { - return umbRequestHelper.postSaveContent({ - restApiUrl: umbRequestHelper.getApiUrl( - "memberApiBaseUrl", - "PostSave"), - content: content, - action: action, - files: files, - dataFormatter: function (c, a) { - return umbDataFormatter.formatMemberPostData(c, a); - } - }); - } + return umbRequestHelper.postSaveContent({ + restApiUrl: umbRequestHelper.getApiUrl( + "memberApiBaseUrl", + "PostSave"), + content: content, + action: action, + files: files, + dataFormatter: function (c, a) { + return umbDataFormatter.formatMemberPostData(c, a); + } + }); + } - return { + return { - getPagedResults: function (memberTypeAlias, options) { + getPagedResults: function (memberTypeAlias, options) { - if (memberTypeAlias === 'all-members') { - memberTypeAlias = null; - } + if (memberTypeAlias === 'all-members') { + memberTypeAlias = null; + } - var defaults = { - pageSize: 25, - pageNumber: 1, - filter: '', - orderDirection: "Ascending", - orderBy: "LoginName", - orderBySystemField: true - }; - if (options === undefined) { - options = {}; - } - //overwrite the defaults if there are any specified - angular.extend(defaults, options); - //now copy back to the options we will use - options = defaults; - //change asc/desct - if (options.orderDirection === "asc") { - options.orderDirection = "Ascending"; - } - else if (options.orderDirection === "desc") { - options.orderDirection = "Descending"; - } + var defaults = { + pageSize: 25, + pageNumber: 1, + filter: '', + orderDirection: "Ascending", + orderBy: "LoginName", + orderBySystemField: true + }; + if (options === undefined) { + options = {}; + } + //overwrite the defaults if there are any specified + angular.extend(defaults, options); + //now copy back to the options we will use + options = defaults; + //change asc/desct + if (options.orderDirection === "asc") { + options.orderDirection = "Ascending"; + } + else if (options.orderDirection === "desc") { + options.orderDirection = "Descending"; + } - var params = [ - { pageNumber: options.pageNumber }, - { pageSize: options.pageSize }, - { orderBy: options.orderBy }, - { orderDirection: options.orderDirection }, - { orderBySystemField: options.orderBySystemField }, - { filter: options.filter } - ]; - if (memberTypeAlias != null) { - params.push({ memberTypeAlias: memberTypeAlias }); - } + //converts the value to a js bool + function toBool(v) { + if (angular.isNumber(v)) { + return v > 0; + } + if (angular.isString(v)) { + return v === "true"; + } + if (typeof v === "boolean") { + return v; + } + return false; + } - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "memberApiBaseUrl", - "GetPagedResults", - params)), - 'Failed to retrieve member paged result'); - }, + var params = [ + { pageNumber: options.pageNumber }, + { pageSize: options.pageSize }, + { orderBy: options.orderBy }, + { orderDirection: options.orderDirection }, + { orderBySystemField: toBool(options.orderBySystemField) }, + { filter: options.filter } + ]; + if (memberTypeAlias != null) { + params.push({ memberTypeAlias: memberTypeAlias }); + } - getListNode: function (listName) { - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "memberApiBaseUrl", - "GetListNodeDisplay", - [{ listName: listName }])), - 'Failed to retrieve data for member list ' + listName); - }, - - /** - * @ngdoc method - * @name umbraco.resources.memberResource#getByKey - * @methodOf umbraco.resources.memberResource - * - * @description - * Gets a member item with a given key - * - * ##usage - *
-        * memberResource.getByKey("0000-0000-000-00000-000")
-        *    .then(function(member) {
-        *        var mymember = member; 
-        *        alert('its here!');
-        *    });
-        * 
- * - * @param {Guid} key key of member item to return - * @returns {Promise} resourcePromise object containing the member item. - * - */ - getByKey: function (key) { - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "memberApiBaseUrl", - "GetByKey", - [{ key: key }])), - 'Failed to retrieve data for member id ' + key); - }, - - /** - * @ngdoc method - * @name umbraco.resources.memberResource#deleteByKey - * @methodOf umbraco.resources.memberResource - * - * @description - * Deletes a member item with a given key - * - * ##usage - *
-        * memberResource.deleteByKey("0000-0000-000-00000-000")
-        *    .then(function() {
-        *        alert('its gone!');
-        *    });
-        * 
- * - * @param {Guid} key id of member item to delete - * @returns {Promise} resourcePromise object. - * - */ - deleteByKey: function (key) { - return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "memberApiBaseUrl", - "DeleteByKey", - [{ key: key }])), - 'Failed to delete item ' + key); - }, - - /** - * @ngdoc method - * @name umbraco.resources.memberResource#getScaffold - * @methodOf umbraco.resources.memberResource - * - * @description - * Returns a scaffold of an empty member item, given the id of the member item to place it underneath and the member type alias. - * - * - Member Type alias must be provided so umbraco knows which properties to put on the member scaffold - * - * The scaffold is used to build editors for member that has not yet been populated with data. - * - * ##usage - *
-        * memberResource.getScaffold('client')
-        *    .then(function(scaffold) {
-        *        var myDoc = scaffold;
-        *        myDoc.name = "My new member item"; 
-        *
-        *        memberResource.save(myDoc, true)
-        *            .then(function(member){
-        *                alert("Retrieved, updated and saved again");
-        *            });
-        *    });
-        * 
- * - * @param {String} alias membertype alias to base the scaffold on - * @returns {Promise} resourcePromise object containing the member scaffold. - * - */ - getScaffold: function (alias) { - - if (alias) { return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "memberApiBaseUrl", - "GetEmpty", - [{ contentTypeAlias: alias }])), - 'Failed to retrieve data for empty member item type ' + alias); - } - else { + $http.get( + umbRequestHelper.getApiUrl( + "memberApiBaseUrl", + "GetPagedResults", + params)), + 'Failed to retrieve member paged result'); + }, + + getListNode: function (listName) { + return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "memberApiBaseUrl", - "GetEmpty")), - 'Failed to retrieve data for empty member item type ' + alias); - } + $http.get( + umbRequestHelper.getApiUrl( + "memberApiBaseUrl", + "GetListNodeDisplay", + [{ listName: listName }])), + 'Failed to retrieve data for member list ' + listName); + }, - }, + /** + * @ngdoc method + * @name umbraco.resources.memberResource#getByKey + * @methodOf umbraco.resources.memberResource + * + * @description + * Gets a member item with a given key + * + * ##usage + *
+          * memberResource.getByKey("0000-0000-000-00000-000")
+          *    .then(function(member) {
+          *        var mymember = member; 
+          *        alert('its here!');
+          *    });
+          * 
+ * + * @param {Guid} key key of member item to return + * @returns {Promise} resourcePromise object containing the member item. + * + */ + getByKey: function (key) { - /** - * @ngdoc method - * @name umbraco.resources.memberResource#save - * @methodOf umbraco.resources.memberResource - * - * @description - * Saves changes made to a member, if the member is new, the isNew paramater must be passed to force creation - * if the member needs to have files attached, they must be provided as the files param and passed separately - * - * - * ##usage - *
-        * memberResource.getBykey("23234-sd8djsd-3h8d3j-sdh8d")
-        *    .then(function(member) {
-        *          member.name = "Bob";
-        *          memberResource.save(member, false)
-        *            .then(function(member){
-        *                alert("Retrieved, updated and saved again");
-        *            });
-        *    });
-        * 
- * - * @param {Object} media The member item object with changes applied - * @param {Bool} isNew set to true to create a new item or to update an existing - * @param {Array} files collection of files for the media item - * @returns {Promise} resourcePromise object containing the saved media item. - * - */ - save: function (member, isNew, files) { - return saveMember(member, "save" + (isNew ? "New" : ""), files); - } - }; + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "memberApiBaseUrl", + "GetByKey", + [{ key: key }])), + 'Failed to retrieve data for member id ' + key); + }, + + /** + * @ngdoc method + * @name umbraco.resources.memberResource#deleteByKey + * @methodOf umbraco.resources.memberResource + * + * @description + * Deletes a member item with a given key + * + * ##usage + *
+          * memberResource.deleteByKey("0000-0000-000-00000-000")
+          *    .then(function() {
+          *        alert('its gone!');
+          *    });
+          * 
+ * + * @param {Guid} key id of member item to delete + * @returns {Promise} resourcePromise object. + * + */ + deleteByKey: function (key) { + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "memberApiBaseUrl", + "DeleteByKey", + [{ key: key }])), + 'Failed to delete item ' + key); + }, + + /** + * @ngdoc method + * @name umbraco.resources.memberResource#getScaffold + * @methodOf umbraco.resources.memberResource + * + * @description + * Returns a scaffold of an empty member item, given the id of the member item to place it underneath and the member type alias. + * + * - Member Type alias must be provided so umbraco knows which properties to put on the member scaffold + * + * The scaffold is used to build editors for member that has not yet been populated with data. + * + * ##usage + *
+          * memberResource.getScaffold('client')
+          *    .then(function(scaffold) {
+          *        var myDoc = scaffold;
+          *        myDoc.name = "My new member item"; 
+          *
+          *        memberResource.save(myDoc, true)
+          *            .then(function(member){
+          *                alert("Retrieved, updated and saved again");
+          *            });
+          *    });
+          * 
+ * + * @param {String} alias membertype alias to base the scaffold on + * @returns {Promise} resourcePromise object containing the member scaffold. + * + */ + getScaffold: function (alias) { + + if (alias) { + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "memberApiBaseUrl", + "GetEmpty", + [{ contentTypeAlias: alias }])), + 'Failed to retrieve data for empty member item type ' + alias); + } + else { + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "memberApiBaseUrl", + "GetEmpty")), + 'Failed to retrieve data for empty member item type ' + alias); + } + + }, + + /** + * @ngdoc method + * @name umbraco.resources.memberResource#save + * @methodOf umbraco.resources.memberResource + * + * @description + * Saves changes made to a member, if the member is new, the isNew paramater must be passed to force creation + * if the member needs to have files attached, they must be provided as the files param and passed separately + * + * + * ##usage + *
+          * memberResource.getBykey("23234-sd8djsd-3h8d3j-sdh8d")
+          *    .then(function(member) {
+          *          member.name = "Bob";
+          *          memberResource.save(member, false)
+          *            .then(function(member){
+          *                alert("Retrieved, updated and saved again");
+          *            });
+          *    });
+          * 
+ * + * @param {Object} media The member item object with changes applied + * @param {Bool} isNew set to true to create a new item or to update an existing + * @param {Array} files collection of files for the media item + * @returns {Promise} resourcePromise object containing the saved media item. + * + */ + save: function (member, isNew, files) { + return saveMember(member, "save" + (isNew ? "New" : ""), files); + } + }; } angular.module('umbraco.resources').factory('memberResource', memberResource); From 75be22a778321415f815a0e1c135f3d97905fcb7 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 6 Apr 2016 12:05:52 +0200 Subject: [PATCH 18/46] Fixes tests, adds null checks --- src/Umbraco.Tests/UmbracoExamine/IndexInitializer.cs | 3 ++- src/Umbraco.Web/PublishedContentExtensions.cs | 10 +++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Tests/UmbracoExamine/IndexInitializer.cs b/src/Umbraco.Tests/UmbracoExamine/IndexInitializer.cs index 476c37d85c..aea5243058 100644 --- a/src/Umbraco.Tests/UmbracoExamine/IndexInitializer.cs +++ b/src/Umbraco.Tests/UmbracoExamine/IndexInitializer.cs @@ -74,9 +74,10 @@ namespace Umbraco.Tests.UmbracoExamine mediaService = Mock.Of( x => x.GetPagedDescendants( - It.IsAny(), It.IsAny(), It.IsAny(), out totalRecs, It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()) + It.IsAny(), It.IsAny(), It.IsAny(), out totalRecs, It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()) == allRecs); + } if (dataTypeService == null) { diff --git a/src/Umbraco.Web/PublishedContentExtensions.cs b/src/Umbraco.Web/PublishedContentExtensions.cs index 197175366e..578aab2755 100644 --- a/src/Umbraco.Web/PublishedContentExtensions.cs +++ b/src/Umbraco.Web/PublishedContentExtensions.cs @@ -1201,6 +1201,7 @@ namespace Umbraco.Web /// Enumerates bottom-up ie walking up the tree (parent, grand-parent, etc). internal static IEnumerable EnumerateAncestors(this IPublishedContent content, bool orSelf) { + if (content == null) throw new ArgumentNullException("content"); if (orSelf) yield return content; while ((content = content.Parent) != null) yield return content; @@ -1373,6 +1374,7 @@ namespace Umbraco.Web internal static IEnumerable EnumerateDescendants(this IPublishedContent content, bool orSelf) { + if (content == null) throw new ArgumentNullException("content"); if (orSelf) yield return content; foreach (var child in content.Children) @@ -1707,6 +1709,7 @@ namespace Umbraco.Web public static T Parent(this IPublishedContent content) where T : class, IPublishedContent { + if (content == null) throw new ArgumentNullException("content"); return content.Parent as T; } @@ -1724,9 +1727,10 @@ namespace Umbraco.Web /// This method exists for consistency, it is the same as calling content.Children as a property. /// public static IEnumerable Children(this IPublishedContent content) - { - return content.Children; - } + { + if (content == null) throw new ArgumentNullException("content"); + return content.Children; + } /// /// Gets the children of the content, filtered by a predicate. From e2954fbf39b3f5350ce8349e81118aea1760c9f4 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 6 Apr 2016 13:23:39 +0200 Subject: [PATCH 19/46] refactors interface methods so they are not breaking existing signatures and not overlapping based on optional parameters --- src/Umbraco.Core/Services/ContentService.cs | 37 ++++++++++++++++++-- src/Umbraco.Core/Services/IContentService.cs | 32 +++++++++++++++-- src/Umbraco.Core/Services/IMediaService.cs | 32 +++++++++++++++-- src/Umbraco.Core/Services/IMemberService.cs | 17 ++++++++- src/Umbraco.Core/Services/MediaService.cs | 37 ++++++++++++++++++-- src/Umbraco.Core/Services/MemberService.cs | 8 ++++- src/UmbracoExamine/UmbracoMemberIndexer.cs | 3 +- 7 files changed, 154 insertions(+), 12 deletions(-) diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index 2d08632905..bbae07eb6a 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -493,6 +493,23 @@ namespace Umbraco.Core.Services return result; } + /// + /// Gets a collection of objects by Parent Id + /// + /// Id of the Parent to retrieve Children from + /// Page index (zero based) + /// Page size + /// Total records query would return without paging + /// Field to order by + /// Direction to order by + /// Search text filter + /// An Enumerable list of objects + public IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize, out long totalChildren, + string orderBy, Direction orderDirection, string filter = "") + { + return GetPagedChildren(id, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, true, filter); + } + /// /// Gets a collection of objects by Parent Id /// @@ -506,7 +523,7 @@ namespace Umbraco.Core.Services /// Search text filter /// An Enumerable list of objects public IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize, out long totalChildren, - string orderBy, Direction orderDirection, bool orderBySystemField = true, string filter = "") + string orderBy, Direction orderDirection, bool orderBySystemField, string filter) { Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); Mandate.ParameterCondition(pageSize > 0, "pageSize"); @@ -535,6 +552,22 @@ namespace Umbraco.Core.Services return result; } + /// + /// Gets a collection of objects by Parent Id + /// + /// Id of the Parent to retrieve Descendants from + /// Page number + /// Page size + /// Total records query would return without paging + /// Field to order by + /// Direction to order by + /// Search text filter + /// An Enumerable list of objects + public IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalChildren, string orderBy = "Path", Direction orderDirection = Direction.Ascending, string filter = "") + { + return GetPagedDescendants(id, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, true, filter); + } + /// /// Gets a collection of objects by Parent Id /// @@ -547,7 +580,7 @@ namespace Umbraco.Core.Services /// Flag to indicate when ordering by system field /// Search text filter /// An Enumerable list of objects - public IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalChildren, string orderBy = "Path", Direction orderDirection = Direction.Ascending, bool orderBySystemField = true, string filter = "") + public IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalChildren, string orderBy, Direction orderDirection, bool orderBySystemField, string filter) { Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); Mandate.ParameterCondition(pageSize > 0, "pageSize"); diff --git a/src/Umbraco.Core/Services/IContentService.cs b/src/Umbraco.Core/Services/IContentService.cs index 75a412e683..df70fa8094 100644 --- a/src/Umbraco.Core/Services/IContentService.cs +++ b/src/Umbraco.Core/Services/IContentService.cs @@ -205,6 +205,20 @@ namespace Umbraco.Core.Services IEnumerable GetPagedChildren(int id, int pageIndex, int pageSize, out int totalRecords, string orderBy = "SortOrder", Direction orderDirection = Direction.Ascending, string filter = ""); + /// + /// Gets a collection of objects by Parent Id + /// + /// Id of the Parent to retrieve Children from + /// Page number + /// Page size + /// Total records query would return without paging + /// Field to order by + /// Direction to order by + /// Search text filter + /// An Enumerable list of objects + IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize, out long totalRecords, + string orderBy = "SortOrder", Direction orderDirection = Direction.Ascending, string filter = ""); + /// /// Gets a collection of objects by Parent Id /// @@ -218,13 +232,27 @@ namespace Umbraco.Core.Services /// Search text filter /// An Enumerable list of objects IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize, out long totalRecords, - string orderBy = "SortOrder", Direction orderDirection = Direction.Ascending, bool orderBySystemField = true, string filter = ""); + string orderBy, Direction orderDirection, bool orderBySystemField, string filter); [Obsolete("Use the overload with 'long' parameter types instead")] [EditorBrowsable(EditorBrowsableState.Never)] IEnumerable GetPagedDescendants(int id, int pageIndex, int pageSize, out int totalRecords, string orderBy = "Path", Direction orderDirection = Direction.Ascending, string filter = ""); + /// + /// Gets a collection of objects by Parent Id + /// + /// Id of the Parent to retrieve Descendants from + /// Page number + /// Page size + /// Total records query would return without paging + /// Field to order by + /// Direction to order by + /// Search text filter + /// An Enumerable list of objects + IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalRecords, + string orderBy = "Path", Direction orderDirection = Direction.Ascending, string filter = ""); + /// /// Gets a collection of objects by Parent Id /// @@ -238,7 +266,7 @@ namespace Umbraco.Core.Services /// Search text filter /// An Enumerable list of objects IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalRecords, - string orderBy = "Path", Direction orderDirection = Direction.Ascending, bool orderBySystemField = true, string filter = ""); + string orderBy, Direction orderDirection, bool orderBySystemField, string filter); /// /// Gets a collection of an objects versions by its Id diff --git a/src/Umbraco.Core/Services/IMediaService.cs b/src/Umbraco.Core/Services/IMediaService.cs index 8359b487b1..6ff8f75402 100644 --- a/src/Umbraco.Core/Services/IMediaService.cs +++ b/src/Umbraco.Core/Services/IMediaService.cs @@ -123,6 +123,20 @@ namespace Umbraco.Core.Services IEnumerable GetPagedChildren(int id, int pageIndex, int pageSize, out int totalRecords, string orderBy = "SortOrder", Direction orderDirection = Direction.Ascending, string filter = ""); + /// + /// Gets a collection of objects by Parent Id + /// + /// Id of the Parent to retrieve Children from + /// Page number + /// Page size + /// Total records query would return without paging + /// Field to order by + /// Direction to order by + /// Search text filter + /// An Enumerable list of objects + IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize, out long totalRecords, + string orderBy = "SortOrder", Direction orderDirection = Direction.Ascending, string filter = ""); + /// /// Gets a collection of objects by Parent Id /// @@ -136,13 +150,27 @@ namespace Umbraco.Core.Services /// Search text filter /// An Enumerable list of objects IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize, out long totalRecords, - string orderBy = "SortOrder", Direction orderDirection = Direction.Ascending, bool orderBySystemField = true, string filter = ""); + string orderBy, Direction orderDirection, bool orderBySystemField, string filter); [Obsolete("Use the overload with 'long' parameter types instead")] [EditorBrowsable(EditorBrowsableState.Never)] IEnumerable GetPagedDescendants(int id, int pageIndex, int pageSize, out int totalRecords, string orderBy = "Path", Direction orderDirection = Direction.Ascending, string filter = ""); + /// + /// Gets a collection of objects by Parent Id + /// + /// Id of the Parent to retrieve Descendants from + /// Page number + /// Page size + /// Total records query would return without paging + /// Field to order by + /// Direction to order by + /// Search text filter + /// An Enumerable list of objects + IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalRecords, + string orderBy = "Path", Direction orderDirection = Direction.Ascending, string filter = ""); + /// /// Gets a collection of objects by Parent Id /// @@ -156,7 +184,7 @@ namespace Umbraco.Core.Services /// Search text filter /// An Enumerable list of objects IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalRecords, - string orderBy = "Path", Direction orderDirection = Direction.Ascending, bool orderBySystemField = true, string filter = ""); + string orderBy, Direction orderDirection, bool orderBySystemField, string filter); /// /// Gets descendants of a object by its Id diff --git a/src/Umbraco.Core/Services/IMemberService.cs b/src/Umbraco.Core/Services/IMemberService.cs index ee37d983aa..a893d89feb 100644 --- a/src/Umbraco.Core/Services/IMemberService.cs +++ b/src/Umbraco.Core/Services/IMemberService.cs @@ -27,6 +27,21 @@ namespace Umbraco.Core.Services IEnumerable GetAll(int pageIndex, int pageSize, out int totalRecords, string orderBy, Direction orderDirection, string memberTypeAlias = null, string filter = ""); + /// + /// Gets a list of paged objects + /// + /// An can be of type + /// Current page index + /// Size of the page + /// Total number of records found (out) + /// Field to order by + /// Direction to order by + /// + /// Search text filter + /// + IEnumerable GetAll(long pageIndex, int pageSize, out long totalRecords, + string orderBy, Direction orderDirection, string memberTypeAlias = null, string filter = ""); + /// /// Gets a list of paged objects /// @@ -41,7 +56,7 @@ namespace Umbraco.Core.Services /// Search text filter /// IEnumerable GetAll(long pageIndex, int pageSize, out long totalRecords, - string orderBy, Direction orderDirection, bool orderBySystemField = true, string memberTypeAlias = null, string filter = ""); + string orderBy, Direction orderDirection, bool orderBySystemField, string memberTypeAlias, string filter); /// /// Creates an object without persisting it diff --git a/src/Umbraco.Core/Services/MediaService.cs b/src/Umbraco.Core/Services/MediaService.cs index d73ee52c2e..34d130d725 100644 --- a/src/Umbraco.Core/Services/MediaService.cs +++ b/src/Umbraco.Core/Services/MediaService.cs @@ -403,6 +403,23 @@ namespace Umbraco.Core.Services return result; } + /// + /// Gets a collection of objects by Parent Id + /// + /// Id of the Parent to retrieve Children from + /// Page index (zero based) + /// Page size + /// Total records query would return without paging + /// Field to order by + /// Direction to order by + /// Search text filter + /// An Enumerable list of objects + public IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize, out long totalChildren, + string orderBy, Direction orderDirection, string filter = "") + { + return GetPagedChildren(id, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, true, filter); + } + /// /// Gets a collection of objects by Parent Id /// @@ -416,7 +433,7 @@ namespace Umbraco.Core.Services /// Search text filter /// An Enumerable list of objects public IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize, out long totalChildren, - string orderBy, Direction orderDirection, bool orderBySystemField = true, string filter = "") + string orderBy, Direction orderDirection, bool orderBySystemField, string filter) { Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); Mandate.ParameterCondition(pageSize > 0, "pageSize"); @@ -441,6 +458,22 @@ namespace Umbraco.Core.Services return result; } + /// + /// Gets a collection of objects by Parent Id + /// + /// Id of the Parent to retrieve Descendants from + /// Page number + /// Page size + /// Total records query would return without paging + /// Field to order by + /// Direction to order by + /// Search text filter + /// An Enumerable list of objects + public IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalChildren, string orderBy = "Path", Direction orderDirection = Direction.Ascending, string filter = "") + { + return GetPagedDescendants(id, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, true, filter); + } + /// /// Gets a collection of objects by Parent Id /// @@ -453,7 +486,7 @@ namespace Umbraco.Core.Services /// Flag to indicate when ordering by system field /// Search text filter /// An Enumerable list of objects - public IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalChildren, string orderBy = "Path", Direction orderDirection = Direction.Ascending, bool orderBySystemField = true, string filter = "") + public IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalChildren, string orderBy, Direction orderDirection, bool orderBySystemField, string filter) { Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); Mandate.ParameterCondition(pageSize > 0, "pageSize"); diff --git a/src/Umbraco.Core/Services/MemberService.cs b/src/Umbraco.Core/Services/MemberService.cs index 93d7a1a89f..094539d66e 100644 --- a/src/Umbraco.Core/Services/MemberService.cs +++ b/src/Umbraco.Core/Services/MemberService.cs @@ -704,7 +704,13 @@ namespace Umbraco.Core.Services } public IEnumerable GetAll(long pageIndex, int pageSize, out long totalRecords, - string orderBy, Direction orderDirection, bool orderBySystemField = true, string memberTypeAlias = null, string filter = "") + string orderBy, Direction orderDirection, string memberTypeAlias = null, string filter = "") + { + return GetAll(pageIndex, pageSize, out totalRecords, orderBy, orderDirection, true, memberTypeAlias, filter); + } + + public IEnumerable GetAll(long pageIndex, int pageSize, out long totalRecords, + string orderBy, Direction orderDirection, bool orderBySystemField, string memberTypeAlias, string filter) { var uow = UowProvider.GetUnitOfWork(); using (var repository = RepositoryFactory.CreateMemberRepository(uow)) diff --git a/src/UmbracoExamine/UmbracoMemberIndexer.cs b/src/UmbracoExamine/UmbracoMemberIndexer.cs index 4a460e0f38..64a574822f 100644 --- a/src/UmbracoExamine/UmbracoMemberIndexer.cs +++ b/src/UmbracoExamine/UmbracoMemberIndexer.cs @@ -142,8 +142,7 @@ namespace UmbracoExamine do { long total; - members = _memberService.GetAll(pageIndex, pageSize, out total, "LoginName" - , Direction.Ascending, true, nodeType).ToArray(); + members = _memberService.GetAll(pageIndex, pageSize, out total, "LoginName", Direction.Ascending, true, null, nodeType).ToArray(); AddNodesToIndex(GetSerializedMembers(members), type); From 76054bb78338c9997bbf63c5e6821f15151e2044 Mon Sep 17 00:00:00 2001 From: Claus Date: Tue, 12 Apr 2016 15:36:28 +0200 Subject: [PATCH 20/46] Enabling sorting by the Name column. OrderDirection is only reversed if reclicking the same sort column. --- .../src/common/services/listviewhelper.service.js | 9 ++++----- .../src/views/components/umb-table.html | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/listviewhelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/listviewhelper.service.js index e211218ce1..0dda0d4a96 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/listviewhelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/listviewhelper.service.js @@ -248,13 +248,12 @@ function setSorting(field, allow, options) { if (allow) { - options.orderBy = field; - - if (options.orderDirection === "desc") { - options.orderDirection = "asc"; - } else { + if (options.orderBy === field && options.orderDirection === 'asc') { options.orderDirection = "desc"; + } else { + options.orderDirection = "asc"; } + options.orderBy = field; } } diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-table.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-table.html index 8d9b45e6be..fb5e992d81 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-table.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-table.html @@ -10,7 +10,7 @@ ng-checked="isSelectedAll()">
- + Name From 1361e017a25588c61a0fe240872a403ea4a9d68c Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 12 Apr 2016 18:07:25 +0200 Subject: [PATCH 21/46] Merge branch 'u4-222' of https://github.com/AndyButland/Umbraco-CMS into AndyButland-u4-222 Conflicts: src/Umbraco.Core/Security/BackOfficeUserManager.cs src/Umbraco.Web.UI.Client/src/less/pages/login.less src/Umbraco.Web.UI.Client/src/routes.js src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.controller.js src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml src/Umbraco.Web.UI/web.Template.config src/Umbraco.Web/Editors/AuthenticationController.cs src/Umbraco.Web/Editors/BackOfficeController.cs src/Umbraco.Web/Umbraco.Web.csproj --- .../UmbracoSettings/ISecuritySection.cs | 2 + .../UmbracoSettings/SecurityElement.cs | 17 ++ .../Security/BackOfficeUserManager.cs | 2 +- src/Umbraco.Core/Security/EmailService.cs | 23 +++ src/Umbraco.Core/Umbraco.Core.csproj | 1 + .../UmbracoSettings/SecurityElementTests.cs | 9 ++ .../UmbracoSettings/umbracoSettings.config | 5 +- .../src/common/resources/auth.resource.js | 152 ++++++++++++++++++ .../src/common/services/user.service.js | 32 ++++ .../src/less/pages/login.less | 9 +- src/Umbraco.Web.UI.Client/src/routes.js | 6 +- .../views/common/dialogs/login.controller.js | 115 +++++++++++-- .../src/views/common/dialogs/login.html | 146 +++++++++++++---- .../config/umbracoSettings.config | 3 + src/Umbraco.Web.UI/umbraco/config/lang/en.xml | 12 ++ .../umbraco/config/lang/en_us.xml | 12 ++ src/Umbraco.Web.UI/web.Template.config | 2 +- .../Editors/AuthenticationController.cs | 126 ++++++++++++--- .../Editors/BackOfficeController.cs | 1 + .../HtmlHelperBackOfficeExtensions.cs | 5 + .../Models/RequestPasswordResetModel.cs | 12 ++ src/Umbraco.Web/Models/SetPasswordModel.cs | 20 +++ .../Models/ValidatePasswordResetCodeModel.cs | 16 ++ src/Umbraco.Web/Umbraco.Web.csproj | 3 + src/umbraco.businesslogic/UmbracoSettings.cs | 8 + 25 files changed, 664 insertions(+), 75 deletions(-) create mode 100644 src/Umbraco.Core/Security/EmailService.cs create mode 100644 src/Umbraco.Web/Models/RequestPasswordResetModel.cs create mode 100644 src/Umbraco.Web/Models/SetPasswordModel.cs create mode 100644 src/Umbraco.Web/Models/ValidatePasswordResetCodeModel.cs diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/ISecuritySection.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/ISecuritySection.cs index c3a1df301d..c44c0cf0df 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/ISecuritySection.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/ISecuritySection.cs @@ -6,6 +6,8 @@ bool HideDisabledUsersInBackoffice { get; } + bool AllowPasswordReset { get; } + string AuthCookieName { get; } string AuthCookieDomain { get; } diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/SecurityElement.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/SecurityElement.cs index 34642c8c90..f280b3e20c 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/SecurityElement.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/SecurityElement.cs @@ -28,6 +28,18 @@ namespace Umbraco.Core.Configuration.UmbracoSettings } } + [ConfigurationProperty("allowPasswordReset")] + internal InnerTextConfigurationElement AllowPasswordReset + { + get + { + return new OptionalInnerTextConfigurationElement( + (InnerTextConfigurationElement)this["allowPasswordReset"], + //set the default + true); + } + } + [ConfigurationProperty("authCookieName")] internal InnerTextConfigurationElement AuthCookieName { @@ -62,6 +74,11 @@ namespace Umbraco.Core.Configuration.UmbracoSettings get { return HideDisabledUsersInBackoffice; } } + bool ISecuritySection.AllowPasswordReset + { + get { return AllowPasswordReset; } + } + string ISecuritySection.AuthCookieName { get { return AuthCookieName; } diff --git a/src/Umbraco.Core/Security/BackOfficeUserManager.cs b/src/Umbraco.Core/Security/BackOfficeUserManager.cs index dfe59e1783..ba7e615771 100644 --- a/src/Umbraco.Core/Security/BackOfficeUserManager.cs +++ b/src/Umbraco.Core/Security/BackOfficeUserManager.cs @@ -132,7 +132,7 @@ namespace Umbraco.Core.Security // BodyFormat = "Your security code is: {0}" //}); - //manager.EmailService = new EmailService(); + manager.EmailService = new EmailService(); //manager.SmsService = new SmsService(); } diff --git a/src/Umbraco.Core/Security/EmailService.cs b/src/Umbraco.Core/Security/EmailService.cs new file mode 100644 index 0000000000..0a4ca95ceb --- /dev/null +++ b/src/Umbraco.Core/Security/EmailService.cs @@ -0,0 +1,23 @@ +using System.Net.Mail; +using System.Threading.Tasks; +using Microsoft.AspNet.Identity; + +namespace Umbraco.Core.Security +{ + public class EmailService : IIdentityMessageService + { + public async Task SendAsync(IdentityMessage message) + { + using (var client = new SmtpClient()) + using (var mailMessage = new MailMessage()) + { + mailMessage.Body = message.Body; + mailMessage.To.Add(message.Destination); + mailMessage.Subject = message.Subject; + mailMessage.IsBodyHtml = true; + + await client.SendMailAsync(mailMessage); + } + } + } +} diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 01774dc7a2..ce34b6ef8f 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -490,6 +490,7 @@ + diff --git a/src/Umbraco.Tests/Configurations/UmbracoSettings/SecurityElementTests.cs b/src/Umbraco.Tests/Configurations/UmbracoSettings/SecurityElementTests.cs index 8fbf4a1523..58a9a438a2 100644 --- a/src/Umbraco.Tests/Configurations/UmbracoSettings/SecurityElementTests.cs +++ b/src/Umbraco.Tests/Configurations/UmbracoSettings/SecurityElementTests.cs @@ -11,16 +11,25 @@ namespace Umbraco.Tests.Configurations.UmbracoSettings { Assert.IsTrue(SettingsSection.Security.KeepUserLoggedIn == true); } + [Test] public void HideDisabledUsersInBackoffice() { Assert.IsTrue(SettingsSection.Security.HideDisabledUsersInBackoffice == false); } + + [Test] + public void AllowPasswordReset() + { + Assert.IsTrue(SettingsSection.Security.AllowPasswordReset == true); + } + [Test] public void AuthCookieDomain() { Assert.IsTrue(SettingsSection.Security.AuthCookieDomain == null); } + [Test] public void AuthCookieName() { diff --git a/src/Umbraco.Tests/Configurations/UmbracoSettings/umbracoSettings.config b/src/Umbraco.Tests/Configurations/UmbracoSettings/umbracoSettings.config index 80f61371c2..80eaee77d3 100644 --- a/src/Umbraco.Tests/Configurations/UmbracoSettings/umbracoSettings.config +++ b/src/Umbraco.Tests/Configurations/UmbracoSettings/umbracoSettings.config @@ -110,7 +110,10 @@ false - + + + true + diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/auth.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/auth.resource.js index f32602bda6..b170df3c97 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/auth.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/auth.resource.js @@ -51,7 +51,159 @@ function authResource($q, $http, umbRequestHelper, angularHelper) { }), 'Login failed for user ' + username); }, + + /** + * @ngdoc method + * @name umbraco.resources.authResource#performRequestPasswordReset + * @methodOf umbraco.resources.authResource + * + * @description + * Checks to see if the provided email address is a valid user account and sends a link + * to allow them to reset their password + * + * ##usage + *
+         * authResource.performRequestPasswordReset(email)
+         *    .then(function(data) {
+         *        //Do stuff for password reset request...
+         *    });
+         * 
+ * @param {string} email Email address of backoffice user + * @returns {Promise} resourcePromise object + * + */ + performRequestPasswordReset: function (email) { + + if (!email) { + return angularHelper.rejectedPromise({ + errorMsg: 'Email address cannot be empty' + }); + } + + var emailRegex = /\S+@\S+\.\S+/; + if (!emailRegex.test(email)) { + return angularHelper.rejectedPromise({ + errorMsg: 'Email address is not valid' + }); + } + + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "authenticationApiBaseUrl", + "PostRequestPasswordReset"), { + email: email + }), + 'Request password reset failed for email ' + email); + }, + /** + * @ngdoc method + * @name umbraco.resources.authResource#performValidatePasswordResetCode + * @methodOf umbraco.resources.authResource + * + * @description + * Checks to see if the provided password reset code is valid + * + * ##usage + *
+         * authResource.performValidatePasswordResetCode(resetCode)
+         *    .then(function(data) {
+         *        //Allow reset of password
+         *    });
+         * 
+ * @param {integer} userId User Id + * @param {string} resetCode Password reset code + * @returns {Promise} resourcePromise object + * + */ + performValidatePasswordResetCode: function (userId, resetCode) { + + if (!userId) { + return angularHelper.rejectedPromise({ + errorMsg: 'User Id cannot be empty' + }); + } + + if (!resetCode) { + return angularHelper.rejectedPromise({ + errorMsg: 'Reset code cannot be empty' + }); + } + + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "authenticationApiBaseUrl", + "PostValidatePasswordResetCode"), + { + userId: userId, + resetCode: resetCode + }), + 'Password reset code validation failed for userId + ' + userId + ', code' + resetCode); + }, + + /** + * @ngdoc method + * @name umbraco.resources.authResource#performSetPassword + * @methodOf umbraco.resources.authResource + * + * @description + * Checks to see if the provided password reset code is valid and sets the user's password + * + * ##usage + *
+         * authResource.performSetPassword(userId, password, confirmPassword, resetCode)
+         *    .then(function(data) {
+         *        //Password set
+         *    });
+         * 
+ * @param {integer} userId User Id + * @param {string} password New password + * @param {string} confirmPassword Confirmation of new password + * @param {string} resetCode Password reset code + * @returns {Promise} resourcePromise object + * + */ + performSetPassword: function (userId, password, confirmPassword, resetCode) { + + if (!userId) { + return angularHelper.rejectedPromise({ + errorMsg: 'User Id cannot be empty' + }); + } + + if (!password) { + return angularHelper.rejectedPromise({ + errorMsg: 'Password cannot be empty' + }); + } + + if (password !== confirmPassword) { + return angularHelper.rejectedPromise({ + errorMsg: 'Password and confirmation do not match' + }); + } + + if (!resetCode) { + return angularHelper.rejectedPromise({ + errorMsg: 'Reset code cannot be empty' + }); + } + + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "authenticationApiBaseUrl", + "PostSetPassword"), + { + userId: userId, + password: password, + resetCode: resetCode + }), + 'Password reset code validation failed for userId + ' + userId); + }, + unlinkLogin: function (loginProvider, providerKey) { if (!loginProvider || !providerKey) { return angularHelper.rejectedPromise({ diff --git a/src/Umbraco.Web.UI.Client/src/common/services/user.service.js b/src/Umbraco.Web.UI.Client/src/common/services/user.service.js index 3fb291619d..27f0165f4d 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/user.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/user.service.js @@ -212,6 +212,38 @@ angular.module('umbraco.services') }); }, + /** Returns a promise, sends a request to the server to send a password reset email if the email is recognised */ + requestPasswordReset: function (email) { + + return authResource.performRequestPasswordReset(email) + .then(function (data) { + // Note that we don't actually confirm if the email address was matched or not, to avoid + // allowing an attacker to determine which email addresses are valid. + var result = { success: true }; + return result; + }); + }, + + /** Returns a promise, sends a request to the server to validate a password reset code */ + validatePasswordResetCode: function (userId, resetCode) { + + return authResource.performValidatePasswordResetCode(userId, resetCode) + .then(function (data) { + var result = { success: data }; + return result; + }); + }, + + /** Returns a promise, sends a request to the server to validate a password reset code */ + setPassword: function (userId, password, confirmPassword, resetCode) { + + return authResource.performSetPassword(userId, password, confirmPassword, resetCode) + .then(function (data) { + var result = { success: data }; + return result; + }); + }, + /** Logs the user out */ logout: function () { diff --git a/src/Umbraco.Web.UI.Client/src/less/pages/login.less b/src/Umbraco.Web.UI.Client/src/less/pages/login.less index 081a194b7e..ff7fbc243a 100644 --- a/src/Umbraco.Web.UI.Client/src/less/pages/login.less +++ b/src/Umbraco.Web.UI.Client/src/less/pages/login.less @@ -66,7 +66,7 @@ font-weight: normal; } -.login-overlay .alert.alert-error { +.login-overlay .alert { display: inline-block; padding-right: 6px; padding-left: 6px; @@ -74,6 +74,13 @@ text-align: center; } +.login-overlay .switch-view { + margin-top: 10px; + a { + color: #fff; + } +} + @media (max-width: 565px) { // Remove padding on login-form on smaller devices .login-overlay .form { diff --git a/src/Umbraco.Web.UI.Client/src/routes.js b/src/Umbraco.Web.UI.Client/src/routes.js index 5d641fcb6c..69e6779a15 100644 --- a/src/Umbraco.Web.UI.Client/src/routes.js +++ b/src/Umbraco.Web.UI.Client/src/routes.js @@ -93,10 +93,10 @@ app.config(function ($routeProvider) { //ensure auth is *not* required so it will redirect to / resolve: canRoute(false) }) - .when('/logout', { + .when('/logout', { redirectTo: '/login/false', - resolve: doLogout() - }) + resolve: doLogout() + }) .when('/:section', { templateUrl: function (rp) { if (rp.section.toLowerCase() === "default" || rp.section.toLowerCase() === "umbraco" || rp.section === "") diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.controller.js index 325af02bbc..d76d66a7c4 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.controller.js @@ -1,22 +1,38 @@ angular.module("umbraco").controller("Umbraco.Dialogs.LoginController", - function ($scope, $cookies, localizationService, userService, externalLoginInfo) { + function ($scope, $cookies, localizationService, userService, externalLoginInfo, $timeout, $location) { + + var setFieldFocus = function(form, field) { + $timeout(function() { + $("form[name='" + form + "'] input[name='" + field + "']").focus(); + }); + } + + $scope.allowPasswordReset = Umbraco.Sys.ServerVariables.umbracoSettings.allowPasswordReset; + + $scope.showLogin = function () { + $scope.errorMsg = ""; + $scope.view = "login"; + setFieldFocus("loginForm", "username"); + } + + $scope.showRequestPasswordReset = function () { + $scope.errorMsg = ""; + $scope.view = "request-password-reset"; + $scope.showEmailResetConfirmation = false; + setFieldFocus("requestPasswordResetForm", "email"); + } + + $scope.showSetPassword = function () { + $scope.view = "set-password"; + setFieldFocus("setPasswordForm", "password"); + } - /** - * @ngdoc function - * @name signin - * @methodOf MainController - * @function - * - * @description - * signs the user in - */ var d = new Date(); var konamiGreetings = new Array("Suze Sunday", "Malibu Monday", "Tequila Tuesday", "Whiskey Wednesday", "Negroni Day", "Fernet Friday", "Sancerre Saturday"); var konamiMode = $cookies.konamiLogin; - //var weekday = new Array("Super Sunday", "Manic Monday", "Tremendous Tuesday", "Wonderful Wednesday", "Thunder Thursday", "Friendly Friday", "Shiny Saturday"); if (konamiMode == "1") { $scope.greeting = "Happy " + konamiGreetings[d.getDay()]; } else { @@ -43,18 +59,32 @@ } } + // Set initial view - either set password if reset code provided in querystring + // otherwise login form + var userId = $location.search().userId; + var resetCode = $location.search().resetCode; + if (userId && resetCode) { + userService.validatePasswordResetCode(userId, resetCode) + .then(function () { + $scope.showSetPassword(); + }, function () { + $scope.view = "password-reset-code-expired"; + }); + } else { + $scope.showLogin(); + } + $scope.loginSubmit = function (login, password) { //if the login and password are not empty we need to automatically // validate them - this is because if there are validation errors on the server // then the user has to change both username & password to resubmit which isn't ideal, - // so if they're not empty , we'l just make sure to set them to valid. + // so if they're not empty, we'll just make sure to set them to valid. if (login && password && login.length > 0 && password.length > 0) { $scope.loginForm.username.$setValidity('auth', true); $scope.loginForm.password.$setValidity('auth', true); } - if ($scope.loginForm.$invalid) { return; } @@ -84,4 +114,63 @@ } }); }; + + $scope.requestPasswordResetSubmit = function (email) { + + $scope.errorMsg = ""; + $scope.showEmailResetConfirmation = false; + + if ($scope.requestPasswordResetForm.$invalid) { + return; + } + + userService.requestPasswordReset(email) + .then(function () { + $scope.showEmailResetConfirmation = true; + }, function (reason) { + $scope.errorMsg = reason.errorMsg; + $scope.requestPasswordResetForm.email.$setValidity("auth", false); + }); + + $scope.requestPasswordResetForm.email.$viewChangeListeners.push(function () { + if ($scope.requestPasswordResetForm.email.$invalid) { + $scope.requestPasswordResetForm.email.$setValidity('auth', true); + } + }); + }; + + $scope.setPasswordSubmit = function (password, confirmPassword) { + + $scope.showSetPasswordConfirmation = false; + + if (password && confirmPassword && password.length > 0 && confirmPassword.length > 0) { + $scope.setPasswordForm.password.$setValidity('auth', true); + $scope.setPasswordForm.confirmPassword.$setValidity('auth', true); + } + + if ($scope.setPasswordForm.$invalid) { + return; + } + + userService.setPassword(userId, password, confirmPassword, resetCode) + .then(function () { + $scope.showSetPasswordConfirmation = true; + }, function (reason) { + $scope.errorMsg = reason.errorMsg; + $scope.setPasswordForm.password.$setValidity("auth", false); + $scope.setPasswordForm.confirmPassword.$setValidity("auth", false); + }); + + $scope.setPasswordForm.password.$viewChangeListeners.push(function () { + if ($scope.setPasswordForm.password.$invalid) { + $scope.setPasswordForm.password.$setValidity('auth', true); + } + }); + $scope.setPasswordForm.confirmPassword.$viewChangeListeners.push(function () { + if ($scope.setPasswordForm.confirmPassword.$invalid) { + $scope.setPasswordForm.confirmPassword.$setValidity('auth', true); + } + }); + } + }); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html index 3ca9bcda10..992b98942e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html @@ -2,54 +2,130 @@

{{greeting}}

-

- Log in below. - Log in below -

- -
- -
- {{error}} + +
+ +

+ Log in below. + Log in below +

+ +
+ +
+ {{error}} +
+ +
+ +
+ + + +
+
+ +
+
+
Or
+
+
-
+ +
+ +
-
+
+ +
- + +
+
{{errorMsg}}
+
+ + - -
-
Or
-
-
-
-
- -
+
+

+ Please enter your email address. If your account is located an email will be sent to you containing a link from which you can reset your password. +

-
- -
+ +
+ +
- + -
-
{{errorMsg}}
+
+
{{errorMsg}}
+
+ +
+
+ If your email address has been matched an email with password reset instructions has been sent. +
+
+ + + +
+ +
+

+ Please provide a new password. +

+ +
+ +
+ +
+ +
+ +
+ + + +
+
{{errorMsg}}
+
+ +

+ Your new password has been set and you may now use it to log in. +

+ + +
+
+ +
+
+ The link you have clicked on is invalid or has expired.
- - + +
\ No newline at end of file diff --git a/src/Umbraco.Web.UI/config/umbracoSettings.config b/src/Umbraco.Web.UI/config/umbracoSettings.config index 118b992ae5..b21894e07a 100644 --- a/src/Umbraco.Web.UI/config/umbracoSettings.config +++ b/src/Umbraco.Web.UI/config/umbracoSettings.config @@ -112,6 +112,9 @@ false + + true + diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml index b810183d42..95f68b1219 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml @@ -290,6 +290,7 @@ Enter your username Enter your password + Confirm your password Name the %0%... Enter a name... Label... @@ -297,6 +298,7 @@ Type to search... Type to filter... Type to add tags (press enter after each tag)... + Enter your email Allow at root @@ -660,6 +662,16 @@ To manage your website, simply open the Umbraco back office and start adding con Log in below Session timed out © 2001 - %0%
Umbraco.com

]]>
+ Forgotten password? + Please enter your email address. If your account is located an email will be sent to you containing a link from which you can reset your password. + If your email address has been matched an email with password reset instructions has been sent. + Return to login form + Please provide a new password. + Your new password has been set and you may now use it to log in. + The link you have clicked on is invalid or has expired. + Umbraco: Reset Password + Your username to login to the Umbraco back-office is: {0} + here. ]]> Dashboard 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 402902f48c..24380a5680 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml @@ -291,6 +291,7 @@ Enter your username Enter your password + Confirm your password Name the %0%... Enter a name... Label... @@ -298,6 +299,7 @@ Type to search... Type to filter... Type to add tags (press enter after each tag)... + Enter your email @@ -658,6 +660,16 @@ To manage your website, simply open the Umbraco back office and start adding con Log in below Session timed out © 2001 - %0%
Umbraco.com

]]>
+ Forgotten password? + Please enter your email address. If your account is located an email will be sent to you containing a link from which you can reset your password. + If your email address has been matched an email with password reset instructions has been sent. + Return to login form + Please provide a new password. + Your new password has been set and you may now use it to log in. + The link you have clicked on is invalid or has expired. + Umbraco: Reset Password + Your username to login to the Umbraco back-office is: {0} + here. ]]> Dashboard diff --git a/src/Umbraco.Web.UI/web.Template.config b/src/Umbraco.Web.UI/web.Template.config index 7f8752fff7..dd66460408 100644 --- a/src/Umbraco.Web.UI/web.Template.config +++ b/src/Umbraco.Web.UI/web.Template.config @@ -74,7 +74,7 @@ - + diff --git a/src/Umbraco.Web/Editors/AuthenticationController.cs b/src/Umbraco.Web/Editors/AuthenticationController.cs index ca6a69e238..09027c58f4 100644 --- a/src/Umbraco.Web/Editors/AuthenticationController.cs +++ b/src/Umbraco.Web/Editors/AuthenticationController.cs @@ -3,41 +3,29 @@ using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; -using System.Security.Claims; -using System.ServiceModel.Channels; -using System.Text; using System.Threading.Tasks; using System.Web; -using System.Web.Helpers; using System.Web.Http; -using System.Web.Http.Controllers; -using System.Web.Security; using AutoMapper; using Microsoft.AspNet.Identity; +using Microsoft.AspNet.Identity.Owin; using Microsoft.Owin; -using Microsoft.Owin.Security; using Umbraco.Core; using Umbraco.Core.Configuration; -using Umbraco.Core.Models.Membership; +using Umbraco.Core.Logging; +using Umbraco.Core.Security; +using Umbraco.Core.Services; using Umbraco.Web.Models; using Umbraco.Web.Models.ContentEditing; -using Umbraco.Web.Models.Mapping; using Umbraco.Web.Mvc; -using Umbraco.Core.Security; using Umbraco.Web.Security; +using Umbraco.Web.Security.Identity; using Umbraco.Web.WebApi; using Umbraco.Web.WebApi.Filters; -using umbraco.providers; -using Microsoft.AspNet.Identity.Owin; -using Umbraco.Core.Logging; -using Newtonsoft.Json.Linq; -using Umbraco.Core.Models.Identity; -using Umbraco.Web.Security.Identity; using IUser = Umbraco.Core.Models.Membership.IUser; namespace Umbraco.Web.Editors { - /// /// The API controller used for editing content /// @@ -163,9 +151,7 @@ namespace Umbraco.Web.Editors [SetAngularAntiForgeryTokens] public async Task PostLogin(LoginModel loginModel) { - var http = this.TryGetHttpContext(); - if (http.Success == false) - throw new InvalidOperationException("This method requires that an HttpContext be active"); + var http = EnsureHttpContext(); var result = await SignInManager.PasswordSignInAsync( loginModel.Username, loginModel.Password, isPersistent: true, shouldLockout: true); @@ -231,6 +217,106 @@ namespace Umbraco.Web.Editors } } + /// + /// Processes a password reset request. Looks for a match on the provided email address + /// and if found sends an email with a link to reset it + /// + /// + [SetAngularAntiForgeryTokens] + public async Task PostRequestPasswordReset(RequestPasswordResetModel model) + { + // If this feature is switched off in configuration the UI will be amended to not make the request to reset password available. + // So this is just a server-side secondary check. + if (UmbracoConfig.For.UmbracoSettings().Security.AllowPasswordReset == false) + { + throw new HttpResponseException(HttpStatusCode.BadRequest); + } + + var http = EnsureHttpContext(); + + var identityUser = await SignInManager.UserManager.FindByEmailAsync(model.Email); + if (identityUser != null) + { + var user = Services.UserService.GetByEmail(model.Email); + if (user != null && user.IsLockedOut == false) + { + var code = await UserManager.GeneratePasswordResetTokenAsync(identityUser.Id); + var callbackUrl = ConstuctCallbackUrl(http.Request.Url, identityUser.Id, code); + var message = ConstructPasswordResetEmailMessage(user, callbackUrl); + await UserManager.SendEmailAsync(identityUser.Id, + Services.TextService.Localize("login/resetPasswordEmailCopySubject"), + message); + } + } + + return Request.CreateResponse(HttpStatusCode.OK); + } + + private static string ConstuctCallbackUrl(Uri url, int userId, string code) + { + return string.Format("{0}://{1}/umbraco/#/login?userId={2}&resetCode={3}", + url.Scheme, + url.Host + (url.Port == 80 ? string.Empty : ":" + url.Port), + userId, + HttpUtility.UrlEncode(code)); + } + + private string ConstructPasswordResetEmailMessage(IUser user, string callbackUrl) + { + var emailCopy1 = Services.TextService.Localize("login/resetPasswordEmailCopyFormat1"); + var emailCopy2 = Services.TextService.Localize("login/resetPasswordEmailCopyFormat2"); + var message = string.Format("

" + emailCopy1 + "

\n\n" + + "

" + emailCopy2 + "

", + user.Username, + callbackUrl); + return message; + } + + /// + /// Processes a password reset request. Looks for a match on the provided email address + /// and if found sends an email with a link to reset it + /// + /// + [SetAngularAntiForgeryTokens] + public async Task PostValidatePasswordResetCode(ValidatePasswordResetCodeModel model) + { + var user = UserManager.FindById(model.UserId); + if (user != null) + { + var result = await UserManager.UserTokenProvider.ValidateAsync("ResetPassword", + model.ResetCode, UserManager, user); + if (result) + { + return Request.CreateResponse(HttpStatusCode.OK); + } + } + + return Request.CreateValidationErrorResponse("Password reset code not valid"); + } + + /// + /// Processes a set password request. Validates the request and sets a new password. + /// + /// + [SetAngularAntiForgeryTokens] + public async Task PostSetPassword(SetPasswordModel model) + { + var result = await UserManager.ResetPasswordAsync(model.UserId, model.ResetCode, model.Password); + if (result.Succeeded) + { + return Request.CreateResponse(HttpStatusCode.OK); + } + + return Request.CreateValidationErrorResponse("Set password failed"); + } + + private HttpContextBase EnsureHttpContext() + { + var attempt = this.TryGetHttpContext(); + if (attempt.Success == false) + throw new InvalidOperationException("This method requires that an HttpContext be active"); + return attempt.Result; + } /// /// Logs the current user out diff --git a/src/Umbraco.Web/Editors/BackOfficeController.cs b/src/Umbraco.Web/Editors/BackOfficeController.cs index 0b7a611fbb..297221dacc 100644 --- a/src/Umbraco.Web/Editors/BackOfficeController.cs +++ b/src/Umbraco.Web/Editors/BackOfficeController.cs @@ -361,6 +361,7 @@ namespace Umbraco.Web.Editors }, {"keepUserLoggedIn", UmbracoConfig.For.UmbracoSettings().Security.KeepUserLoggedIn}, {"cssPath", IOHelper.ResolveUrl(SystemDirectories.Css).TrimEnd('/')}, + {"allowPasswordReset", UmbracoConfig.For.UmbracoSettings().Security.AllowPasswordReset}, } }, { diff --git a/src/Umbraco.Web/HtmlHelperBackOfficeExtensions.cs b/src/Umbraco.Web/HtmlHelperBackOfficeExtensions.cs index 3f38ae35ec..2dc9841293 100644 --- a/src/Umbraco.Web/HtmlHelperBackOfficeExtensions.cs +++ b/src/Umbraco.Web/HtmlHelperBackOfficeExtensions.cs @@ -12,6 +12,8 @@ using Umbraco.Web.Editors; namespace Umbraco.Web { + using Umbraco.Core.Configuration; + /// /// HtmlHelper extensions for the back office /// @@ -43,6 +45,9 @@ namespace Umbraco.Web ""serverVarsJs"": """ + uri.GetUrlWithCacheBust("ServerVariables", "BackOffice") + @""", ""externalLoginsUrl"": """ + externalLoginsUrl + @""" }, + ""umbracoSettings"": { + ""allowPasswordReset"": " + (UmbracoConfig.For.UmbracoSettings().Security.AllowPasswordReset ? "true" : "false") + @" + }, ""application"": { ""applicationPath"": """ + html.ViewContext.HttpContext.Request.ApplicationPath + @""", ""version"": """ + version + @""", diff --git a/src/Umbraco.Web/Models/RequestPasswordResetModel.cs b/src/Umbraco.Web/Models/RequestPasswordResetModel.cs new file mode 100644 index 0000000000..650dd8ed11 --- /dev/null +++ b/src/Umbraco.Web/Models/RequestPasswordResetModel.cs @@ -0,0 +1,12 @@ +using System.ComponentModel.DataAnnotations; +using System.Runtime.Serialization; + +namespace Umbraco.Web.Models +{ + public class RequestPasswordResetModel + { + [Required] + [DataMember(Name = "email", IsRequired = true)] + public string Email { get; set; } + } +} diff --git a/src/Umbraco.Web/Models/SetPasswordModel.cs b/src/Umbraco.Web/Models/SetPasswordModel.cs new file mode 100644 index 0000000000..cc70989d66 --- /dev/null +++ b/src/Umbraco.Web/Models/SetPasswordModel.cs @@ -0,0 +1,20 @@ +using System.ComponentModel.DataAnnotations; +using System.Runtime.Serialization; + +namespace Umbraco.Web.Models +{ + public class SetPasswordModel + { + [Required] + [DataMember(Name = "userId", IsRequired = true)] + public int UserId { get; set; } + + [Required] + [DataMember(Name = "password", IsRequired = true)] + public string Password { get; set; } + + [Required] + [DataMember(Name = "resetCode", IsRequired = true)] + public string ResetCode { get; set; } + } +} diff --git a/src/Umbraco.Web/Models/ValidatePasswordResetCodeModel.cs b/src/Umbraco.Web/Models/ValidatePasswordResetCodeModel.cs new file mode 100644 index 0000000000..61db6907ef --- /dev/null +++ b/src/Umbraco.Web/Models/ValidatePasswordResetCodeModel.cs @@ -0,0 +1,16 @@ +using System.ComponentModel.DataAnnotations; +using System.Runtime.Serialization; + +namespace Umbraco.Web.Models +{ + public class ValidatePasswordResetCodeModel + { + [Required] + [DataMember(Name = "userId", IsRequired = true)] + public int UserId { get; set; } + + [Required] + [DataMember(Name = "resetCode", IsRequired = true)] + public string ResetCode { get; set; } + } +} diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 3aeffcfe16..16b220b232 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -341,6 +341,9 @@ + + + diff --git a/src/umbraco.businesslogic/UmbracoSettings.cs b/src/umbraco.businesslogic/UmbracoSettings.cs index 3d489cfaa4..d73ea22844 100644 --- a/src/umbraco.businesslogic/UmbracoSettings.cs +++ b/src/umbraco.businesslogic/UmbracoSettings.cs @@ -102,6 +102,14 @@ namespace umbraco get { return UmbracoConfig.For.UmbracoSettings().Security.HideDisabledUsersInBackoffice; } } + /// + /// Enable the UI and API to allow back-office users to reset their passwords? Default is true + /// + public static bool AllowPasswordReset + { + get { return UmbracoConfig.For.UmbracoSettings().Security.AllowPasswordReset; } + } + /// /// Gets a value indicating whether the logs will be auto cleaned /// From d424b13ac41e83186760af172a4a18be2663abc3 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 12 Apr 2016 18:59:39 +0200 Subject: [PATCH 22/46] Fixes some styling, wording and validation --- .../src/less/pages/login.less | 11 +++-- .../views/common/dialogs/login.controller.js | 21 +++++++-- .../src/views/common/dialogs/login.html | 44 ++++++++++--------- src/Umbraco.Web.UI/umbraco/config/lang/en.xml | 4 +- .../umbraco/config/lang/en_us.xml | 4 +- 5 files changed, 52 insertions(+), 32 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/pages/login.less b/src/Umbraco.Web.UI.Client/src/less/pages/login.less index ff7fbc243a..6805783a86 100644 --- a/src/Umbraco.Web.UI.Client/src/less/pages/login.less +++ b/src/Umbraco.Web.UI.Client/src/less/pages/login.less @@ -75,10 +75,7 @@ } .login-overlay .switch-view { - margin-top: 10px; - a { - color: #fff; - } + margin-top: 10px; } @media (max-width: 565px) { @@ -115,3 +112,9 @@ margin: auto; color: @grayLight; } + +.login-overlay .text-error, +.login-overlay .text-info +{ + font-weight:bold; +} diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.controller.js index d76d66a7c4..71dbb8902f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.controller.js @@ -7,29 +7,44 @@ }); } + function resetInputValidation() { + if ($scope.loginForm) { + $scope.loginForm.username.$setValidity('auth', true); + $scope.loginForm.password.$setValidity('auth', true); + } + if ($scope.requestPasswordResetForm) { + $scope.requestPasswordResetForm.email.$setValidity("auth", true); + } + if ($scope.setPasswordForm) { + $scope.setPasswordForm.password.$setValidity('auth', true); + $scope.setPasswordForm.confirmPassword.$setValidity('auth', true); + } + } + $scope.allowPasswordReset = Umbraco.Sys.ServerVariables.umbracoSettings.allowPasswordReset; $scope.showLogin = function () { $scope.errorMsg = ""; + resetInputValidation(); $scope.view = "login"; setFieldFocus("loginForm", "username"); } $scope.showRequestPasswordReset = function () { $scope.errorMsg = ""; + resetInputValidation(); $scope.view = "request-password-reset"; $scope.showEmailResetConfirmation = false; setFieldFocus("requestPasswordResetForm", "email"); } $scope.showSetPassword = function () { + $scope.errorMsg = ""; + resetInputValidation(); $scope.view = "set-password"; setFieldFocus("setPasswordForm", "password"); } - - - var d = new Date(); var konamiGreetings = new Array("Suze Sunday", "Malibu Monday", "Tequila Tuesday", "Whiskey Wednesday", "Negroni Day", "Fernet Friday", "Sancerre Saturday"); var konamiMode = $cookies.konamiLogin; diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html index 992b98942e..76b90a8250 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html @@ -1,5 +1,5 @@ 
-
+

{{greeting}}

@@ -12,7 +12,7 @@
-
+
{{error}}
@@ -46,15 +46,15 @@
+ +
+
{{errorMsg}}
+
-
-
{{errorMsg}}
-
-
@@ -68,21 +68,21 @@
- - - +
-
{{errorMsg}}
+
{{errorMsg}}
- +
-
+
If your email address has been matched an email with password reset instructions has been sent.
+ +
@@ -101,17 +101,19 @@
- - - +
-
{{errorMsg}}
+
{{errorMsg}}
-

- Your new password has been set and you may now use it to log in. +

+

+ Your new password has been set and you may now use it to log in. +

+ + @@ -119,7 +121,7 @@
-
+
The link you have clicked on is invalid or has expired.
diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml index 95f68b1219..cc75ea0038 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml @@ -663,8 +663,8 @@ To manage your website, simply open the Umbraco back office and start adding con Session timed out © 2001 - %0%
Umbraco.com

]]>
Forgotten password? - Please enter your email address. If your account is located an email will be sent to you containing a link from which you can reset your password. - If your email address has been matched an email with password reset instructions has been sent. + An email will be sent to the address specified with a link to reset your password + An email with password reset instructions will be sent to the specified address if it matched our records Return to login form Please provide a new password. Your new password has been set and you may now use it to log in. 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 24380a5680..23c627830c 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml @@ -661,8 +661,8 @@ To manage your website, simply open the Umbraco back office and start adding con Session timed out © 2001 - %0%
Umbraco.com

]]>
Forgotten password? - Please enter your email address. If your account is located an email will be sent to you containing a link from which you can reset your password. - If your email address has been matched an email with password reset instructions has been sent. + An email will be sent to the address specified with a link to reset your password + An email with password reset instructions will be sent to the specified address if it matched our records Return to login form Please provide a new password. Your new password has been set and you may now use it to log in. From f279000d00029e311ed2d9c60fa5c35348a8af62 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 12 Apr 2016 19:54:04 +0200 Subject: [PATCH 23/46] removes unecessary methods from user.service.js, ensures correct error msg when resetting password on the server, fixes email copy and allows for non-html emails if the copy is plain text, removes the need for more than one email msg in the lang files and uses the correct way to replace tokens, --- .../Security/BackOfficeUserManager.cs | 3 +- src/Umbraco.Core/Security/EmailService.cs | 5 ++- .../src/common/resources/auth.resource.js | 9 +++-- .../src/common/services/user.service.js | 34 +------------------ .../views/common/dialogs/login.controller.js | 21 +++++++++--- .../src/views/common/dialogs/login.html | 25 +++++++------- src/Umbraco.Web.UI/umbraco/config/lang/en.xml | 11 +++--- .../umbraco/config/lang/en_us.xml | 11 +++--- .../Editors/AuthenticationController.cs | 18 +++------- 9 files changed, 58 insertions(+), 79 deletions(-) diff --git a/src/Umbraco.Core/Security/BackOfficeUserManager.cs b/src/Umbraco.Core/Security/BackOfficeUserManager.cs index ba7e615771..e48079c10b 100644 --- a/src/Umbraco.Core/Security/BackOfficeUserManager.cs +++ b/src/Umbraco.Core/Security/BackOfficeUserManager.cs @@ -118,6 +118,8 @@ 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(); + //NOTE: Not implementing these, if people need custom 2 factor auth, they'll need to implement their own UserStore to suport it //// Register two factor authentication providers. This application uses Phone and Emails as a step of receiving a code for verifying the user @@ -132,7 +134,6 @@ namespace Umbraco.Core.Security // BodyFormat = "Your security code is: {0}" //}); - manager.EmailService = new EmailService(); //manager.SmsService = new SmsService(); } diff --git a/src/Umbraco.Core/Security/EmailService.cs b/src/Umbraco.Core/Security/EmailService.cs index 0a4ca95ceb..93864aa37c 100644 --- a/src/Umbraco.Core/Security/EmailService.cs +++ b/src/Umbraco.Core/Security/EmailService.cs @@ -14,7 +14,10 @@ namespace Umbraco.Core.Security mailMessage.Body = message.Body; mailMessage.To.Add(message.Destination); mailMessage.Subject = message.Subject; - mailMessage.IsBodyHtml = true; + + //TODO: This check could be nicer but that is the way it is currently + mailMessage.IsBodyHtml = message.Body.IsNullOrWhiteSpace() == false + && message.Body.Contains("<") && message.Body.Contains("/>"); await client.SendMailAsync(mailMessage); } diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/auth.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/auth.resource.js index b170df3c97..30585d326d 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/auth.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/auth.resource.js @@ -79,7 +79,10 @@ function authResource($q, $http, umbRequestHelper, angularHelper) { errorMsg: 'Email address cannot be empty' }); } - + + //TODO: This validation shouldn't really be done here, the validation on the login dialog + // is pretty hacky which is why this is here, ideally validation on the login dialog would + // be done properly. var emailRegex = /\S+@\S+\.\S+/; if (!emailRegex.test(email)) { return angularHelper.rejectedPromise({ @@ -140,7 +143,7 @@ function authResource($q, $http, umbRequestHelper, angularHelper) { userId: userId, resetCode: resetCode }), - 'Password reset code validation failed for userId + ' + userId + ', code' + resetCode); + 'Password reset code validation failed for userId ' + userId + ', code' + resetCode); }, /** @@ -201,7 +204,7 @@ function authResource($q, $http, umbRequestHelper, angularHelper) { password: password, resetCode: resetCode }), - 'Password reset code validation failed for userId + ' + userId); + 'Password reset code validation failed for userId ' + userId); }, unlinkLogin: function (loginProvider, providerKey) { diff --git a/src/Umbraco.Web.UI.Client/src/common/services/user.service.js b/src/Umbraco.Web.UI.Client/src/common/services/user.service.js index 27f0165f4d..dffda24f1d 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/user.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/user.service.js @@ -211,39 +211,7 @@ angular.module('umbraco.services') return result; }); }, - - /** Returns a promise, sends a request to the server to send a password reset email if the email is recognised */ - requestPasswordReset: function (email) { - - return authResource.performRequestPasswordReset(email) - .then(function (data) { - // Note that we don't actually confirm if the email address was matched or not, to avoid - // allowing an attacker to determine which email addresses are valid. - var result = { success: true }; - return result; - }); - }, - - /** Returns a promise, sends a request to the server to validate a password reset code */ - validatePasswordResetCode: function (userId, resetCode) { - - return authResource.performValidatePasswordResetCode(userId, resetCode) - .then(function (data) { - var result = { success: data }; - return result; - }); - }, - - /** Returns a promise, sends a request to the server to validate a password reset code */ - setPassword: function (userId, password, confirmPassword, resetCode) { - - return authResource.performSetPassword(userId, password, confirmPassword, resetCode) - .then(function (data) { - var result = { success: data }; - return result; - }); - }, - + /** Logs the user out */ logout: function () { diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.controller.js index 71dbb8902f..ff9981eba1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.controller.js @@ -1,5 +1,5 @@ angular.module("umbraco").controller("Umbraco.Dialogs.LoginController", - function ($scope, $cookies, localizationService, userService, externalLoginInfo, $timeout, $location) { + function ($scope, $cookies, localizationService, userService, externalLoginInfo, $timeout, $location, authResource) { var setFieldFocus = function(form, field) { $timeout(function() { @@ -8,6 +8,9 @@ } function resetInputValidation() { + $scope.confirmPassword = ""; + $scope.password = ""; + $scope.login = ""; if ($scope.loginForm) { $scope.loginForm.username.$setValidity('auth', true); $scope.loginForm.password.$setValidity('auth', true); @@ -79,7 +82,7 @@ var userId = $location.search().userId; var resetCode = $location.search().resetCode; if (userId && resetCode) { - userService.validatePasswordResetCode(userId, resetCode) + authResource.performValidatePasswordResetCode(userId, resetCode) .then(function () { $scope.showSetPassword(); }, function () { @@ -139,8 +142,10 @@ return; } - userService.requestPasswordReset(email) + authResource.performRequestPasswordReset(email) .then(function () { + //remove the email entered + $scope.email = ""; $scope.showEmailResetConfirmation = true; }, function (reason) { $scope.errorMsg = reason.errorMsg; @@ -167,11 +172,17 @@ return; } - userService.setPassword(userId, password, confirmPassword, resetCode) + authResource.performSetPassword(userId, password, confirmPassword, resetCode) .then(function () { $scope.showSetPasswordConfirmation = true; + $scope.resetComplete = true; }, function (reason) { - $scope.errorMsg = reason.errorMsg; + if (reason.data && reason.data.Message) { + $scope.errorMsg = reason.data.Message; + } + else { + $scope.errorMsg = reason.errorMsg; + } $scope.setPasswordForm.password.$setValidity("auth", false); $scope.setPasswordForm.confirmPassword.$setValidity("auth", false); }); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html index 76b90a8250..f97588cee0 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html @@ -61,7 +61,7 @@

- Please enter your email address. If your account is located an email will be sent to you containing a link from which you can reset your password. + An email will be sent to the address specified with a link to reset your password

@@ -75,7 +75,7 @@
- If your email address has been matched an email with password reset instructions has been sent. + An email with password reset instructions will be sent to the specified address if it matched our records
@@ -88,34 +88,35 @@
-

+ +

Please provide a new password.

-
- +
+
-
+
-
+
{{errorMsg}}
-

+

Your new password has been set and you may now use it to log in.
-

+
- +
@@ -125,7 +126,7 @@ The link you have clicked on is invalid or has expired.
diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml index cc75ea0038..a5ac0ed911 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml @@ -666,12 +666,13 @@ To manage your website, simply open the Umbraco back office and start adding con An email will be sent to the address specified with a link to reset your password An email with password reset instructions will be sent to the specified address if it matched our records Return to login form - Please provide a new password. - Your new password has been set and you may now use it to log in. - The link you have clicked on is invalid or has expired. + Please provide a new password + Your new password has been set and you may now use it to log in + The link you have clicked on is invalid or has expired Umbraco: Reset Password - Your username to login to the Umbraco back-office is: {0} - here. ]]> + + Your username to login to the Umbraco back-office is: %0%

Click here to reset your password or copy/paste this URL into your browser:

%1%

]]> +
Dashboard 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 23c627830c..4384b3577c 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml @@ -664,12 +664,13 @@ To manage your website, simply open the Umbraco back office and start adding con An email will be sent to the address specified with a link to reset your password An email with password reset instructions will be sent to the specified address if it matched our records Return to login form - Please provide a new password. - Your new password has been set and you may now use it to log in. - The link you have clicked on is invalid or has expired. + Please provide a new password + Your new password has been set and you may now use it to log in + The link you have clicked on is invalid or has expired Umbraco: Reset Password - Your username to login to the Umbraco back-office is: {0} - here. ]]> + + Your username to login to the Umbraco back-office is: %0%

Click here to reset your password or copy/paste this URL into your browser:

%1%

]]> +
Dashboard diff --git a/src/Umbraco.Web/Editors/AuthenticationController.cs b/src/Umbraco.Web/Editors/AuthenticationController.cs index 09027c58f4..50deec6360 100644 --- a/src/Umbraco.Web/Editors/AuthenticationController.cs +++ b/src/Umbraco.Web/Editors/AuthenticationController.cs @@ -242,7 +242,7 @@ namespace Umbraco.Web.Editors { var code = await UserManager.GeneratePasswordResetTokenAsync(identityUser.Id); var callbackUrl = ConstuctCallbackUrl(http.Request.Url, identityUser.Id, code); - var message = ConstructPasswordResetEmailMessage(user, callbackUrl); + var message = Services.TextService.Localize("resetPasswordEmailCopyFormat", new[] {identityUser.UserName, callbackUrl}); await UserManager.SendEmailAsync(identityUser.Id, Services.TextService.Localize("login/resetPasswordEmailCopySubject"), message); @@ -259,18 +259,7 @@ namespace Umbraco.Web.Editors url.Host + (url.Port == 80 ? string.Empty : ":" + url.Port), userId, HttpUtility.UrlEncode(code)); - } - - private string ConstructPasswordResetEmailMessage(IUser user, string callbackUrl) - { - var emailCopy1 = Services.TextService.Localize("login/resetPasswordEmailCopyFormat1"); - var emailCopy2 = Services.TextService.Localize("login/resetPasswordEmailCopyFormat2"); - var message = string.Format("

" + emailCopy1 + "

\n\n" + - "

" + emailCopy2 + "

", - user.Username, - callbackUrl); - return message; - } + } /// /// Processes a password reset request. Looks for a match on the provided email address @@ -307,7 +296,8 @@ namespace Umbraco.Web.Editors return Request.CreateResponse(HttpStatusCode.OK); } - return Request.CreateValidationErrorResponse("Set password failed"); + return Request.CreateValidationErrorResponse( + result.Errors.Any() ? result.Errors.First() : "Set password failed"); } private HttpContextBase EnsureHttpContext() From e610a5ef54c2a67146c1b6b58281af164da57d9e Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 13 Apr 2016 13:51:12 +0200 Subject: [PATCH 24/46] Changes the password reset link to be a real link (not an angular deep link), this means there is less logging of the reset code in a query string and less visibility of it, this also means that the validation of the code happens instantly. The premise for this is the same as how we deal with external authentication requests and uses ViewData/TempData with redirects. Fixes the models to have the correct attributes to be able to directly json serialize them. --- .../src/common/resources/auth.resource.js | 2 +- .../views/common/dialogs/login.controller.js | 33 +++++---- .../src/views/common/dialogs/login.html | 5 +- .../umbraco/Views/Default.cshtml | 4 +- .../Editors/AuthenticationController.cs | 51 ++++++-------- .../Editors/BackOfficeController.cs | 67 ++++++++++++++----- .../HtmlHelperBackOfficeExtensions.cs | 38 ++++++++++- .../Models/RequestPasswordResetModel.cs | 2 + src/Umbraco.Web/Models/SetPasswordModel.cs | 1 + .../Models/ValidatePasswordResetCodeModel.cs | 1 + src/Umbraco.Web/Umbraco.Web.csproj | 2 +- 11 files changed, 132 insertions(+), 74 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/auth.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/auth.resource.js index 30585d326d..20ebaa10c0 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/auth.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/auth.resource.js @@ -170,7 +170,7 @@ function authResource($q, $http, umbRequestHelper, angularHelper) { */ performSetPassword: function (userId, password, confirmPassword, resetCode) { - if (!userId) { + if (userId === undefined || userId === null) { return angularHelper.rejectedPromise({ errorMsg: 'User Id cannot be empty' }); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.controller.js index ff9981eba1..1b1b3bb402 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.controller.js @@ -1,5 +1,5 @@ angular.module("umbraco").controller("Umbraco.Dialogs.LoginController", - function ($scope, $cookies, localizationService, userService, externalLoginInfo, $timeout, $location, authResource) { + function ($scope, $cookies, localizationService, userService, externalLoginInfo, resetPasswordCodeInfo, $timeout, authResource) { var setFieldFocus = function(form, field) { $timeout(function() { @@ -63,6 +63,7 @@ $scope.externalLoginFormAction = Umbraco.Sys.ServerVariables.umbracoUrls.externalLoginsUrl; $scope.externalLoginProviders = externalLoginInfo.providers; $scope.externalLoginInfo = externalLoginInfo; + $scope.resetPasswordCodeInfo = resetPasswordCodeInfo; $scope.activateKonamiMode = function () { if ($cookies.konamiLogin == "1") { @@ -77,21 +78,6 @@ } } - // Set initial view - either set password if reset code provided in querystring - // otherwise login form - var userId = $location.search().userId; - var resetCode = $location.search().resetCode; - if (userId && resetCode) { - authResource.performValidatePasswordResetCode(userId, resetCode) - .then(function () { - $scope.showSetPassword(); - }, function () { - $scope.view = "password-reset-code-expired"; - }); - } else { - $scope.showLogin(); - } - $scope.loginSubmit = function (login, password) { //if the login and password are not empty we need to automatically @@ -172,7 +158,7 @@ return; } - authResource.performSetPassword(userId, password, confirmPassword, resetCode) + authResource.performSetPassword($scope.resetPasswordCodeInfo.resetCodeModel.userId, password, confirmPassword, $scope.resetPasswordCodeInfo.resetCodeModel.resetCode) .then(function () { $scope.showSetPasswordConfirmation = true; $scope.resetComplete = true; @@ -199,4 +185,17 @@ }); } + + //Now, show the correct panel: + + if ($scope.resetPasswordCodeInfo.resetCodeModel) { + $scope.showSetPassword(); + } + else if ($scope.resetPasswordCodeInfo.errors.length > 0) { + $scope.view = "password-reset-code-expired"; + } + else { + $scope.showLogin(); + } + }); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html index f97588cee0..12f25f42a5 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html @@ -122,9 +122,10 @@
-
- The link you have clicked on is invalid or has expired. +
+ {{error}}
+ diff --git a/src/Umbraco.Web.UI/umbraco/Views/Default.cshtml b/src/Umbraco.Web.UI/umbraco/Views/Default.cshtml index a63fd3c670..07063934b3 100644 --- a/src/Umbraco.Web.UI/umbraco/Views/Default.cshtml +++ b/src/Umbraco.Web.UI/umbraco/Views/Default.cshtml @@ -72,8 +72,8 @@ diff --git a/src/Umbraco.Web/Editors/AuthenticationController.cs b/src/Umbraco.Web/Editors/AuthenticationController.cs index 50deec6360..c03ff624a4 100644 --- a/src/Umbraco.Web/Editors/AuthenticationController.cs +++ b/src/Umbraco.Web/Editors/AuthenticationController.cs @@ -6,6 +6,7 @@ using System.Net.Http; using System.Threading.Tasks; using System.Web; using System.Web.Http; +using System.Web.Mvc; using AutoMapper; using Microsoft.AspNet.Identity; using Microsoft.AspNet.Identity.Owin; @@ -99,7 +100,7 @@ namespace Umbraco.Web.Editors /// Checks if the current user's cookie is valid and if so returns OK or a 400 (BadRequest) ///
/// - [HttpGet] + [System.Web.Http.HttpGet] public bool IsAuthenticated() { var attempt = UmbracoContext.Security.AuthorizeRequest(); @@ -231,9 +232,6 @@ namespace Umbraco.Web.Editors { throw new HttpResponseException(HttpStatusCode.BadRequest); } - - var http = EnsureHttpContext(); - var identityUser = await SignInManager.UserManager.FindByEmailAsync(model.Email); if (identityUser != null) { @@ -241,7 +239,7 @@ namespace Umbraco.Web.Editors if (user != null && user.IsLockedOut == false) { var code = await UserManager.GeneratePasswordResetTokenAsync(identityUser.Id); - var callbackUrl = ConstuctCallbackUrl(http.Request.Url, identityUser.Id, code); + var callbackUrl = ConstuctCallbackUrl(identityUser.Id, code); var message = Services.TextService.Localize("resetPasswordEmailCopyFormat", new[] {identityUser.UserName, callbackUrl}); await UserManager.SendEmailAsync(identityUser.Id, Services.TextService.Localize("login/resetPasswordEmailCopySubject"), @@ -252,37 +250,28 @@ namespace Umbraco.Web.Editors return Request.CreateResponse(HttpStatusCode.OK); } - private static string ConstuctCallbackUrl(Uri url, int userId, string code) + private string ConstuctCallbackUrl(int userId, string code) { - return string.Format("{0}://{1}/umbraco/#/login?userId={2}&resetCode={3}", - url.Scheme, - url.Host + (url.Port == 80 ? string.Empty : ":" + url.Port), - userId, - HttpUtility.UrlEncode(code)); - } + //get an mvc helper to get the url + var http = EnsureHttpContext(); + var urlHelper = new UrlHelper(http.Request.RequestContext); - /// - /// Processes a password reset request. Looks for a match on the provided email address - /// and if found sends an email with a link to reset it - /// - /// - [SetAngularAntiForgeryTokens] - public async Task PostValidatePasswordResetCode(ValidatePasswordResetCodeModel model) - { - var user = UserManager.FindById(model.UserId); - if (user != null) - { - var result = await UserManager.UserTokenProvider.ValidateAsync("ResetPassword", - model.ResetCode, UserManager, user); - if (result) + var action = urlHelper.Action("ValidatePasswordResetCode", "BackOffice", + new { - return Request.CreateResponse(HttpStatusCode.OK); - } - } + area = GlobalSettings.UmbracoMvcArea, + u = userId, + r = code + }); - return Request.CreateValidationErrorResponse("Password reset code not valid"); - } + //TODO: Virtual path? + return string.Format("{0}://{1}{2}", + http.Request.Url.Scheme, + http.Request.Url.Host + (http.Request.Url.Port == 80 ? string.Empty : ":" + http.Request.Url.Port), + action); + } + /// /// Processes a set password request. Validates the request and sets a new password. /// diff --git a/src/Umbraco.Web/Editors/BackOfficeController.cs b/src/Umbraco.Web/Editors/BackOfficeController.cs index 297221dacc..b5cea7a945 100644 --- a/src/Umbraco.Web/Editors/BackOfficeController.cs +++ b/src/Umbraco.Web/Editors/BackOfficeController.cs @@ -4,6 +4,8 @@ using System.Configuration; using System.Globalization; using System.IO; using System.Linq; +using System.Net; +using System.Net.Http; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -26,6 +28,7 @@ using Umbraco.Core.Manifest; using Umbraco.Core.Models; using Umbraco.Core.Models.Identity; using Umbraco.Core.Security; +using Umbraco.Web.Models; using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Mvc; using Umbraco.Web.PropertyEditors; @@ -34,6 +37,7 @@ using Umbraco.Web.Trees; using Umbraco.Web.UI.JavaScript; using Umbraco.Web.WebApi.Filters; using Umbraco.Web.WebServices; +using Umbraco.Core.Services; using Action = umbraco.BusinessLogic.Actions.Action; using Constants = Umbraco.Core.Constants; @@ -49,6 +53,10 @@ namespace Umbraco.Web.Editors private BackOfficeUserManager _userManager; private BackOfficeSignInManager _signInManager; + private const string TokenExternalSignInError = "ExternalSignInError"; + private const string TokenPasswordResetCode = "PasswordResetCode"; + private static readonly string[] TempDataTokenNames = { TokenExternalSignInError, TokenPasswordResetCode }; + protected BackOfficeSignInManager SignInManager { get { return _signInManager ?? (_signInManager = OwinContext.Get()); } @@ -431,7 +439,25 @@ namespace Umbraco.Web.Editors User.Identity.GetUserId()); } + [HttpGet] + public async Task ValidatePasswordResetCode([Bind(Prefix = "u")]int userId, [Bind(Prefix = "r")]string resetCode) + { + var user = UserManager.FindById(userId); + if (user != null) + { + var result = await UserManager.UserTokenProvider.ValidateAsync("ResetPassword", resetCode, UserManager, user); + if (result) + { + //Add a flag and redirect for it to be displayed + TempData[TokenPasswordResetCode] = new ValidatePasswordResetCodeModel {UserId = userId, ResetCode = resetCode}; + return RedirectToLocal(Url.Action("Default", "BackOffice")); + } + } + //Add error and redirect for it to be displayed + TempData[TokenPasswordResetCode] = new[] { Services.TextService.Localize("login/resetCodeExpired") }; + return RedirectToLocal(Url.Action("Default", "BackOffice")); + } [HttpGet] public async Task ExternalLinkLoginCallback() @@ -443,7 +469,7 @@ namespace Umbraco.Web.Editors if (loginInfo == null) { //Add error and redirect for it to be displayed - TempData["ExternalSignInError"] = new[] { "An error occurred, could not get external login info" }; + TempData[TokenExternalSignInError] = new[] { "An error occurred, could not get external login info" }; return RedirectToLocal(Url.Action("Default", "BackOffice")); } @@ -454,27 +480,32 @@ namespace Umbraco.Web.Editors } //Add errors and redirect for it to be displayed - TempData["ExternalSignInError"] = result.Errors; + TempData[TokenExternalSignInError] = result.Errors; return RedirectToLocal(Url.Action("Default", "BackOffice")); } /// - /// Used by Default and AuthorizeUpgrade to render as per normal if there's no external login info, otherwise - /// process the external login info. + /// Used by Default and AuthorizeUpgrade to render as per normal if there's no external login info, + /// otherwise process the external login info. /// - /// - private async Task RenderDefaultOrProcessExternalLoginAsync(Func defaultResponse, Func externalSignInResponse) + /// + private async Task RenderDefaultOrProcessExternalLoginAsync( + Func defaultResponse, + Func externalSignInResponse) { if (defaultResponse == null) throw new ArgumentNullException("defaultResponse"); if (externalSignInResponse == null) throw new ArgumentNullException("externalSignInResponse"); ViewBag.UmbracoPath = GlobalSettings.UmbracoMvcArea; - //check if there's errors in the TempData, assign to view bag and render the view - if (TempData["ExternalSignInError"] != null) - { - ViewBag.ExternalSignInError = TempData["ExternalSignInError"]; - return defaultResponse(); + //check if there is the TempData with the any token name specified, if so, assign to view bag and render the view + foreach (var tempDataTokenName in TempDataTokenNames) + { + if (TempData[tempDataTokenName] != null) + { + ViewData[tempDataTokenName] = TempData[tempDataTokenName]; + return defaultResponse(); + } } //First check if there's external login info, if there's not proceed as normal @@ -512,7 +543,7 @@ namespace Umbraco.Web.Editors { if (await AutoLinkAndSignInExternalAccount(loginInfo) == false) { - ViewBag.ExternalSignInError = new[] { "The requested provider (" + loginInfo.Login.LoginProvider + ") has not been linked to to an account" }; + ViewData[TokenExternalSignInError] = new[] { "The requested provider (" + loginInfo.Login.LoginProvider + ") has not been linked to to an account" }; } //Remove the cookie otherwise this message will keep appearing @@ -547,7 +578,7 @@ namespace Umbraco.Web.Editors //we are allowing auto-linking/creating of local accounts if (loginInfo.Email.IsNullOrWhiteSpace()) { - ViewBag.ExternalSignInError = new[] { "The requested provider (" + loginInfo.Login.LoginProvider + ") has not provided an email address, the account cannot be linked." }; + ViewData[TokenExternalSignInError] = new[] { "The requested provider (" + loginInfo.Login.LoginProvider + ") has not provided an email address, the account cannot be linked." }; } else { @@ -556,7 +587,7 @@ namespace Umbraco.Web.Editors var foundByEmail = Services.UserService.GetByEmail(loginInfo.Email); if (foundByEmail != null) { - ViewBag.ExternalSignInError = new[] { "A user with this email address already exists locally. You will need to login locally to Umbraco and link this external provider: " + loginInfo.Login.LoginProvider }; + ViewData[TokenExternalSignInError] = new[] { "A user with this email address already exists locally. You will need to login locally to Umbraco and link this external provider: " + loginInfo.Login.LoginProvider }; } else { @@ -564,7 +595,7 @@ namespace Umbraco.Web.Editors var userType = Services.UserService.GetUserTypeByAlias(defaultUserType); if (userType == null) { - ViewBag.ExternalSignInError = new[] { "Could not auto-link this account, the specified User Type does not exist: " + defaultUserType }; + ViewData[TokenExternalSignInError] = new[] { "Could not auto-link this account, the specified User Type does not exist: " + defaultUserType }; } else { @@ -592,21 +623,21 @@ namespace Umbraco.Web.Editors if (userCreationResult.Succeeded == false) { - ViewBag.ExternalSignInError = userCreationResult.Errors; + ViewData[TokenExternalSignInError] = userCreationResult.Errors; } else { var linkResult = await UserManager.AddLoginAsync(autoLinkUser.Id, loginInfo.Login); if (linkResult.Succeeded == false) { - ViewBag.ExternalSignInError = linkResult.Errors; + ViewData[TokenExternalSignInError] = linkResult.Errors; //If this fails, we should really delete the user since it will be in an inconsistent state! var deleteResult = await UserManager.DeleteAsync(autoLinkUser); if (deleteResult.Succeeded == false) { //DOH! ... this isn't good, combine all errors to be shown - ViewBag.ExternalSignInError = linkResult.Errors.Concat(deleteResult.Errors); + ViewData[TokenExternalSignInError] = linkResult.Errors.Concat(deleteResult.Errors); } } else diff --git a/src/Umbraco.Web/HtmlHelperBackOfficeExtensions.cs b/src/Umbraco.Web/HtmlHelperBackOfficeExtensions.cs index 2dc9841293..71fc3be8f1 100644 --- a/src/Umbraco.Web/HtmlHelperBackOfficeExtensions.cs +++ b/src/Umbraco.Web/HtmlHelperBackOfficeExtensions.cs @@ -9,6 +9,7 @@ using Newtonsoft.Json; using Umbraco.Core; using Umbraco.Core.Configuration; using Umbraco.Web.Editors; +using Umbraco.Web.Models; namespace Umbraco.Web { @@ -61,12 +62,12 @@ namespace Umbraco.Web } /// - /// Used to render the script that will pass in the angular externalLoginInfo service on page load + /// Used to render the script that will pass in the angular "externalLoginInfo" service/value on page load /// /// /// /// - public static IHtmlString AngularExternalLoginInfoValuesScript(this HtmlHelper html, IEnumerable externalLoginErrors) + public static IHtmlString AngularValueExternalLoginInfoScript(this HtmlHelper html, IEnumerable externalLoginErrors) { var loginProviders = html.ViewContext.HttpContext.GetOwinContext().Authentication.GetExternalAuthenticationTypes() .Where(p => p.Properties.ContainsKey("UmbracoBackOffice")) @@ -98,5 +99,38 @@ namespace Umbraco.Web return html.Raw(sb.ToString()); } + + /// + /// Used to render the script that will pass in the angular "resetPasswordCodeInfo" service/value on page load + /// + /// + /// + /// + public static IHtmlString AngularValueResetPasswordCodeInfoScript(this HtmlHelper html, object val) + { + var sb = new StringBuilder(); + sb.AppendLine(); + sb.AppendLine(@"var errors = [];"); + + var errors = val as IEnumerable; + if (errors != null) + { + foreach (var error in errors) + { + sb.AppendFormat(@"errors.push(""{0}"");", error).AppendLine(); + } + } + + var resetCodeModel = val as ValidatePasswordResetCodeModel; + + + sb.AppendLine(@"app.value(""resetPasswordCodeInfo"", {"); + sb.AppendLine(@"errors: errors,"); + sb.Append(@"resetCodeModel: "); + sb.AppendLine(JsonConvert.SerializeObject(resetCodeModel)); + sb.AppendLine(@"});"); + + return html.Raw(sb.ToString()); + } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Models/RequestPasswordResetModel.cs b/src/Umbraco.Web/Models/RequestPasswordResetModel.cs index 650dd8ed11..0ea173bfd6 100644 --- a/src/Umbraco.Web/Models/RequestPasswordResetModel.cs +++ b/src/Umbraco.Web/Models/RequestPasswordResetModel.cs @@ -3,6 +3,8 @@ using System.Runtime.Serialization; namespace Umbraco.Web.Models { + + [DataContract(Name = "requestPasswordReset", Namespace = "")] public class RequestPasswordResetModel { [Required] diff --git a/src/Umbraco.Web/Models/SetPasswordModel.cs b/src/Umbraco.Web/Models/SetPasswordModel.cs index cc70989d66..02d0e4f901 100644 --- a/src/Umbraco.Web/Models/SetPasswordModel.cs +++ b/src/Umbraco.Web/Models/SetPasswordModel.cs @@ -3,6 +3,7 @@ using System.Runtime.Serialization; namespace Umbraco.Web.Models { + [DataContract(Name = "setPassword", Namespace = "")] public class SetPasswordModel { [Required] diff --git a/src/Umbraco.Web/Models/ValidatePasswordResetCodeModel.cs b/src/Umbraco.Web/Models/ValidatePasswordResetCodeModel.cs index 61db6907ef..cba92eeff7 100644 --- a/src/Umbraco.Web/Models/ValidatePasswordResetCodeModel.cs +++ b/src/Umbraco.Web/Models/ValidatePasswordResetCodeModel.cs @@ -3,6 +3,7 @@ using System.Runtime.Serialization; namespace Umbraco.Web.Models { + [DataContract(Name = "validatePasswordReset", Namespace = "")] public class ValidatePasswordResetCodeModel { [Required] diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 16b220b232..48005b1209 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -342,9 +342,9 @@ - + From ce0ecd81147b7fb624919dedb49aa618370f2b90 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 13 Apr 2016 14:35:40 +0200 Subject: [PATCH 25/46] Ensures that the emails sent our are in the culture of the user, fixes issue of logging in after resetting password and then logging out again --- .../src/views/common/dialogs/login.controller.js | 4 ++++ src/Umbraco.Web.UI/umbraco/config/lang/en.xml | 2 +- src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml | 2 +- src/Umbraco.Web/Editors/AuthenticationController.cs | 12 ++++++++++-- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.controller.js index 1b1b3bb402..c38c55fcfb 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.controller.js @@ -162,6 +162,10 @@ .then(function () { $scope.showSetPasswordConfirmation = true; $scope.resetComplete = true; + + //reset the values in the resetPasswordCodeInfo angular so if someone logs out the change password isn't shown again + resetPasswordCodeInfo.resetCodeModel = null; + }, function (reason) { if (reason.data && reason.data.Message) { $scope.errorMsg = reason.data.Message; diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml index a5ac0ed911..93a570efe2 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml @@ -667,7 +667,7 @@ To manage your website, simply open the Umbraco back office and start adding con An email with password reset instructions will be sent to the specified address if it matched our records Return to login form Please provide a new password - Your new password has been set and you may now use it to log in + You Password has been updated! The link you have clicked on is invalid or has expired Umbraco: Reset Password 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 4384b3577c..6bcca5db86 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml @@ -665,7 +665,7 @@ To manage your website, simply open the Umbraco back office and start adding con An email with password reset instructions will be sent to the specified address if it matched our records Return to login form Please provide a new password - Your new password has been set and you may now use it to log in + You Password has been updated! The link you have clicked on is invalid or has expired Umbraco: Reset Password diff --git a/src/Umbraco.Web/Editors/AuthenticationController.cs b/src/Umbraco.Web/Editors/AuthenticationController.cs index c03ff624a4..1cfd0312c2 100644 --- a/src/Umbraco.Web/Editors/AuthenticationController.cs +++ b/src/Umbraco.Web/Editors/AuthenticationController.cs @@ -14,6 +14,7 @@ using Microsoft.Owin; using Umbraco.Core; using Umbraco.Core.Configuration; using Umbraco.Core.Logging; +using Umbraco.Core.Models; using Umbraco.Core.Security; using Umbraco.Core.Services; using Umbraco.Web.Models; @@ -240,9 +241,16 @@ namespace Umbraco.Web.Editors { var code = await UserManager.GeneratePasswordResetTokenAsync(identityUser.Id); var callbackUrl = ConstuctCallbackUrl(identityUser.Id, code); - var message = Services.TextService.Localize("resetPasswordEmailCopyFormat", new[] {identityUser.UserName, callbackUrl}); + + var message = Services.TextService.Localize("resetPasswordEmailCopyFormat", + //Ensure the culture of the found user is used for the email! + UserExtensions.GetUserCulture(identityUser.Culture, Services.TextService), + new[] {identityUser.UserName, callbackUrl}); + await UserManager.SendEmailAsync(identityUser.Id, - Services.TextService.Localize("login/resetPasswordEmailCopySubject"), + Services.TextService.Localize("login/resetPasswordEmailCopySubject", + //Ensure the culture of the found user is used for the email! + UserExtensions.GetUserCulture(identityUser.Culture, Services.TextService)), message); } } From 10bb16316d8b1c5ca2150d2e2e1133a1947f20d6 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 13 Apr 2016 14:49:37 +0200 Subject: [PATCH 26/46] Updates sorting code to support decimals - need to test on MySql --- .../Repositories/VersionableRepositoryBase.cs | 10 ++++++---- .../Persistence/SqlSyntax/ISqlSyntaxProvider.cs | 1 + .../Persistence/SqlSyntax/MySqlSyntaxProvider.cs | 3 ++- .../Persistence/SqlSyntax/SqlSyntaxProviderBase.cs | 1 + 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs index 2d55a3dd05..0215ba27bb 100644 --- a/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs @@ -274,12 +274,14 @@ namespace Umbraco.Core.Persistence.Repositories var sortedInt = string.Format(SqlSyntax.ConvertIntegerToOrderableString, "dataInt"); var sortedDate = string.Format(SqlSyntax.ConvertDateToOrderableString, "dataDate"); var sortedString = string.Format(SqlSyntax.IsNull, "dataNvarchar", "''"); + var sortedDecimal = string.Format(SqlSyntax.ConvertDecimalToOrderableString, "dataInt"); var orderBySql = string.Format(@"ORDER BY ( SELECT CASE - WHEN dataInt Is Not Null THEN {0} - WHEN dataDate Is Not Null THEN {1} - ELSE {2} + WHEN dataInt Is Not Null THEN {0} + WHEN dataDecimal Is Not Null THEN {1} + WHEN dataDate Is Not Null THEN {2} + ELSE {3} END FROM cmsContent c INNER JOIN cmsContentVersion cv ON cv.ContentId = c.nodeId AND VersionDate = ( @@ -290,7 +292,7 @@ namespace Umbraco.Core.Persistence.Repositories INNER JOIN cmsPropertyData cpd ON cpd.contentNodeId = c.nodeId AND cpd.versionId = cv.VersionId INNER JOIN cmsPropertyType cpt ON cpt.Id = cpd.propertytypeId - WHERE c.nodeId = umbracoNode.Id and cpt.Alias = @0)", sortedInt, sortedDate, sortedString); + WHERE c.nodeId = umbracoNode.Id and cpt.Alias = @0)", sortedInt, sortedDecimal, sortedDate, sortedString); sortedSql.Append(orderBySql, orderBy); if (orderDirection == Direction.Descending) diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/ISqlSyntaxProvider.cs b/src/Umbraco.Core/Persistence/SqlSyntax/ISqlSyntaxProvider.cs index e663c8e64b..c70b6a571a 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/ISqlSyntaxProvider.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/ISqlSyntaxProvider.cs @@ -73,6 +73,7 @@ namespace Umbraco.Core.Persistence.SqlSyntax string IsNull { get; } string ConvertIntegerToOrderableString { get; } string ConvertDateToOrderableString { get; } + string ConvertDecimalToOrderableString { get; } IEnumerable GetTablesInSchema(Database db); IEnumerable GetColumnsInSchema(Database db); diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/MySqlSyntaxProvider.cs b/src/Umbraco.Core/Persistence/SqlSyntax/MySqlSyntaxProvider.cs index d02a5fb8dc..d4585f5aa1 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/MySqlSyntaxProvider.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/MySqlSyntaxProvider.cs @@ -363,7 +363,8 @@ ORDER BY TABLE_NAME, INDEX_NAME", public override string IsNull { get { return "IFNULL({0},{1})"; } } public override string ConvertIntegerToOrderableString { get { return "LPAD({0}, 8, '0')"; } } public override string ConvertDateToOrderableString { get { return "DATE_FORMAT({0}, '%Y%m%d')"; } } - + public override string ConvertDecimalToOrderableString { get { return "LPAD({0}, 25, '0')"; } } + public override bool? SupportsCaseInsensitiveQueries(Database db) { bool? supportsCaseInsensitiveQueries = null; diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs b/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs index 35c133ce6e..b7b58d929f 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs @@ -540,5 +540,6 @@ namespace Umbraco.Core.Persistence.SqlSyntax public virtual string IsNull { get { return "ISNULL({0},{1})"; } } public virtual string ConvertIntegerToOrderableString { get { return "RIGHT('00000000' + CAST({0} AS varchar(8)),8)"; } } public virtual string ConvertDateToOrderableString { get { return "CONVERT(varchar, {0}, 102)"; } } + public virtual string ConvertDecimalToOrderableString { get { return "RIGHT('0000000000000000000000000' + CAST({0} AS varchar(25)),25)"; } } } } \ No newline at end of file From c9c391e477290b172232fc60e71f6bbeb444a785 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 13 Apr 2016 15:05:23 +0200 Subject: [PATCH 27/46] fixed type, decimal sorting now works --- .../Persistence/Repositories/VersionableRepositoryBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs index 0215ba27bb..934555ac6e 100644 --- a/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs @@ -274,7 +274,7 @@ namespace Umbraco.Core.Persistence.Repositories var sortedInt = string.Format(SqlSyntax.ConvertIntegerToOrderableString, "dataInt"); var sortedDate = string.Format(SqlSyntax.ConvertDateToOrderableString, "dataDate"); var sortedString = string.Format(SqlSyntax.IsNull, "dataNvarchar", "''"); - var sortedDecimal = string.Format(SqlSyntax.ConvertDecimalToOrderableString, "dataInt"); + var sortedDecimal = string.Format(SqlSyntax.ConvertDecimalToOrderableString, "dataDecimal"); var orderBySql = string.Format(@"ORDER BY ( SELECT CASE From c3b4a9128ad41f3f174a7e955ffb7a09d32a9182 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 13 Apr 2016 15:47:21 +0200 Subject: [PATCH 28/46] ensures all GetPagedDescendant methods are mocked - fixes tests. --- .../UmbracoExamine/IndexInitializer.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Tests/UmbracoExamine/IndexInitializer.cs b/src/Umbraco.Tests/UmbracoExamine/IndexInitializer.cs index aea5243058..9990c58a43 100644 --- a/src/Umbraco.Tests/UmbracoExamine/IndexInitializer.cs +++ b/src/Umbraco.Tests/UmbracoExamine/IndexInitializer.cs @@ -49,7 +49,8 @@ namespace Umbraco.Tests.UmbracoExamine } if (mediaService == null) { - long totalRecs; + long longTotalRecs; + int intTotalRecs; var allRecs = dataService.MediaService.GetLatestMediaByXpath("//node") .Root @@ -74,7 +75,15 @@ namespace Umbraco.Tests.UmbracoExamine mediaService = Mock.Of( x => x.GetPagedDescendants( - It.IsAny(), It.IsAny(), It.IsAny(), out totalRecs, It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()) + It.IsAny(), It.IsAny(), It.IsAny(), out longTotalRecs, It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()) + == + allRecs + && x.GetPagedDescendants( + It.IsAny(), It.IsAny(), It.IsAny(), out longTotalRecs, It.IsAny(), It.IsAny(), It.IsAny()) + == + allRecs + && x.GetPagedDescendants( + It.IsAny(), It.IsAny(), It.IsAny(), out intTotalRecs, It.IsAny(), It.IsAny(), It.IsAny()) == allRecs); From b8d8c9e59a2ad16a48b5a57467ffd0f6bbc16c5b Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 13 Apr 2016 16:41:45 +0200 Subject: [PATCH 29/46] fixes merge - but the sorting on custom field will not currently work, need to fix that now. --- .../Repositories/ContentRepository.cs | 4 +- .../Repositories/MediaRepository.cs | 2 +- .../Repositories/MemberRepository.cs | 2 +- .../Repositories/VersionableRepositoryBase.cs | 82 ++++++++++++++----- src/Umbraco.Core/Services/ContentService.cs | 41 ---------- src/Umbraco.Core/Services/IContentService.cs | 14 +--- src/Umbraco.Core/Services/IMediaService.cs | 14 +--- src/Umbraco.Core/Services/MediaService.cs | 44 +--------- .../UmbracoExamine/IndexInitializer.cs | 4 - .../Editors/AuthenticationController.cs | 3 + 10 files changed, 72 insertions(+), 138 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs index 7222217606..fb0121f8dd 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs @@ -252,7 +252,7 @@ namespace Umbraco.Core.Persistence.Repositories // want latest content items because a pulished content item might not actually be the latest. // see: http://issues.umbraco.org/issue/U4-6322 & http://issues.umbraco.org/issue/U4-5982 var descendants = GetPagedResultsByQuery(query, pageIndex, pageSize, out total, - MapQueryDtos, "Path", Direction.Ascending, //TODO: Think this needs a true here); + MapQueryDtos, "Path", Direction.Ascending, true); var xmlItems = (from descendant in descendants let xml = serializer(descendant) @@ -789,7 +789,7 @@ namespace Umbraco.Core.Persistence.Repositories return GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, MapQueryDtos, - orderBy, orderDirection, + orderBy, orderDirection, orderBySystemField, filterSql); } diff --git a/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs index 4be1e2e4e5..62257f08e2 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs @@ -499,7 +499,7 @@ namespace Umbraco.Core.Persistence.Repositories : Sql().Append("AND (umbracoNode." + SqlSyntax.GetQuotedColumnName("text") + " LIKE @0)", "%" + filter + "%"); return GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, - MapQueryDtos, orderBy, orderDirection, + MapQueryDtos, orderBy, orderDirection, orderBySystemField, filterSql); } diff --git a/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs index c1afbee590..51fac1149c 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs @@ -631,7 +631,7 @@ namespace Umbraco.Core.Persistence.Repositories "OR (cmsMember.LoginName LIKE @0))", "%" + filter + "%"); return GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, - MapQueryDtos, orderBy, orderDirection, TODO: Do we need a orderBySystemField here?, + MapQueryDtos, orderBy, orderDirection, orderBySystemField, filterSql); } diff --git a/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs index 53a3884431..b32336e9b2 100644 --- a/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs @@ -255,7 +255,7 @@ namespace Umbraco.Core.Persistence.Repositories } - private Sql PrepareSqlForPagedResults(Sql sql, Sql filterSql, string orderBy, Direction orderDirection) + private Sql PrepareSqlForPagedResults(Sql sql, Sql filterSql, string orderBy, Direction orderDirection, bool orderBySystemField) { if (filterSql == null && string.IsNullOrEmpty(orderBy)) return sql; @@ -271,29 +271,67 @@ namespace Umbraco.Core.Persistence.Repositories // apply sort if (string.IsNullOrEmpty(orderBy) == false) { - // get the database field eg "[table].[column]" - var dbfield = GetDatabaseFieldNameForOrderBy(orderBy); + if (orderBySystemField) + { + // get the database field eg "[table].[column]" + var dbfield = GetDatabaseFieldNameForOrderBy(orderBy); - // for SqlServer pagination to work, the "order by" field needs to be the alias eg if - // the select statement has "umbracoNode.text AS NodeDto__Text" then the order field needs - // to be "NodeDto__Text" and NOT "umbracoNode.text". - // not sure about SqlCE nor MySql, so better do it too. initially thought about patching - // NPoco but that would be expensive and not 100% possible, so better give NPoco proper - // queries to begin with. - // thought about maintaining a map of columns-to-aliases in the sql context but that would - // be expensive and most of the time, useless. so instead we parse the SQL looking for the - // alias. somewhat expensive too but nothing's free. + // for SqlServer pagination to work, the "order by" field needs to be the alias eg if + // the select statement has "umbracoNode.text AS NodeDto__Text" then the order field needs + // to be "NodeDto__Text" and NOT "umbracoNode.text". + // not sure about SqlCE nor MySql, so better do it too. initially thought about patching + // NPoco but that would be expensive and not 100% possible, so better give NPoco proper + // queries to begin with. + // thought about maintaining a map of columns-to-aliases in the sql context but that would + // be expensive and most of the time, useless. so instead we parse the SQL looking for the + // alias. somewhat expensive too but nothing's free. - var matches = VersionableRepositoryBaseAliasRegex.For(SqlSyntax).Matches(sql.SQL); - var match = matches.Cast().FirstOrDefault(m => m.Groups[1].Value == dbfield); - if (match != null) - dbfield = match.Groups[2].Value; + var matches = VersionableRepositoryBaseAliasRegex.For(SqlSyntax).Matches(sql.SQL); + var match = matches.Cast().FirstOrDefault(m => m.Groups[1].Value == dbfield); + if (match != null) + dbfield = match.Groups[2].Value; - var orderByParams = new object[] { dbfield }; - if (orderDirection == Direction.Ascending) - psql.OrderBy(orderByParams); + var orderByParams = new object[] {dbfield}; + if (orderDirection == Direction.Ascending) + psql.OrderBy(orderByParams); + else + psql.OrderByDescending(orderByParams); + } else - psql.OrderByDescending(orderByParams); + { + // Sorting by a custom field, so set-up sub-query for ORDER BY clause to pull through valie + // from most recent content version for the given order by field + var sortedInt = string.Format(SqlSyntax.ConvertIntegerToOrderableString, "dataInt"); + var sortedDate = string.Format(SqlSyntax.ConvertDateToOrderableString, "dataDate"); + var sortedString = string.Format(SqlSyntax.IsNull, "dataNvarchar", "''"); + var sortedDecimal = string.Format(SqlSyntax.ConvertDecimalToOrderableString, "dataDecimal"); + + var orderBySql = string.Format(@"ORDER BY ( + SELECT CASE + WHEN dataInt Is Not Null THEN {0} + WHEN dataDecimal Is Not Null THEN {1} + WHEN dataDate Is Not Null THEN {2} + ELSE {3} + END + FROM cmsContent c + INNER JOIN cmsContentVersion cv ON cv.ContentId = c.nodeId AND VersionDate = ( + SELECT Max(VersionDate) + FROM cmsContentVersion + WHERE ContentId = c.nodeId + ) + INNER JOIN cmsPropertyData cpd ON cpd.contentNodeId = c.nodeId + AND cpd.versionId = cv.VersionId + INNER JOIN cmsPropertyType cpt ON cpt.Id = cpd.propertytypeId + WHERE c.nodeId = umbracoNode.Id and cpt.Alias = @0)", sortedInt, sortedDecimal, sortedDate, sortedString); + + throw new NotImplementedException("FIX ME!"); + + //sortedSql.Append(orderBySql, orderBy); + //if (orderDirection == Direction.Descending) + //{ + // sortedSql.Append(" DESC"); + //} + } } return psql; @@ -301,7 +339,7 @@ namespace Umbraco.Core.Persistence.Repositories protected IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, Func, IEnumerable> mapper, - string orderBy, Direction orderDirection, + string orderBy, Direction orderDirection, bool orderBySystemField, Sql filterSql = null) { if (orderBy == null) throw new ArgumentNullException(nameof(orderBy)); @@ -313,7 +351,7 @@ namespace Umbraco.Core.Persistence.Repositories var sqlNodeIds = translator.Translate(); // sort and filter - sqlNodeIds = PrepareSqlForPagedResults(sqlNodeIds, filterSql, orderBy, orderDirection); + sqlNodeIds = PrepareSqlForPagedResults(sqlNodeIds, filterSql, orderBy, orderDirection, orderBySystemField); // get a page of DTOs and the total count var pagedResult = Database.Page(pageIndex + 1, pageSize, sqlNodeIds); diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index 0cf7263246..f213a584be 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -487,27 +487,6 @@ namespace Umbraco.Core.Services } } - [Obsolete("Use the overload with 'long' parameter types instead")] - [EditorBrowsable(EditorBrowsableState.Never)] - public IEnumerable GetPagedChildren(int id, int pageIndex, int pageSize, out int totalChildren, - string orderBy, Direction orderDirection, string filter = "") - { - Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); - Mandate.ParameterCondition(pageSize > 0, "pageSize"); - using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) - { - //if the id is System Root, then just get all - if (id != Constants.System.Root) - { - repository.Query.Where(x => x.ParentId == id); - } - long total; - var contents = repository.GetPagedResultsByQuery(repository.Query, pageIndex, pageSize, out total, orderBy, orderDirection, filter); - totalChildren = Convert.ToInt32(total); - return contents; - } - } - /// /// Gets a collection of objects by Parent Id /// @@ -557,26 +536,6 @@ namespace Umbraco.Core.Services } } - [Obsolete("Use the overload with 'long' parameter types instead")] - [EditorBrowsable(EditorBrowsableState.Never)] - public IEnumerable GetPagedDescendants(int id, int pageIndex, int pageSize, out int totalChildren, string orderBy = "Path", Direction orderDirection = Direction.Ascending, string filter = "") - { - Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); - Mandate.ParameterCondition(pageSize > 0, "pageSize"); - using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) - { - //if the id is System Root, then just get all - if (id != Constants.System.Root) - { - repository.Query.Where(x => x.Path.SqlContains(string.Format(",{0},", id), TextColumnType.NVarchar)); - } - long total; - var contents = repository.GetPagedResultsByQuery(repository.Query, pageIndex, pageSize, out total, orderBy, orderDirection, filter); - totalChildren = Convert.ToInt32(total); - return contents; - } - } - /// /// Gets a collection of objects by Parent Id /// diff --git a/src/Umbraco.Core/Services/IContentService.cs b/src/Umbraco.Core/Services/IContentService.cs index 8c58f57a83..625f917ffe 100644 --- a/src/Umbraco.Core/Services/IContentService.cs +++ b/src/Umbraco.Core/Services/IContentService.cs @@ -215,12 +215,7 @@ namespace Umbraco.Core.Services /// Id of the Parent to retrieve Children from /// An Enumerable list of objects IEnumerable GetChildren(int id); - - [Obsolete("Use the overload with 'long' parameter types instead")] - [EditorBrowsable(EditorBrowsableState.Never)] - IEnumerable GetPagedChildren(int id, int pageIndex, int pageSize, out int totalRecords, - string orderBy = "SortOrder", Direction orderDirection = Direction.Ascending, string filter = ""); - + /// /// Gets a collection of objects by Parent Id /// @@ -249,12 +244,7 @@ namespace Umbraco.Core.Services /// An Enumerable list of objects IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize, out long totalRecords, string orderBy, Direction orderDirection, bool orderBySystemField, string filter); - - [Obsolete("Use the overload with 'long' parameter types instead")] - [EditorBrowsable(EditorBrowsableState.Never)] - IEnumerable GetPagedDescendants(int id, int pageIndex, int pageSize, out int totalRecords, - string orderBy = "Path", Direction orderDirection = Direction.Ascending, string filter = ""); - + /// /// Gets a collection of objects by Parent Id /// diff --git a/src/Umbraco.Core/Services/IMediaService.cs b/src/Umbraco.Core/Services/IMediaService.cs index 6ff8f75402..645bcfd1e0 100644 --- a/src/Umbraco.Core/Services/IMediaService.cs +++ b/src/Umbraco.Core/Services/IMediaService.cs @@ -117,12 +117,7 @@ namespace Umbraco.Core.Services /// Id of the Parent to retrieve Children from /// An Enumerable list of objects IEnumerable GetChildren(int id); - - [Obsolete("Use the overload with 'long' parameter types instead")] - [EditorBrowsable(EditorBrowsableState.Never)] - IEnumerable GetPagedChildren(int id, int pageIndex, int pageSize, out int totalRecords, - string orderBy = "SortOrder", Direction orderDirection = Direction.Ascending, string filter = ""); - + /// /// Gets a collection of objects by Parent Id /// @@ -151,12 +146,7 @@ namespace Umbraco.Core.Services /// An Enumerable list of objects IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize, out long totalRecords, string orderBy, Direction orderDirection, bool orderBySystemField, string filter); - - [Obsolete("Use the overload with 'long' parameter types instead")] - [EditorBrowsable(EditorBrowsableState.Never)] - IEnumerable GetPagedDescendants(int id, int pageIndex, int pageSize, out int totalRecords, - string orderBy = "Path", Direction orderDirection = Direction.Ascending, string filter = ""); - + /// /// Gets a collection of objects by Parent Id /// diff --git a/src/Umbraco.Core/Services/MediaService.cs b/src/Umbraco.Core/Services/MediaService.cs index 18b7b7f9ec..2a459174c0 100644 --- a/src/Umbraco.Core/Services/MediaService.cs +++ b/src/Umbraco.Core/Services/MediaService.cs @@ -395,26 +395,6 @@ namespace Umbraco.Core.Services } } - [Obsolete("Use the overload with 'long' parameter types instead")] - [EditorBrowsable(EditorBrowsableState.Never)] - public IEnumerable GetPagedChildren(int id, int pageIndex, int pageSize, out int totalChildren, - string orderBy, Direction orderDirection, string filter = "") - { - Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); - Mandate.ParameterCondition(pageSize > 0, "pageSize"); - using (var repository = RepositoryFactory.CreateMediaRepository(UowProvider.GetUnitOfWork())) - { - var query = repository.Query; - query.Where(x => x.ParentId == id); - - long total; - var medias = repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out total, orderBy, orderDirection, filter); - - totalChildren = Convert.ToInt32(total); - return medias; - } - } - /// /// Gets a collection of objects by Parent Id /// @@ -458,29 +438,7 @@ namespace Umbraco.Core.Services return medias; } - } - - [Obsolete("Use the overload with 'long' parameter types instead")] - [EditorBrowsable(EditorBrowsableState.Never)] - public IEnumerable GetPagedDescendants(int id, int pageIndex, int pageSize, out int totalChildren, string orderBy = "Path", Direction orderDirection = Direction.Ascending, string filter = "") - { - Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); - Mandate.ParameterCondition(pageSize > 0, "pageSize"); - using (var repository = RepositoryFactory.CreateMediaRepository(UowProvider.GetUnitOfWork())) - { - - var query = repository.Query; - //if the id is -1, then just get all - if (id != -1) - { - query.Where(x => x.Path.SqlContains(string.Format(",{0},", id), TextColumnType.NVarchar)); - } - long total; - var contents = repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out total, orderBy, orderDirection, filter); - totalChildren = Convert.ToInt32(total); - return contents; - } - } + } /// /// Gets a collection of objects by Parent Id diff --git a/src/Umbraco.Tests/UmbracoExamine/IndexInitializer.cs b/src/Umbraco.Tests/UmbracoExamine/IndexInitializer.cs index d0b2f98229..5dbb1ee29f 100644 --- a/src/Umbraco.Tests/UmbracoExamine/IndexInitializer.cs +++ b/src/Umbraco.Tests/UmbracoExamine/IndexInitializer.cs @@ -82,10 +82,6 @@ namespace Umbraco.Tests.UmbracoExamine && x.GetPagedDescendants( It.IsAny(), It.IsAny(), It.IsAny(), out longTotalRecs, It.IsAny(), It.IsAny(), It.IsAny()) == - allRecs - && x.GetPagedDescendants( - It.IsAny(), It.IsAny(), It.IsAny(), out intTotalRecs, It.IsAny(), It.IsAny(), It.IsAny()) - == allRecs); } diff --git a/src/Umbraco.Web/Editors/AuthenticationController.cs b/src/Umbraco.Web/Editors/AuthenticationController.cs index 71c0a79b8f..0b9b555287 100644 --- a/src/Umbraco.Web/Editors/AuthenticationController.cs +++ b/src/Umbraco.Web/Editors/AuthenticationController.cs @@ -4,7 +4,9 @@ using System.Net; using System.Net.Http; using System.Collections.Generic; using System.Threading.Tasks; +using System.Web; using System.Web.Http; +using System.Web.Mvc; using AutoMapper; using Microsoft.AspNet.Identity; using Microsoft.AspNet.Identity.Owin; @@ -21,6 +23,7 @@ using Umbraco.Web.Security.Identity; using Umbraco.Web.WebApi; using Umbraco.Web.WebApi.Filters; using Umbraco.Core.Configuration; +using Umbraco.Core.Logging; namespace Umbraco.Web.Editors { From 38f6a7a8b69846e23988b0097dc484f8ac6f2a35 Mon Sep 17 00:00:00 2001 From: Shannon Deminick Date: Wed, 13 Apr 2016 16:45:01 +0200 Subject: [PATCH 30/46] fixed typo --- src/Umbraco.Web.UI/umbraco/config/lang/en.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml index 93a570efe2..08521f4580 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml @@ -667,7 +667,7 @@ To manage your website, simply open the Umbraco back office and start adding con An email with password reset instructions will be sent to the specified address if it matched our records Return to login form Please provide a new password - You Password has been updated! + Your Password has been updated The link you have clicked on is invalid or has expired Umbraco: Reset Password From a015032442bfacbd68a59b1d6ec6ae7d1052f050 Mon Sep 17 00:00:00 2001 From: Shannon Deminick Date: Wed, 13 Apr 2016 16:45:47 +0200 Subject: [PATCH 31/46] fixed typo --- src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 6bcca5db86..6b78e66e7f 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml @@ -665,7 +665,7 @@ To manage your website, simply open the Umbraco back office and start adding con An email with password reset instructions will be sent to the specified address if it matched our records Return to login form Please provide a new password - You Password has been updated! + Your Password has been updated The link you have clicked on is invalid or has expired Umbraco: Reset Password From 9e3941e8e1337e7c65f53ce32506a4c0eb51c93b Mon Sep 17 00:00:00 2001 From: Aleksey Fedotov <0xc0dec@gmail.com> Date: Wed, 13 Apr 2016 22:14:21 +0500 Subject: [PATCH 32/46] Fixed logger context class --- src/Umbraco.Web/Scheduling/KeepAlive.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web/Scheduling/KeepAlive.cs b/src/Umbraco.Web/Scheduling/KeepAlive.cs index 0557e56e57..380ae85401 100644 --- a/src/Umbraco.Web/Scheduling/KeepAlive.cs +++ b/src/Umbraco.Web/Scheduling/KeepAlive.cs @@ -32,7 +32,7 @@ namespace Umbraco.Web.Scheduling // ensure we do not run if not main domain, but do NOT lock it if (_appContext.MainDom.IsMainDom == false) { - LogHelper.Debug("Does not run if not MainDom."); + LogHelper.Debug("Does not run if not MainDom."); return false; // do NOT repeat, going down } From bf70e208adfc2ffce7bc86f86e9f2e8aac4f8111 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Thu, 14 Apr 2016 09:34:53 +0200 Subject: [PATCH 33/46] U4-8325 XSLT macro rendering logs too much in Info mode (after upgrade 7.1.4 => 7.4.0) --- src/Umbraco.Web/umbraco.presentation/macro.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web/umbraco.presentation/macro.cs b/src/Umbraco.Web/umbraco.presentation/macro.cs index 66ba6fea15..77027749e2 100644 --- a/src/Umbraco.Web/umbraco.presentation/macro.cs +++ b/src/Umbraco.Web/umbraco.presentation/macro.cs @@ -1589,7 +1589,7 @@ namespace umbraco //Trace out to profiling... doesn't actually profile, just for informational output. if (excludeProfiling == false) { - using (ApplicationContext.Current.ProfilingLogger.TraceDuration(string.Format("{0}", message))) + using (ApplicationContext.Current.ProfilingLogger.DebugDuration(string.Format("{0}", message))) { } } From fcaa4ad8d686dfd4c90be09d10bc6b136dc3e5fa Mon Sep 17 00:00:00 2001 From: Per Ploug Date: Thu, 14 Apr 2016 11:36:24 +0200 Subject: [PATCH 34/46] JsDocs comments on listviewhelper (U4-7185) --- .../common/services/listviewhelper.service.js | 239 ++++++++++++++++-- 1 file changed, 221 insertions(+), 18 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/listviewhelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/listviewhelper.service.js index e211218ce1..c21ca4e313 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/listviewhelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/listviewhelper.service.js @@ -1,3 +1,47 @@ +/** + @ngdoc service + * @name umbraco.services.listViewHelper + * + * + * @description + * Service for performing operations against items in the list view UI. Used by the built-in internal listviews + * as well as custom listview. + * + * A custom listview is always used inside a wrapper listview, so there are a number of inherited values on its + * scope by default: + * + * **$scope.selection**: Array containing all items currently selected in the listview + * + * **$scope.items**: Array containing all items currently displayed in the listview + * + * **$scope.folders**: Array containing all folders in the current listview (only for media) + * + * **$scope.options**: configuration object containing information such as pagesize, permissions, order direction etc. + * + * **$scope.model.config.layouts**: array of available layouts to apply to the listview (grid, list or custom layout) + * + * ##Usage## + * To use, inject listViewHelper into custom listview controller, listviewhelper expects you + * to pass in the full collection of items in the listview in several of its methods + * this collection is inherited from the parent controller and is available on $scope.selection + * + *
+ *      angular.module("umbraco").controller("my.listVieweditor". function($scope, listViewHelper){
+ *
+ *          //current items in the listview
+ *          var items = $scope.items;
+ *
+ *          //current selection
+ *          var selection = $scope.selection;
+ *
+ *          //deselect an item , $scope.selection is inherited, item is picked from inherited $scope.items
+ *          listViewHelper.deselectItem(item, $scope.selection);
+ *
+ *          //test if all items are selected, $scope.items + $scope.selection are inherited
+ *          listViewhelper.isSelectedAll($scope.items, $scope.selection);
+ *      });
+ * 
+ */ (function () { 'use strict'; @@ -6,6 +50,19 @@ var firstSelectedIndex = 0; var localStorageKey = "umblistViewLayout"; + /** + * @ngdoc method + * @name umbraco.services.listViewHelper#getLayout + * @methodOf umbraco.services.listViewHelper + * + * @description + * Method for internal use, based on the collection of layouts passed, the method selects either + * any previous layout from local storage, or picks the first allowed layout + * + * @param {Number} nodeId The id of the current node displayed in the content editor + * @param {Array} availableLayouts Array of all allowed layouts, available from $scope.model.config.layouts + */ + function getLayout(nodeId, availableLayouts) { var storedLayouts = []; @@ -28,6 +85,19 @@ } + /** + * @ngdoc method + * @name umbraco.services.listViewHelper#setLayout + * @methodOf umbraco.services.listViewHelper + * + * @description + * Changes the current layout used by the listview to the layout passed in. Stores selection in localstorage + * + * @param {Number} nodeID Id of the current node displayed in the content editor + * @param {Object} selectedLayout Layout selected as the layout to set as the current layout + * @param {Array} availableLayouts Array of all allowed layouts, available from $scope.model.config.layouts + */ + function setLayout(nodeId, selectedLayout, availableLayouts) { var activeLayout = {}; @@ -54,6 +124,18 @@ } + /** + * @ngdoc method + * @name umbraco.services.listViewHelper#saveLayoutInLocalStorage + * @methodOf umbraco.services.listViewHelper + * + * @description + * Stores a given layout as the current default selection in local storage + * + * @param {Number} nodeId Id of the current node displayed in the content editor + * @param {Object} selectedLayout Layout selected as the layout to set as the current layout + */ + function saveLayoutInLocalStorage(nodeId, selectedLayout) { var layoutFound = false; var storedLayouts = []; @@ -84,6 +166,17 @@ } + /** + * @ngdoc method + * @name umbraco.services.listViewHelper#getFirstAllowedLayout + * @methodOf umbraco.services.listViewHelper + * + * @description + * Returns currently selected layout, or alternatively the first layout in the available layouts collection + * + * @param {Array} layouts Array of all allowed layouts, available from $scope.model.config.layouts + */ + function getFirstAllowedLayout(layouts) { var firstAllowedLayout = {}; @@ -99,6 +192,23 @@ return firstAllowedLayout; } + /** + * @ngdoc method + * @name umbraco.services.listViewHelper#selectHandler + * @methodOf umbraco.services.listViewHelper + * + * @description + * Helper method for working with item selection via a checkbox, internally it uses selectItem and deselectItem. + * Working with this method, requires its triggered via a checkbox which can then pass in its triggered $event + * When the checkbox is clicked, this method will toggle selection of the associated item so it matches the state of the checkbox + * + * @param {Object} selectedItem Item being selected or deselected by the checkbox + * @param {Number} selectedIndex Index of item being selected/deselected, usually passed as $index + * @param {Array} items All items in the current listview, available as $scope.items + * @param {Array} selection All selected items in the current listview, available as $scope.selection + * @param {Event} $event Event triggered by the checkbox being checked to select / deselect an item + */ + function selectHandler(selectedItem, selectedIndex, items, selection, $event) { var start = 0; @@ -143,6 +253,18 @@ } + /** + * @ngdoc method + * @name umbraco.services.listViewHelper#selectItem + * @methodOf umbraco.services.listViewHelper + * + * @description + * Selects a given item to the listview selection array, requires you pass in the inherited $scope.selection collection + * + * @param {Object} item Item to select + * @param {Array} selection Listview selection, available as $scope.selection + */ + function selectItem(item, selection) { var isSelected = false; for (var i = 0; selection.length > i; i++) { @@ -157,6 +279,18 @@ } } + /** + * @ngdoc method + * @name umbraco.services.listViewHelper#deselectItem + * @methodOf umbraco.services.listViewHelper + * + * @description + * Deselects a given item from the listviews selection array, requires you pass in the inherited $scope.selection collection + * + * @param {Object} item Item to deselect + * @param {Array} selection Listview selection, available as $scope.selection + */ + function deselectItem(item, selection) { for (var i = 0; selection.length > i; i++) { var selectedItem = selection[i]; @@ -167,6 +301,20 @@ } } + /** + * @ngdoc method + * @name umbraco.services.listViewHelper#clearSelection + * @methodOf umbraco.services.listViewHelper + * + * @description + * Removes a given number of items and folders from the listviews selection array + * Folders can only be passed in if the listview is used in the media section which has a concept of folders. + * + * @param {Array} items Items to remove, can be null + * @param {Array} folders Folders to remove, can be null + * @param {Array} selection Listview selection, available as $scope.selection + */ + function clearSelection(items, folders, selection) { var i = 0; @@ -180,7 +328,7 @@ } } - if (angular.isArray(items)) { + if(angular.isArray(folders)) { for (i = 0; folders.length > i; i++) { var folder = folders[i]; folder.selected = false; @@ -188,6 +336,20 @@ } } + /** + * @ngdoc method + * @name umbraco.services.listViewHelper#selectAllItems + * @methodOf umbraco.services.listViewHelper + * + * @description + * Helper method for toggling the select state on all items in the active listview + * Can only be used from a checkbox as a checkbox $event is required to pass in. + * + * @param {Array} items Items to toggle selection on, should be $scope.items + * @param {Array} selection Listview selection, available as $scope.selection + * @param {$event} $event Event passed from the checkbox being toggled + */ + function selectAllItems(items, selection, $event) { var checkbox = $event.target; @@ -219,6 +381,20 @@ } + /** + * @ngdoc method + * @name umbraco.services.listViewHelper#isSelectedAll + * @methodOf umbraco.services.listViewHelper + * + * @description + * Method to determine if all items on the current page in the list has been selected + * Given the current items in the view, and the current selection, it will return true/false + * + * @param {Array} items Items to test if all are selected, should be $scope.items + * @param {Array} selection Listview selection, available as $scope.selection + * @returns {Boolean} boolean indicate if all items in the listview have been selected + */ + function isSelectedAll(items, selection) { var numberOfSelectedItem = 0; @@ -242,10 +418,35 @@ } + /** + * @ngdoc method + * @name umbraco.services.listViewHelper#setSortingDirection + * @methodOf umbraco.services.listViewHelper + * + * @description + * *Internal* method for changing sort order icon + * @param {String} col Column alias to order after + * @param {String} direction Order direction `asc` or `desc` + * @param {Object} options object passed from the parent listview available as $scope.options + */ + function setSortingDirection(col, direction, options) { return options.orderBy.toUpperCase() === col.toUpperCase() && options.orderDirection === direction; } + /** + * @ngdoc method + * @name umbraco.services.listViewHelper#setSorting + * @methodOf umbraco.services.listViewHelper + * + * @description + * Method for setting the field on which the listview will order its items after. + * + * @param {String} field Field alias to order after + * @param {Boolean} allow Determines if the user is allowed to set this field, normally true + * @param {Object} options Options object passed from the parent listview available as $scope.options + */ + function setSorting(field, allow, options) { if (allow) { options.orderBy = field; @@ -257,12 +458,12 @@ } } } - + //This takes in a dictionary of Ids with Permissions and determines // the intersect of all permissions to return an object representing the // listview button permissions function getButtonPermissions(unmergedPermissions, currentIdsWithPermissions) { - + if (currentIdsWithPermissions == null) { currentIdsWithPermissions = {}; } @@ -286,26 +487,28 @@ canCopy: _.contains(intersectPermissions, 'O'), //Magic Char = O canCreate: _.contains(intersectPermissions, 'C'), //Magic Char = C canDelete: _.contains(intersectPermissions, 'D'), //Magic Char = D - canMove: _.contains(intersectPermissions, 'M'), //Magic Char = M + canMove: _.contains(intersectPermissions, 'M'), //Magic Char = M canPublish: _.contains(intersectPermissions, 'U'), //Magic Char = U - canUnpublish: _.contains(intersectPermissions, 'U'), //Magic Char = Z (however UI says it can't be set, so if we can publish 'U' we can unpublish) + canUnpublish: _.contains(intersectPermissions, 'U'), //Magic Char = Z (however UI says it can't be set, so if we can publish 'U' we can unpublish) }; } var service = { - getLayout: getLayout, - getFirstAllowedLayout: getFirstAllowedLayout, - setLayout: setLayout, - saveLayoutInLocalStorage: saveLayoutInLocalStorage, - selectHandler: selectHandler, - selectItem: selectItem, - deselectItem: deselectItem, - clearSelection: clearSelection, - selectAllItems: selectAllItems, - isSelectedAll: isSelectedAll, - setSortingDirection: setSortingDirection, - setSorting: setSorting, - getButtonPermissions: getButtonPermissions + + getLayout: getLayout, + getFirstAllowedLayout: getFirstAllowedLayout, + setLayout: setLayout, + saveLayoutInLocalStorage: saveLayoutInLocalStorage, + selectHandler: selectHandler, + selectItem: selectItem, + deselectItem: deselectItem, + clearSelection: clearSelection, + selectAllItems: selectAllItems, + isSelectedAll: isSelectedAll, + setSortingDirection: setSortingDirection, + setSorting: setSorting, + getButtonPermissions: getButtonPermissions + }; return service; From f149ca9c763135f3a7aab1e4b6dc2d56d5ff4455 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 20 Apr 2016 12:50:52 +0200 Subject: [PATCH 35/46] Fixes SqlCe and MySQL sorting on custom properties for list view, this also makes the query work much better and there is no longer a requirement for a sub-query in the order by statement. --- .../Repositories/VersionableRepositoryBase.cs | 43 ++++++------- .../SqlSyntax/ISqlSyntaxProvider.cs | 3 +- .../SqlSyntax/MySqlSyntaxProvider.cs | 5 +- .../SqlSyntax/SqlCeSyntaxProvider.cs | 1 - .../SqlSyntax/SqlSyntaxProviderBase.cs | 7 +-- .../Persistence/UmbracoDatabase.cs | 19 ++++-- .../Repositories/ContentRepositoryTest.cs | 60 +++++++++++++++++-- 7 files changed, 99 insertions(+), 39 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs index b32336e9b2..62d7259065 100644 --- a/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs @@ -303,34 +303,37 @@ namespace Umbraco.Core.Persistence.Repositories // from most recent content version for the given order by field var sortedInt = string.Format(SqlSyntax.ConvertIntegerToOrderableString, "dataInt"); var sortedDate = string.Format(SqlSyntax.ConvertDateToOrderableString, "dataDate"); - var sortedString = string.Format(SqlSyntax.IsNull, "dataNvarchar", "''"); + var sortedString = string.Format("COALESCE({0},'')", "dataNvarchar"); var sortedDecimal = string.Format(SqlSyntax.ConvertDecimalToOrderableString, "dataDecimal"); - var orderBySql = string.Format(@"ORDER BY ( + var innerJoinTempTable = string.Format(@"INNER JOIN ( SELECT CASE - WHEN dataInt Is Not Null THEN {0} + WHEN dataInt Is Not Null THEN {0} WHEN dataDecimal Is Not Null THEN {1} WHEN dataDate Is Not Null THEN {2} ELSE {3} - END - FROM cmsContent c - INNER JOIN cmsContentVersion cv ON cv.ContentId = c.nodeId AND VersionDate = ( - SELECT Max(VersionDate) - FROM cmsContentVersion - WHERE ContentId = c.nodeId - ) - INNER JOIN cmsPropertyData cpd ON cpd.contentNodeId = c.nodeId - AND cpd.versionId = cv.VersionId - INNER JOIN cmsPropertyType cpt ON cpt.Id = cpd.propertytypeId - WHERE c.nodeId = umbracoNode.Id and cpt.Alias = @0)", sortedInt, sortedDecimal, sortedDate, sortedString); + END AS CustomPropVal, + cd.nodeId AS CustomPropValContentId + FROM cmsDocument cd + INNER JOIN cmsPropertyData cpd ON cpd.contentNodeId = cd.nodeId AND cpd.versionId = cd.versionId + INNER JOIN cmsPropertyType cpt ON cpt.Id = cpd.propertytypeId + WHERE cpt.Alias = @2 AND cd.newest = 1) AS CustomPropData + ON CustomPropData.CustomPropValContentId = umbracoNode.id +", sortedInt, sortedDecimal, sortedDate, sortedString); - throw new NotImplementedException("FIX ME!"); + //insert this just above the LEFT OUTER JOIN + var newSql = psql.SQL.Insert(psql.SQL.IndexOf("LEFT OUTER JOIN"), innerJoinTempTable); + var newArgs = psql.Arguments.ToList(); + newArgs.Add(orderBy); - //sortedSql.Append(orderBySql, orderBy); - //if (orderDirection == Direction.Descending) - //{ - // sortedSql.Append(" DESC"); - //} + psql = new Sql(psql.SqlContext, newSql, newArgs.ToArray()); + + psql.OrderBy("CustomPropData.CustomPropVal"); + //psql.Append(innerJoinTempTable, orderBy); + if (orderDirection == Direction.Descending) + { + psql.Append(" DESC"); + } } } diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/ISqlSyntaxProvider.cs b/src/Umbraco.Core/Persistence/SqlSyntax/ISqlSyntaxProvider.cs index 33197dced8..fc26bcd7c9 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/ISqlSyntaxProvider.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/ISqlSyntaxProvider.cs @@ -70,8 +70,7 @@ namespace Umbraco.Core.Persistence.SqlSyntax bool SupportsClustered(); bool SupportsIdentityInsert(); bool? SupportsCaseInsensitiveQueries(Database db); - - string IsNull { get; } + string ConvertIntegerToOrderableString { get; } string ConvertDateToOrderableString { get; } string ConvertDecimalToOrderableString { get; } diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/MySqlSyntaxProvider.cs b/src/Umbraco.Core/Persistence/SqlSyntax/MySqlSyntaxProvider.cs index f9a193d731..462f601323 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/MySqlSyntaxProvider.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/MySqlSyntaxProvider.cs @@ -361,10 +361,9 @@ ORDER BY TABLE_NAME, INDEX_NAME", public override string DropIndex { get { return "DROP INDEX {0} ON {1}"; } } public override string RenameColumn { get { return "ALTER TABLE {0} CHANGE {1} {2}"; } } - public override string IsNull { get { return "IFNULL({0},{1})"; } } - public override string ConvertIntegerToOrderableString { get { return "LPAD({0}, 8, '0')"; } } + public override string ConvertIntegerToOrderableString { get { return "LPAD(FORMAT({0}, 0), 8, '0')"; } } public override string ConvertDateToOrderableString { get { return "DATE_FORMAT({0}, '%Y%m%d')"; } } - public override string ConvertDecimalToOrderableString { get { return "LPAD({0}, 25, '0')"; } } + public override string ConvertDecimalToOrderableString { get { return "LPAD(FORMAT({0}, 9), 20, '0')"; } } public override bool? SupportsCaseInsensitiveQueries(Database db) { diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/SqlCeSyntaxProvider.cs b/src/Umbraco.Core/Persistence/SqlSyntax/SqlCeSyntaxProvider.cs index 41b88abec9..4a3eb2e4a8 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/SqlCeSyntaxProvider.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/SqlCeSyntaxProvider.cs @@ -209,7 +209,6 @@ ORDER BY TABLE_NAME, INDEX_NAME"); public override string DropIndex { get { return "DROP INDEX {1}.{0}"; } } - } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs b/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs index 36aad20b29..a992cdd231 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs @@ -538,9 +538,8 @@ namespace Umbraco.Core.Persistence.SqlSyntax public virtual string DeleteConstraint { get { return "ALTER TABLE {0} DROP CONSTRAINT {1}"; } } public virtual string CreateForeignKeyConstraint { get { return "ALTER TABLE {0} ADD CONSTRAINT {1} FOREIGN KEY ({2}) REFERENCES {3} ({4}){5}{6}"; } } - public virtual string IsNull { get { return "ISNULL({0},{1})"; } } - public virtual string ConvertIntegerToOrderableString { get { return "RIGHT('00000000' + CAST({0} AS varchar(8)),8)"; } } - public virtual string ConvertDateToOrderableString { get { return "CONVERT(varchar, {0}, 102)"; } } - public virtual string ConvertDecimalToOrderableString { get { return "RIGHT('0000000000000000000000000' + CAST({0} AS varchar(25)),25)"; } } + public virtual string ConvertIntegerToOrderableString { get { return "REPLACE(STR({0}, 8), SPACE(1), '0')"; } } + public virtual string ConvertDateToOrderableString { get { return "CONVERT(nvarchar, {0}, 102)"; } } + public virtual string ConvertDecimalToOrderableString { get { return "REPLACE(STR({0}, 20, 9), SPACE(1), '0')"; } } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/UmbracoDatabase.cs b/src/Umbraco.Core/Persistence/UmbracoDatabase.cs index eaf637df4b..451fbf36d7 100644 --- a/src/Umbraco.Core/Persistence/UmbracoDatabase.cs +++ b/src/Umbraco.Core/Persistence/UmbracoDatabase.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Data; using System.Data.Common; using System.Linq; +using System.Text; using NPoco; using StackExchange.Profiling; using Umbraco.Core.Logging; @@ -115,15 +116,25 @@ namespace Umbraco.Core.Persistence // if no timeout is specified, and the connection has a longer timeout, use it if (OneTimeCommandTimeout == 0 && CommandTimeout == 0 && cmd.Connection.ConnectionTimeout > 30) cmd.CommandTimeout = cmd.Connection.ConnectionTimeout; + + if (EnableSqlTrace) + { + var sb = new StringBuilder(); + sb.Append(cmd.CommandText); + foreach (DbParameter p in cmd.Parameters) + { + sb.Append(" - "); + sb.Append(p.Value); + } + + _logger.Debug(sb.ToString()); + } + base.OnExecutingCommand(cmd); } protected override void OnExecutedCommand(DbCommand cmd) { - if (EnableSqlTrace) - { - _logger.Debug(cmd.CommandText); - } if (_enableCount) { SqlCount++; diff --git a/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs index 65c5187120..a6a26c203d 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text.RegularExpressions; using System.Xml.Linq; using Moq; using NUnit.Framework; @@ -556,6 +557,44 @@ namespace Umbraco.Tests.Persistence.Repositories Assert.AreEqual("[alias2]", matches[1].Groups[2].Value); } + [Test] + public void Can_Perform_GetPagedResultsByQuery_Sorting_On_Custom_Property() + { + // Arrange + var provider = new NPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + ContentTypeRepository contentTypeRepository; + using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) + { + // Act + var query = repository.Query.Where(x => x.Name.Contains("Text")); + long totalRecords; + + //var origRegex = NPoco.PagingHelper.rxOrderBy; + try + { + //NPoco.PagingHelper.rxOrderBy = new Regex("(?!.*(?:\\s+FROM[\\s\\(]+))ORDER\\s+BY\\s+([\\w\\.\\[\\]\\(\\) \"`,'@\\s\\+\\=]+)(?!.*\\))", RegexOptions.IgnoreCase | RegexOptions.Compiled); + DatabaseContext.Database.EnableSqlTrace = true; + DatabaseContext.Database.EnableSqlCount(); + + var result = repository.GetPagedResultsByQuery(query, 0, 2, out totalRecords, "title", Direction.Ascending, false); + + Assert.AreEqual(3, totalRecords); + Assert.AreEqual(2, result.Count()); + + result = repository.GetPagedResultsByQuery(query, 1, 2, out totalRecords, "title", Direction.Ascending, false); + + Assert.AreEqual(1, result.Count()); + } + finally + { + //NPoco.PagingHelper.rxOrderBy = origRegex; + DatabaseContext.Database.EnableSqlTrace = false; + DatabaseContext.Database.DisableSqlCount(); + } + } + } + [Test] public void Can_Perform_GetPagedResultsByQuery_ForFirstPage_On_ContentRepository() { @@ -568,12 +607,23 @@ namespace Umbraco.Tests.Persistence.Repositories // Act var query = repository.Query.Where(x => x.Level == 2); long totalRecords; - var result = repository.GetPagedResultsByQuery(query, 0, 1, out totalRecords, "Name", Direction.Ascending, true); - // Assert - Assert.That(totalRecords, Is.GreaterThanOrEqualTo(2)); - Assert.That(result.Count(), Is.EqualTo(1)); - Assert.That(result.First().Name, Is.EqualTo("Text Page 1")); + try + { + DatabaseContext.Database.EnableSqlTrace = true; + DatabaseContext.Database.EnableSqlCount(); + var result = repository.GetPagedResultsByQuery(query, 0, 1, out totalRecords, "Name", Direction.Ascending, true); + + // Assert + Assert.That(totalRecords, Is.GreaterThanOrEqualTo(2)); + Assert.That(result.Count(), Is.EqualTo(1)); + Assert.That(result.First().Name, Is.EqualTo("Text Page 1")); + } + finally + { + DatabaseContext.Database.EnableSqlTrace = false; + DatabaseContext.Database.DisableSqlCount(); + } } } From 289d5f5741768cd156a1b6a2dfbcfc7191f8ad78 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 20 Apr 2016 12:51:42 +0200 Subject: [PATCH 36/46] removes the need for the custom regex for the order by since we no longer have an order by sub-query --- .../Persistence/Repositories/ContentRepositoryTest.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs index a6a26c203d..f9244c3987 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs @@ -569,11 +569,9 @@ namespace Umbraco.Tests.Persistence.Repositories // Act var query = repository.Query.Where(x => x.Name.Contains("Text")); long totalRecords; - - //var origRegex = NPoco.PagingHelper.rxOrderBy; + try { - //NPoco.PagingHelper.rxOrderBy = new Regex("(?!.*(?:\\s+FROM[\\s\\(]+))ORDER\\s+BY\\s+([\\w\\.\\[\\]\\(\\) \"`,'@\\s\\+\\=]+)(?!.*\\))", RegexOptions.IgnoreCase | RegexOptions.Compiled); DatabaseContext.Database.EnableSqlTrace = true; DatabaseContext.Database.EnableSqlCount(); @@ -587,8 +585,7 @@ namespace Umbraco.Tests.Persistence.Repositories Assert.AreEqual(1, result.Count()); } finally - { - //NPoco.PagingHelper.rxOrderBy = origRegex; + { DatabaseContext.Database.EnableSqlTrace = false; DatabaseContext.Database.DisableSqlCount(); } From 612412683527dad1cdc3dbd4df61969b4695d7b0 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 20 Apr 2016 15:50:38 +0200 Subject: [PATCH 37/46] Fixes the custom field sorting, no longer has a sub query for order by, now supports both mysql and sqlce, adds a unit test (should add more though) --- .../Repositories/ContentRepository.cs | 1 + .../Repositories/VersionableRepositoryBase.cs | 48 +++++++++------- .../SqlSyntax/ISqlSyntaxProvider.cs | 3 +- .../SqlSyntax/MySqlSyntaxProvider.cs | 5 +- .../SqlSyntax/SqlCeSyntaxProvider.cs | 1 - .../SqlSyntax/SqlSyntaxProviderBase.cs | 7 +-- .../Persistence/UmbracoDatabase.cs | 19 +++++-- .../Repositories/ContentRepositoryTest.cs | 57 +++++++++++++++++-- 8 files changed, 101 insertions(+), 40 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs index 270b209553..5f96cad114 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs @@ -105,6 +105,7 @@ namespace Umbraco.Core.Persistence.Repositories #region Overrides of PetaPocoRepositoryBase + protected override Sql GetBaseQuery(bool isCount) { var sqlx = string.Format("LEFT OUTER JOIN {0} {1} ON ({1}.{2}={0}.{2} AND {1}.{3}=1)", diff --git a/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs index 934555ac6e..4b2befb3e3 100644 --- a/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs @@ -248,6 +248,7 @@ namespace Umbraco.Core.Persistence.Repositories private Sql GetSortedSqlForPagedResults(Sql sql, Direction orderDirection, string orderBy, bool orderBySystemField) { + //copy to var so that the original isn't changed var sortedSql = new Sql(sql.SQL, sql.Arguments); @@ -273,35 +274,40 @@ namespace Umbraco.Core.Persistence.Repositories // from most recent content version for the given order by field var sortedInt = string.Format(SqlSyntax.ConvertIntegerToOrderableString, "dataInt"); var sortedDate = string.Format(SqlSyntax.ConvertDateToOrderableString, "dataDate"); - var sortedString = string.Format(SqlSyntax.IsNull, "dataNvarchar", "''"); + var sortedString = string.Format("COALESCE({0},'')", "dataNvarchar"); var sortedDecimal = string.Format(SqlSyntax.ConvertDecimalToOrderableString, "dataDecimal"); - var orderBySql = string.Format(@"ORDER BY ( - SELECT CASE - WHEN dataInt Is Not Null THEN {0} - WHEN dataDecimal Is Not Null THEN {1} - WHEN dataDate Is Not Null THEN {2} - ELSE {3} - END - FROM cmsContent c - INNER JOIN cmsContentVersion cv ON cv.ContentId = c.nodeId AND VersionDate = ( - SELECT Max(VersionDate) - FROM cmsContentVersion - WHERE ContentId = c.nodeId - ) - INNER JOIN cmsPropertyData cpd ON cpd.contentNodeId = c.nodeId - AND cpd.versionId = cv.VersionId - INNER JOIN cmsPropertyType cpt ON cpt.Id = cpd.propertytypeId - WHERE c.nodeId = umbracoNode.Id and cpt.Alias = @0)", sortedInt, sortedDecimal, sortedDate, sortedString); + var innerJoinTempTable = string.Format(@"INNER JOIN ( + SELECT CASE + WHEN dataInt Is Not Null THEN {0} + WHEN dataDecimal Is Not Null THEN {1} + WHEN dataDate Is Not Null THEN {2} + ELSE {3} + END AS CustomPropVal, + cd.nodeId AS CustomPropValContentId + FROM cmsDocument cd + INNER JOIN cmsPropertyData cpd ON cpd.contentNodeId = cd.nodeId AND cpd.versionId = cd.versionId + INNER JOIN cmsPropertyType cpt ON cpt.Id = cpd.propertytypeId + WHERE cpt.Alias = @{4} AND cd.newest = 1) AS CustomPropData + ON CustomPropData.CustomPropValContentId = umbracoNode.id + ", sortedInt, sortedDecimal, sortedDate, sortedString, sortedSql.Arguments.Length); - sortedSql.Append(orderBySql, orderBy); + //insert this just above the LEFT OUTER JOIN + var newSql = sortedSql.SQL.Insert(sortedSql.SQL.IndexOf("LEFT OUTER JOIN"), innerJoinTempTable); + var newArgs = sortedSql.Arguments.ToList(); + newArgs.Add(orderBy); + + sortedSql = new Sql(newSql, newArgs.ToArray()); + + sortedSql.OrderBy("CustomPropData.CustomPropVal"); if (orderDirection == Direction.Descending) { sortedSql.Append(" DESC"); - } + } } - + return sortedSql; + } /// diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/ISqlSyntaxProvider.cs b/src/Umbraco.Core/Persistence/SqlSyntax/ISqlSyntaxProvider.cs index c70b6a571a..e61b23c8ec 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/ISqlSyntaxProvider.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/ISqlSyntaxProvider.cs @@ -69,8 +69,7 @@ namespace Umbraco.Core.Persistence.SqlSyntax bool SupportsClustered(); bool SupportsIdentityInsert(); bool? SupportsCaseInsensitiveQueries(Database db); - - string IsNull { get; } + string ConvertIntegerToOrderableString { get; } string ConvertDateToOrderableString { get; } string ConvertDecimalToOrderableString { get; } diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/MySqlSyntaxProvider.cs b/src/Umbraco.Core/Persistence/SqlSyntax/MySqlSyntaxProvider.cs index d4585f5aa1..7167a5af95 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/MySqlSyntaxProvider.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/MySqlSyntaxProvider.cs @@ -360,10 +360,9 @@ ORDER BY TABLE_NAME, INDEX_NAME", public override string DropIndex { get { return "DROP INDEX {0} ON {1}"; } } public override string RenameColumn { get { return "ALTER TABLE {0} CHANGE {1} {2}"; } } - public override string IsNull { get { return "IFNULL({0},{1})"; } } - public override string ConvertIntegerToOrderableString { get { return "LPAD({0}, 8, '0')"; } } + public override string ConvertIntegerToOrderableString { get { return "LPAD(FORMAT({0}, 0), 8, '0')"; } } public override string ConvertDateToOrderableString { get { return "DATE_FORMAT({0}, '%Y%m%d')"; } } - public override string ConvertDecimalToOrderableString { get { return "LPAD({0}, 25, '0')"; } } + public override string ConvertDecimalToOrderableString { get { return "LPAD(FORMAT({0}, 9), 20, '0')"; } } public override bool? SupportsCaseInsensitiveQueries(Database db) { diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/SqlCeSyntaxProvider.cs b/src/Umbraco.Core/Persistence/SqlSyntax/SqlCeSyntaxProvider.cs index 1c6e1bba52..23e0a1bb87 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/SqlCeSyntaxProvider.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/SqlCeSyntaxProvider.cs @@ -208,7 +208,6 @@ ORDER BY TABLE_NAME, INDEX_NAME"); public override string DropIndex { get { return "DROP INDEX {1}.{0}"; } } - } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs b/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs index b7b58d929f..50417761aa 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs @@ -537,9 +537,8 @@ namespace Umbraco.Core.Persistence.SqlSyntax public virtual string DeleteConstraint { get { return "ALTER TABLE {0} DROP CONSTRAINT {1}"; } } public virtual string CreateForeignKeyConstraint { get { return "ALTER TABLE {0} ADD CONSTRAINT {1} FOREIGN KEY ({2}) REFERENCES {3} ({4}){5}{6}"; } } - public virtual string IsNull { get { return "ISNULL({0},{1})"; } } - public virtual string ConvertIntegerToOrderableString { get { return "RIGHT('00000000' + CAST({0} AS varchar(8)),8)"; } } - public virtual string ConvertDateToOrderableString { get { return "CONVERT(varchar, {0}, 102)"; } } - public virtual string ConvertDecimalToOrderableString { get { return "RIGHT('0000000000000000000000000' + CAST({0} AS varchar(25)),25)"; } } + public virtual string ConvertIntegerToOrderableString { get { return "REPLACE(STR({0}, 8), SPACE(1), '0')"; } } + public virtual string ConvertDateToOrderableString { get { return "CONVERT(nvarchar, {0}, 102)"; } } + public virtual string ConvertDecimalToOrderableString { get { return "REPLACE(STR({0}, 20, 9), SPACE(1), '0')"; } } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/UmbracoDatabase.cs b/src/Umbraco.Core/Persistence/UmbracoDatabase.cs index 8fc1150409..a0490348f3 100644 --- a/src/Umbraco.Core/Persistence/UmbracoDatabase.cs +++ b/src/Umbraco.Core/Persistence/UmbracoDatabase.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Data; using System.Data.Common; using System.Linq; +using System.Text; using StackExchange.Profiling; using Umbraco.Core.Logging; @@ -128,15 +129,25 @@ namespace Umbraco.Core.Persistence // if no timeout is specified, and the connection has a longer timeout, use it if (OneTimeCommandTimeout == 0 && CommandTimeout == 0 && cmd.Connection.ConnectionTimeout > 30) cmd.CommandTimeout = cmd.Connection.ConnectionTimeout; + + if (EnableSqlTrace) + { + var sb = new StringBuilder(); + sb.Append(cmd.CommandText); + foreach (DbParameter p in cmd.Parameters) + { + sb.Append(" - "); + sb.Append(p.Value); + } + + _logger.Debug(sb.ToString()); + } + base.OnExecutingCommand(cmd); } public override void OnExecutedCommand(IDbCommand cmd) { - if (EnableSqlTrace) - { - _logger.Debug(cmd.CommandText); - } if (_enableCount) { SqlCount++; diff --git a/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs index 2cdc40d6f6..d29b073df7 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text.RegularExpressions; using System.Xml.Linq; using Moq; using NUnit.Framework; @@ -543,6 +544,41 @@ namespace Umbraco.Tests.Persistence.Repositories } } + [Test] + public void Can_Perform_GetPagedResultsByQuery_Sorting_On_Custom_Property() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + ContentTypeRepository contentTypeRepository; + using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) + { + // Act + var query = Query.Builder.Where(x => x.Name.Contains("Text")); + long totalRecords; + + try + { + DatabaseContext.Database.EnableSqlTrace = true; + DatabaseContext.Database.EnableSqlCount(); + + var result = repository.GetPagedResultsByQuery(query, 0, 2, out totalRecords, "title", Direction.Ascending, false); + + Assert.AreEqual(3, totalRecords); + Assert.AreEqual(2, result.Count()); + + result = repository.GetPagedResultsByQuery(query, 1, 2, out totalRecords, "title", Direction.Ascending, false); + + Assert.AreEqual(1, result.Count()); + } + finally + { + DatabaseContext.Database.EnableSqlTrace = false; + DatabaseContext.Database.DisableSqlCount(); + } + } + } + [Test] public void Can_Perform_GetPagedResultsByQuery_ForFirstPage_On_ContentRepository() { @@ -555,12 +591,23 @@ namespace Umbraco.Tests.Persistence.Repositories // Act var query = Query.Builder.Where(x => x.Level == 2); long totalRecords; - var result = repository.GetPagedResultsByQuery(query, 0, 1, out totalRecords, "Name", Direction.Ascending, true); - // Assert - Assert.That(totalRecords, Is.GreaterThanOrEqualTo(2)); - Assert.That(result.Count(), Is.EqualTo(1)); - Assert.That(result.First().Name, Is.EqualTo("Text Page 1")); + try + { + DatabaseContext.Database.EnableSqlTrace = true; + DatabaseContext.Database.EnableSqlCount(); + var result = repository.GetPagedResultsByQuery(query, 0, 1, out totalRecords, "Name", Direction.Ascending, true); + + // Assert + Assert.That(totalRecords, Is.GreaterThanOrEqualTo(2)); + Assert.That(result.Count(), Is.EqualTo(1)); + Assert.That(result.First().Name, Is.EqualTo("Text Page 1")); + } + finally + { + DatabaseContext.Database.EnableSqlTrace = false; + DatabaseContext.Database.DisableSqlCount(); + } } } From b04e070757f2e3ccadaf18270200b298d8274ceb Mon Sep 17 00:00:00 2001 From: Stephan Date: Wed, 20 Apr 2016 13:13:34 +0200 Subject: [PATCH 38/46] Fixing tests - Plugin and TypeFinder --- .../Plugins/PluginManagerTests.cs | 6 ++--- src/Umbraco.Tests/Plugins/TypeFinderTests.cs | 23 +++++++++++++++---- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Tests/Plugins/PluginManagerTests.cs b/src/Umbraco.Tests/Plugins/PluginManagerTests.cs index 975259fe32..b8f5e305a6 100644 --- a/src/Umbraco.Tests/Plugins/PluginManagerTests.cs +++ b/src/Umbraco.Tests/Plugins/PluginManagerTests.cs @@ -263,7 +263,7 @@ namespace Umbraco.Tests.Plugins public void Resolves_Assigned_Mappers() { var foundTypes1 = _manager.ResolveAssignedMapperTypes(); - Assert.AreEqual(28, foundTypes1.Count()); + Assert.AreEqual(29, foundTypes1.Count()); // 29 classes in the solution implement BaseMapper } [Test] @@ -277,14 +277,14 @@ namespace Umbraco.Tests.Plugins public void Resolves_Actions() { var actions = _manager.ResolveActions(); - Assert.AreEqual(37, actions.Count()); + Assert.AreEqual(36, actions.Count()); // 36 classes in the solution implement IAction } [Test] public void Resolves_Trees() { var trees = _manager.ResolveTrees(); - Assert.AreEqual(39, trees.Count()); + Assert.AreEqual(8, trees.Count()); // 8 classes in the solution implement BaseTree } [Test] diff --git a/src/Umbraco.Tests/Plugins/TypeFinderTests.cs b/src/Umbraco.Tests/Plugins/TypeFinderTests.cs index be2a462cfe..d2161bdfe5 100644 --- a/src/Umbraco.Tests/Plugins/TypeFinderTests.cs +++ b/src/Umbraco.Tests/Plugins/TypeFinderTests.cs @@ -19,6 +19,7 @@ using Umbraco.Core; using Umbraco.Core.IO; using umbraco.DataLayer; using umbraco.uicontrols; +using Umbraco.Web; using Umbraco.Web.Models.Trees; using Umbraco.Web.Trees; @@ -73,17 +74,29 @@ namespace Umbraco.Tests.Plugins var typesFound = TypeFinder.FindClassesOfType(_assemblies); var originalTypesFound = TypeFinderOriginal.FindClassesOfType(_assemblies); - Assert.AreEqual(originalTypesFound.Count(), typesFound.Count()); - Assert.AreEqual(9, typesFound.Count()); - Assert.AreEqual(9, originalTypesFound.Count()); + foreach (var type in typesFound) + Console.WriteLine(type); + Console.WriteLine(); + foreach (var type in originalTypesFound) + Console.WriteLine(type); + + // 6 classes in _assemblies implement IApplicationEventHandler + Assert.AreEqual(6, typesFound.Count()); + + // however, + // Umbraco.Core.Profiling.WebProfiler is internal and is not returned by TypeFinderOriginal, + // that's a known issue of the legacy type finder, so we have to tweak the count here. + Assert.AreEqual(5, originalTypesFound.Count()); } [Test] public void Find_Classes_With_Attribute() { var typesFound = TypeFinder.FindClassesWithAttribute(_assemblies); - //TODO: Fix this with the correct count - Assert.AreEqual(1, typesFound.Count()); + Assert.AreEqual(0, typesFound.Count()); // 0 classes in _assemblies are marked with [Tree] + + typesFound = TypeFinder.FindClassesWithAttribute(new[] { typeof (UmbracoContext).Assembly }); + Assert.AreEqual(23, typesFound.Count()); // 23 classes in Umbraco.Web are marked with [Tree] } [Ignore] From 7e8b4d13a72e3f7216482ea8628050214c26bf49 Mon Sep 17 00:00:00 2001 From: Stephan Date: Wed, 20 Apr 2016 14:06:13 +0200 Subject: [PATCH 39/46] Fixing tests - ValueListPreValueEditor --- .../MultiValuePropertyEditorTests.cs | 27 +++++++++++++++++++ .../ValueListPreValueEditor.cs | 2 +- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Tests/PropertyEditors/MultiValuePropertyEditorTests.cs b/src/Umbraco.Tests/PropertyEditors/MultiValuePropertyEditorTests.cs index 42b9d174df..0ba844ca6b 100644 --- a/src/Umbraco.Tests/PropertyEditors/MultiValuePropertyEditorTests.cs +++ b/src/Umbraco.Tests/PropertyEditors/MultiValuePropertyEditorTests.cs @@ -1,10 +1,21 @@ using System; using System.Collections.Generic; +using System.ComponentModel; +using System.Globalization; +using System.Linq; using Moq; using NUnit.Framework; +using Umbraco.Core; +using Umbraco.Core.Logging; using Umbraco.Core.Models; +using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.SqlSyntax; +using Umbraco.Core.Persistence.UnitOfWork; +using Umbraco.Core.Profiling; using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Publishing; using Umbraco.Core.Services; +using Umbraco.Core.Strings; using Umbraco.Web.PropertyEditors; namespace Umbraco.Tests.PropertyEditors @@ -88,6 +99,22 @@ namespace Umbraco.Tests.PropertyEditors [Test] public void DropDownPreValueEditor_Format_Data_For_Editor() { + // editor wants ApplicationContext.Current.Services.TextService + // (that should be fixed with proper injection) + var logger = Mock.Of(); + var textService = new Mock(); + textService.Setup(x => x.Localize(It.IsAny(), It.IsAny(), It.IsAny>())).Returns("blah"); + var appContext = new ApplicationContext( + new DatabaseContext(Mock.Of(), logger, new SqlSyntaxProviders(Enumerable.Empty())), + new ServiceContext( + localizedTextService: textService.Object + ), + Mock.Of(), + new ProfilingLogger(logger, Mock.Of())) + { + IsReady = true + }; + ApplicationContext.Current = appContext; var defaultVals = new Dictionary(); var persisted = new PreValueCollection(new Dictionary diff --git a/src/Umbraco.Web/PropertyEditors/ValueListPreValueEditor.cs b/src/Umbraco.Web/PropertyEditors/ValueListPreValueEditor.cs index 41c45efb98..2c6357de3d 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueListPreValueEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueListPreValueEditor.cs @@ -46,7 +46,7 @@ namespace Umbraco.Web.PropertyEditors //It's also important to note that by default the dropdown angular controller is expecting the // config options to come in with a property called 'items' Key = "items", - Name = ApplicationContext.Current.Services.TextService.Localize("editdatatype/addPrevalue"), + Name = ApplicationContext.Current.Services.TextService.Localize("editdatatype/addPrevalue"), // todo: inject View = "multivalues" } }; From bf51be950f1cb49e09d99b12cec16970dd370877 Mon Sep 17 00:00:00 2001 From: Stephan Date: Wed, 20 Apr 2016 14:09:40 +0200 Subject: [PATCH 40/46] Fixing tests - CoreBootManager --- .../Persistence/Migrations/MigrationStartupHandlerTests.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Umbraco.Tests/Persistence/Migrations/MigrationStartupHandlerTests.cs b/src/Umbraco.Tests/Persistence/Migrations/MigrationStartupHandlerTests.cs index 3ebd40577b..0b39985028 100644 --- a/src/Umbraco.Tests/Persistence/Migrations/MigrationStartupHandlerTests.cs +++ b/src/Umbraco.Tests/Persistence/Migrations/MigrationStartupHandlerTests.cs @@ -113,6 +113,12 @@ namespace Umbraco.Tests.Persistence.Migrations private readonly string _prodName; private readonly Args _changed; + // need that one else it breaks IoC + public TestMigrationHandler() + { + _changed = new Args(); + } + public TestMigrationHandler(Args changed) { _changed = changed; From 41f6012eb38e7ebc222f91391f928219138a48d5 Mon Sep 17 00:00:00 2001 From: Stephan Date: Wed, 20 Apr 2016 15:00:52 +0200 Subject: [PATCH 41/46] Fixing tests - ServiceContext --- src/Umbraco.Core/Services/ServiceContext.cs | 398 +++--------------- .../Services/ThreadSafetyServiceTest.cs | 2 +- .../TestHelpers/BaseDatabaseFactoryTest.cs | 2 +- .../TestHelpers/BaseUmbracoApplicationTest.cs | 2 +- .../TestHelpers/ServiceContextHelper.cs | 134 ++++++ src/Umbraco.Tests/Umbraco.Tests.csproj | 1 + 6 files changed, 195 insertions(+), 344 deletions(-) create mode 100644 src/Umbraco.Tests/TestHelpers/ServiceContextHelper.cs diff --git a/src/Umbraco.Core/Services/ServiceContext.cs b/src/Umbraco.Core/Services/ServiceContext.cs index a00e7307b8..0017df01e9 100644 --- a/src/Umbraco.Core/Services/ServiceContext.cs +++ b/src/Umbraco.Core/Services/ServiceContext.cs @@ -1,55 +1,43 @@ using System; -using System.Collections.Generic; -using log4net; -using Umbraco.Core.Logging; -using System.IO; -using System.Linq; -using Umbraco.Core.IO; -using Umbraco.Core.Persistence; -using Umbraco.Core.Persistence.UnitOfWork; -using Umbraco.Core.Publishing; -using Umbraco.Core.Events; -using Umbraco.Core.Strings; namespace Umbraco.Core.Services { /// - /// The Umbraco ServiceContext, which provides access to the following services: - /// , , , - /// , and . + /// Represents the Umbraco Service context, which provides access to all services. /// public class ServiceContext { - private Lazy _migrationEntryService; - private Lazy _publicAccessService; - private Lazy _taskService; - private Lazy _domainService; - private Lazy _auditService; - private Lazy _localizedTextService; - private Lazy _tagService; - private Lazy _contentService; - private Lazy _userService; - private Lazy _memberService; - private Lazy _mediaService; - private Lazy _contentTypeService; - private Lazy _dataTypeService; - private Lazy _fileService; - private Lazy _localizationService; - private Lazy _packagingService; - private Lazy _serverRegistrationService; - private Lazy _entityService; - private Lazy _relationService; - private Lazy _treeService; - private Lazy _sectionService; - private Lazy _macroService; - private Lazy _memberTypeService; - private Lazy _memberGroupService; - private Lazy _notificationService; - private Lazy _externalLoginService; + private readonly Lazy _migrationEntryService; + private readonly Lazy _publicAccessService; + private readonly Lazy _taskService; + private readonly Lazy _domainService; + private readonly Lazy _auditService; + private readonly Lazy _localizedTextService; + private readonly Lazy _tagService; + private readonly Lazy _contentService; + private readonly Lazy _userService; + private readonly Lazy _memberService; + private readonly Lazy _mediaService; + private readonly Lazy _contentTypeService; + private readonly Lazy _dataTypeService; + private readonly Lazy _fileService; + private readonly Lazy _localizationService; + private readonly Lazy _packagingService; + private readonly Lazy _serverRegistrationService; + private readonly Lazy _entityService; + private readonly Lazy _relationService; + private readonly Lazy _treeService; + private readonly Lazy _sectionService; + private readonly Lazy _macroService; + private readonly Lazy _memberTypeService; + private readonly Lazy _memberGroupService; + private readonly Lazy _notificationService; + private readonly Lazy _externalLoginService; /// - /// Constructor used for IoC + /// Initializes a new instance of the class with lazy services. /// + /// Used by IoC. Note that LightInject will favor lazy args when picking a constructor. public ServiceContext(Lazy migrationEntryService, Lazy publicAccessService, Lazy taskService, Lazy domainService, Lazy auditService, Lazy localizedTextService, Lazy tagService, Lazy contentService, Lazy userService, Lazy memberService, Lazy mediaService, Lazy contentTypeService, Lazy dataTypeService, Lazy fileService, Lazy localizationService, Lazy packagingService, Lazy serverRegistrationService, Lazy entityService, Lazy relationService, Lazy treeService, Lazy sectionService, Lazy macroService, Lazy memberTypeService, Lazy memberGroupService, Lazy notificationService, Lazy externalLoginService) { _migrationEntryService = migrationEntryService; @@ -81,34 +69,9 @@ namespace Umbraco.Core.Services } /// - /// public ctor - will generally just be used for unit testing all items are optional and if not specified, the defaults will be used + /// Initializes a new instance of the class with services. /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// + /// Used in tests. All items are optional and remain null if not specified. public ServiceContext( IContentService contentService = null, IMediaService mediaService = null, @@ -165,378 +128,131 @@ namespace Umbraco.Core.Services if (publicAccessService != null) _publicAccessService = new Lazy(() => publicAccessService); } - /// - /// Creates a service context with a RepositoryFactory which is used to construct Services - /// - /// - /// - /// - /// - /// - /// - /// - /// - public ServiceContext( - RepositoryFactory repositoryFactory, - IDatabaseUnitOfWorkProvider dbUnitOfWorkProvider, - IUnitOfWorkProvider fileUnitOfWorkProvider, - IPublishingStrategy publishingStrategy, - CacheHelper cache, - ILogger logger, - IEventMessagesFactory eventMessagesFactory, - IEnumerable urlSegmentProviders) - { - if (repositoryFactory == null) throw new ArgumentNullException("repositoryFactory"); - if (dbUnitOfWorkProvider == null) throw new ArgumentNullException("dbUnitOfWorkProvider"); - if (fileUnitOfWorkProvider == null) throw new ArgumentNullException("fileUnitOfWorkProvider"); - if (publishingStrategy == null) throw new ArgumentNullException("publishingStrategy"); - if (cache == null) throw new ArgumentNullException("cache"); - if (logger == null) throw new ArgumentNullException("logger"); - if (eventMessagesFactory == null) throw new ArgumentNullException("eventMessagesFactory"); - - BuildServiceCache(dbUnitOfWorkProvider, fileUnitOfWorkProvider, publishingStrategy, cache, - repositoryFactory, - logger, eventMessagesFactory, - urlSegmentProviders); - } - - /// - /// Builds the various services - /// - private void BuildServiceCache( - IDatabaseUnitOfWorkProvider dbUnitOfWorkProvider, - IUnitOfWorkProvider fileUnitOfWorkProvider, - IPublishingStrategy publishingStrategy, - CacheHelper cache, - RepositoryFactory repositoryFactory, - ILogger logger, - IEventMessagesFactory eventMessagesFactory, - IEnumerable urlSegmentProviders) - { - var provider = dbUnitOfWorkProvider; - var fileProvider = fileUnitOfWorkProvider; - - if (_migrationEntryService == null) - _migrationEntryService = new Lazy(() => new MigrationEntryService(provider, repositoryFactory, logger, eventMessagesFactory)); - - if (_externalLoginService == null) - _externalLoginService = new Lazy(() => new ExternalLoginService(provider, repositoryFactory, logger, eventMessagesFactory)); - - if (_publicAccessService == null) - _publicAccessService = new Lazy(() => new PublicAccessService(provider, repositoryFactory, logger, eventMessagesFactory)); - - if (_taskService == null) - _taskService = new Lazy(() => new TaskService(provider, repositoryFactory, logger, eventMessagesFactory)); - - if (_domainService == null) - _domainService = new Lazy(() => new DomainService(provider, repositoryFactory, logger, eventMessagesFactory)); - - if (_auditService == null) - _auditService = new Lazy(() => new AuditService(provider, repositoryFactory, logger, eventMessagesFactory)); - - if (_localizedTextService == null) - { - - _localizedTextService = new Lazy(() => new LocalizedTextService( - new Lazy(() => - { - var mainLangFolder = new DirectoryInfo(IOHelper.MapPath(SystemDirectories.Umbraco + "/config/lang/")); - var appPlugins = new DirectoryInfo(IOHelper.MapPath(SystemDirectories.AppPlugins)); - var configLangFolder = new DirectoryInfo(IOHelper.MapPath(SystemDirectories.Config + "/lang/")); - - var pluginLangFolders = appPlugins.Exists == false - ? Enumerable.Empty() - : appPlugins.GetDirectories() - .SelectMany(x => x.GetDirectories("Lang")) - .SelectMany(x => x.GetFiles("*.xml", SearchOption.TopDirectoryOnly)) - .Where(x => Path.GetFileNameWithoutExtension(x.FullName).Length == 5) - .Select(x => new LocalizedTextServiceSupplementaryFileSource(x, false)); - - //user defined langs that overwrite the default, these should not be used by plugin creators - var userLangFolders = configLangFolder.Exists == false - ? Enumerable.Empty() - : configLangFolder - .GetFiles("*.user.xml", SearchOption.TopDirectoryOnly) - .Where(x => Path.GetFileNameWithoutExtension(x.FullName).Length == 10) - .Select(x => new LocalizedTextServiceSupplementaryFileSource(x, true)); - - return new LocalizedTextServiceFileSources( - logger, - cache.RuntimeCache, - mainLangFolder, - pluginLangFolders.Concat(userLangFolders)); - - }), - logger)); - } - - - if (_notificationService == null) - _notificationService = new Lazy(() => new NotificationService(provider, _userService.Value, _contentService.Value, repositoryFactory, logger)); - - if (_serverRegistrationService == null) - _serverRegistrationService = new Lazy(() => new ServerRegistrationService(provider, repositoryFactory, logger, eventMessagesFactory)); - - if (_userService == null) - _userService = new Lazy(() => new UserService(provider, repositoryFactory, logger, eventMessagesFactory)); - - if (_memberService == null) - _memberService = new Lazy(() => new MemberService(provider, repositoryFactory, logger, eventMessagesFactory, _memberGroupService.Value, _dataTypeService.Value)); - - if (_contentService == null) - _contentService = new Lazy(() => new ContentService(provider, repositoryFactory, logger, eventMessagesFactory, publishingStrategy, _dataTypeService.Value, _userService.Value, urlSegmentProviders)); - - if (_mediaService == null) - _mediaService = new Lazy(() => new MediaService(provider, repositoryFactory, logger, eventMessagesFactory, _dataTypeService.Value, _userService.Value, urlSegmentProviders)); - - if (_contentTypeService == null) - _contentTypeService = new Lazy(() => new ContentTypeService(provider, repositoryFactory, logger, eventMessagesFactory, _contentService.Value, _mediaService.Value)); - - if (_dataTypeService == null) - _dataTypeService = new Lazy(() => new DataTypeService(provider, repositoryFactory, logger, eventMessagesFactory)); - - if (_fileService == null) - _fileService = new Lazy(() => new FileService(fileProvider, provider, repositoryFactory, logger, eventMessagesFactory)); - - if (_localizationService == null) - _localizationService = new Lazy(() => new LocalizationService(provider, repositoryFactory, logger, eventMessagesFactory)); - - if (_entityService == null) - _entityService = new Lazy(() => new EntityService( - provider, repositoryFactory, logger, eventMessagesFactory, - _contentService.Value, _contentTypeService.Value, _mediaService.Value, _dataTypeService.Value, _memberService.Value, _memberTypeService.Value, - //TODO: Consider making this an isolated cache instead of using the global one - cache.RuntimeCache)); - - if (_packagingService == null) - _packagingService = new Lazy(() => new PackagingService(logger, _contentService.Value, _contentTypeService.Value, _mediaService.Value, _macroService.Value, _dataTypeService.Value, _fileService.Value, _localizationService.Value, _entityService.Value, _userService.Value, repositoryFactory, provider, urlSegmentProviders)); - - if (_relationService == null) - _relationService = new Lazy(() => new RelationService(provider, repositoryFactory, logger, eventMessagesFactory, _entityService.Value)); - - if (_treeService == null) - _treeService = new Lazy(() => new EmptyApplicationTreeService()); - - if (_sectionService == null) - _sectionService = new Lazy(() => new EmptySectionService()); - - if (_macroService == null) - _macroService = new Lazy(() => new MacroService(provider, repositoryFactory, logger, eventMessagesFactory)); - - if (_memberTypeService == null) - _memberTypeService = new Lazy(() => new MemberTypeService(provider, repositoryFactory, logger, eventMessagesFactory, _memberService.Value)); - - if (_tagService == null) - _tagService = new Lazy(() => new TagService(provider, repositoryFactory, logger, eventMessagesFactory)); - - if (_memberGroupService == null) - _memberGroupService = new Lazy(() => new MemberGroupService(provider, repositoryFactory, logger, eventMessagesFactory)); - - } - /// /// Gets the /// - public IMigrationEntryService MigrationEntryService - { - get { return _migrationEntryService.Value; } - } + public IMigrationEntryService MigrationEntryService => _migrationEntryService.Value; /// /// Gets the /// - public IPublicAccessService PublicAccessService - { - get { return _publicAccessService.Value; } - } + public IPublicAccessService PublicAccessService => _publicAccessService.Value; /// /// Gets the /// - public ITaskService TaskService - { - get { return _taskService.Value; } - } + public ITaskService TaskService => _taskService.Value; /// /// Gets the /// - public IDomainService DomainService - { - get { return _domainService.Value; } - } + public IDomainService DomainService => _domainService.Value; /// /// Gets the /// - public IAuditService AuditService - { - get { return _auditService.Value; } - } + public IAuditService AuditService => _auditService.Value; /// /// Gets the /// - public ILocalizedTextService TextService - { - get { return _localizedTextService.Value; } - } + public ILocalizedTextService TextService => _localizedTextService.Value; /// /// Gets the /// - public INotificationService NotificationService - { - get { return _notificationService.Value; } - } + public INotificationService NotificationService => _notificationService.Value; /// /// Gets the /// - public IServerRegistrationService ServerRegistrationService - { - get { return _serverRegistrationService.Value; } - } + public IServerRegistrationService ServerRegistrationService => _serverRegistrationService.Value; /// /// Gets the /// - public ITagService TagService - { - get { return _tagService.Value; } - } + public ITagService TagService => _tagService.Value; /// /// Gets the /// - public IMacroService MacroService - { - get { return _macroService.Value; } - } + public IMacroService MacroService => _macroService.Value; /// /// Gets the /// - public IEntityService EntityService - { - get { return _entityService.Value; } - } + public IEntityService EntityService => _entityService.Value; /// /// Gets the /// - public IRelationService RelationService - { - get { return _relationService.Value; } - } + public IRelationService RelationService => _relationService.Value; /// /// Gets the /// - public IContentService ContentService - { - get { return _contentService.Value; } - } + public IContentService ContentService => _contentService.Value; /// /// Gets the /// - public IContentTypeService ContentTypeService - { - get { return _contentTypeService.Value; } - } + public IContentTypeService ContentTypeService => _contentTypeService.Value; /// /// Gets the /// - public IDataTypeService DataTypeService - { - get { return _dataTypeService.Value; } - } + public IDataTypeService DataTypeService => _dataTypeService.Value; /// /// Gets the /// - public IFileService FileService - { - get { return _fileService.Value; } - } + public IFileService FileService => _fileService.Value; /// /// Gets the /// - public ILocalizationService LocalizationService - { - get { return _localizationService.Value; } - } + public ILocalizationService LocalizationService => _localizationService.Value; /// /// Gets the /// - public IMediaService MediaService - { - get { return _mediaService.Value; } - } + public IMediaService MediaService => _mediaService.Value; /// /// Gets the /// - public IPackagingService PackagingService - { - get { return _packagingService.Value; } - } + public IPackagingService PackagingService => _packagingService.Value; /// /// Gets the /// - public IUserService UserService - { - get { return _userService.Value; } - } + public IUserService UserService => _userService.Value; /// /// Gets the /// - public IMemberService MemberService - { - get { return _memberService.Value; } - } + public IMemberService MemberService => _memberService.Value; /// /// Gets the /// - public ISectionService SectionService - { - get { return _sectionService.Value; } - } + public ISectionService SectionService => _sectionService.Value; /// /// Gets the /// - public IApplicationTreeService ApplicationTreeService - { - get { return _treeService.Value; } - } + public IApplicationTreeService ApplicationTreeService => _treeService.Value; /// /// Gets the MemberTypeService /// - public IMemberTypeService MemberTypeService - { - get { return _memberTypeService.Value; } - } + public IMemberTypeService MemberTypeService => _memberTypeService.Value; /// /// Gets the MemberGroupService /// - public IMemberGroupService MemberGroupService - { - get { return _memberGroupService.Value; } - } + public IMemberGroupService MemberGroupService => _memberGroupService.Value; - public IExternalLoginService ExternalLoginService - { - get { return _externalLoginService.Value; } - } + public IExternalLoginService ExternalLoginService => _externalLoginService.Value; } } \ No newline at end of file diff --git a/src/Umbraco.Tests/Services/ThreadSafetyServiceTest.cs b/src/Umbraco.Tests/Services/ThreadSafetyServiceTest.cs index b4560b072b..e25892224c 100644 --- a/src/Umbraco.Tests/Services/ThreadSafetyServiceTest.cs +++ b/src/Umbraco.Tests/Services/ThreadSafetyServiceTest.cs @@ -49,7 +49,7 @@ namespace Umbraco.Tests.Services var repositoryFactory = new RepositoryFactory(SqlSyntax, Container); _uowProvider = new PerThreadNPocoUnitOfWorkProvider(_dbFactory); var evtMsgs = new TransientMessagesFactory(); - ApplicationContext.Services = new ServiceContext( + ApplicationContext.Services = ServiceContextHelper.GetServiceContext( repositoryFactory, _uowProvider, new FileUnitOfWorkProvider(), diff --git a/src/Umbraco.Tests/TestHelpers/BaseDatabaseFactoryTest.cs b/src/Umbraco.Tests/TestHelpers/BaseDatabaseFactoryTest.cs index abc3c0ee40..2276f73a04 100644 --- a/src/Umbraco.Tests/TestHelpers/BaseDatabaseFactoryTest.cs +++ b/src/Umbraco.Tests/TestHelpers/BaseDatabaseFactoryTest.cs @@ -97,7 +97,7 @@ namespace Umbraco.Tests.TestHelpers //assign the db context new DatabaseContext(dbFactory, Logger, SqlSyntax, "System.Data.SqlServerCe.4.0"), //assign the service context - new ServiceContext( + ServiceContextHelper.GetServiceContext( Container.GetInstance(), new NPocoUnitOfWorkProvider(dbFactory), new FileUnitOfWorkProvider(), diff --git a/src/Umbraco.Tests/TestHelpers/BaseUmbracoApplicationTest.cs b/src/Umbraco.Tests/TestHelpers/BaseUmbracoApplicationTest.cs index 24a5c5f34d..49def25762 100644 --- a/src/Umbraco.Tests/TestHelpers/BaseUmbracoApplicationTest.cs +++ b/src/Umbraco.Tests/TestHelpers/BaseUmbracoApplicationTest.cs @@ -214,7 +214,7 @@ namespace Umbraco.Tests.TestHelpers new DatabaseContext(new DefaultDatabaseFactory(Core.Configuration.GlobalSettings.UmbracoConnectionName, Logger), Logger, SqlSyntax, "System.Data.SqlServerCe.4.0"), //assign the service context - new ServiceContext( + ServiceContextHelper.GetServiceContext( Container.GetInstance(), new NPocoUnitOfWorkProvider(Logger), new FileUnitOfWorkProvider(), diff --git a/src/Umbraco.Tests/TestHelpers/ServiceContextHelper.cs b/src/Umbraco.Tests/TestHelpers/ServiceContextHelper.cs new file mode 100644 index 0000000000..5c4485871e --- /dev/null +++ b/src/Umbraco.Tests/TestHelpers/ServiceContextHelper.cs @@ -0,0 +1,134 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Umbraco.Core; +using Umbraco.Core.Events; +using Umbraco.Core.IO; +using Umbraco.Core.Logging; +using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.UnitOfWork; +using Umbraco.Core.Publishing; +using Umbraco.Core.Services; +using Umbraco.Core.Strings; +using Umbraco.Web.Services; + +namespace Umbraco.Tests.TestHelpers +{ + class ServiceContextHelper + { + public static ServiceContext GetServiceContext(RepositoryFactory repositoryFactory, + IDatabaseUnitOfWorkProvider dbUnitOfWorkProvider, + IUnitOfWorkProvider fileUnitOfWorkProvider, + IPublishingStrategy publishingStrategy, + CacheHelper cache, + ILogger logger, + IEventMessagesFactory eventMessagesFactory, + IEnumerable urlSegmentProviders) + { + if (repositoryFactory == null) throw new ArgumentNullException(nameof(repositoryFactory)); + if (dbUnitOfWorkProvider == null) throw new ArgumentNullException(nameof(dbUnitOfWorkProvider)); + if (fileUnitOfWorkProvider == null) throw new ArgumentNullException(nameof(fileUnitOfWorkProvider)); + if (publishingStrategy == null) throw new ArgumentNullException(nameof(publishingStrategy)); + if (cache == null) throw new ArgumentNullException(nameof(cache)); + if (logger == null) throw new ArgumentNullException(nameof(logger)); + if (eventMessagesFactory == null) throw new ArgumentNullException(nameof(eventMessagesFactory)); + + var provider = dbUnitOfWorkProvider; + var fileProvider = fileUnitOfWorkProvider; + + var migrationEntryService = new Lazy(() => new MigrationEntryService(provider, repositoryFactory, logger, eventMessagesFactory)); + var externalLoginService = new Lazy(() => new ExternalLoginService(provider, repositoryFactory, logger, eventMessagesFactory)); + var publicAccessService = new Lazy(() => new PublicAccessService(provider, repositoryFactory, logger, eventMessagesFactory)); + var taskService = new Lazy(() => new TaskService(provider, repositoryFactory, logger, eventMessagesFactory)); + var domainService = new Lazy(() => new DomainService(provider, repositoryFactory, logger, eventMessagesFactory)); + var auditService = new Lazy(() => new AuditService(provider, repositoryFactory, logger, eventMessagesFactory)); + + var localizedTextService = new Lazy(() => new LocalizedTextService( + new Lazy(() => + { + var mainLangFolder = new DirectoryInfo(IOHelper.MapPath(SystemDirectories.Umbraco + "/config/lang/")); + var appPlugins = new DirectoryInfo(IOHelper.MapPath(SystemDirectories.AppPlugins)); + var configLangFolder = new DirectoryInfo(IOHelper.MapPath(SystemDirectories.Config + "/lang/")); + + var pluginLangFolders = appPlugins.Exists == false + ? Enumerable.Empty() + : appPlugins.GetDirectories() + .SelectMany(x => x.GetDirectories("Lang")) + .SelectMany(x => x.GetFiles("*.xml", SearchOption.TopDirectoryOnly)) + .Where(x => Path.GetFileNameWithoutExtension(x.FullName).Length == 5) + .Select(x => new LocalizedTextServiceSupplementaryFileSource(x, false)); + + //user defined langs that overwrite the default, these should not be used by plugin creators + var userLangFolders = configLangFolder.Exists == false + ? Enumerable.Empty() + : configLangFolder + .GetFiles("*.user.xml", SearchOption.TopDirectoryOnly) + .Where(x => Path.GetFileNameWithoutExtension(x.FullName).Length == 10) + .Select(x => new LocalizedTextServiceSupplementaryFileSource(x, true)); + + return new LocalizedTextServiceFileSources( + logger, + cache.RuntimeCache, + mainLangFolder, + pluginLangFolders.Concat(userLangFolders)); + + }), + logger)); + + var userService = new Lazy(() => new UserService(provider, repositoryFactory, logger, eventMessagesFactory)); + var dataTypeService = new Lazy(() => new DataTypeService(provider, repositoryFactory, logger, eventMessagesFactory)); + var contentService = new Lazy(() => new ContentService(provider, repositoryFactory, logger, eventMessagesFactory, publishingStrategy, dataTypeService.Value, userService.Value, urlSegmentProviders)); + var notificationService = new Lazy(() => new NotificationService(provider, userService.Value, contentService.Value, repositoryFactory, logger)); + var serverRegistrationService = new Lazy(() => new ServerRegistrationService(provider, repositoryFactory, logger, eventMessagesFactory)); + var memberGroupService = new Lazy(() => new MemberGroupService(provider, repositoryFactory, logger, eventMessagesFactory)); + var memberService = new Lazy(() => new MemberService(provider, repositoryFactory, logger, eventMessagesFactory, memberGroupService.Value, dataTypeService.Value)); + var mediaService = new Lazy(() => new MediaService(provider, repositoryFactory, logger, eventMessagesFactory, dataTypeService.Value, userService.Value, urlSegmentProviders)); + var contentTypeService = new Lazy(() => new ContentTypeService(provider, repositoryFactory, logger, eventMessagesFactory, contentService.Value, mediaService.Value)); + var fileService = new Lazy(() => new FileService(fileProvider, provider, repositoryFactory, logger, eventMessagesFactory)); + var localizationService = new Lazy(() => new LocalizationService(provider, repositoryFactory, logger, eventMessagesFactory)); + + var memberTypeService = new Lazy(() => new MemberTypeService(provider, repositoryFactory, logger, eventMessagesFactory, memberService.Value)); + var entityService = new Lazy(() => new EntityService( + provider, repositoryFactory, logger, eventMessagesFactory, + contentService.Value, contentTypeService.Value, mediaService.Value, dataTypeService.Value, memberService.Value, memberTypeService.Value, + //TODO: Consider making this an isolated cache instead of using the global one + cache.RuntimeCache)); + + var macroService = new Lazy(() => new MacroService(provider, repositoryFactory, logger, eventMessagesFactory)); + var packagingService = new Lazy(() => new PackagingService(logger, contentService.Value, contentTypeService.Value, mediaService.Value, macroService.Value, dataTypeService.Value, fileService.Value, localizationService.Value, entityService.Value, userService.Value, repositoryFactory, provider, urlSegmentProviders)); + var relationService = new Lazy(() => new RelationService(provider, repositoryFactory, logger, eventMessagesFactory, entityService.Value)); + var treeService = new Lazy(() => new ApplicationTreeService(logger, cache)); + var tagService = new Lazy(() => new TagService(provider, repositoryFactory, logger, eventMessagesFactory)); + var sectionService = new Lazy(() => new SectionService(userService.Value, treeService.Value, provider, cache)); + + return new ServiceContext( + migrationEntryService, + publicAccessService, + taskService, + domainService, + auditService, + localizedTextService, + tagService, + contentService, + userService, + memberService, + mediaService, + contentTypeService, + dataTypeService, + fileService, + localizationService, + packagingService, + serverRegistrationService, + entityService, + relationService, + treeService, + sectionService, + macroService, + memberTypeService, + memberGroupService, + notificationService, + externalLoginService); + } + } +} diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index fb3e318538..4766505912 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -181,6 +181,7 @@ + From 4e702c7c79d0f0a660fdd81ecf1648376b628438 Mon Sep 17 00:00:00 2001 From: Stephan Date: Wed, 20 Apr 2016 15:09:41 +0200 Subject: [PATCH 42/46] Fixing tests - kill KegacyShortStringHelper --- .../Strings/LegacyShortStringHelper.cs | 531 ------------------ src/Umbraco.Core/Umbraco.Core.csproj | 1 - .../Routing/RenderRouteHandlerTests.cs | 2 +- .../Strings/CmsHelperCasingTests.cs | 13 - .../Strings/LegacyShortStringHelperTests.cs | 358 ------------ .../Strings/LegacyStringExtensionsTests.cs | 181 ------ src/Umbraco.Tests/Umbraco.Tests.csproj | 2 - 7 files changed, 1 insertion(+), 1087 deletions(-) delete mode 100644 src/Umbraco.Core/Strings/LegacyShortStringHelper.cs delete mode 100644 src/Umbraco.Tests/Strings/LegacyShortStringHelperTests.cs delete mode 100644 src/Umbraco.Tests/Strings/LegacyStringExtensionsTests.cs diff --git a/src/Umbraco.Core/Strings/LegacyShortStringHelper.cs b/src/Umbraco.Core/Strings/LegacyShortStringHelper.cs deleted file mode 100644 index 901c9ee299..0000000000 --- a/src/Umbraco.Core/Strings/LegacyShortStringHelper.cs +++ /dev/null @@ -1,531 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; -using System.Xml; -using Umbraco.Core.Configuration; - -namespace Umbraco.Core.Strings -{ - /// - /// Legacy implementation of string functions for short strings such as aliases or url segments. - /// - /// - /// Not necessarily optimized to work on large bodies of text. - /// Can expose surprising or bogus behavior. - /// Uses invariant culture everywhere. - /// - internal class LegacyShortStringHelper : IShortStringHelper - { - #region Ctor and vars - - /// - /// Freezes the helper so it can prevents its configuration from being modified. - /// - /// Will be called by ShortStringHelperResolver when resolution freezes. - public void Freeze() - { - // we have nothing to protect. - } - - const string UmbracoValidAliasCharacters = "_-abcdefghijklmnopqrstuvwxyz1234567890"; - const string UmbracoInvalidFirstCharacters = "0123456789"; - - #endregion - - #region Short string services JavaScript - - const string SssjsValidCharacters = "_-abcdefghijklmnopqrstuvwxyz1234567890"; - const string SssjsInvalidFirstCharacters = "0123456789"; - - private const string SssjsFormat = @" -var UMBRACO_FORCE_SAFE_ALIAS = {0}; -var UMBRACO_FORCE_SAFE_ALIAS_VALIDCHARS = '{1}'; -var UMBRACO_FORCE_SAFE_ALIAS_INVALID_FIRST_CHARS = '{2}'; - -function safeAlias(alias) {{ - if (UMBRACO_FORCE_SAFE_ALIAS) {{ - var safeAlias = ''; - var aliasLength = alias.length; - for (var i = 0; i < aliasLength; i++) {{ - currentChar = alias.substring(i, i + 1); - if (UMBRACO_FORCE_SAFE_ALIAS_VALIDCHARS.indexOf(currentChar.toLowerCase()) > -1) {{ - // check for camel (if previous character is a space, we'll upper case the current one - if (safeAlias == '' && UMBRACO_FORCE_SAFE_ALIAS_INVALID_FIRST_CHARS.indexOf(currentChar.toLowerCase()) > 0) {{ - currentChar = ''; - }} else {{ - // first char should always be lowercase (camel style) - if (safeAlias.length == 0) - currentChar = currentChar.toLowerCase(); - - if (i < aliasLength - 1 && safeAlias != '' && alias.substring(i - 1, i) == ' ') - currentChar = currentChar.toUpperCase(); - - safeAlias += currentChar; - }} - }} - }} - - alias = safeAlias; - }} - return alias; -}} - -function getSafeAlias(input, value, immediate, callback) {{ - callback(safeAlias(value)); -}} - -function validateSafeAlias(input, value, immediate, callback) {{ - callback(value == safeAlias(value)); -}} - -// legacy backward compatibility requires that one -function isValidAlias(alias) {{ - return alias == safeAlias(alias); -}} -"; - - /// - /// Gets the JavaScript code defining client-side short string services. - /// - public string GetShortStringServicesJavaScript(string controllerPath) - { - return string.Format(SssjsFormat, - UmbracoConfig.For.UmbracoSettings().Content.ForceSafeAliases ? "true" : "false", SssjsValidCharacters, SssjsInvalidFirstCharacters); - } - - #endregion - - #region IShortStringHelper CleanFor... - - /// - /// Cleans a string to produce a string that can safely be used in an alias. - /// - /// The text to filter. - /// The safe alias. - /// The string will be cleaned in the context of invariant culture. - public string CleanStringForSafeAlias(string text) - { - // ported from StringExtensions.ToSafeAlias() - - const string validAliasCharacters = UmbracoValidAliasCharacters; - const string invalidFirstCharacters = UmbracoInvalidFirstCharacters; - var safeString = new StringBuilder(); - int aliasLength = text.Length; - for (var i = 0; i < aliasLength; i++) - { - var currentChar = text.Substring(i, 1); - if (validAliasCharacters.Contains(currentChar.ToLowerInvariant())) - { - // check for camel (if previous character is a space, we'll upper case the current one - if (safeString.Length == 0 && invalidFirstCharacters.Contains(currentChar.ToLowerInvariant())) - { - //currentChar = ""; - } - else - { - if (i < aliasLength - 1 && i > 0 && text.Substring(i - 1, 1) == " ") - currentChar = currentChar.ToUpperInvariant(); - - safeString.Append(currentChar); - } - } - } - - return safeString.ToString(); - } - - /// - /// Cleans a string, in the context of the invariant culture, to produce a string that can safely be used in an alias. - /// - /// The text to filter. - /// The culture. - /// The safe alias. - /// Legacy does not support culture contexts. - public string CleanStringForSafeAlias(string text, CultureInfo culture) - { - return CleanStringForSafeAlias(text); - } - - /// - /// Cleans a string to produce a string that can safely be used in an url segment, in the context of the invariant culture. - /// - /// The text to filter. - /// The safe url segment. - public string CleanStringForUrlSegment(string text) - { - return LegacyFormatUrl(text); - } - - /// - /// Cleans a string, in the context of the invariant culture, to produce a string that can safely be used in an url segment. - /// - /// The text to filter. - /// The culture. - /// The safe url segment. - /// Legacy does not support culture contexts. - public string CleanStringForUrlSegment(string text, CultureInfo culture) - { - return CleanStringForUrlSegment(text); - } - - /// - /// Cleans a string, in the context of the invariant culture, to produce a string that can safely be used as a filename, - /// both internally (on disk) and externally (as a url). - /// - /// The text to filter. - /// The safe filename. - /// Legacy says this was used to "overcome an issue when Umbraco is used in IE in an intranet environment" but that issue is not documented. - public string CleanStringForSafeFileName(string text) - { - var filePath = text; - - // ported from Core.IO.IOHelper.SafeFileName() - - if (String.IsNullOrEmpty(filePath)) - return String.Empty; - - if (!String.IsNullOrWhiteSpace(filePath)) - { - foreach (var character in Path.GetInvalidFileNameChars()) - { - filePath = filePath.Replace(character, '-'); - } - } - else - { - filePath = String.Empty; - } - - //Break up the file in name and extension before applying the UrlReplaceCharacters - var fileNamePart = filePath.Substring(0, filePath.LastIndexOf('.')); - var ext = filePath.Substring(filePath.LastIndexOf('.')); - - //Because the file usually is downloadable as well we check characters against 'UmbracoSettings.UrlReplaceCharacters' - foreach (var n in UmbracoConfig.For.UmbracoSettings().RequestHandler.CharCollection) - { - if (n.Char.IsNullOrWhiteSpace() == false) - fileNamePart = fileNamePart.Replace(n.Char, n.Replacement); - } - - filePath = string.Concat(fileNamePart, ext); - - // Adapted from: http://stackoverflow.com/a/4827510/5018 - // Combined both Reserved Characters and Character Data - // from http://en.wikipedia.org/wiki/Percent-encoding - var stringBuilder = new StringBuilder(); - - const string reservedCharacters = "!*'();:@&=+$,/?%#[]-~{}\"<>\\^`| "; - - foreach (var character in filePath) - { - if (reservedCharacters.IndexOf(character) == -1) - stringBuilder.Append(character); - else - stringBuilder.Append("-"); - } - - // Remove repeating dashes - // From: http://stackoverflow.com/questions/5111967/regex-to-remove-a-specific-repeated-character - var reducedString = Regex.Replace(stringBuilder.ToString(), "-+", "-"); - - return reducedString; - } - - /// - /// Cleans a string, in the context of the invariant culture, to produce a string that can safely be used as a filename, - /// both internally (on disk) and externally (as a url). - /// - /// The text to filter. - /// The culture. - /// The safe filename. - /// Legacy does not support culture contexts. - public string CleanStringForSafeFileName(string text, CultureInfo culture) - { - return CleanStringForSafeFileName(text); - } - - #endregion - - #region CleanString - - // legacy does not implement these - - public string CleanString(string text, CleanStringType stringType) - { - return text; - } - - public string CleanString(string text, CleanStringType stringType, char separator) - { - return text; - } - - public string CleanString(string text, CleanStringType stringType, CultureInfo culture) - { - return text; - } - - public string CleanString(string text, CleanStringType stringType, char separator, CultureInfo culture) - { - return text; - } - - #endregion - - #region SplitPascalCasing - - /// - /// Splits a pascal-cased string by inserting a separator in between each term. - /// - /// The text to split. - /// The separator. - /// The splitted string. - /// Probably only supports Ascii strings. - public string SplitPascalCasing(string text, char separator) - { - // ported from StringExtensions.SplitPascalCasing() - - var replacement = "$1" + separator; - var result = Regex.Replace(text, "([a-z](?=[A-Z])|[A-Z](?=[A-Z][a-z]))", replacement); - return result; - } - - #endregion - - #region Legacy - - /// - /// Cleans a string to produce a string that can safely be used in an alias. - /// - /// The text to filter. - /// The safe alias. - /// The string will be cleaned in the context of invariant culture. - public string LegacyCleanStringForUmbracoAlias(string text) - { - // ported from StringExtensions.ToUmbracoAlias() - // kept here for reference, not used anymore - - if (string.IsNullOrEmpty(text)) return string.Empty; - - //convert case first - //var tmp = text.ConvertCase(caseType); - // note: always Camel anyway - var tmp = LegacyConvertStringCase(text, CleanStringType.CamelCase); - - //remove non-alphanumeric chars - var result = Regex.Replace(tmp, @"[^a-zA-Z0-9\s\.-]+", "", RegexOptions.Compiled); - - // note: spaces are always removed anyway - //if (removeSpaces) - // result = result.Replace(" ", ""); - - return result; - } - - /// - /// Filters a string to convert case, and more. - /// - /// the text to filter. - /// The string case type. - /// The filtered text. - /// - /// This is the legacy method, so we can't really change it, although it has issues (see unit tests). - /// It does more than "converting the case", and also remove spaces, etc. - /// - public string LegacyConvertStringCase(string phrase, CleanStringType cases) - { - // ported from StringExtensions.ConvertCase - - cases &= CleanStringType.CaseMask; - - var splittedPhrase = Regex.Split(phrase, @"[^a-zA-Z0-9\']", RegexOptions.Compiled); - - if (cases == CleanStringType.Unchanged) - return string.Join("", splittedPhrase); - - //var splittedPhrase = phrase.Split(' ', '-', '.'); - var sb = new StringBuilder(); - - foreach (var splittedPhraseChars in splittedPhrase.Select(s => s.ToCharArray())) - { - if (splittedPhraseChars.Length > 0) - { - splittedPhraseChars[0] = ((new String(splittedPhraseChars[0], 1)).ToUpperInvariant().ToCharArray())[0]; - } - sb.Append(new String(splittedPhraseChars)); - } - - var result = sb.ToString(); - - if (cases == CleanStringType.CamelCase) - { - if (result.Length > 1) - { - var pattern = new Regex("^([A-Z]*)([A-Z].*)$", RegexOptions.Singleline | RegexOptions.Compiled); - var match = pattern.Match(result); - if (match.Success) - { - result = match.Groups[1].Value.ToLowerInvariant() + match.Groups[2].Value; - - return result.Substring(0, 1).ToLowerInvariant() + result.Substring(1); - } - - return result; - } - - return result.ToLowerInvariant(); - } - - return result; - } - - /// - /// Converts string to a URL alias. - /// - /// The value. - /// The char replacements. - /// if set to true replace double dashes. - /// if set to true strip non ASCII. - /// if set to true URL encode. - /// - /// - /// This ensures that ONLY ascii chars are allowed and of those ascii chars, only digits and lowercase chars, all - /// punctuation, etc... are stripped out, however this method allows you to pass in string's to replace with the - /// specified replacement character before the string is converted to ascii and it has invalid characters stripped out. - /// This allows you to replace strings like & , etc.. with your replacement character before the automatic - /// reduction. - /// - public string LegacyToUrlAlias(string value, IDictionary charReplacements, bool replaceDoubleDashes, bool stripNonAscii, bool urlEncode) - { - // to lower case invariant - // replace chars one by one using charReplacements - // (opt) convert to ASCII then remove anything that's not ASCII - // trim - and _ then (opt) remove double - - // (opt) url-encode - - // charReplacement is actually *string* replacement ie it can replace " " by a non-breaking space - // so it's kind of a pre-filter actually... - // we need pre-filters, and post-filters, within each token... - // not so... we may want to replace   with a space BEFORE cutting into tokens... - - //first to lower case - value = value.ToLowerInvariant(); - - //then replacement chars - value = charReplacements.Aggregate(value, (current, kvp) => current.Replace(kvp.Key, kvp.Value)); - - //then convert to only ascii, this will remove the rest of any invalid chars - if (stripNonAscii) - { - value = Encoding.ASCII.GetString( - Encoding.Convert( - Encoding.UTF8, - Encoding.GetEncoding( - Encoding.ASCII.EncodingName, - new EncoderReplacementFallback(String.Empty), - new DecoderExceptionFallback()), - Encoding.UTF8.GetBytes(value))); - - //remove all characters that do not fall into the following categories (apart from the replacement val) - var validCodeRanges = - //digits - Enumerable.Range(48, 10).Concat( - //lowercase chars - Enumerable.Range(97, 26)); - - var sb = new StringBuilder(); - foreach (var c in value.Where(c => charReplacements.Values.Contains(c.ToString(CultureInfo.InvariantCulture)) || validCodeRanges.Contains(c))) - { - sb.Append(c); - } - - value = sb.ToString(); - } - - //trim dashes from end - value = value.Trim('-', '_'); - - //replace double occurances of - or _ - value = replaceDoubleDashes ? Regex.Replace(value, @"([-_]){2,}", "$1", RegexOptions.Compiled) : value; - - //url encode result - return urlEncode ? System.Web.HttpUtility.UrlEncode(value) : value; - } - - /// - /// Cleans a string to produce a string that can safely be used in an url segment. - /// - /// The text to filter. - /// The safe url segment. - /// - /// Uses UmbracoSettings.UrlReplaceCharacters - /// and UmbracoSettings.RemoveDoubleDashesFromUrlReplacing. - /// - public string LegacyFormatUrl(string url) - { - var newUrl = url.ToLowerInvariant(); - foreach (var n in UmbracoConfig.For.UmbracoSettings().RequestHandler.CharCollection) - { - if (n.Char != "") - newUrl = newUrl.Replace(n.Char, n.Replacement); - } - - // check for double dashes - if (UmbracoConfig.For.UmbracoSettings().RequestHandler.RemoveDoubleDashes) - { - newUrl = Regex.Replace(newUrl, @"[-]{2,}", "-"); - } - - return newUrl; - } - - #endregion - - #region ReplaceMany - - /// - /// Returns a new string in which all occurences of specified strings are replaced by other specified strings. - /// - /// The string to filter. - /// The replacements definition. - /// The filtered string. - public string ReplaceMany(string text, IDictionary replacements) - { - // Have done various tests, implementing my own "super fast" state machine to handle - // replacement of many items, or via regexes, but on short strings and not too - // many replacements (which prob. is going to be our case) nothing can beat this... - // (at least with safe and checked code -- we don't want unsafe/unchecked here) - - // Note that it will do chained-replacements ie replaced items can be replaced - // in turn by another replacement (ie the order of replacements is important) - - return replacements.Aggregate(text, (current, kvp) => current.Replace(kvp.Key, kvp.Value)); - } - - /// - /// Returns a new string in which all occurences of specified characters are replaced by a specified character. - /// - /// The string to filter. - /// The characters to replace. - /// The replacement character. - /// The filtered string. - public string ReplaceMany(string text, char[] chars, char replacement) - { - // be safe - if (text == null) - throw new ArgumentNullException("text"); - if (chars == null) - throw new ArgumentNullException("chars"); - - // see note above - - return chars.Aggregate(text, (current, c) => current.Replace(c, replacement)); - } - - #endregion - } -} diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index e699a351a5..57c9263ea7 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -1300,7 +1300,6 @@ - diff --git a/src/Umbraco.Tests/Routing/RenderRouteHandlerTests.cs b/src/Umbraco.Tests/Routing/RenderRouteHandlerTests.cs index 8faa22ae25..4b750b6894 100644 --- a/src/Umbraco.Tests/Routing/RenderRouteHandlerTests.cs +++ b/src/Umbraco.Tests/Routing/RenderRouteHandlerTests.cs @@ -46,7 +46,7 @@ namespace Umbraco.Tests.Routing UmbracoApiControllerResolver.Current = new UmbracoApiControllerResolver( new ActivatorServiceProvider(), Logger, PluginManager.Current.ResolveUmbracoApiControllers()); - ShortStringHelperResolver.Current = new ShortStringHelperResolver(new LegacyShortStringHelper()); + ShortStringHelperResolver.Current = new ShortStringHelperResolver(new DefaultShortStringHelper(SettingsForTests.GetDefault())); base.FreezeResolution(); } diff --git a/src/Umbraco.Tests/Strings/CmsHelperCasingTests.cs b/src/Umbraco.Tests/Strings/CmsHelperCasingTests.cs index 77682661b2..dc39650baa 100644 --- a/src/Umbraco.Tests/Strings/CmsHelperCasingTests.cs +++ b/src/Umbraco.Tests/Strings/CmsHelperCasingTests.cs @@ -30,19 +30,6 @@ namespace Umbraco.Tests.Strings Assert.AreEqual(expected, output); } - [TestCase("thisIsTheEnd", "This Is The End")] - [TestCase("th", "Th")] - [TestCase("t", "t")] - [TestCase("thisis", "Thisis")] - [TestCase("ThisIsTheEnd", "This Is The End")] - [TestCase("WhoIsNumber6InTheVillage", "Who Is Number6In The Village")] // we're happy to reproduce the issue - public void CompatibleLegacyReplacement(string input, string expected) - { - var helper = new LegacyShortStringHelper(); - var output = input.Length < 2 ? input : helper.SplitPascalCasing(input, ' ').ToFirstUpperInvariant(); - Assert.AreEqual(expected, output); - } - [TestCase("thisIsTheEnd", "This Is The End")] [TestCase("th", "Th")] [TestCase("t", "t")] diff --git a/src/Umbraco.Tests/Strings/LegacyShortStringHelperTests.cs b/src/Umbraco.Tests/Strings/LegacyShortStringHelperTests.cs deleted file mode 100644 index 073b7add49..0000000000 --- a/src/Umbraco.Tests/Strings/LegacyShortStringHelperTests.cs +++ /dev/null @@ -1,358 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using NUnit.Framework; -using Umbraco.Core.Strings; -using Umbraco.Tests.TestHelpers; - -namespace Umbraco.Tests.Strings -{ - [TestFixture] - public class LegacyShortStringHelperTests - { - private LegacyShortStringHelper _helper; - - [SetUp] - public void Setup() - { - var config = SettingsForTests.GetDefault(); - SettingsForTests.ConfigureSettings(config); - _helper = new LegacyShortStringHelper(); - } - - [TearDown] - public void TearDown() - { - } - - - #region Cases - [TestCase("foo", "foo")] - [TestCase(" foo ", "Foo")] - [TestCase("Foo", "Foo")] - [TestCase("FoO", "FoO")] - [TestCase("FoO bar", "FoOBar")] - [TestCase("FoO bar NIL", "FoOBarNIL")] - [TestCase("FoO 33bar 22NIL", "FoO33bar22NIL")] - [TestCase("FoO 33bar 22NI", "FoO33bar22NI")] - [TestCase("0foo", "foo")] - [TestCase("2foo bar", "fooBar")] - [TestCase("9FOO", "FOO")] - [TestCase("foo-BAR", "foo-BAR")] - [TestCase("foo-BA-dang", "foo-BA-dang")] - [TestCase("foo_BAR", "foo_BAR")] - [TestCase("foo'BAR", "fooBAR")] - [TestCase("sauté dans l'espace", "sauteDansLespace", IgnoreReason = "non-supported non-ascii chars")] - [TestCase("foo\"\"bar", "foobar")] - [TestCase("-foo-", "-foo-")] - [TestCase("_foo_", "_foo_")] - [TestCase("spécial", "special", IgnoreReason = "non-supported non-ascii chars")] - [TestCase("brô dëk ", "broDek", IgnoreReason = "non-supported non-ascii chars")] - [TestCase("1235brô dëk ", "broDek", IgnoreReason = "non-supported non-ascii chars")] - [TestCase("汉#字*/漢?字", "")] - [TestCase("aa DB cd EFG X KLMN OP qrst", "aaDBCdEFGXKLMNOPQrst")] - [TestCase("AA db cd EFG X KLMN OP qrst", "AADbCdEFGXKLMNOPQrst")] - [TestCase("AAA db cd EFG X KLMN OP qrst", "AAADbCdEFGXKLMNOPQrst")] - [TestCase("4 ways selector", "WaysSelector")] - [TestCase("WhatIfWeDoItAgain", "WhatIfWeDoItAgain")] - [TestCase("whatIfWeDoItAgain", "whatIfWeDoItAgain")] - [TestCase("WhatIfWEDOITAgain", "WhatIfWEDOITAgain")] - [TestCase("WhatIfWe doItAgain", "WhatIfWeDoItAgain")] - #endregion - public void CleanStringForSafeAlias(string input, string expected) - { - // NOTE legacy CleanStringForSafeAlias has issues w/some cases - // -> ignore test cases - // also, some aliases are strange... how can "-foo-" be a valid alias? - var output = _helper.CleanStringForSafeAlias(input); - Assert.AreEqual(expected, output); - } - - #region Cases - [TestCase("Tab 1", "tab1")] - [TestCase("Home - Page", "homePage")] - [TestCase("Home.Page", "homePage")] - [TestCase("Shannon's Document Type", "shannonsDocumentType")] // look, lowercase s and the end of shannons - [TestCase("!BADDLY nam-ed Document Type", "baddlyNamEdDocumentType")] - [TestCase("i %Want!thisTo end up In Proper@case", "iWantThisToEndUpInProperCase")] - [TestCase("Räksmörgås %%$£¤¤¤§ kéKé", "raksmorgasKeKe", IgnoreReason = "non-supported non-ascii chars")] - [TestCase("TRii", "tRii")] - [TestCase("**TRii", "tRii")] - [TestCase("trII", "trII")] - [TestCase("**trII", "trII")] - [TestCase("trIIX", "trIIX")] - [TestCase("**trIIX", "trIIX")] - #endregion - public void LegacyCleanStringForUmbracoAlias(string input, string expected) - { - // NOTE ToUmbracoAlias has issues w/non-ascii, and a few other things - // -> ignore test cases - // also all those tests should, in theory, fail because removeSpaces is false by default - var output = _helper.LegacyCleanStringForUmbracoAlias(input); - Assert.AreEqual(expected, output); - } - - #region Cases - [TestCase("Home Page", "home-page")] - [TestCase("Shannon's Home Page!", "shannons-home-page!")] - [TestCase("#Someones's Twitter $h1z%n", "someoness-twitter-$h1zn")] - [TestCase("Räksmörgås", "raeksmoergaas")] - [TestCase("'em guys-over there, are#goin' a \"little\"bit crazy eh!! :)", "em-guys-over-there,-aregoin-a-littlebit-crazy-eh!!-)")] - [TestCase("汉#字*/漢?字", "汉字star漢字")] - [TestCase("Réalösk fix bran#lo'sk", "realosk-fix-bran-lo-sk", IgnoreReason = "cannot handle it")] - #endregion - public void LegacyFormatUrl(string input, string expected) - { - // NOTE CleanStringForUrlSegment has issues with a few cases - // -> ignore test cases - // also some results are a bit strange... - var output = _helper.LegacyFormatUrl(input); - Assert.AreEqual(expected, output); - - // NOTE: not testing the overload with culture - // in legacy, they are the same - } - - #region Cases - [TestCase("Home Page", "home-page", true, true, false)] - [TestCase("Shannon's Home Page!", "shannons-home-page", true, true, false)] - [TestCase("#Someones's Twitter $h1z%n", "someoness-twitter-h1zn", true, true, false)] - [TestCase("Räksmörgås", "rksmrgs", true, true, false)] - [TestCase("'em guys-over there, are#goin' a \"little\"bit crazy eh!! :)", "em-guys-over-there-aregoin-a-littlebit-crazy-eh", true, true, false)] - [TestCase("汉#字*/漢?字", "", true, true, false)] - [TestCase("汉#字*/漢?字", "汉字漢字", true, false, false)] - [TestCase("汉#字*/漢?字", "%e6%b1%89%e5%ad%97%e6%bc%a2%e5%ad%97", true, false, true)] - [TestCase("Réalösk fix bran#lo'sk", "realosk-fix-bran-lo-sk", true, true, false, IgnoreReason = "cannot handle it")] - #endregion - public void LegacyToUrlAlias(string input, string expected, bool replaceDoubleDashes, bool stripNonAscii, bool urlEncode) - { - var replacements = new Dictionary - { - {" ", "-"}, - {"\"", ""}, - {""", ""}, - {"@", ""}, - {"%", ""}, - {".", ""}, - {";", ""}, - {"/", ""}, - {":", ""}, - {"#", ""}, - {"+", ""}, - {"*", ""}, - {"&", ""}, - {"?", ""} - }; - - // NOTE CleanStringForUrlSegment has issues with a few cases - // -> ignore test cases - // also some results are a bit strange... - var output = _helper.LegacyToUrlAlias(input, replacements, replaceDoubleDashes, stripNonAscii, urlEncode); - Assert.AreEqual(expected, output); - - // NOTE: not testing the overload with culture - // in legacy, they are the same - } - - #region Cases - [TestCase("Tab 1", "tab1", CleanStringType.CamelCase)] - [TestCase("Home - Page", "homePage", CleanStringType.CamelCase)] - [TestCase("Shannon's document type", "shannon'sDocumentType", CleanStringType.CamelCase)] - [TestCase("This is the FIRSTTIME of TheDay.", "ThisistheFIRSTTIMEofTheDay", CleanStringType.Unchanged)] - [TestCase("Sépàyô lüx.", "Sepayolux", CleanStringType.Unchanged, IgnoreReason = "non-supported non-ascii chars")] - [TestCase("This is the FIRSTTIME of TheDay.", "ThisIsTheFIRSTTIMEOfTheDay", CleanStringType.PascalCase)] - [TestCase("This is the FIRSTTIME of TheDay.", "thisIsTheFIRSTTIMEOfTheDay", CleanStringType.CamelCase)] - #endregion - public void LegacyConvertStringCase(string input, string expected, CleanStringType caseType) - { - // NOTE LegacyConvertStringCase has issues with a few cases - // -> ignore test cases - // also it removes symbols, etc... except the quote? - var output = _helper.LegacyConvertStringCase(input, caseType); - Assert.AreEqual(expected, output); - } - - #region Cases - [TestCase("ThisIsTheEndMyFriend", "This Is The End My Friend")] - [TestCase("ThisIsTHEEndMyFriend", "This Is THE End My Friend")] - [TestCase("THISIsTHEEndMyFriend", "THIS Is THE End My Friend")] - [TestCase("This33I33sThe33EndMyFriend", "This33 I33s The33 End My Friend", IgnoreReason = "fails")] - [TestCase("ThisIsTHEEndMyFriendX", "This Is THE End My Friend X")] - [TestCase("ThisIsTHEEndMyFriendXYZ", "This Is THE End My Friend XYZ")] - [TestCase("ThisIsTHEEndMyFriendXYZt", "This Is THE End My Friend XY Zt")] - [TestCase("UneÉlévationÀPartir", "Une Élévation À Partir", IgnoreReason = "non-supported non-ascii chars")] - #endregion - public void SplitPascalCasing(string input, string expected) - { - // NOTE legacy SplitPascalCasing has issues w/some cases - // -> ignore test cases - var output = _helper.SplitPascalCasing(input, ' '); - Assert.AreEqual(expected, output); - } - - #region Cases - [TestCase("foo", "foo")] - [TestCase(" foo ", "foo")] - [TestCase("Foo", "foo")] - [TestCase("FoO", "foO")] - [TestCase("FoO bar", "foOBar")] - [TestCase("FoO bar NIL", "foOBarNil")] - [TestCase("FoO 33bar 22NIL", "foO33bar22Nil")] - [TestCase("FoO 33bar 22NI", "foO33bar22NI")] - [TestCase("0foo", "foo")] - [TestCase("2foo bar", "fooBar")] - [TestCase("9FOO", "foo")] - [TestCase("foo-BAR", "fooBar")] - [TestCase("foo-BA-dang", "fooBADang")] - [TestCase("foo_BAR", "fooBar")] - [TestCase("foo'BAR", "fooBar")] - [TestCase("sauté dans l'espace", "sauteDansLEspace")] - [TestCase("foo\"\"bar", "fooBar")] - [TestCase("-foo-", "foo")] - [TestCase("_foo_", "foo")] - [TestCase("spécial", "special")] - [TestCase("brô dëk ", "broDek")] - [TestCase("1235brô dëk ", "broDek")] - [TestCase("汉#字*/漢?字", "")] - [TestCase("aa DB cd EFG X KLMN OP qrst", "aaDBCdEfgXKlmnOPQrst")] - [TestCase("AA db cd EFG X KLMN OP qrst", "aaDbCdEfgXKlmnOPQrst")] - [TestCase("AAA db cd EFG X KLMN OP qrst", "aaaDbCdEfgXKlmnOPQrst")] - #endregion - public void CleanStringToAscii(string input, string expected) - { - var output = _helper.CleanString(input, CleanStringType.Ascii | CleanStringType.CamelCase); - // legacy does nothing - Assert.AreEqual(input, output); - } - - #region Cases - [TestCase("1235brô dëK tzARlan ban123!pOo", "brodeKtzARlanban123pOo", CleanStringType.Unchanged)] - [TestCase(" 1235brô dëK tzARlan ban123!pOo ", "brodeKtzARlanban123pOo", CleanStringType.Unchanged)] - [TestCase("1235brô dëK tzARlan ban123!pOo", "BroDeKTzARLanBan123POo", CleanStringType.PascalCase)] - [TestCase("1235brô dëK tzARlan ban123!pOo", "broDeKTzARLanBan123POo", CleanStringType.CamelCase)] - [TestCase("1235brô dëK tzARlan ban123!pOo", "BRODEKTZARLANBAN123POO", CleanStringType.UpperCase)] - [TestCase("1235brô dëK tzARlan ban123!pOo", "brodektzarlanban123poo", CleanStringType.LowerCase)] - [TestCase("aa DB cd EFG X KLMN OP qrst", "aaDBCdEfgXKlmnOPQrst", CleanStringType.CamelCase)] - [TestCase("aaa DB cd EFG X KLMN OP qrst", "aaaDBCdEfgXKlmnOPQrst", CleanStringType.CamelCase)] - [TestCase("aa DB cd EFG X KLMN OP qrst", "AaDBCdEfgXKlmnOPQrst", CleanStringType.PascalCase)] - [TestCase("aaa DB cd EFG X KLMN OP qrst", "AaaDBCdEfgXKlmnOPQrst", CleanStringType.PascalCase)] - [TestCase("AA db cd EFG X KLMN OP qrst", "aaDbCdEfgXKlmnOPQrst", CleanStringType.CamelCase)] - [TestCase("AAA db cd EFG X KLMN OP qrst", "aaaDbCdEfgXKlmnOPQrst", CleanStringType.CamelCase)] - [TestCase("AA db cd EFG X KLMN OP qrst", "AADbCdEfgXKlmnOPQrst", CleanStringType.PascalCase)] - [TestCase("AAA db cd EFG X KLMN OP qrst", "AaaDbCdEfgXKlmnOPQrst", CleanStringType.PascalCase)] - [TestCase("We store some HTML in the DB for performance", "WeStoreSomeHtmlInTheDBForPerformance", CleanStringType.PascalCase)] - [TestCase("We store some HTML in the DB for performance", "weStoreSomeHtmlInTheDBForPerformance", CleanStringType.CamelCase)] - [TestCase("X is true", "XIsTrue", CleanStringType.PascalCase)] - [TestCase("X is true", "xIsTrue", CleanStringType.CamelCase)] - [TestCase("IO are slow", "IOAreSlow", CleanStringType.PascalCase)] - [TestCase("IO are slow", "ioAreSlow", CleanStringType.CamelCase)] - [TestCase("RAM is fast", "RamIsFast", CleanStringType.PascalCase)] - [TestCase("RAM is fast", "ramIsFast", CleanStringType.CamelCase)] - [TestCase("Tab 1", "tab1", CleanStringType.CamelCase)] - [TestCase("Home - Page", "homePage", CleanStringType.CamelCase)] - [TestCase("Shannon's Document Type", "shannonsDocumentType", CleanStringType.CamelCase)] - [TestCase("!BADDLY nam-ed Document Type", "baddlyNamEdDocumentType", CleanStringType.CamelCase)] - [TestCase(" !BADDLY nam-ed Document Type", "BADDLYnamedDocumentType", CleanStringType.Unchanged)] - [TestCase("!BADDLY nam-ed Document Type", "BaddlyNamEdDocumentType", CleanStringType.PascalCase)] - [TestCase("i %Want!thisTo end up In Proper@case", "IWantThisToEndUpInProperCase", CleanStringType.PascalCase)] - [TestCase("Räksmörgås %%$£¤¤¤§ kéKé", "raksmorgasKeKe", CleanStringType.CamelCase)] - [TestCase("Räksmörgås %%$£¤¤¤§ kéKé", "RaksmorgasKeKe", CleanStringType.PascalCase)] - [TestCase("Räksmörgås %%$£¤¤¤§ kéKé", "RaksmorgaskeKe", CleanStringType.Unchanged)] - [TestCase("TRii", "TRii", CleanStringType.Unchanged)] - [TestCase("**TRii", "TRii", CleanStringType.Unchanged)] - [TestCase("TRii", "trIi", CleanStringType.CamelCase)] - [TestCase("**TRii", "trIi", CleanStringType.CamelCase)] - [TestCase("TRii", "TRIi", CleanStringType.PascalCase)] - [TestCase("**TRii", "TRIi", CleanStringType.PascalCase)] - [TestCase("trII", "trII", CleanStringType.Unchanged)] - [TestCase("**trII", "trII", CleanStringType.Unchanged)] - [TestCase("trII", "trII", CleanStringType.CamelCase)] - [TestCase("**trII", "trII", CleanStringType.CamelCase)] - [TestCase("trII", "TrII", CleanStringType.PascalCase)] - [TestCase("**trII", "TrII", CleanStringType.PascalCase)] - [TestCase("trIIX", "trIix", CleanStringType.CamelCase)] - [TestCase("**trIIX", "trIix", CleanStringType.CamelCase)] - [TestCase("trIIX", "TrIix", CleanStringType.PascalCase)] - [TestCase("**trIIX", "TrIix", CleanStringType.PascalCase)] - #endregion - public void CleanStringToAsciiWithCase(string input, string expected, CleanStringType caseType) - { - var output = _helper.CleanString(input, caseType | CleanStringType.Ascii); - // legacy does nothing - Assert.AreEqual(input, output); - } - - #region Cases - [TestCase("1235brô dëK tzARlan ban123!pOo", "bro de K tz AR lan ban123 p Oo", ' ', CleanStringType.Unchanged)] - [TestCase(" 1235brô dëK tzARlan ban123!pOo ", "bro de K tz AR lan ban123 p Oo", ' ', CleanStringType.Unchanged)] - [TestCase("1235brô dëK tzARlan ban123!pOo", "Bro De K Tz AR Lan Ban123 P Oo", ' ', CleanStringType.PascalCase)] - [TestCase("1235brô dëK tzARlan ban123!pOo", "Bro De K Tz AR Lan Ban123 P Oo", ' ', CleanStringType.PascalCase)] - [TestCase("1235brô dëK tzARlan ban123!pOo", "bro De K Tz AR Lan Ban123 P Oo", ' ', CleanStringType.CamelCase)] - [TestCase("1235brô dëK tzARlan ban123!pOo", "bro-De-K-Tz-AR-Lan-Ban123-P-Oo", '-', CleanStringType.CamelCase)] - [TestCase("1235brô dëK tzARlan ban123!pOo", "BRO-DE-K-TZ-AR-LAN-BAN123-P-OO", '-', CleanStringType.UpperCase)] - [TestCase("1235brô dëK tzARlan ban123!pOo", "bro-de-k-tz-ar-lan-ban123-p-oo", '-', CleanStringType.LowerCase)] - [TestCase("Tab 1", "tab 1", ' ', CleanStringType.CamelCase)] - [TestCase("Home - Page", "home Page", ' ', CleanStringType.CamelCase)] - [TestCase("Shannon's Document Type", "shannons Document Type", ' ', CleanStringType.CamelCase)] - [TestCase("!BADDLY nam-ed Document Type", "baddly Nam Ed Document Type", ' ', CleanStringType.CamelCase)] - [TestCase(" !BADDLY nam-ed Document Type", "BADDLY nam ed Document Type", ' ', CleanStringType.Unchanged)] - [TestCase("!BADDLY nam-ed Document Type", "Baddly Nam Ed Document Type", ' ', CleanStringType.PascalCase)] - [TestCase("i %Want!thisTo end up In Proper@case", "I Want This To End Up In Proper Case", ' ', CleanStringType.PascalCase)] - [TestCase("Räksmörgås %%$£¤¤¤§ kéKé", "raksmorgas Ke Ke", ' ', CleanStringType.CamelCase)] - [TestCase("Räksmörgås %%$£¤¤¤§ kéKé", "Raksmorgas Ke Ke", ' ', CleanStringType.PascalCase)] - [TestCase("Räksmörgås %%$£¤¤¤§ kéKé", "Raksmorgas ke Ke", ' ', CleanStringType.Unchanged)] - #endregion - public void CleanStringToAsciiWithCaseAndSeparator(string input, string expected, char separator, CleanStringType caseType) - { - var output = _helper.CleanString(input, caseType | CleanStringType.Ascii, separator); - // legacy does nothing - Assert.AreEqual(input, output); - } - - [Test] // can't do cases with an IDictionary - public void ReplaceManyWithCharMap() - { - const string input = "télévisiön tzvâr ßup   pof"; - const string expected = "television tzvar ssup pof"; - IDictionary replacements = new Dictionary - { - { "é", "e" }, - { "ö", "o" }, - { "â", "a" }, - { "ß", "ss" }, - { " ", " " }, - }; - var output = _helper.ReplaceMany(input, replacements); - Assert.AreEqual(expected, output); - } - - #region Cases - [TestCase("val$id!ate|this|str'ing", "$!'", '-', "val-id-ate|this|str-ing")] - [TestCase("val$id!ate|this|str'ing", "$!'", '*', "val*id*ate|this|str*ing")] - #endregion - public void ReplaceManyByOneChar(string input, string toReplace, char replacement, string expected) - { - var output = _helper.ReplaceMany(input, toReplace.ToArray(), replacement); - Assert.AreEqual(expected, output); - } - - #region Cases - [TestCase("foo.txt", "foo.txt")] - [TestCase("foo", "foo", IgnoreReason = "fails when no extension")] - [TestCase(".txt", ".txt")] - [TestCase("nag*dog/poo:xit.txt", "nag-dog-poo-xit.txt")] - [TestCase("the dog is in the house.txt", "the-dog-is-in-the-house.txt")] - [TestCase("nil.nil.nil.txt", "nilnilnil.txt")] // because of chars map - [TestCase("taradabum", "taradabum", IgnoreReason = "fails when no extension")] - [TestCase("tara$$da:b/u - { - {" ", "-"}, - {"\"", ""}, - {""", ""}, - {"@", ""}, - {"%", ""}, - {".", ""}, - {";", ""}, - {"/", ""}, - {":", ""}, - {"#", ""}, - {"+", ""}, - {"*", ""}, - {"&", ""}, - {"?", ""} - }; - - var name1 = "Home Page"; - var name2 = "Shannon's Home Page!"; - var name3 = "#Someones's Twitter $h1z%n"; - var name4 = "Räksmörgås"; - var name5 = "'em guys-over there, are#goin' a \"little\"bit crazy eh!! :)"; - var name6 = "汉#字*/漢?字"; - - var url1 = name1.ToUrlSegment(); - var url2 = name2.ToUrlSegment(); - var url3 = name3.ToUrlSegment(); - var url4 = name4.ToUrlSegment(); - var url5 = name5.ToUrlSegment(); - var url6 = name6.ToUrlSegment(); - var url7 = name6.ToUrlSegment(); - var url8 = name6.ToUrlSegment(); - - Assert.AreEqual("home-page", url1); - Assert.AreEqual("shannons-home-page", url2); - Assert.AreEqual("someoness-twitter-h1zn", url3); - Assert.AreEqual("rksmrgs", url4); - Assert.AreEqual("em-guys-over-there-aregoin-a-littlebit-crazy-eh", url5); - Assert.AreEqual("", url6); - Assert.AreEqual("汉字漢字", url7); - Assert.AreEqual("%e6%b1%89%e5%ad%97%e6%bc%a2%e5%ad%97", url8); - - } - - [TestCase] - public void StringExtensions_To_Camel_Case() - { - //Arrange - - var name1 = "Tab 1"; - var name2 = "Home - Page"; - var name3 = "Shannon's document type"; - - //Act - - var camelCase1 = name1.ToCleanString(CleanStringType.CamelCase); - var camelCase2 = name2.ToCleanString(CleanStringType.CamelCase); - var camelCase3 = name3.ToCleanString(CleanStringType.CamelCase); - - //Assert - - Assert.AreEqual("tab1", camelCase1); - Assert.AreEqual("homePage", camelCase2); - Assert.AreEqual("shannon'sDocumentType", camelCase3); - } - - [TestCase] - public void StringExtensions_To_Entity_Alias() - { - //Arrange - - var name1 = "Tab 1"; - var name2 = "Home - Page"; - var name3 = "Shannon's Document Type"; - var name4 = "!BADDLY nam-ed Document Type"; - var name5 = "i %Want!thisTo end up In Proper@case"; - - //Act - - var alias1 = name1.ToSafeAlias(); - var alias2 = name2.ToSafeAlias(); - var alias3 = name3.ToSafeAlias(); - var alias4 = name4.ToSafeAlias(); - var alias5 = name5.ToSafeAlias(/*StringAliasCaseType.PascalCase*/); - - //Assert - - Assert.AreEqual("tab1", alias1); - Assert.AreEqual("homePage", alias2); - Assert.AreEqual("shannonsDocumentType", alias3); - Assert.AreEqual("baddlyNamEdDocumentType", alias4); - - // disable: does not support PascalCase anymore - //Assert.AreEqual("IWantThisToEndUpInProperCase", alias5); - } - - } -} diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 4766505912..e04d17d1c8 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -350,9 +350,7 @@ - - From 07b410459f123f206ff78be8e9f1a8eae55722a4 Mon Sep 17 00:00:00 2001 From: Stephan Date: Wed, 20 Apr 2016 17:40:23 +0200 Subject: [PATCH 43/46] Fixing tests - file systems --- .../Services/Importing/PackageImportTests.cs | 5 +++ .../TestHelpers/BaseUmbracoApplicationTest.cs | 35 +++++++++++-------- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/src/Umbraco.Tests/Services/Importing/PackageImportTests.cs b/src/Umbraco.Tests/Services/Importing/PackageImportTests.cs index f506933631..c69efed5d8 100644 --- a/src/Umbraco.Tests/Services/Importing/PackageImportTests.cs +++ b/src/Umbraco.Tests/Services/Importing/PackageImportTests.cs @@ -147,15 +147,20 @@ namespace Umbraco.Tests.Services.Importing var xml = XElement.Parse(strXml); var element = xml.Descendants("Templates").First(); var packagingService = ServiceContext.PackagingService; + var init = ServiceContext.FileService.GetTemplates().Count(); // Act var templates = packagingService.ImportTemplates(element); var numberOfTemplates = (from doc in element.Elements("Template") select doc).Count(); + var allTemplates = ServiceContext.FileService.GetTemplates(); // Assert Assert.That(templates, Is.Not.Null); Assert.That(templates.Any(), Is.True); Assert.That(templates.Count(), Is.EqualTo(numberOfTemplates)); + + Assert.AreEqual(init + numberOfTemplates, allTemplates.Count()); + Assert.IsTrue(allTemplates.All(x => x.Content.Contains("UmbracoTemplatePage"))); } [Test] diff --git a/src/Umbraco.Tests/TestHelpers/BaseUmbracoApplicationTest.cs b/src/Umbraco.Tests/TestHelpers/BaseUmbracoApplicationTest.cs index 49def25762..8db672f59c 100644 --- a/src/Umbraco.Tests/TestHelpers/BaseUmbracoApplicationTest.cs +++ b/src/Umbraco.Tests/TestHelpers/BaseUmbracoApplicationTest.cs @@ -70,7 +70,7 @@ namespace Umbraco.Tests.TestHelpers SetupApplicationContext(); - InitializeMappers(); + InitializeMappers(); FreezeResolution(); @@ -80,7 +80,7 @@ namespace Umbraco.Tests.TestHelpers public override void TearDown() { base.TearDown(); - + //reset settings SettingsForTests.Reset(); UmbracoContext.Current = null; @@ -97,9 +97,10 @@ namespace Umbraco.Tests.TestHelpers protected virtual void ConfigureContainer() { + // oh no! should not use a container in unit tests? var settings = SettingsForTests.GetDefault(); - //register mappers + //register mappers Container.RegisterFrom(); Container.RegisterFrom(); @@ -109,7 +110,7 @@ namespace Umbraco.Tests.TestHelpers //Default Datalayer/Repositories/SQL/Database/etc... Container.RegisterFrom(); - //register basic stuff that might need to be there for some container resolvers to work, we can + //register basic stuff that might need to be there for some container resolvers to work, we can // add more to this in base classes in resolution freezing Container.RegisterSingleton(factory => Logger); Container.Register(factory => CacheHelper); @@ -127,8 +128,12 @@ namespace Umbraco.Tests.TestHelpers Container.RegisterSingleton(factory => Mock.Of(), "PartialViewFileSystem"); Container.RegisterSingleton(factory => Mock.Of(), "PartialViewMacroFileSystem"); Container.RegisterSingleton(factory => Mock.Of(), "StylesheetFileSystem"); - Container.RegisterSingleton(factory => Mock.Of(), "MasterpageFileSystem"); - Container.RegisterSingleton(factory => Mock.Of(), "ViewFileSystem"); + + // need real file systems here as templates content is on-disk only + //Container.RegisterSingleton(factory => Mock.Of(), "MasterpageFileSystem"); + //Container.RegisterSingleton(factory => Mock.Of(), "ViewFileSystem"); + Container.RegisterSingleton(factory => new PhysicalFileSystem("Views", "/views"), "ViewFileSystem"); + Container.RegisterSingleton(factory => new PhysicalFileSystem("MasterPages", "/masterpages"), "MasterpageFileSystem"); } private static readonly object Locker = new object(); @@ -173,7 +178,7 @@ namespace Umbraco.Tests.TestHelpers } /// - /// By default this returns false which means the plugin manager will not be reset so it doesn't need to re-scan + /// By default this returns false which means the plugin manager will not be reset so it doesn't need to re-scan /// all of the assemblies. Inheritors can override this if plugin manager resetting is required, generally needs /// to be set to true if the SetupPluginManager has been overridden. /// @@ -207,7 +212,7 @@ namespace Umbraco.Tests.TestHelpers /// Inheritors can override this if they wish to create a custom application context /// protected virtual void SetupApplicationContext() - { + { var evtMsgs = new TransientMessagesFactory(); ApplicationContext.Current = new ApplicationContext( //assign the db context @@ -215,13 +220,13 @@ namespace Umbraco.Tests.TestHelpers Logger, SqlSyntax, "System.Data.SqlServerCe.4.0"), //assign the service context ServiceContextHelper.GetServiceContext( - Container.GetInstance(), - new NPocoUnitOfWorkProvider(Logger), + Container.GetInstance(), + new NPocoUnitOfWorkProvider(Logger), new FileUnitOfWorkProvider(), - new PublishingStrategy(evtMsgs, Logger), - CacheHelper, - Logger, - evtMsgs, + new PublishingStrategy(evtMsgs, Logger), + CacheHelper, + Logger, + evtMsgs, Enumerable.Empty()), CacheHelper, ProfilingLogger) @@ -273,7 +278,7 @@ namespace Umbraco.Tests.TestHelpers protected ProfilingLogger ProfilingLogger { get; private set; } protected CacheHelper CacheHelper { get; private set; } - //I know tests shouldn't use IoC, but for all these tests inheriting from this class are integration tests + //I know tests shouldn't use IoC, but for all these tests inheriting from this class are integration tests // and the number of these will hopefully start getting greatly reduced now that most things are mockable. internal IServiceContainer Container { get; private set; } From 23817cdce520da60269b71aa844dab00611058f5 Mon Sep 17 00:00:00 2001 From: Stephan Date: Wed, 20 Apr 2016 22:51:01 +0200 Subject: [PATCH 44/46] Fixing tests - MigrationStartupHandler is IDisposable --- .../MigrationStartupHandlerTests.cs | 2 +- .../ClearCsrfCookiesAfterUpgrade.cs | 2 +- ...ediaXmlCacheForDeletedItemsAfterUpgrade.cs | 2 +- .../EnsureListViewDataTypeIsCreated.cs | 2 +- ...upHander.cs => MigrationStartupHandler.cs} | 25 +++++++++++++++++-- .../OverwriteStylesheetFilesFromTempFiles.cs | 2 +- .../PublishAfterUpgradeToVersionSixth.cs | 2 +- .../RebuildMediaXmlCacheAfterUpgrade.cs | 2 +- src/Umbraco.Web/Umbraco.Web.csproj | 4 +-- 9 files changed, 32 insertions(+), 11 deletions(-) rename src/Umbraco.Web/Strategies/Migrations/{MigrationStartupHander.cs => MigrationStartupHandler.cs} (78%) diff --git a/src/Umbraco.Tests/Persistence/Migrations/MigrationStartupHandlerTests.cs b/src/Umbraco.Tests/Persistence/Migrations/MigrationStartupHandlerTests.cs index 0b39985028..13bfb7f7af 100644 --- a/src/Umbraco.Tests/Persistence/Migrations/MigrationStartupHandlerTests.cs +++ b/src/Umbraco.Tests/Persistence/Migrations/MigrationStartupHandlerTests.cs @@ -108,7 +108,7 @@ namespace Umbraco.Tests.Persistence.Migrations public int CountExecuted { get; set; } } - public class TestMigrationHandler : MigrationStartupHander + public class TestMigrationHandler : MigrationStartupHandler { private readonly string _prodName; private readonly Args _changed; diff --git a/src/Umbraco.Web/Strategies/Migrations/ClearCsrfCookiesAfterUpgrade.cs b/src/Umbraco.Web/Strategies/Migrations/ClearCsrfCookiesAfterUpgrade.cs index 24b706aee9..f40490c2d1 100644 --- a/src/Umbraco.Web/Strategies/Migrations/ClearCsrfCookiesAfterUpgrade.cs +++ b/src/Umbraco.Web/Strategies/Migrations/ClearCsrfCookiesAfterUpgrade.cs @@ -10,7 +10,7 @@ namespace Umbraco.Web.Strategies.Migrations /// /// After upgrade we clear out the csrf tokens /// - public class ClearCsrfCookiesAfterUpgrade : MigrationStartupHander + public class ClearCsrfCookiesAfterUpgrade : MigrationStartupHandler { protected override void AfterMigration(MigrationRunner sender, MigrationEventArgs e) { diff --git a/src/Umbraco.Web/Strategies/Migrations/ClearMediaXmlCacheForDeletedItemsAfterUpgrade.cs b/src/Umbraco.Web/Strategies/Migrations/ClearMediaXmlCacheForDeletedItemsAfterUpgrade.cs index 6e0580a2c4..dcf56fd03f 100644 --- a/src/Umbraco.Web/Strategies/Migrations/ClearMediaXmlCacheForDeletedItemsAfterUpgrade.cs +++ b/src/Umbraco.Web/Strategies/Migrations/ClearMediaXmlCacheForDeletedItemsAfterUpgrade.cs @@ -16,7 +16,7 @@ namespace Umbraco.Web.Strategies.Migrations /// /// * If current is less than or equal to 7.0.0 /// - public class ClearMediaXmlCacheForDeletedItemsAfterUpgrade : MigrationStartupHander + public class ClearMediaXmlCacheForDeletedItemsAfterUpgrade : MigrationStartupHandler { private readonly ISqlSyntaxProvider _sqlSyntax; diff --git a/src/Umbraco.Web/Strategies/Migrations/EnsureListViewDataTypeIsCreated.cs b/src/Umbraco.Web/Strategies/Migrations/EnsureListViewDataTypeIsCreated.cs index 92325ecaa7..706ffe181d 100644 --- a/src/Umbraco.Web/Strategies/Migrations/EnsureListViewDataTypeIsCreated.cs +++ b/src/Umbraco.Web/Strategies/Migrations/EnsureListViewDataTypeIsCreated.cs @@ -17,7 +17,7 @@ namespace Umbraco.Web.Strategies.Migrations /// /// Creates the built in list view data types /// - public class EnsureDefaultListViewDataTypesCreated : MigrationStartupHander + public class EnsureDefaultListViewDataTypesCreated : MigrationStartupHandler { private readonly ISqlSyntaxProvider _sqlSyntax; diff --git a/src/Umbraco.Web/Strategies/Migrations/MigrationStartupHander.cs b/src/Umbraco.Web/Strategies/Migrations/MigrationStartupHandler.cs similarity index 78% rename from src/Umbraco.Web/Strategies/Migrations/MigrationStartupHander.cs rename to src/Umbraco.Web/Strategies/Migrations/MigrationStartupHandler.cs index a350911580..3b40bbdf5d 100644 --- a/src/Umbraco.Web/Strategies/Migrations/MigrationStartupHander.cs +++ b/src/Umbraco.Web/Strategies/Migrations/MigrationStartupHandler.cs @@ -1,4 +1,5 @@ -using System.Linq; +using System; +using System.Linq; using Umbraco.Core; using Umbraco.Core.Persistence.Migrations; @@ -7,8 +8,10 @@ namespace Umbraco.Web.Strategies.Migrations /// /// Base class that can be used to run code after the migration runner has executed /// - public abstract class MigrationStartupHander : ApplicationEventHandler + public abstract class MigrationStartupHandler : ApplicationEventHandler, IDisposable { + private bool _disposed; + /// /// Ensure this is run when not configured /// @@ -57,5 +60,23 @@ namespace Umbraco.Web.Strategies.Migrations /// Leaving empty will run for all migration products /// public virtual string[] TargetProductNames { get { return new string[] {}; } } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + ~MigrationStartupHandler() + { + Dispose(false); + } + + protected virtual void Dispose(bool disposing) + { + if (_disposed) return; + _disposed = true; + Unsubscribe(); + } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Strategies/Migrations/OverwriteStylesheetFilesFromTempFiles.cs b/src/Umbraco.Web/Strategies/Migrations/OverwriteStylesheetFilesFromTempFiles.cs index 9a8b6a6044..d8e5ded98f 100644 --- a/src/Umbraco.Web/Strategies/Migrations/OverwriteStylesheetFilesFromTempFiles.cs +++ b/src/Umbraco.Web/Strategies/Migrations/OverwriteStylesheetFilesFromTempFiles.cs @@ -19,7 +19,7 @@ namespace Umbraco.Web.Strategies.Migrations /// files during the migration since other parts of the migration might fail. So once the migration is complete, we'll then copy over the temp /// files that this migration created over top of the developer's files. We'll also create a backup of their files. ///
- public sealed class OverwriteStylesheetFilesFromTempFiles : MigrationStartupHander + public sealed class OverwriteStylesheetFilesFromTempFiles : MigrationStartupHandler { protected override void AfterMigration(MigrationRunner sender, MigrationEventArgs e) { diff --git a/src/Umbraco.Web/Strategies/Migrations/PublishAfterUpgradeToVersionSixth.cs b/src/Umbraco.Web/Strategies/Migrations/PublishAfterUpgradeToVersionSixth.cs index 66958a2c9d..125b8f7a86 100644 --- a/src/Umbraco.Web/Strategies/Migrations/PublishAfterUpgradeToVersionSixth.cs +++ b/src/Umbraco.Web/Strategies/Migrations/PublishAfterUpgradeToVersionSixth.cs @@ -17,7 +17,7 @@ namespace Umbraco.Web.Strategies.Migrations /// This event ensures that upgrades from (configured) versions lower then 6.0.0 /// have their publish state updated after the database schema has been migrated. ///
- public class PublishAfterUpgradeToVersionSixth : MigrationStartupHander + public class PublishAfterUpgradeToVersionSixth : MigrationStartupHandler { private readonly ISqlSyntaxProvider _sqlSyntax; diff --git a/src/Umbraco.Web/Strategies/Migrations/RebuildMediaXmlCacheAfterUpgrade.cs b/src/Umbraco.Web/Strategies/Migrations/RebuildMediaXmlCacheAfterUpgrade.cs index 2af3c402aa..63c8d5db16 100644 --- a/src/Umbraco.Web/Strategies/Migrations/RebuildMediaXmlCacheAfterUpgrade.cs +++ b/src/Umbraco.Web/Strategies/Migrations/RebuildMediaXmlCacheAfterUpgrade.cs @@ -17,7 +17,7 @@ namespace Umbraco.Web.Strategies.Migrations /// /// * If current is less than or equal to 7.0.0 /// - public class RebuildMediaXmlCacheAfterUpgrade : MigrationStartupHander + public class RebuildMediaXmlCacheAfterUpgrade : MigrationStartupHandler { protected override void AfterMigration(MigrationRunner sender, MigrationEventArgs e) { diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index c381646598..9109efab24 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -697,7 +697,7 @@ - + @@ -1108,7 +1108,7 @@ ASPXCodeBehind - + From 55ae0d3af6d263c5ae6a5f798058c3eae7e828a8 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 21 Apr 2016 11:38:43 +0200 Subject: [PATCH 45/46] Ensure the correct services are injected in to property editors so they are not using singletons --- .../PropertyEditors/ColorListValidatorTest.cs | 7 ++++--- .../EnsureUniqueValuesValidatorTest.cs | 11 ++++++----- .../PropertyEditors/MultiValuePropertyEditorTests.cs | 2 +- .../PropertyEditors/CheckBoxListPropertyEditor.cs | 8 ++++++-- .../PropertyEditors/ColorListPreValueEditor.cs | 3 ++- .../PropertyEditors/ColorPickerPropertyEditor.cs | 8 ++++++-- .../PropertyEditors/DropDownMultiplePropertyEditor.cs | 3 ++- .../DropDownMultipleWithKeysPropertyEditor.cs | 10 +++++++--- .../PropertyEditors/DropDownPropertyEditor.cs | 3 ++- .../PropertyEditors/DropDownWithKeysPropertyEditor.cs | 8 ++++++-- .../PropertyEditors/FileUploadPropertyEditor.cs | 11 +++++++---- .../PropertyEditors/RadioButtonsPropertyEditor.cs | 3 ++- .../PropertyEditors/ValueListPreValueEditor.cs | 6 ++++-- 13 files changed, 55 insertions(+), 28 deletions(-) diff --git a/src/Umbraco.Tests/PropertyEditors/ColorListValidatorTest.cs b/src/Umbraco.Tests/PropertyEditors/ColorListValidatorTest.cs index 900b8399e7..a6ef28a40e 100644 --- a/src/Umbraco.Tests/PropertyEditors/ColorListValidatorTest.cs +++ b/src/Umbraco.Tests/PropertyEditors/ColorListValidatorTest.cs @@ -3,6 +3,7 @@ using Moq; using NUnit.Framework; using Newtonsoft.Json.Linq; using Umbraco.Core.Logging; +using Umbraco.Core.Services; using Umbraco.Web.PropertyEditors; namespace Umbraco.Tests.PropertyEditors @@ -14,7 +15,7 @@ namespace Umbraco.Tests.PropertyEditors public void Only_Tests_On_JArray() { var validator = new ColorListPreValueEditor.ColorListValidator(); - var result = validator.Validate("hello", null, new ColorPickerPropertyEditor(Mock.Of())); + var result = validator.Validate("hello", null, new ColorPickerPropertyEditor(Mock.Of(), Mock.Of())); Assert.AreEqual(0, result.Count()); } @@ -22,7 +23,7 @@ namespace Umbraco.Tests.PropertyEditors public void Only_Tests_On_JArray_Of_Item_JObject() { var validator = new ColorListPreValueEditor.ColorListValidator(); - var result = validator.Validate(new JArray("hello", "world"), null, new ColorPickerPropertyEditor(Mock.Of())); + var result = validator.Validate(new JArray("hello", "world"), null, new ColorPickerPropertyEditor(Mock.Of(), Mock.Of())); Assert.AreEqual(0, result.Count()); } @@ -35,7 +36,7 @@ namespace Umbraco.Tests.PropertyEditors JObject.FromObject(new { value = "zxcvzxcvxzcv" }), JObject.FromObject(new { value = "ABC" }), JObject.FromObject(new { value = "1234567" })), - null, new ColorPickerPropertyEditor(Mock.Of())); + null, new ColorPickerPropertyEditor(Mock.Of(), Mock.Of())); Assert.AreEqual(2, result.Count()); } } diff --git a/src/Umbraco.Tests/PropertyEditors/EnsureUniqueValuesValidatorTest.cs b/src/Umbraco.Tests/PropertyEditors/EnsureUniqueValuesValidatorTest.cs index c24aa65846..bd50d92a94 100644 --- a/src/Umbraco.Tests/PropertyEditors/EnsureUniqueValuesValidatorTest.cs +++ b/src/Umbraco.Tests/PropertyEditors/EnsureUniqueValuesValidatorTest.cs @@ -3,6 +3,7 @@ using Moq; using NUnit.Framework; using Newtonsoft.Json.Linq; using Umbraco.Core.Logging; +using Umbraco.Core.Services; using Umbraco.Web.PropertyEditors; namespace Umbraco.Tests.PropertyEditors @@ -14,7 +15,7 @@ namespace Umbraco.Tests.PropertyEditors public void Only_Tests_On_JArray() { var validator = new ValueListPreValueEditor.EnsureUniqueValuesValidator(); - var result = validator.Validate("hello", null, new ColorPickerPropertyEditor(Mock.Of())); + var result = validator.Validate("hello", null, new ColorPickerPropertyEditor(Mock.Of(), Mock.Of())); Assert.AreEqual(0, result.Count()); } @@ -22,7 +23,7 @@ namespace Umbraco.Tests.PropertyEditors public void Only_Tests_On_JArray_Of_Item_JObject() { var validator = new ValueListPreValueEditor.EnsureUniqueValuesValidator(); - var result = validator.Validate(new JArray("hello", "world"), null, new ColorPickerPropertyEditor(Mock.Of())); + var result = validator.Validate(new JArray("hello", "world"), null, new ColorPickerPropertyEditor(Mock.Of(), Mock.Of())); Assert.AreEqual(0, result.Count()); } @@ -30,7 +31,7 @@ namespace Umbraco.Tests.PropertyEditors public void Allows_Unique_Values() { var validator = new ValueListPreValueEditor.EnsureUniqueValuesValidator(); - var result = validator.Validate(new JArray(JObject.FromObject(new { value = "hello" }), JObject.FromObject(new { value = "world" })), null, new ColorPickerPropertyEditor(Mock.Of())); + var result = validator.Validate(new JArray(JObject.FromObject(new { value = "hello" }), JObject.FromObject(new { value = "world" })), null, new ColorPickerPropertyEditor(Mock.Of(), Mock.Of())); Assert.AreEqual(0, result.Count()); } @@ -39,7 +40,7 @@ namespace Umbraco.Tests.PropertyEditors { var validator = new ValueListPreValueEditor.EnsureUniqueValuesValidator(); var result = validator.Validate(new JArray(JObject.FromObject(new { value = "hello" }), JObject.FromObject(new { value = "hello" })), - null, new ColorPickerPropertyEditor(Mock.Of())); + null, new ColorPickerPropertyEditor(Mock.Of(), Mock.Of())); Assert.AreEqual(1, result.Count()); } @@ -52,7 +53,7 @@ namespace Umbraco.Tests.PropertyEditors JObject.FromObject(new { value = "hello" }), JObject.FromObject(new { value = "world" }), JObject.FromObject(new { value = "world" })), - null, new ColorPickerPropertyEditor(Mock.Of())); + null, new ColorPickerPropertyEditor(Mock.Of(), Mock.Of())); Assert.AreEqual(2, result.Count()); } } diff --git a/src/Umbraco.Tests/PropertyEditors/MultiValuePropertyEditorTests.cs b/src/Umbraco.Tests/PropertyEditors/MultiValuePropertyEditorTests.cs index 0ba844ca6b..6fe7259b92 100644 --- a/src/Umbraco.Tests/PropertyEditors/MultiValuePropertyEditorTests.cs +++ b/src/Umbraco.Tests/PropertyEditors/MultiValuePropertyEditorTests.cs @@ -124,7 +124,7 @@ namespace Umbraco.Tests.PropertyEditors {"item3", new PreValue(3, "Item 3")} }); - var editor = new ValueListPreValueEditor(); + var editor = new ValueListPreValueEditor(Mock.Of()); var result = editor.ConvertDbToEditor(defaultVals, persisted); diff --git a/src/Umbraco.Web/PropertyEditors/CheckBoxListPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/CheckBoxListPropertyEditor.cs index 2cebd665cd..e1b72f0ad4 100644 --- a/src/Umbraco.Web/PropertyEditors/CheckBoxListPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/CheckBoxListPropertyEditor.cs @@ -1,6 +1,7 @@ using Umbraco.Core; using Umbraco.Core.Logging; using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Services; namespace Umbraco.Web.PropertyEditors { @@ -15,11 +16,14 @@ namespace Umbraco.Web.PropertyEditors [PropertyEditor(Constants.PropertyEditors.CheckBoxListAlias, "Checkbox list", "checkboxlist", Icon="icon-bulleted-list", Group="lists")] public class CheckBoxListPropertyEditor : PropertyEditor { + private readonly ILocalizedTextService _textService; + /// /// The constructor will setup the property editor based on the attribute if one is found /// - public CheckBoxListPropertyEditor(ILogger logger) : base(logger) + public CheckBoxListPropertyEditor(ILogger logger, ILocalizedTextService textService) : base(logger) { + _textService = textService; } /// @@ -31,7 +35,7 @@ namespace Umbraco.Web.PropertyEditors /// protected override PreValueEditor CreatePreValueEditor() { - return new ValueListPreValueEditor(); + return new ValueListPreValueEditor(_textService); } /// diff --git a/src/Umbraco.Web/PropertyEditors/ColorListPreValueEditor.cs b/src/Umbraco.Web/PropertyEditors/ColorListPreValueEditor.cs index e7750c4c65..e9e721c108 100644 --- a/src/Umbraco.Web/PropertyEditors/ColorListPreValueEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/ColorListPreValueEditor.cs @@ -6,13 +6,14 @@ using Newtonsoft.Json.Linq; using Umbraco.Core; using Umbraco.Core.Models; using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Services; namespace Umbraco.Web.PropertyEditors { internal class ColorListPreValueEditor : ValueListPreValueEditor { - public ColorListPreValueEditor() + public ColorListPreValueEditor(ILocalizedTextService textService) : base(textService) { var field = Fields.First(); diff --git a/src/Umbraco.Web/PropertyEditors/ColorPickerPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/ColorPickerPropertyEditor.cs index 95d2c5f07e..3d2beb64fb 100644 --- a/src/Umbraco.Web/PropertyEditors/ColorPickerPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/ColorPickerPropertyEditor.cs @@ -1,17 +1,21 @@ using Umbraco.Core; using Umbraco.Core.Logging; using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Services; namespace Umbraco.Web.PropertyEditors { [PropertyEditor(Constants.PropertyEditors.ColorPickerAlias, "Color Picker", "colorpicker", Icon="icon-colorpicker", Group="Pickers")] public class ColorPickerPropertyEditor : PropertyEditor { + private readonly ILocalizedTextService _textService; + /// /// The constructor will setup the property editor based on the attribute if one is found /// - public ColorPickerPropertyEditor(ILogger logger) : base(logger) + public ColorPickerPropertyEditor(ILogger logger, ILocalizedTextService textService) : base(logger) { + _textService = textService; } /// @@ -23,7 +27,7 @@ namespace Umbraco.Web.PropertyEditors /// protected override PreValueEditor CreatePreValueEditor() { - return new ColorListPreValueEditor(); + return new ColorListPreValueEditor(_textService); } } diff --git a/src/Umbraco.Web/PropertyEditors/DropDownMultiplePropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/DropDownMultiplePropertyEditor.cs index e7cf1bb87b..2728f5c86b 100644 --- a/src/Umbraco.Web/PropertyEditors/DropDownMultiplePropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/DropDownMultiplePropertyEditor.cs @@ -2,6 +2,7 @@ using Newtonsoft.Json; using Umbraco.Core; using Umbraco.Core.Logging; using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Services; namespace Umbraco.Web.PropertyEditors { @@ -21,7 +22,7 @@ namespace Umbraco.Web.PropertyEditors /// /// The constructor will setup the property editor based on the attribute if one is found /// - public DropDownMultiplePropertyEditor(ILogger logger) : base(logger) + public DropDownMultiplePropertyEditor(ILogger logger, ILocalizedTextService textService) : base(logger, textService) { } diff --git a/src/Umbraco.Web/PropertyEditors/DropDownMultipleWithKeysPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/DropDownMultipleWithKeysPropertyEditor.cs index 9540c4fdc5..f16d9872ec 100644 --- a/src/Umbraco.Web/PropertyEditors/DropDownMultipleWithKeysPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/DropDownMultipleWithKeysPropertyEditor.cs @@ -3,6 +3,7 @@ using Umbraco.Core; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Services; namespace Umbraco.Web.PropertyEditors { @@ -16,11 +17,14 @@ namespace Umbraco.Web.PropertyEditors [PropertyEditor(Constants.PropertyEditors.DropdownlistMultiplePublishKeysAlias, "Dropdown list multiple, publish keys", "dropdown", Group = "lists", Icon = "icon-bulleted-list")] public class DropDownMultipleWithKeysPropertyEditor : DropDownPropertyEditor { + private readonly ILocalizedTextService _textService; + /// /// The constructor will setup the property editor based on the attribute if one is found /// - public DropDownMultipleWithKeysPropertyEditor(ILogger logger) : base(logger) + public DropDownMultipleWithKeysPropertyEditor(ILogger logger, ILocalizedTextService textService) : base(logger, textService) { + _textService = textService; } protected override PropertyValueEditor CreateValueEditor() @@ -30,7 +34,7 @@ namespace Umbraco.Web.PropertyEditors protected override PreValueEditor CreatePreValueEditor() { - return new DropDownMultiplePreValueEditor(); + return new DropDownMultiplePreValueEditor(_textService); } /// @@ -42,7 +46,7 @@ namespace Umbraco.Web.PropertyEditors /// internal class DropDownMultiplePreValueEditor : ValueListPreValueEditor { - public DropDownMultiplePreValueEditor() + public DropDownMultiplePreValueEditor(ILocalizedTextService textService) : base(textService) { //add the multiple field, we'll make it hidden so it is not seen in the pre-value editor Fields.Add(new PreValueField diff --git a/src/Umbraco.Web/PropertyEditors/DropDownPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/DropDownPropertyEditor.cs index 3607a8b5fa..4981e88412 100644 --- a/src/Umbraco.Web/PropertyEditors/DropDownPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/DropDownPropertyEditor.cs @@ -5,6 +5,7 @@ using Umbraco.Core.Logging; using Umbraco.Core.PropertyEditors; using umbraco; using ClientDependency.Core; +using Umbraco.Core.Services; using Constants = Umbraco.Core.Constants; namespace Umbraco.Web.PropertyEditors @@ -23,7 +24,7 @@ namespace Umbraco.Web.PropertyEditors /// /// The constructor will setup the property editor based on the attribute if one is found /// - public DropDownPropertyEditor(ILogger logger) : base(logger) + public DropDownPropertyEditor(ILogger logger, ILocalizedTextService textService) : base(logger, textService) { } diff --git a/src/Umbraco.Web/PropertyEditors/DropDownWithKeysPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/DropDownWithKeysPropertyEditor.cs index b4138d170a..2192af8d4f 100644 --- a/src/Umbraco.Web/PropertyEditors/DropDownWithKeysPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/DropDownWithKeysPropertyEditor.cs @@ -1,6 +1,7 @@ using Umbraco.Core; using Umbraco.Core.Logging; using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Services; namespace Umbraco.Web.PropertyEditors { @@ -15,11 +16,14 @@ namespace Umbraco.Web.PropertyEditors [PropertyEditor(Constants.PropertyEditors.DropdownlistPublishingKeysAlias, "Dropdown list, publishing keys", "dropdown", ValueType = "INT", Group = "lists", Icon = "icon-indent")] public class DropDownWithKeysPropertyEditor : PropertyEditor { + private readonly ILocalizedTextService _textService; + /// /// The constructor will setup the property editor based on the attribute if one is found /// - public DropDownWithKeysPropertyEditor(ILogger logger) : base(logger) + public DropDownWithKeysPropertyEditor(ILogger logger, ILocalizedTextService textService) : base(logger) { + _textService = textService; } /// @@ -28,7 +32,7 @@ namespace Umbraco.Web.PropertyEditors /// protected override PreValueEditor CreatePreValueEditor() { - return new ValueListPreValueEditor(); + return new ValueListPreValueEditor(_textService); } } } \ No newline at end of file diff --git a/src/Umbraco.Web/PropertyEditors/FileUploadPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/FileUploadPropertyEditor.cs index 9b4e722b30..809716af62 100644 --- a/src/Umbraco.Web/PropertyEditors/FileUploadPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/FileUploadPropertyEditor.cs @@ -24,14 +24,17 @@ namespace Umbraco.Web.PropertyEditors { private readonly MediaFileSystem _mediaFileSystem; private readonly IContentSection _contentSettings; + private readonly ILocalizedTextService _textService; - public FileUploadPropertyEditor(ILogger logger, MediaFileSystem mediaFileSystem, IContentSection contentSettings) + public FileUploadPropertyEditor(ILogger logger, MediaFileSystem mediaFileSystem, IContentSection contentSettings, ILocalizedTextService textService) : base(logger) { if (mediaFileSystem == null) throw new ArgumentNullException("mediaFileSystem"); if (contentSettings == null) throw new ArgumentNullException("contentSettings"); + if (textService == null) throw new ArgumentNullException("textService"); _mediaFileSystem = mediaFileSystem; _contentSettings = contentSettings; + _textService = textService; MemberService.Deleted += (sender, args) => args.MediaFilesToDelete.AddRange(ServiceDeleted(args.DeletedEntities.Cast())); } @@ -49,7 +52,7 @@ namespace Umbraco.Web.PropertyEditors protected override PreValueEditor CreatePreValueEditor() { - return new FileUploadPreValueEditor(); + return new FileUploadPreValueEditor(_textService); } /// @@ -172,8 +175,8 @@ namespace Umbraco.Web.PropertyEditors /// internal class FileUploadPreValueEditor : ValueListPreValueEditor { - public FileUploadPreValueEditor() - : base() + public FileUploadPreValueEditor(ILocalizedTextService textService) + : base(textService) { var field = Fields.First(); field.Description = "Enter a max width/height for each thumbnail"; diff --git a/src/Umbraco.Web/PropertyEditors/RadioButtonsPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/RadioButtonsPropertyEditor.cs index 24c3a275fb..8ac6bff69e 100644 --- a/src/Umbraco.Web/PropertyEditors/RadioButtonsPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/RadioButtonsPropertyEditor.cs @@ -1,6 +1,7 @@ using Umbraco.Core; using Umbraco.Core.Logging; using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Services; namespace Umbraco.Web.PropertyEditors { @@ -18,7 +19,7 @@ namespace Umbraco.Web.PropertyEditors /// /// The constructor will setup the property editor based on the attribute if one is found /// - public RadioButtonsPropertyEditor(ILogger logger) : base(logger) + public RadioButtonsPropertyEditor(ILogger logger, ILocalizedTextService textService) : base(logger, textService) { } } diff --git a/src/Umbraco.Web/PropertyEditors/ValueListPreValueEditor.cs b/src/Umbraco.Web/PropertyEditors/ValueListPreValueEditor.cs index 2c6357de3d..e11a969bfe 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueListPreValueEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueListPreValueEditor.cs @@ -22,9 +22,11 @@ namespace Umbraco.Web.PropertyEditors /// internal class ValueListPreValueEditor : PreValueEditor { + private readonly ILocalizedTextService _textService; - public ValueListPreValueEditor() + public ValueListPreValueEditor(ILocalizedTextService textService) { + _textService = textService; Fields.AddRange(CreatePreValueFields()); } @@ -46,7 +48,7 @@ namespace Umbraco.Web.PropertyEditors //It's also important to note that by default the dropdown angular controller is expecting the // config options to come in with a property called 'items' Key = "items", - Name = ApplicationContext.Current.Services.TextService.Localize("editdatatype/addPrevalue"), // todo: inject + Name = _textService.Localize("editdatatype/addPrevalue"), // todo: inject View = "multivalues" } }; From 081d6ecc079db510376991952b38e821fcf00ddd Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 21 Apr 2016 12:15:39 +0200 Subject: [PATCH 46/46] adds notes --- src/Umbraco.Tests/TestHelpers/ServiceContextHelper.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Umbraco.Tests/TestHelpers/ServiceContextHelper.cs b/src/Umbraco.Tests/TestHelpers/ServiceContextHelper.cs index 5c4485871e..468c4b19b3 100644 --- a/src/Umbraco.Tests/TestHelpers/ServiceContextHelper.cs +++ b/src/Umbraco.Tests/TestHelpers/ServiceContextHelper.cs @@ -17,6 +17,8 @@ namespace Umbraco.Tests.TestHelpers { class ServiceContextHelper { + //NOTE: Should be used sparingly for integration tests only - for unit tests you can just mock the services to be passed to the + // ctor of the ServiceContext. public static ServiceContext GetServiceContext(RepositoryFactory repositoryFactory, IDatabaseUnitOfWorkProvider dbUnitOfWorkProvider, IUnitOfWorkProvider fileUnitOfWorkProvider,