diff --git a/src/Umbraco.Core/Persistence/PetaPocoExtensions.cs b/src/Umbraco.Core/Persistence/PetaPocoExtensions.cs index bebbb43013..74b5a85a52 100644 --- a/src/Umbraco.Core/Persistence/PetaPocoExtensions.cs +++ b/src/Umbraco.Core/Persistence/PetaPocoExtensions.cs @@ -120,7 +120,17 @@ namespace Umbraco.Core.Persistence public static void DropTable(this Database db, string tableName) { - var sql = new Sql(string.Format("DROP TABLE {0}", SqlSyntaxContext.SqlSyntaxProvider.GetQuotedTableName(tableName))); + var sql = new Sql(string.Format( + SqlSyntaxContext.SqlSyntaxProvider.DropTable, + SqlSyntaxContext.SqlSyntaxProvider.GetQuotedTableName(tableName))); + db.Execute(sql); + } + + public static void TruncateTable(this Database db, string tableName) + { + var sql = new Sql(string.Format( + SqlSyntaxContext.SqlSyntaxProvider.TruncateTable, + SqlSyntaxContext.SqlSyntaxProvider.GetQuotedTableName(tableName))); db.Execute(sql); } diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/ISqlSyntaxProvider.cs b/src/Umbraco.Core/Persistence/SqlSyntax/ISqlSyntaxProvider.cs index cbd31b3d43..e2a13d1fef 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/ISqlSyntaxProvider.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/ISqlSyntaxProvider.cs @@ -31,6 +31,7 @@ namespace Umbraco.Core.Persistence.SqlSyntax string InsertData { get; } string UpdateData { get; } string DeleteData { get; } + string TruncateTable { get; } string CreateConstraint { get; } string DeleteConstraint { get; } string CreateForeignKeyConstraint { get; } diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/SqlCeSyntaxProvider.cs b/src/Umbraco.Core/Persistence/SqlSyntax/SqlCeSyntaxProvider.cs index 703019412c..106b45ffed 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/SqlCeSyntaxProvider.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/SqlCeSyntaxProvider.cs @@ -42,6 +42,14 @@ namespace Umbraco.Core.Persistence.SqlSyntax return false; } + /// + /// SqlCe doesn't support the Truncate Table syntax, so we just have to do a DELETE FROM which is slower but we have no choice. + /// + public override string TruncateTable + { + get { return "DELETE FROM {0}"; } + } + public override string GetIndexType(IndexTypes indexTypes) { string indexType; diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs b/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs index cbdc4804b4..e86222ac50 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs @@ -436,6 +436,7 @@ namespace Umbraco.Core.Persistence.SqlSyntax 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}"; } } diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index 438897cec8..8d4af2e131 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Web; using System.Xml.Linq; using Umbraco.Core.Auditing; @@ -25,6 +26,9 @@ namespace Umbraco.Core.Services private readonly IDatabaseUnitOfWorkProvider _uowProvider; private readonly IPublishingStrategy _publishingStrategy; private readonly RepositoryFactory _repositoryFactory; + //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() : this(new RepositoryFactory()) @@ -407,15 +411,28 @@ namespace Umbraco.Core.Services return IsPublishable(content, false); } - /// - /// Re-Publishes all Content - /// - /// Optional Id of the User issueing the publishing - /// True if publishing succeeded, otherwise False - public bool RePublishAll(int userId = 0) - { - return RePublishAllDo(false, userId); - } + /// + /// This will rebuild the xml structures for content in the database. + /// + /// Optional Id of the User issueing the publishing + /// 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 + { + RePublishAllDo(); + return true; + } + catch (Exception ex) + { + LogHelper.Error("An error occurred executing RePublishAll", ex); + return false; + } + } /// /// Publishes a single object @@ -490,41 +507,43 @@ namespace Umbraco.Core.Services if (Saving.IsRaisedEventCancelled(new SaveEventArgs(contents), this)) return; } + using (new WriteLock(Locker)) + { + var containsNew = contents.Any(x => x.HasIdentity == false); - var containsNew = contents.Any(x => x.HasIdentity == false); + var uow = _uowProvider.GetUnitOfWork(); + using (var repository = _repositoryFactory.CreateContentRepository(uow)) + { + if (containsNew) + { + foreach (var content in contents) + { + content.WriterId = userId; - var uow = _uowProvider.GetUnitOfWork(); - using (var repository = _repositoryFactory.CreateContentRepository(uow)) - { - if (containsNew) - { - foreach (var content in contents) - { - content.WriterId = userId; + //Only change the publish state if the "previous" version was actually published + if (content.Published) + content.ChangePublishedState(PublishedState.Saved); - //Only change the publish state if the "previous" version was actually published - if (content.Published) - content.ChangePublishedState(PublishedState.Saved); + repository.AddOrUpdate(content); + uow.Commit(); + } + } + else + { + foreach (var content in contents) + { + content.WriterId = userId; + repository.AddOrUpdate(content); + } + uow.Commit(); + } + } - repository.AddOrUpdate(content); - uow.Commit(); - } - } - else - { - foreach (var content in contents) - { - content.WriterId = userId; - repository.AddOrUpdate(content); - } - uow.Commit(); - } - } - - if(raiseEvents) - Saved.RaiseEvent(new SaveEventArgs(contents, false), this); + if (raiseEvents) + Saved.RaiseEvent(new SaveEventArgs(contents, false), this); - Audit.Add(AuditTypes.Save, "Bulk Save content performed by user", userId == -1 ? 0 : userId, -1); + Audit.Add(AuditTypes.Save, "Bulk Save content performed by user", userId == -1 ? 0 : userId, -1); + } } /// @@ -534,38 +553,41 @@ namespace Umbraco.Core.Services /// Id of the /// Optional Id of the user issueing the delete operation public void DeleteContentOfType(int contentTypeId, int userId = 0) - { - 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); + { + 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); - if (Deleting.IsRaisedEventCancelled(new DeleteEventArgs(contents), this)) - return; + 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 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); - } + foreach (var child in children) + { + if (child.ContentType.Id != contentTypeId) + MoveToRecycleBin(child, userId); + } - //Permantly delete the content - Delete(content, userId); - } - } + //Permantly delete the content + Delete(content, userId); + } + } - Audit.Add(AuditTypes.Delete, - string.Format("Delete Content of Type {0} performed by user", contentTypeId), - userId, -1); + Audit.Add(AuditTypes.Delete, + string.Format("Delete Content of Type {0} performed by user", contentTypeId), + userId, -1); + } } /// @@ -579,32 +601,35 @@ namespace Umbraco.Core.Services /// Optional Id of the User deleting the Content public void Delete(IContent content, int userId = 0) { - if (Deleting.IsRaisedEventCancelled(new DeleteEventArgs(content), this)) - return; + using (new WriteLock(Locker)) + { + if (Deleting.IsRaisedEventCancelled(new DeleteEventArgs(content), this)) + return; - //Make sure that published content is unpublished before being deleted - if (HasPublishedVersion(content.Id)) - { - UnPublish(content, userId); - } + //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); - } + //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 uow = _uowProvider.GetUnitOfWork(); + using (var repository = _repositoryFactory.CreateContentRepository(uow)) + { + repository.Delete(content); + uow.Commit(); + } - Deleted.RaiseEvent(new DeleteEventArgs(content, false), this); + Deleted.RaiseEvent(new DeleteEventArgs(content, false), this); - Audit.Add(AuditTypes.Delete, "Delete Content performed by user", userId, content.Id); + Audit.Add(AuditTypes.Delete, "Delete Content performed by user", userId, content.Id); + } } /// @@ -615,22 +640,22 @@ namespace Umbraco.Core.Services /// Optional Id of the User deleting versions of a Content object public void DeleteVersions(int id, DateTime versionDate, int userId = 0) { - //TODO: We should check if we are going to delete the most recent version because if that happens it means the - // entity is completely deleted and we should raise the normal Deleting/Deleted event + //TODO: We should check if we are going to delete the most recent version because if that happens it means the + // entity is completely deleted and we should raise the normal Deleting/Deleted event - if (DeletingVersions.IsRaisedEventCancelled(new DeleteRevisionsEventArgs(id, dateToRetain: versionDate), this)) - return; + 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(); - } + 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.Add(AuditTypes.Delete, "Delete Content by version date performed by user", userId, -1); + DeletedVersions.RaiseEvent(new DeleteRevisionsEventArgs(id, false, dateToRetain: versionDate), this); + + Audit.Add(AuditTypes.Delete, "Delete Content by version date performed by user", userId, -1); } /// @@ -642,28 +667,31 @@ namespace Umbraco.Core.Services /// Optional Id of the User deleting versions of a Content object public void DeleteVersion(int id, Guid versionId, bool deletePriorVersions, int userId = 0) { - //TODO: We should check if we are going to delete the most recent version because if that happens it means the - // entity is completely deleted and we should raise the normal Deleting/Deleted event + using (new WriteLock(Locker)) + { + //TODO: We should check if we are going to delete the most recent version because if that happens it means the + // entity is completely deleted and we should raise the normal Deleting/Deleted event - if (deletePriorVersions) - { - var content = GetByVersion(versionId); - DeleteVersions(id, content.UpdateDate, userId); - } + if (deletePriorVersions) + { + var content = GetByVersion(versionId); + DeleteVersions(id, content.UpdateDate, userId); + } - if (DeletingVersions.IsRaisedEventCancelled(new DeleteRevisionsEventArgs(id, specificVersion: versionId), this)) - return; + if (DeletingVersions.IsRaisedEventCancelled(new DeleteRevisionsEventArgs(id, specificVersion: versionId), this)) + return; - var uow = _uowProvider.GetUnitOfWork(); - using (var repository = _repositoryFactory.CreateContentRepository(uow)) - { - repository.DeleteVersion(versionId); - uow.Commit(); - } + var uow = _uowProvider.GetUnitOfWork(); + using (var repository = _repositoryFactory.CreateContentRepository(uow)) + { + repository.DeleteVersion(versionId); + uow.Commit(); + } - DeletedVersions.RaiseEvent(new DeleteRevisionsEventArgs(id, false, specificVersion:versionId), this); + DeletedVersions.RaiseEvent(new DeleteRevisionsEventArgs(id, false, specificVersion: versionId), this); - Audit.Add(AuditTypes.Delete, "Delete Content by version performed by user", userId, -1); + Audit.Add(AuditTypes.Delete, "Delete Content by version performed by user", userId, -1); + } } /// @@ -674,43 +702,46 @@ namespace Umbraco.Core.Services /// Optional Id of the User deleting the Content public void MoveToRecycleBin(IContent content, int userId = 0) { - if (Trashing.IsRaisedEventCancelled(new MoveEventArgs(content, -20), this)) - return; - - //Make sure that published content is unpublished before being moved to the Recycle Bin - if (HasPublishedVersion(content.Id)) - { - UnPublish(content, userId); - } - - //Unpublish descendents of the content item that is being moved to trash - var descendants = GetDescendants(content).ToList(); - foreach (var descendant in descendants) + using (new WriteLock(Locker)) { - UnPublish(descendant, userId); - } + if (Trashing.IsRaisedEventCancelled(new MoveEventArgs(content, -20), this)) + return; - var uow = _uowProvider.GetUnitOfWork(); - using (var repository = _repositoryFactory.CreateContentRepository(uow)) - { - content.WriterId = userId; - content.ChangeTrashedState(true); - repository.AddOrUpdate(content); + //Make sure that published content is unpublished before being moved to the Recycle Bin + if (HasPublishedVersion(content.Id)) + { + UnPublish(content, userId); + } - //Loop through descendants to update their trash state, but ensuring structure by keeping the ParentId - foreach (var descendant in descendants) - { - descendant.WriterId = userId; - descendant.ChangeTrashedState(true, descendant.ParentId); - repository.AddOrUpdate(descendant); - } + //Unpublish descendents of the content item that is being moved to trash + var descendants = GetDescendants(content).ToList(); + foreach (var descendant in descendants) + { + UnPublish(descendant, userId); + } - uow.Commit(); - } + var uow = _uowProvider.GetUnitOfWork(); + using (var repository = _repositoryFactory.CreateContentRepository(uow)) + { + content.WriterId = userId; + content.ChangeTrashedState(true); + repository.AddOrUpdate(content); - Trashed.RaiseEvent(new MoveEventArgs(content, false, -20), this); + //Loop through descendants to update their trash state, but ensuring structure by keeping the ParentId + foreach (var descendant in descendants) + { + descendant.WriterId = userId; + descendant.ChangeTrashedState(true, descendant.ParentId); + repository.AddOrUpdate(descendant); + } - Audit.Add(AuditTypes.Move, "Move Content to Recycle Bin performed by user", userId, content.Id); + uow.Commit(); + } + + Trashed.RaiseEvent(new MoveEventArgs(content, false, -20), this); + + Audit.Add(AuditTypes.Move, "Move Content to Recycle Bin performed by user", userId, content.Id); + } } /// @@ -726,84 +757,87 @@ namespace Umbraco.Core.Services /// Optional Id of the User moving the Content public void Move(IContent content, int parentId, int userId = 0) { - //This ensures that the correct method is called if this method is used to Move to recycle bin. - if (parentId == -20) + using (new WriteLock(Locker)) { - MoveToRecycleBin(content, userId); - return; - } - - if (Moving.IsRaisedEventCancelled(new MoveEventArgs(content, parentId), this)) - return; - - content.WriterId = userId; - if (parentId == -1) - { - content.Path = string.Concat("-1,", 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 != -20) - { - 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)) + //This ensures that the correct method is called if this method is used to Move to recycle bin. + if (parentId == -20) { - SaveAndPublish(content, userId); + MoveToRecycleBin(content, userId); + return; + } + + if (Moving.IsRaisedEventCancelled(new MoveEventArgs(content, parentId), this)) + return; + + content.WriterId = userId; + if (parentId == -1) + { + content.Path = string.Concat("-1,", content.Id); + content.Level = 1; } else { - Save(content, false, userId, true); + var parent = GetById(parentId); + content.Path = string.Concat(parent.Path, ",", content.Id); + content.Level = parent.Level + 1; + } - using (var uow = _uowProvider.GetUnitOfWork()) + + //If Content is being moved away from Recycle Bin, its state should be un-trashed + if (content.Trashed && parentId != -20) + { + 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)) { - var xml = content.ToXml(); - var poco = new ContentXmlDto {NodeId = content.Id, Xml = xml.ToString(SaveOptions.None)}; - 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)); + SaveAndPublish(content, userId); + } + else + { + Save(content, false, userId, true); + + using (var uow = _uowProvider.GetUnitOfWork()) + { + var xml = content.ToXml(); + var poco = new ContentXmlDto { NodeId = content.Id, Xml = xml.ToString(SaveOptions.None) }; + 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 - { - Save(content, userId); - } + else + { + Save(content, userId); + } - //Ensure that Path and Level is updated on children - var children = GetChildren(content.Id); - if (children.Any()) - { - foreach (var child in children) - { - Move(child, content.Id, userId); - } - } + //Ensure that Path and Level is updated on children + var children = GetChildren(content.Id); + if (children.Any()) + { + foreach (var child in children) + { + Move(child, content.Id, userId); + } + } - Moved.RaiseEvent(new MoveEventArgs(content, false, parentId), this); + Moved.RaiseEvent(new MoveEventArgs(content, false, parentId), this); - Audit.Add(AuditTypes.Move, "Move Content performed by user", userId, content.Id); + Audit.Add(AuditTypes.Move, "Move Content performed by user", userId, content.Id); + } } /// @@ -845,105 +879,108 @@ namespace Umbraco.Core.Services /// The newly created object public IContent Copy(IContent content, int parentId, bool relateToOriginal, int userId = 0) { - var copy = ((Content)content).Clone(); - copy.ParentId = parentId; + using (new WriteLock(Locker)) + { + var copy = ((Content) content).Clone(); + copy.ParentId = parentId; - // A copy should never be set to published automatically even if the original was. - copy.ChangePublishedState(PublishedState.Unpublished); + // 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; + if (Copying.IsRaisedEventCancelled(new CopyEventArgs(content, copy, parentId), this)) + return null; - var uow = _uowProvider.GetUnitOfWork(); - using (var repository = _repositoryFactory.CreateContentRepository(uow)) - { - content.WriterId = userId; - repository.AddOrUpdate(copy); - uow.Commit(); - - //Special case for the Upload DataType - var uploadDataTypeId = new Guid("5032a6e6-69e3-491d-bb28-cd31cd11086c"); - if (content.Properties.Any(x => x.PropertyType.DataTypeId == uploadDataTypeId)) - { - bool isUpdated = false; - var fs = FileSystemProviderManager.Current.GetFileSystemProvider(); - - //Loop through properties to check if the content contains media that should be deleted - foreach (var property in content.Properties.Where(x => x.PropertyType.DataTypeId == uploadDataTypeId - && string.IsNullOrEmpty(x.Value.ToString()) == false)) - { - if (fs.FileExists(IOHelper.MapPath(property.Value.ToString()))) - { - var currentPath = fs.GetRelativePath(property.Value.ToString()); - var propertyId = copy.Properties.First(x => x.Alias == property.Alias).Id; - var newPath = fs.GetRelativePath(propertyId, System.IO.Path.GetFileName(currentPath)); - - fs.CopyFile(currentPath, newPath); - copy.SetValue(property.Alias, fs.GetUrl(newPath)); - - //Copy thumbnails - foreach (var thumbPath in fs.GetThumbnails(currentPath)) - { - var newThumbPath = fs.GetRelativePath(propertyId, System.IO.Path.GetFileName(thumbPath)); - fs.CopyFile(thumbPath, newThumbPath); - } - isUpdated = true; - } - } - - if (isUpdated) - { - repository.AddOrUpdate(copy); - uow.Commit(); - } - } - - //Special case for the Tags DataType - var tagsDataTypeId = new Guid("4023e540-92f5-11dd-ad8b-0800200c9a66"); - if (content.Properties.Any(x => x.PropertyType.DataTypeId == tagsDataTypeId)) + var uow = _uowProvider.GetUnitOfWork(); + using (var repository = _repositoryFactory.CreateContentRepository(uow)) { - var tags = uow.Database.Fetch("WHERE nodeId = @Id", new {Id = content.Id}); - foreach (var tag in tags) + content.WriterId = userId; + repository.AddOrUpdate(copy); + uow.Commit(); + + //Special case for the Upload DataType + var uploadDataTypeId = new Guid("5032a6e6-69e3-491d-bb28-cd31cd11086c"); + if (content.Properties.Any(x => x.PropertyType.DataTypeId == uploadDataTypeId)) { - uow.Database.Insert(new TagRelationshipDto {NodeId = copy.Id, TagId = tag.TagId}); + bool isUpdated = false; + var fs = FileSystemProviderManager.Current.GetFileSystemProvider(); + + //Loop through properties to check if the content contains media that should be deleted + foreach (var property in content.Properties.Where(x => x.PropertyType.DataTypeId == uploadDataTypeId + && string.IsNullOrEmpty(x.Value.ToString()) == false)) + { + if (fs.FileExists(IOHelper.MapPath(property.Value.ToString()))) + { + var currentPath = fs.GetRelativePath(property.Value.ToString()); + var propertyId = copy.Properties.First(x => x.Alias == property.Alias).Id; + var newPath = fs.GetRelativePath(propertyId, System.IO.Path.GetFileName(currentPath)); + + fs.CopyFile(currentPath, newPath); + copy.SetValue(property.Alias, fs.GetUrl(newPath)); + + //Copy thumbnails + foreach (var thumbPath in fs.GetThumbnails(currentPath)) + { + var newThumbPath = fs.GetRelativePath(propertyId, System.IO.Path.GetFileName(thumbPath)); + fs.CopyFile(thumbPath, newThumbPath); + } + isUpdated = true; + } + } + + if (isUpdated) + { + repository.AddOrUpdate(copy); + uow.Commit(); + } + } + + //Special case for the Tags DataType + var tagsDataTypeId = new Guid("4023e540-92f5-11dd-ad8b-0800200c9a66"); + if (content.Properties.Any(x => x.PropertyType.DataTypeId == tagsDataTypeId)) + { + 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}); + } } } - } - //NOTE This 'Relation' part should eventually be delegated to a RelationService - if (relateToOriginal) - { - RelationType relationType = null; - using (var relationTypeRepository = _repositoryFactory.CreateRelationTypeRepository(uow)) - { - relationType = relationTypeRepository.Get(1); - } + //NOTE This 'Relation' part should eventually be delegated to a RelationService + if (relateToOriginal) + { + RelationType relationType = null; + using (var relationTypeRepository = _repositoryFactory.CreateRelationTypeRepository(uow)) + { + relationType = relationTypeRepository.Get(1); + } - using (var relationRepository = _repositoryFactory.CreateRelationRepository(uow)) - { - var relation = new Relation(content.Id, copy.Id, relationType); - relationRepository.AddOrUpdate(relation); - uow.Commit(); - } + using (var relationRepository = _repositoryFactory.CreateRelationRepository(uow)) + { + var relation = new Relation(content.Id, copy.Id, relationType); + relationRepository.AddOrUpdate(relation); + uow.Commit(); + } - Audit.Add(AuditTypes.Copy, - string.Format("Copied content with Id: '{0}' related to original content with Id: '{1}'", - copy.Id, content.Id), copy.WriterId, copy.Id); - } + Audit.Add(AuditTypes.Copy, + string.Format("Copied content with Id: '{0}' related to original content with Id: '{1}'", + copy.Id, content.Id), copy.WriterId, copy.Id); + } - //Look for children and copy those as well - var children = GetChildren(content.Id); - foreach (var child in children) - { - Copy(child, copy.Id, relateToOriginal, userId); - } + //Look for children and copy those as well + var children = GetChildren(content.Id); + foreach (var child in children) + { + Copy(child, copy.Id, relateToOriginal, userId); + } - Copied.RaiseEvent(new CopyEventArgs(content, copy, false, parentId), this); + Copied.RaiseEvent(new CopyEventArgs(content, copy, false, parentId), this); - Audit.Add(AuditTypes.Copy, "Copy Content performed by user", content.WriterId, content.Id); + Audit.Add(AuditTypes.Copy, "Copy Content performed by user", content.WriterId, content.Id); - return copy; - } + return copy; + } + } /// /// Sends an to Publication, which executes handlers and events for the 'Send to Publication' action. @@ -980,40 +1017,30 @@ namespace Umbraco.Core.Services /// The newly created object public IContent Rollback(int id, Guid versionId, int userId = 0) { - var content = GetByVersion(versionId); + var content = GetByVersion(versionId); - if (RollingBack.IsRaisedEventCancelled(new RollbackEventArgs(content), this)) - return content; + 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; + var uow = _uowProvider.GetUnitOfWork(); + using (var repository = _repositoryFactory.CreateContentRepository(uow)) + { + content.WriterId = userId; + content.CreatorId = userId; - repository.AddOrUpdate(content); - uow.Commit(); - } + repository.AddOrUpdate(content); + uow.Commit(); + } - RolledBack.RaiseEvent(new RollbackEventArgs(content, false), this); + RolledBack.RaiseEvent(new RollbackEventArgs(content, false), this); - Audit.Add(AuditTypes.RollBack, "Content rollback performed by user", content.WriterId, content.Id); + Audit.Add(AuditTypes.RollBack, "Content rollback performed by user", content.WriterId, content.Id); - return content; + return content; } #region Internal Methods - /// - /// Internal method to Re-Publishes all Content for legacy purposes. - /// - /// Optional Id of the User issueing the publishing - /// Optional boolean to avoid having the cache refreshed when calling this RePublish method. By default this method will not update the cache. - /// True if publishing succeeded, otherwise False - internal bool RePublishAll(bool omitCacheRefresh = true, int userId = 0) - { - return RePublishAllDo(omitCacheRefresh, userId); - } - + /// /// Internal method that Publishes a single object for legacy purposes. /// @@ -1084,65 +1111,61 @@ namespace Umbraco.Core.Services #region Private Methods /// - /// Re-Publishes all Content + /// Rebuilds all xml content in the cmsContentXml table for all published documents /// - /// Optional Id of the User issueing the publishing - /// Optional boolean to avoid having the cache refreshed when calling this RePublish method. By default this method will update the cache. /// True if publishing succeeded, otherwise False - private bool RePublishAllDo(bool omitCacheRefresh = false, int userId = 0) + private void RePublishAllDo(params int[] contentTypeIds) { - var list = new List(); - var updated = new List(); - - //Consider creating a Path query instead of recursive method: - //var query = Query.Builder.Where(x => x.Path.StartsWith("-1")); - - var rootContent = GetRootContent(); - foreach (var content in rootContent) + using (new WriteLock(Locker)) { - if (content.IsValid()) - { - list.Add(content); - list.AddRange(GetDescendants(content)); - } - } + var list = new List(); - //Publish and then update the database with new status - var published = _publishingStrategy.PublishWithChildren(list, userId); - if (published) - { var uow = _uowProvider.GetUnitOfWork(); using (var repository = _repositoryFactory.CreateContentRepository(uow)) { - //Only loop through content where the Published property has been updated - foreach (var item in list.Where(x => ((ICanBeDirty)x).IsPropertyDirty("Published"))) + if (!contentTypeIds.Any()) { - item.WriterId = userId; - repository.AddOrUpdate(item); - updated.Add(item); + //since we're updating all records, it will be much faster to just clear the table first + uow.Database.TruncateTable("cmsContentXml"); + + //get all content items that are published + // Consider creating a Path query instead of recursive method: + // var query = Query.Builder.Where(x => x.Path.StartsWith("-1")); + var rootContent = GetRootContent(); + foreach (var content in rootContent.Where(content => content.Published)) + { + list.Add(content); + list.AddRange(GetPublishedDescendants(content)); + } + } + else + { + foreach (var id in contentTypeIds) + { + //first we'll clear out the data from the cmsContentXml table for this type + uow.Database.Execute(@"delete from cmsContentXml where nodeId in +(select cmsDocument.nodeId from cmsDocument + inner join cmsContent on cmsDocument.nodeId = cmsContent.nodeId + where published = 1 and contentType = @contentTypeId)", new { contentTypeId = id }); + + //now get all published content objects of this type and add to the list + list.AddRange(GetContentOfContentType(id).Where(content => content.Published)); + } } - uow.Commit(); - - foreach (var c in updated) + foreach (var c in list) { + //generate the xml var xml = c.ToXml(); + //create the dto to insert var poco = new ContentXmlDto { NodeId = c.Id, Xml = xml.ToString(SaveOptions.None) }; - var exists = uow.Database.FirstOrDefault("WHERE nodeId = @Id", new { Id = c.Id }) != - null; - int result = exists - ? uow.Database.Update(poco) - : Convert.ToInt32(uow.Database.Insert(poco)); + //insert it into the database + uow.Database.Insert(poco); } } - //Updating content to published state is finished, so we fire event through PublishingStrategy to have cache updated - if (omitCacheRefresh == false) - _publishingStrategy.PublishingFinalized(updated, true); + + Audit.Add(AuditTypes.Publish, "RePublish All completed, the xml has been regenerated in the database", 0, -1); } - - Audit.Add(AuditTypes.Publish, "RePublish All performed by user", userId, -1); - - return published; } /// @@ -1228,28 +1251,31 @@ namespace Umbraco.Core.Services /// True if unpublishing succeeded, otherwise False private bool UnPublishDo(IContent content, bool omitCacheRefresh = false, int userId = 0) { - var unpublished = _publishingStrategy.UnPublish(content, userId); - if (unpublished) + using (new WriteLock(Locker)) { - var uow = _uowProvider.GetUnitOfWork(); - using (var repository = _repositoryFactory.CreateContentRepository(uow)) + var unpublished = _publishingStrategy.UnPublish(content, userId); + if (unpublished) { - content.WriterId = userId; - repository.AddOrUpdate(content); + var uow = _uowProvider.GetUnitOfWork(); + using (var repository = _repositoryFactory.CreateContentRepository(uow)) + { + content.WriterId = userId; + repository.AddOrUpdate(content); - //Remove 'published' xml from the cmsContentXml table for the unpublished content - uow.Database.Delete("WHERE nodeId = @Id", new { Id = content.Id }); + //Remove 'published' xml from the cmsContentXml table for the unpublished content + uow.Database.Delete("WHERE nodeId = @Id", new { Id = content.Id }); - uow.Commit(); + 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.Add(AuditTypes.UnPublish, "UnPublish performed by user", userId, content.Id); } - //Delete xml from db? and call following method to fire event through PublishingStrategy to update cache - if (omitCacheRefresh == false) - _publishingStrategy.UnPublishingFinalized(content); - Audit.Add(AuditTypes.UnPublish, "UnPublish performed by user", userId, content.Id); - } - - return unpublished; + return unpublished; + } } /// @@ -1268,97 +1294,100 @@ namespace Umbraco.Core.Services return false; } - //Has this content item previously been published? If so, we don't need to refresh the children - var previouslyPublished = HasPublishedVersion(content.Id); - var validForPublishing = true; + using (new WriteLock(Locker)) + { + //Has this content item previously been published? If so, we don't need to refresh the children + var previouslyPublished = HasPublishedVersion(content.Id); + var validForPublishing = true; - //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 != -1 && content.ParentId != -20 && IsPublishable(content) == false) - { - LogHelper.Info( - string.Format( - "Content '{0}' with Id '{1}' could not be published because its parent is not published.", - content.Name, content.Id)); - validForPublishing = false; - } + //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 != -1 && content.ParentId != -20 && IsPublishable(content) == false) + { + LogHelper.Info( + string.Format( + "Content '{0}' with Id '{1}' could not be published because its parent is not published.", + content.Name, content.Id)); + validForPublishing = false; + } - //Content contains invalid property values and can therefore not be published - fire event? - if (!content.IsValid()) - { - LogHelper.Info( - string.Format( - "Content '{0}' with Id '{1}' could not be published because of invalid properties.", - content.Name, content.Id)); - validForPublishing = false; - } + //Content contains invalid property values and can therefore not be published - fire event? + if (!content.IsValid()) + { + LogHelper.Info( + string.Format( + "Content '{0}' with Id '{1}' could not be published because of invalid properties.", + content.Name, content.Id)); + validForPublishing = false; + } - //Publish and then update the database with new status - bool published = validForPublishing && _publishingStrategy.Publish(content, userId); + //Publish and then update the database with new status + bool published = validForPublishing && _publishingStrategy.Publish(content, userId); - var uow = _uowProvider.GetUnitOfWork(); - using (var repository = _repositoryFactory.CreateContentRepository(uow)) - { - //Since this is the Save and Publish method, the content should be saved even though the publish fails or isn't allowed - content.WriterId = userId; + var uow = _uowProvider.GetUnitOfWork(); + using (var repository = _repositoryFactory.CreateContentRepository(uow)) + { + //Since this is the Save and Publish method, the content should be saved even though the publish fails or isn't allowed + content.WriterId = userId; - repository.AddOrUpdate(content); + repository.AddOrUpdate(content); - uow.Commit(); + uow.Commit(); - var xml = content.ToXml(); - //Preview Xml - var previewPoco = new PreviewXmlDto - { - NodeId = content.Id, - Timestamp = DateTime.Now, - VersionId = content.Version, - Xml = xml.ToString(SaveOptions.None) - }; - var previewExists = - uow.Database.ExecuteScalar("SELECT COUNT(nodeId) FROM cmsPreviewXml WHERE nodeId = @Id AND versionId = @Version", - new { Id = content.Id, Version = content.Version }) != 0; - int previewResult = previewExists - ? uow.Database.Update( - "SET xml = @Xml, timestamp = @Timestamp WHERE nodeId = @Id AND versionId = @Version", - new - { - Xml = previewPoco.Xml, - Timestamp = previewPoco.Timestamp, - Id = previewPoco.NodeId, - Version = previewPoco.VersionId - }) - : Convert.ToInt32(uow.Database.Insert(previewPoco)); + var xml = content.ToXml(); + //Preview Xml + var previewPoco = new PreviewXmlDto + { + NodeId = content.Id, + Timestamp = DateTime.Now, + VersionId = content.Version, + Xml = xml.ToString(SaveOptions.None) + }; + var previewExists = + uow.Database.ExecuteScalar("SELECT COUNT(nodeId) FROM cmsPreviewXml WHERE nodeId = @Id AND versionId = @Version", + new {Id = content.Id, Version = content.Version}) != 0; + int previewResult = previewExists + ? uow.Database.Update( + "SET xml = @Xml, timestamp = @Timestamp WHERE nodeId = @Id AND versionId = @Version", + new + { + Xml = previewPoco.Xml, + Timestamp = previewPoco.Timestamp, + Id = previewPoco.NodeId, + Version = previewPoco.VersionId + }) + : Convert.ToInt32(uow.Database.Insert(previewPoco)); - if (published) - { - //Content Xml - var contentPoco = new ContentXmlDto { NodeId = content.Id, Xml = xml.ToString(SaveOptions.None) }; - var contentExists = uow.Database.ExecuteScalar("SELECT COUNT(nodeId) FROM cmsContentXml WHERE nodeId = @Id", new { Id = content.Id }) != 0; - int contentResult = contentExists - ? uow.Database.Update(contentPoco) - : Convert.ToInt32(uow.Database.Insert(contentPoco)); + if (published) + { + //Content Xml + var contentPoco = new ContentXmlDto {NodeId = content.Id, Xml = xml.ToString(SaveOptions.None)}; + var contentExists = uow.Database.ExecuteScalar("SELECT COUNT(nodeId) FROM cmsContentXml WHERE nodeId = @Id", new {Id = content.Id}) != 0; + int contentResult = contentExists + ? uow.Database.Update(contentPoco) + : Convert.ToInt32(uow.Database.Insert(contentPoco)); - } - } + } + } - if(raiseEvents) - Saved.RaiseEvent(new SaveEventArgs(content, false), this); + if (raiseEvents) + Saved.RaiseEvent(new SaveEventArgs(content, false), this); - //Save xml to db and call following method to fire event through PublishingStrategy to update cache - if (published && omitCacheRefresh == false) - _publishingStrategy.PublishingFinalized(content); + //Save xml to db and call following method to fire event through PublishingStrategy to update cache + if (published && omitCacheRefresh == false) + _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 && omitCacheRefresh == false && previouslyPublished == false && HasChildren(content.Id)) - { - var descendants = GetPublishedDescendants(content); + //We need to check if children and their publish state to ensure that we 'republish' content that was previously published + if (published && omitCacheRefresh == false && previouslyPublished == false && HasChildren(content.Id)) + { + var descendants = GetPublishedDescendants(content); - _publishingStrategy.PublishingFinalized(descendants, false); - } + _publishingStrategy.PublishingFinalized(descendants, false); + } - Audit.Add(AuditTypes.Publish, "Save and Publish performed by user", userId, content.Id); + Audit.Add(AuditTypes.Publish, "Save and Publish performed by user", userId, content.Id); - return published; + return published; + } } /// @@ -1376,47 +1405,50 @@ namespace Umbraco.Core.Services return; } - var uow = _uowProvider.GetUnitOfWork(); - using (var repository = _repositoryFactory.CreateContentRepository(uow)) - { - content.WriterId = userId; + using (new WriteLock(Locker)) + { + var uow = _uowProvider.GetUnitOfWork(); + using (var repository = _repositoryFactory.CreateContentRepository(uow)) + { + content.WriterId = userId; - //Only change the publish state if the "previous" version was actually published - if (changeState && content.Published) - content.ChangePublishedState(PublishedState.Saved); + //Only change the publish state if the "previous" version was actually published + if (changeState && content.Published) + content.ChangePublishedState(PublishedState.Saved); - repository.AddOrUpdate(content); - uow.Commit(); + repository.AddOrUpdate(content); + uow.Commit(); - //Preview Xml - var xml = content.ToXml(); - var previewPoco = new PreviewXmlDto - { - NodeId = content.Id, - Timestamp = DateTime.Now, - VersionId = content.Version, - Xml = xml.ToString(SaveOptions.None) - }; - var previewExists = - uow.Database.ExecuteScalar("SELECT COUNT(nodeId) FROM cmsPreviewXml WHERE nodeId = @Id AND versionId = @Version", - new { Id = content.Id, Version = content.Version }) != 0; - int previewResult = previewExists - ? uow.Database.Update( - "SET xml = @Xml, timestamp = @Timestamp WHERE nodeId = @Id AND versionId = @Version", - new + //Preview Xml + var xml = content.ToXml(); + var previewPoco = new PreviewXmlDto + { + NodeId = content.Id, + Timestamp = DateTime.Now, + VersionId = content.Version, + Xml = xml.ToString(SaveOptions.None) + }; + var previewExists = + uow.Database.ExecuteScalar("SELECT COUNT(nodeId) FROM cmsPreviewXml WHERE nodeId = @Id AND versionId = @Version", + new { Id = content.Id, Version = content.Version }) != 0; + int previewResult = previewExists + ? uow.Database.Update( + "SET xml = @Xml, timestamp = @Timestamp WHERE nodeId = @Id AND versionId = @Version", + new { Xml = previewPoco.Xml, Timestamp = previewPoco.Timestamp, Id = previewPoco.NodeId, Version = previewPoco.VersionId }) - : Convert.ToInt32(uow.Database.Insert(previewPoco)); - } + : Convert.ToInt32(uow.Database.Insert(previewPoco)); + } - if(raiseEvents) - Saved.RaiseEvent(new SaveEventArgs(content, false), this); + if (raiseEvents) + Saved.RaiseEvent(new SaveEventArgs(content, false), this); - Audit.Add(AuditTypes.Save, "Save Content performed by user", userId, content.Id); + Audit.Add(AuditTypes.Save, "Save Content performed by user", userId, content.Id); + } } /// diff --git a/src/Umbraco.Core/Services/ContentTypeService.cs b/src/Umbraco.Core/Services/ContentTypeService.cs index 75335cac0e..78d7c3711a 100644 --- a/src/Umbraco.Core/Services/ContentTypeService.cs +++ b/src/Umbraco.Core/Services/ContentTypeService.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Text; +using System.Threading; using Umbraco.Core.Auditing; using Umbraco.Core.Configuration; using Umbraco.Core.Events; @@ -22,6 +23,9 @@ namespace Umbraco.Core.Services private readonly IContentService _contentService; private readonly IMediaService _mediaService; private readonly IDatabaseUnitOfWorkProvider _uowProvider; + //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 ContentTypeService(IContentService contentService, IMediaService mediaService) : this(new PetaPocoUnitOfWorkProvider(), new RepositoryFactory(), contentService, mediaService) @@ -173,19 +177,22 @@ namespace Umbraco.Core.Services { if (DeletingContentType.IsRaisedEventCancelled(new DeleteEventArgs(contentType), this)) return; - - _contentService.DeleteContentOfType(contentType.Id); - var uow = _uowProvider.GetUnitOfWork(); - using (var repository = _repositoryFactory.CreateContentTypeRepository(uow)) - { - repository.Delete(contentType); - uow.Commit(); + using (new WriteLock(Locker)) + { + _contentService.DeleteContentOfType(contentType.Id); - DeletedContentType.RaiseEvent(new DeleteEventArgs(contentType, false), this); - } + var uow = _uowProvider.GetUnitOfWork(); + using (var repository = _repositoryFactory.CreateContentTypeRepository(uow)) + { + repository.Delete(contentType); + uow.Commit(); - Audit.Add(AuditTypes.Delete, string.Format("Delete ContentType performed by user"), userId, contentType.Id); + DeletedContentType.RaiseEvent(new DeleteEventArgs(contentType, false), this); + } + + Audit.Add(AuditTypes.Delete, string.Format("Delete ContentType performed by user"), userId, contentType.Id); + } } /// @@ -200,27 +207,30 @@ namespace Umbraco.Core.Services { if (DeletingContentType.IsRaisedEventCancelled(new DeleteEventArgs(contentTypes), this)) return; - - var contentTypeList = contentTypes.ToList(); - foreach (var contentType in contentTypeList) - { - _contentService.DeleteContentOfType(contentType.Id); - } - var uow = _uowProvider.GetUnitOfWork(); - using (var repository = _repositoryFactory.CreateContentTypeRepository(uow)) - { - foreach (var contentType in contentTypeList) - { - repository.Delete(contentType); - } + using (new WriteLock(Locker)) + { + var contentTypeList = contentTypes.ToList(); + foreach (var contentType in contentTypeList) + { + _contentService.DeleteContentOfType(contentType.Id); + } - uow.Commit(); + var uow = _uowProvider.GetUnitOfWork(); + using (var repository = _repositoryFactory.CreateContentTypeRepository(uow)) + { + foreach (var contentType in contentTypeList) + { + repository.Delete(contentType); + } - DeletedContentType.RaiseEvent(new DeleteEventArgs(contentTypes, false), this); - } + uow.Commit(); - Audit.Add(AuditTypes.Delete, string.Format("Delete ContentTypes performed by user"), userId, -1); + DeletedContentType.RaiseEvent(new DeleteEventArgs(contentTypes, false), this); + } + + Audit.Add(AuditTypes.Delete, string.Format("Delete ContentTypes performed by user"), userId, -1); + } } /// @@ -357,20 +367,22 @@ namespace Umbraco.Core.Services { if (DeletingMediaType.IsRaisedEventCancelled(new DeleteEventArgs(mediaType), this)) return; - - _mediaService.DeleteMediaOfType(mediaType.Id, userId); + using (new WriteLock(Locker)) + { + _mediaService.DeleteMediaOfType(mediaType.Id, userId); - var uow = _uowProvider.GetUnitOfWork(); - using (var repository = _repositoryFactory.CreateMediaTypeRepository(uow)) - { + var uow = _uowProvider.GetUnitOfWork(); + using (var repository = _repositoryFactory.CreateMediaTypeRepository(uow)) + { - repository.Delete(mediaType); - uow.Commit(); + repository.Delete(mediaType); + uow.Commit(); - DeletedMediaType.RaiseEvent(new DeleteEventArgs(mediaType, false), this); - } + DeletedMediaType.RaiseEvent(new DeleteEventArgs(mediaType, false), this); + } - Audit.Add(AuditTypes.Delete, string.Format("Delete MediaType performed by user"), userId, mediaType.Id); + Audit.Add(AuditTypes.Delete, string.Format("Delete MediaType performed by user"), userId, mediaType.Id); + } } /// @@ -383,26 +395,28 @@ namespace Umbraco.Core.Services { if (DeletingMediaType.IsRaisedEventCancelled(new DeleteEventArgs(mediaTypes), this)) return; - - var mediaTypeList = mediaTypes.ToList(); - foreach (var mediaType in mediaTypeList) - { - _mediaService.DeleteMediaOfType(mediaType.Id); - } + using (new WriteLock(Locker)) + { + var mediaTypeList = mediaTypes.ToList(); + foreach (var mediaType in mediaTypeList) + { + _mediaService.DeleteMediaOfType(mediaType.Id); + } - var uow = _uowProvider.GetUnitOfWork(); - using (var repository = _repositoryFactory.CreateMediaTypeRepository(uow)) - { - foreach (var mediaType in mediaTypeList) - { - repository.Delete(mediaType); - } - uow.Commit(); + var uow = _uowProvider.GetUnitOfWork(); + using (var repository = _repositoryFactory.CreateMediaTypeRepository(uow)) + { + foreach (var mediaType in mediaTypeList) + { + repository.Delete(mediaType); + } + uow.Commit(); - DeletedMediaType.RaiseEvent(new DeleteEventArgs(mediaTypes, false), this); - } + DeletedMediaType.RaiseEvent(new DeleteEventArgs(mediaTypes, false), this); + } - Audit.Add(AuditTypes.Delete, string.Format("Delete MediaTypes performed by user"), userId, -1); + Audit.Add(AuditTypes.Delete, string.Format("Delete MediaTypes performed by user"), userId, -1); + } } /// diff --git a/src/Umbraco.Core/Services/MediaService.cs b/src/Umbraco.Core/Services/MediaService.cs index 57168ec8c8..11e36cc72b 100644 --- a/src/Umbraco.Core/Services/MediaService.cs +++ b/src/Umbraco.Core/Services/MediaService.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Xml.Linq; using Umbraco.Core.Auditing; using Umbraco.Core.Events; @@ -19,6 +20,9 @@ namespace Umbraco.Core.Services { private readonly IDatabaseUnitOfWorkProvider _uowProvider; private readonly RepositoryFactory _repositoryFactory; + //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 MediaService(RepositoryFactory repositoryFactory) : this(new PetaPocoUnitOfWorkProvider(), repositoryFactory) @@ -306,32 +310,35 @@ namespace Umbraco.Core.Services /// Id of the User moving the Media public void Move(IMedia media, int parentId, int userId = 0) { - //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; - } + 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; + } - if (Moving.IsRaisedEventCancelled(new MoveEventArgs(media, parentId), this)) - return; + if (Moving.IsRaisedEventCancelled(new MoveEventArgs(media, parentId), this)) + return; - media.ParentId = parentId; - Save(media, userId); + media.ParentId = parentId; + Save(media, userId); - //Ensure that Path and Level is updated on children - var children = GetChildren(media.Id); - if (children.Any()) - { - var parentPath = media.Path; - var parentLevel = media.Level; - var updatedDescendents = UpdatePathAndLevelOnChildren(children, parentPath, parentLevel); - Save(updatedDescendents, userId); - } + //Ensure that Path and Level is updated on children + var children = GetChildren(media.Id); + if (children.Any()) + { + var parentPath = media.Path; + var parentLevel = media.Level; + var updatedDescendents = UpdatePathAndLevelOnChildren(children, parentPath, parentLevel); + Save(updatedDescendents, userId); + } - Moved.RaiseEvent(new MoveEventArgs(media, false, parentId), this); + Moved.RaiseEvent(new MoveEventArgs(media, false, parentId), this); - Audit.Add(AuditTypes.Move, "Move Media performed by user", userId, media.Id); + Audit.Add(AuditTypes.Move, "Move Media performed by user", userId, media.Id); + } } /// @@ -403,32 +410,42 @@ namespace Umbraco.Core.Services /// Id of the /// Optional id of the user deleting the media public void DeleteMediaOfType(int mediaTypeId, int userId = 0) - { - var uow = _uowProvider.GetUnitOfWork(); - using (var repository = _repositoryFactory.CreateMediaRepository(uow)) + { + using (new WriteLock(Locker)) { - //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); + 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); - if (Deleting.IsRaisedEventCancelled(new DeleteEventArgs(contents), this)) - return; + if (Deleting.IsRaisedEventCancelled(new DeleteEventArgs(contents), this)) + return; - foreach (var content in contents) - { - ((Core.Models.Media)content).ChangeTrashedState(true); - repository.AddOrUpdate(content); - } + 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); - uow.Commit(); + foreach (var child in children) + { + if (child.ContentType.Id != mediaTypeId) + MoveToRecycleBin(child, userId); + } - Deleted.RaiseEvent(new DeleteEventArgs(contents, false), this); - } + //Permantly delete the content + Delete(content, userId); + } + } - Audit.Add(AuditTypes.Delete, "Delete Media items by Type performed by user", userId, -1); - } + Audit.Add(AuditTypes.Delete, "Delete Media items by Type performed by user", userId, -1); + } + } /// /// Permanently deletes an object @@ -523,25 +540,28 @@ namespace Umbraco.Core.Services return; } - var uow = _uowProvider.GetUnitOfWork(); - using (var repository = _repositoryFactory.CreateMediaRepository(uow)) - { - media.CreatorId = userId; - repository.AddOrUpdate(media); - uow.Commit(); + using (new WriteLock(Locker)) + { + var uow = _uowProvider.GetUnitOfWork(); + using (var repository = _repositoryFactory.CreateMediaRepository(uow)) + { + media.CreatorId = userId; + repository.AddOrUpdate(media); + uow.Commit(); - var xml = media.ToXml(); - var poco = new ContentXmlDto { NodeId = media.Id, Xml = xml.ToString(SaveOptions.None) }; - var exists = uow.Database.FirstOrDefault("WHERE nodeId = @Id", new { Id = media.Id }) != null; - int result = exists - ? uow.Database.Update(poco) - : Convert.ToInt32(uow.Database.Insert(poco)); - } + var xml = media.ToXml(); + var poco = new ContentXmlDto {NodeId = media.Id, Xml = xml.ToString(SaveOptions.None)}; + var exists = uow.Database.FirstOrDefault("WHERE nodeId = @Id", new {Id = media.Id}) != null; + int result = exists + ? uow.Database.Update(poco) + : Convert.ToInt32(uow.Database.Insert(poco)); + } - if(raiseEvents) - Saved.RaiseEvent(new SaveEventArgs(media, false), this); + if (raiseEvents) + Saved.RaiseEvent(new SaveEventArgs(media, false), this); - Audit.Add(AuditTypes.Save, "Save Media performed by user", media.CreatorId, media.Id); + Audit.Add(AuditTypes.Save, "Save Media performed by user", media.CreatorId, media.Id); + } } /// diff --git a/src/Umbraco.Tests/Services/ContentServiceTests.cs b/src/Umbraco.Tests/Services/ContentServiceTests.cs index d5ee00e663..bc36783466 100644 --- a/src/Umbraco.Tests/Services/ContentServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentServiceTests.cs @@ -298,18 +298,30 @@ namespace Umbraco.Tests.Services { // Arrange var contentService = ServiceContext.ContentService; - var contentTypeService = ServiceContext.ContentTypeService; - var contentType = contentTypeService.GetContentType("umbTextpage"); + var rootContent = contentService.GetRootContent(); + foreach (var c in rootContent) + { + contentService.PublishWithChildren(c); + } + //for testing we need to clear out the contentXml table so we can see if it worked + var provider = new PetaPocoUnitOfWorkProvider(); + var uow = provider.GetUnitOfWork(); + using (RepositoryResolver.Current.ResolveByType(uow)) + { + uow.Database.TruncateTable("cmsContentXml"); + } // Act var published = contentService.RePublishAll(0); - var contents = contentService.GetContentOfContentType(contentType.Id); // Assert - Assert.That(published, Is.True); - Assert.That(contents.First(x => x.Id == 1046).Published, Is.True);//No restrictions, so should be published - Assert.That(contents.First(x => x.Id == 1047).Published, Is.True);//Released 5 mins ago, so should be published - Assert.That(contents.First(x => x.Id == 1049).Published, Is.False);//Trashed content, so shouldn't be published + Assert.IsTrue(published); + var allContent = rootContent.Concat(rootContent.SelectMany(x => contentService.GetDescendants(x.Id))); + uow = provider.GetUnitOfWork(); + using (var repo = RepositoryResolver.Current.ResolveByType(uow)) + { + Assert.AreEqual(allContent.Count(), uow.Database.ExecuteScalar("select count(*) from cmsContentXml")); + } } [Test]