using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Xml.Linq;
using Umbraco.Core.Events;
using Umbraco.Core.IO;
using Umbraco.Core.Logging;
using Umbraco.Core.Models;
using Umbraco.Core.Models.Membership;
using Umbraco.Core.Persistence.DatabaseModelDefinitions;
using Umbraco.Core.Persistence.Querying;
using Umbraco.Core.Persistence.Repositories;
using Umbraco.Core.Persistence.UnitOfWork;
using Umbraco.Core.Strings;
namespace Umbraco.Core.Services
{
///
/// Represents the Content Service, which is an easy access to operations involving
///
public class ContentService : RepositoryService, IContentService, IContentServiceOperations
{
private readonly EntityXmlSerializer _entitySerializer = new EntityXmlSerializer();
private readonly IDataTypeService _dataTypeService;
private readonly IUserService _userService;
private readonly IEnumerable _urlSegmentProviders;
private IContentTypeService _contentTypeService;
#region Constructors
public ContentService(
IDatabaseUnitOfWorkProvider provider,
ILogger logger,
IEventMessagesFactory eventMessagesFactory,
IDataTypeService dataTypeService,
IUserService userService,
IEnumerable urlSegmentProviders)
: base(provider, logger, eventMessagesFactory)
{
if (dataTypeService == null) throw new ArgumentNullException(nameof(dataTypeService));
if (userService == null) throw new ArgumentNullException(nameof(userService));
if (urlSegmentProviders == null) throw new ArgumentNullException(nameof(urlSegmentProviders));
_dataTypeService = dataTypeService;
_userService = userService;
_urlSegmentProviders = urlSegmentProviders;
}
// don't change or remove this, will need it later
private IContentTypeService ContentTypeService => _contentTypeService;
//// handle circular dependencies
//internal IContentTypeService ContentTypeService
//{
// get
// {
// if (_contentTypeService == null)
// throw new InvalidOperationException("ContentService.ContentTypeService has not been initialized.");
// return _contentTypeService;
// }
// set { _contentTypeService = value; }
//}
#endregion
#region Count
public int CountPublished(string contentTypeAlias = null)
{
using (var uow = UowProvider.CreateUnitOfWork())
{
uow.ReadLock(Constants.Locks.ContentTree);
var repo = uow.CreateRepository();
var count = repo.CountPublished();
uow.Complete();
return count;
}
}
public int Count(string contentTypeAlias = null)
{
using (var uow = UowProvider.CreateUnitOfWork())
{
uow.ReadLock(Constants.Locks.ContentTree);
var repo = uow.CreateRepository();
var count = repo.Count(contentTypeAlias);
uow.Complete();
return count;
}
}
public int CountChildren(int parentId, string contentTypeAlias = null)
{
using (var uow = UowProvider.CreateUnitOfWork())
{
uow.ReadLock(Constants.Locks.ContentTree);
var repo = uow.CreateRepository();
var count = repo.CountChildren(parentId, contentTypeAlias);
uow.Complete();
return count;
}
}
public int CountDescendants(int parentId, string contentTypeAlias = null)
{
using (var uow = UowProvider.CreateUnitOfWork())
{
uow.ReadLock(Constants.Locks.ContentTree);
var repo = uow.CreateRepository();
var count = repo.CountDescendants(parentId, contentTypeAlias);
uow.Complete();
return count;
}
}
#endregion
#region Permissions
///
/// 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)
{
using (var uow = UowProvider.CreateUnitOfWork())
{
uow.WriteLock(Constants.Locks.ContentTree);
var repo = uow.CreateRepository();
repo.ReplaceContentPermissions(permissionSet);
uow.Complete();
}
}
///
/// Assigns a single permission to the current content item for the specified user ids
///
///
///
///
public void AssignContentPermission(IContent entity, char permission, IEnumerable userIds)
{
using (var uow = UowProvider.CreateUnitOfWork())
{
uow.WriteLock(Constants.Locks.ContentTree);
var repo = uow.CreateRepository();
repo.AssignEntityPermission(entity, permission, userIds);
uow.Complete();
}
}
///
/// Gets the list of permissions for the content item
///
///
///
public IEnumerable GetPermissionsForEntity(IContent content)
{
using (var uow = UowProvider.CreateUnitOfWork())
{
uow.ReadLock(Constants.Locks.ContentTree);
var repo = uow.CreateRepository();
var perms = repo.GetPermissionsForEntity(content.Id);
uow.Complete();
return perms;
}
}
#endregion
#region Create
///
/// Creates an object of a specified content type.
///
/// This method simply returns a new, non-persisted, IContent without any identity. It
/// is intended as a shortcut to creating new content objects that does not invoke a save
/// operation against the database.
///
/// The name of the content object.
/// The identifier of the parent, or -1.
/// The alias of the content type.
/// The optional id of the user creating the content.
/// The content object.
public IContent CreateContent(string name, int parentId, string contentTypeAlias, int userId = 0)
{
var contentType = GetContentType(contentTypeAlias);
if (contentType == null)
throw new ArgumentException("No content type with that alias.", nameof(contentTypeAlias));
var parent = parentId > 0 ? GetById(parentId) : null;
if (parentId > 0 && parent == null)
throw new ArgumentException("No content with that id.", nameof(parentId));
var content = new Content(name, parentId, contentType);
CreateContent(null, content, parent, userId, false);
return content;
}
///
/// Creates an object of a specified content type, at root.
///
/// This method simply returns a new, non-persisted, IContent without any identity. It
/// is intended as a shortcut to creating new content objects that does not invoke a save
/// operation against the database.
///
/// The name of the content object.
/// The alias of the content type.
/// The optional id of the user creating the content.
/// The content object.
public IContent CreateContent(string name, string contentTypeAlias, int userId = 0)
{
// not locking since not saving anything
var contentType = GetContentType(contentTypeAlias);
if (contentType == null)
throw new ArgumentException("No content type with that alias.", nameof(contentTypeAlias));
var content = new Content(name, -1, contentType);
CreateContent(null, content, null, userId, false);
return content;
}
///
/// Creates an object of a specified content type, under a parent.
///
/// This method simply returns a new, non-persisted, IContent without any identity. It
/// is intended as a shortcut to creating new content objects that does not invoke a save
/// operation against the database.
///
/// The name of the content object.
/// The parent content object.
/// The alias of the content type.
/// The optional id of the user creating the content.
/// The content object.
public IContent CreateContent(string name, IContent parent, string contentTypeAlias, int userId = 0)
{
if (parent == null) throw new ArgumentNullException(nameof(parent));
using (var uow = UowProvider.CreateUnitOfWork())
{
// not locking since not saving anything
var contentType = GetContentType(contentTypeAlias);
if (contentType == null)
throw new ArgumentException("No content type with that alias.", nameof(contentTypeAlias)); // causes rollback
var content = new Content(name, parent, contentType);
CreateContent(uow, content, parent, userId, false);
uow.Complete();
return content;
}
}
///
/// Creates an object of a specified content type.
///
/// This method returns a new, persisted, IContent with an identity.
/// The name of the content object.
/// The identifier of the parent, or -1.
/// The alias of the content type.
/// The optional id of the user creating the content.
/// The content object.
public IContent CreateContentWithIdentity(string name, int parentId, string contentTypeAlias, int userId = 0)
{
using (var uow = UowProvider.CreateUnitOfWork())
{
// locking the content tree secures content types too
uow.WriteLock(Constants.Locks.ContentTree);
var contentType = GetContentType(contentTypeAlias); // + locks
if (contentType == null)
throw new ArgumentException("No content type with that alias.", nameof(contentTypeAlias)); // causes rollback
var parent = parentId > 0 ? GetById(parentId) : null; // + locks
if (parentId > 0 && parent == null)
throw new ArgumentException("No content with that id.", nameof(parentId)); // causes rollback
var content = parentId > 0 ? new Content(name, parent, contentType) : new Content(name, parentId, contentType);
CreateContent(uow, content, parent, userId, true);
uow.Complete();
return content;
}
}
///
/// Creates an object of a specified content type, under a parent.
///
/// This method returns a new, persisted, IContent with an identity.
/// The name of the content object.
/// The parent content object.
/// The alias of the content type.
/// The optional id of the user creating the content.
/// The content object.
public IContent CreateContentWithIdentity(string name, IContent parent, string contentTypeAlias, int userId = 0)
{
if (parent == null) throw new ArgumentNullException(nameof(parent));
using (var uow = UowProvider.CreateUnitOfWork())
{
// locking the content tree secures content types too
uow.WriteLock(Constants.Locks.ContentTree);
var contentType = GetContentType(contentTypeAlias); // + locks
if (contentType == null)
throw new ArgumentException("No content type with that alias.", nameof(contentTypeAlias)); // causes rollback
var content = new Content(name, parent, contentType);
CreateContent(uow, content, parent, userId, true);
uow.Complete();
return content;
}
}
private void CreateContent(IDatabaseUnitOfWork uow, Content content, IContent parent, int userId, bool withIdentity)
{
// 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.
var newArgs = parent != null
? new NewEventArgs(content, content.ContentType.Alias, parent)
: new NewEventArgs(content, content.ContentType.Alias, -1);
if (Creating.IsRaisedEventCancelled(newArgs, this))
{
content.WasCancelled = true;
return;
}
content.CreatorId = userId;
content.WriterId = userId;
if (withIdentity)
{
if (Saving.IsRaisedEventCancelled(new SaveEventArgs(content), this))
{
content.WasCancelled = true;
return;
}
var repo = uow.CreateRepository();
repo.AddOrUpdate(content);
repo.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, c));
Saved.RaiseEvent(new SaveEventArgs(content, false), this);
}
Created.RaiseEvent(new NewEventArgs(content, false, content.ContentType.Alias, parent), this);
var msg = withIdentity
? "Content '{0}' was created with Id {1}"
: "Content '{0}' was created";
Audit(AuditType.New, string.Format(msg, content.Name, content.Id), content.CreatorId, content.Id);
}
#endregion
#region Get, Has, Is
///
/// Gets an object by Id
///
/// Id of the Content to retrieve
///
public IContent GetById(int id)
{
using (var uow = UowProvider.CreateUnitOfWork())
{
uow.ReadLock(Constants.Locks.ContentTree);
var repository = uow.CreateRepository();
var content = repository.Get(id);
uow.Complete();
return content;
}
}
///
/// Gets an object by Id
///
/// Ids of the Content to retrieve
///
public IEnumerable GetByIds(IEnumerable ids)
{
var idsA = ids.ToArray();
if (idsA.Length == 0) return Enumerable.Empty();
using (var uow = UowProvider.CreateUnitOfWork())
{
uow.ReadLock(Constants.Locks.ContentTree);
var repository = uow.CreateRepository();
var content = repository.GetAll(idsA);
uow.Complete();
return content;
}
}
///
/// Gets an object by its 'UniqueId'
///
/// Guid key of the Content to retrieve
///
public IContent GetById(Guid key)
{
using (var uow = UowProvider.CreateUnitOfWork())
{
uow.ReadLock(Constants.Locks.ContentTree);
var repository = uow.CreateRepository();
var query = repository.Query.Where(x => x.Key == key);
var contents = repository.GetByQuery(query);
var content = contents.SingleOrDefault();
uow.Complete();
return content;
}
}
///
/// 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 uow = UowProvider.CreateUnitOfWork())
{
uow.ReadLock(Constants.Locks.ContentTree);
var repository = uow.CreateRepository();
var query = repository.Query.Where(x => x.ContentTypeId == id);
var content = repository.GetByQuery(query);
uow.Complete();
return content;
}
}
internal IEnumerable GetPublishedContentOfContentType(int id)
{
using (var uow = UowProvider.CreateUnitOfWork())
{
uow.ReadLock(Constants.Locks.ContentTree);
var repository = uow.CreateRepository();
var query = repository.Query.Where(x => x.ContentTypeId == id);
var content = repository.GetByPublishedVersion(query);
uow.Complete();
return content;
}
}
///
/// Gets a collection of objects by Level
///
/// The level to retrieve Content from
/// An Enumerable list of objects
/// Contrary to most methods, this method filters out trashed content items.
public IEnumerable GetByLevel(int level)
{
using (var uow = UowProvider.CreateUnitOfWork())
{
uow.ReadLock(Constants.Locks.ContentTree);
var repository = uow.CreateRepository();
var query = repository.Query.Where(x => x.Level == level && x.Trashed == false);
var content = repository.GetByQuery(query);
uow.Complete();
return content;
}
}
///
/// Gets a specific version of an item.
///
/// Id of the version to retrieve
/// An item
public IContent GetByVersion(Guid versionId)
{
using (var uow = UowProvider.CreateUnitOfWork())
{
uow.ReadLock(Constants.Locks.ContentTree);
var repository = uow.CreateRepository();
var content = repository.GetByVersion(versionId);
uow.Complete();
return content;
}
}
///
/// Gets a collection of an objects versions by Id
///
///
/// An Enumerable list of objects
public IEnumerable GetVersions(int id)
{
using (var uow = UowProvider.CreateUnitOfWork())
{
uow.ReadLock(Constants.Locks.ContentTree);
var repository = uow.CreateRepository();
var content = repository.GetAllVersions(id);
uow.Complete();
return 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
public IEnumerable GetAncestors(int id)
{
// intentionnaly not locking
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 rootId = Constants.System.Root.ToInvariantString();
var ids = content.Path.Split(',')
.Where(x => x != rootId && x != content.Id.ToString(CultureInfo.InvariantCulture)).Select(int.Parse).ToArray();
if (ids.Any() == false)
return new List();
using (var uow = UowProvider.CreateUnitOfWork())
{
uow.ReadLock(Constants.Locks.ContentTree);
var repository = uow.CreateRepository();
var ancestors = repository.GetAll(ids);
uow.Complete();
return ancestors;
}
}
///
/// 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 uow = UowProvider.CreateUnitOfWork())
{
uow.ReadLock(Constants.Locks.ContentTree);
var repository = uow.CreateRepository();
var query = repository.Query.Where(x => x.ParentId == id);
var children = repository.GetByQuery(query).OrderBy(x => x.SortOrder);
uow.Complete();
return children;
}
}
///
/// Gets a collection of published objects by Parent Id
///
/// Id of the Parent to retrieve Children from
/// An Enumerable list of published objects
public IEnumerable GetPublishedChildren(int id)
{
using (var uow = UowProvider.CreateUnitOfWork())
{
uow.ReadLock(Constants.Locks.ContentTree);
var repository = uow.CreateRepository();
var query = repository.Query.Where(x => x.ParentId == id && x.Published);
var children = repository.GetByQuery(query).OrderBy(x => x.SortOrder);
uow.Complete();
return children;
}
}
///
/// 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
///
/// 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, string filter)
{
Mandate.ParameterCondition(pageIndex >= 0, "pageIndex");
Mandate.ParameterCondition(pageSize > 0, "pageSize");
using (var uow = UowProvider.CreateUnitOfWork())
{
uow.ReadLock(Constants.Locks.ContentTree);
var repository = uow.CreateRepository();
var query = repository.Query;
//if the id is System Root, then just get all
if (id != Constants.System.Root)
query.Where(x => x.ParentId == id);
var children = repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, orderBySystemField, filter);
uow.Complete();
return children;
}
}
///
/// 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
///
/// 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, Direction orderDirection, bool orderBySystemField, string filter)
{
Mandate.ParameterCondition(pageIndex >= 0, nameof(pageIndex));
Mandate.ParameterCondition(pageSize > 0, nameof(pageSize));
using (var uow = UowProvider.CreateUnitOfWork())
{
uow.ReadLock(Constants.Locks.ContentTree);
var repository = uow.CreateRepository();
var query = repository.Query;
//if the id is System Root, then just get all
if (id != Constants.System.Root)
query.Where(x => x.Path.SqlContains($",{id},", TextColumnType.NVarchar));
var contents = repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, orderBySystemField, filter);
uow.Complete();
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 uow = UowProvider.CreateUnitOfWork())
{
uow.ReadLock(Constants.Locks.ContentTree);
var repository = uow.CreateRepository();
var query = repository.Query.Where(x => x.ParentId == parentId && x.Name.Contains(name));
var children = repository.GetByQuery(query);
uow.Complete();
return children;
}
}
///
/// 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)
{
using (var uow = UowProvider.CreateUnitOfWork())
{
uow.ReadLock(Constants.Locks.ContentTree);
var repository = uow.CreateRepository();
var content = GetById(id);
if (content == null)
{
uow.Complete(); // else causes rollback
return Enumerable.Empty();
}
var pathMatch = content.Path + ",";
var query = repository.Query.Where(x => x.Id != content.Id && x.Path.StartsWith(pathMatch));
var descendants = repository.GetByQuery(query);
uow.Complete();
return descendants;
}
}
///
/// 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 uow = UowProvider.CreateUnitOfWork())
{
uow.ReadLock(Constants.Locks.ContentTree);
var repository = uow.CreateRepository();
var pathMatch = content.Path + ",";
var query = repository.Query.Where(x => x.Id != content.Id && x.Path.StartsWith(pathMatch));
var descendants = repository.GetByQuery(query);
uow.Complete();
return descendants;
}
}
///
/// 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)
{
// intentionnaly not locking
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);
}
///
/// 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 uow = UowProvider.CreateUnitOfWork())
{
uow.ReadLock(Constants.Locks.ContentTree);
var repository = uow.CreateRepository();
var query = repository.Query.Where(x => x.ParentId == Constants.System.Root);
var content = repository.GetByQuery(query);
uow.Complete();
return content;
}
}
///
/// Gets all published content items
///
///
internal IEnumerable GetAllPublished()
{
using (var uow = UowProvider.CreateUnitOfWork())
{
uow.ReadLock(Constants.Locks.ContentTree);
var repository = uow.CreateRepository();
var query = repository.Query.Where(x => x.Trashed == false);
var content = repository.GetByPublishedVersion(query);
uow.Complete();
return content;
}
}
///
/// 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 uow = UowProvider.CreateUnitOfWork())
{
uow.ReadLock(Constants.Locks.ContentTree);
var repository = uow.CreateRepository();
var query = repository.Query.Where(x => x.Published && x.ExpireDate <= DateTime.Now);
var content = repository.GetByQuery(query);
uow.Complete();
return content;
}
}
///
/// 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 uow = UowProvider.CreateUnitOfWork())
{
uow.ReadLock(Constants.Locks.ContentTree);
var repository = uow.CreateRepository();
var query = repository.Query.Where(x => x.Published == false && x.ReleaseDate <= DateTime.Now);
var content = repository.GetByQuery(query);
uow.Complete();
return content;
}
}
///
/// Gets a collection of an objects, which resides in the Recycle Bin
///
/// An Enumerable list of objects
public IEnumerable GetContentInRecycleBin()
{
using (var uow = UowProvider.CreateUnitOfWork())
{
uow.ReadLock(Constants.Locks.ContentTree);
var repository = uow.CreateRepository();
var query = repository.Query.Where(x => x.Path.Contains(Constants.System.RecycleBinContent.ToInvariantString()));
var content = repository.GetByQuery(query);
uow.Complete();
return content;
}
}
///
/// 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;
}
///
/// 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 uow = UowProvider.CreateUnitOfWork())
{
uow.ReadLock(Constants.Locks.ContentTree);
var repository = uow.CreateRepository();
var query = repository.Query.Where(x => x.Published && x.Id == id && x.Trashed == false);
var count = repository.Count(query);
uow.Complete();
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)
{
// fast
if (content.ParentId == Constants.System.Root) return true; // root content is always publishable
if (content.Trashed) return false; // trashed content is never publishable
// not trashed and has a parent: publishable if the parent is path-published
using (var uow = UowProvider.CreateUnitOfWork())
{
uow.ReadLock(Constants.Locks.ContentTree);
var repo = uow.CreateRepository();
var parent = repo.Get(content.ParentId);
if (parent == null)
throw new Exception("Out of sync."); // causes rollback
var isPublishable = repo.IsPathPublished(parent);
uow.Complete();
return isPublishable;
}
}
public bool IsPathPublished(IContent content)
{
using (var uow = UowProvider.CreateUnitOfWork())
{
uow.ReadLock(Constants.Locks.ContentTree);
var repo = uow.CreateRepository();
var isPathPublished = repo.IsPathPublished(content);
uow.Complete();
return isPathPublished;
}
}
#endregion
#region Save, Publish, Unpublish
///
/// 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);
}
}
///
/// 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 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)
{
var evtMsgs = EventMessagesFactory.Get();
if (raiseEvents && Saving.IsRaisedEventCancelled(new SaveEventArgs(content, evtMsgs), this))
return OperationStatus.Attempt.Cancel(evtMsgs);
using (var uow = UowProvider.CreateUnitOfWork())
{
uow.WriteLock(Constants.Locks.ContentTree);
var repository = uow.CreateRepository();
if (content.HasIdentity == false)
content.CreatorId = userId;
content.WriterId = userId;
// saving the Published version => indicate we are .Saving
// saving the Unpublished version => remains .Unpublished
if (content.Published)
content.ChangePublishedState(PublishedState.Saving);
repository.AddOrUpdate(content);
repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, c));
uow.Complete();
}
if (raiseEvents)
Saved.RaiseEvent(new SaveEventArgs(content, false, evtMsgs), this);
Audit(AuditType.Save, "Save Content performed by user", userId, content.Id);
return OperationStatus.Attempt.Succeed(evtMsgs);
}
///
/// 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);
}
///
/// 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 evtMsgs = EventMessagesFactory.Get();
var contentsA = contents.ToArray();
if (raiseEvents && Saving.IsRaisedEventCancelled(new SaveEventArgs(contentsA, evtMsgs), this))
return OperationStatus.Attempt.Cancel(evtMsgs);
using (var uow = UowProvider.CreateUnitOfWork())
{
uow.WriteLock(Constants.Locks.ContentTree);
var repository = uow.CreateRepository();
foreach (var content in contentsA)
{
if (content.HasIdentity == false)
content.CreatorId = userId;
content.WriterId = userId;
// saving the Published version => indicate we are .Saving
// saving the Unpublished version => remains .Unpublished
if (content.Published)
content.ChangePublishedState(PublishedState.Saving);
repository.AddOrUpdate(content);
repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, c));
}
uow.Complete();
}
if (raiseEvents)
Saved.RaiseEvent(new SaveEventArgs(contentsA, false, evtMsgs), this);
Audit(AuditType.Save, "Bulk Save content performed by user", userId == -1 ? 0 : userId, Constants.System.Root);
return OperationStatus.Attempt.Succeed(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
[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
Attempt IContentServiceOperations.SaveAndPublish(IContent content, int userId, bool raiseEvents)
{
return SaveAndPublishDo(content, userId, raiseEvents);
}
///
/// 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 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);
}
///
/// 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;
}
///
/// 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);
}
///
/// 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);
}
///
/// 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)
{
// 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
var result = PublishWithChildrenDo(content, userId, true);
// FirstOrDefault() is a pain to use with structs and result contain Attempt structs
// so use this code, which is fast and works - and please ReSharper do NOT suggest otherwise
// ReSharper disable once LoopCanBeConvertedToQuery
foreach (var r in result)
if (r.Result.ContentItem.Id == content.Id) return r.Success;
return false;
}
///
/// 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);
}
///
/// Used to perform scheduled publishing/unpublishing
///
public IEnumerable> PerformScheduledPublish()
{
//TODO: Do I need to move all of this logic to the repo? Or wrap this all in a unit of work?
foreach (var d in GetContentForRelease())
{
d.ReleaseDate = null;
var result = SaveAndPublishWithStatus(d, (int)d.GetWriterProfile(_userService).Id);
if (result.Success == false)
{
if (result.Exception != null)
{
Logger.Error("Could not published the document (" + d.Id + ") based on it's scheduled release, status result: " + result.Result.StatusType, result.Exception);
}
else
{
Logger.Warn("Could not published the document (" + d.Id + ") based on it's scheduled release. Status result: " + result.Result.StatusType);
}
}
yield return result;
}
foreach (var d in GetContentForExpiration())
{
try
{
d.ExpireDate = null;
UnPublish(d, (int)d.GetWriterProfile(_userService).Id);
}
catch (Exception ee)
{
Logger.Error($"Error unpublishing node {d.Id}", ee);
throw;
}
}
}
///
/// 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);
}
#endregion
#region Delete
///
/// 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 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();
if (Deleting.IsRaisedEventCancelled(new DeleteEventArgs(content, evtMsgs), this))
return OperationStatus.Attempt.Cancel(evtMsgs);
using (var uow = UowProvider.CreateUnitOfWork())
{
uow.WriteLock(Constants.Locks.ContentTree);
var repository = uow.CreateRepository();
// if it's not trashed yet, and published, we should unpublish
// but... UnPublishing event makes no sense (not going to cancel?) and no need to save
// just raise the event
if (content.Trashed == false && content.HasPublishedVersion)
UnPublished.RaiseEvent(new PublishEventArgs(content, false, false), this);
DeleteLocked(repository, content);
uow.Complete();
}
Audit(AuditType.Delete, "Delete Content performed by user", userId, content.Id);
return OperationStatus.Attempt.Succeed(evtMsgs);
}
private void DeleteLocked(IContentRepository repository, IContent content)
{
// then recursively delete descendants, bottom-up
// just repository.Delete + an event
var stack = new Stack();
stack.Push(content);
var level = 1;
while (stack.Count > 0)
{
var c = stack.Peek();
IContent[] cc;
if (c.Level == level)
while ((cc = c.Children().ToArray()).Length > 0)
{
foreach (var ci in cc)
stack.Push(ci);
c = cc[cc.Length - 1];
}
c = stack.Pop();
level = c.Level;
repository.Delete(c);
var args = new DeleteEventArgs(c, false); // raise event & get flagged files
Deleted.RaiseEvent(args, this);
IOHelper.DeleteFiles(args.MediaFilesToDelete, // remove flagged files
(file, e) => Logger.Error("An error occurred while deleting file attached to nodes: " + file, e));
}
}
//TODO:
// both DeleteVersions methods below have an issue. Sort of. They do NOT take care of files the way
// Delete does - for a good reason: the file may be referenced by other, non-deleted, versions. BUT,
// if that's not the case, then the file will never be deleted, because when we delete the content,
// the version referencing the file will not be there anymore. SO, we can leak files.
///
/// 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;
using (var uow = UowProvider.CreateUnitOfWork())
{
uow.WriteLock(Constants.Locks.ContentTree);
var repository = uow.CreateRepository();
repository.DeleteVersions(id, versionDate);
uow.Complete();
}
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)
{
if (DeletingVersions.IsRaisedEventCancelled(new DeleteRevisionsEventArgs(id, /*specificVersion:*/ versionId), this))
return;
if (deletePriorVersions)
{
var content = GetByVersion(versionId);
DeleteVersions(id, content.UpdateDate, userId);
}
using (var uow = UowProvider.CreateUnitOfWork())
{
uow.WriteLock(Constants.Locks.ContentTree);
var repository = uow.CreateRepository();
repository.DeleteVersion(versionId);
uow.Complete();
}
DeletedVersions.RaiseEvent(new DeleteRevisionsEventArgs(id, false,/* specificVersion:*/ versionId), this);
Audit(AuditType.Delete, "Delete Content by version performed by user", userId, Constants.System.Root);
}
#endregion
#region Move, RecycleBin
///
/// 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);
}
///
/// 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();
var moves = new List>();
using (var uow = UowProvider.CreateUnitOfWork())
{
uow.WriteLock(Constants.Locks.ContentTree);
var repository = uow.CreateRepository();
var originalPath = content.Path;
if (Trashing.IsRaisedEventCancelled(new MoveEventArgs(new MoveEventInfo(content, originalPath, Constants.System.RecycleBinContent)), this))
return OperationStatus.Attempt.Cancel(evtMsgs); // causes rollback
// if it's published we may want to force-unpublish it - that would be backward-compatible... but...
// making a radical decision here: trashing is equivalent to moving under an unpublished node so
// it's NOT unpublishing, only the content is now masked - allowing us to restore it if wanted
//if (content.HasPublishedVersion)
//{ }
PerformMoveLocked(repository, content, Constants.System.RecycleBinContent, null, userId, moves, true);
uow.Complete();
}
var moveInfo = moves
.Select(x => new MoveEventInfo(x.Item1, x.Item2, x.Item1.ParentId))
.ToArray();
Trashed.RaiseEvent(new MoveEventArgs(false, evtMsgs, moveInfo), this);
Audit(AuditType.Move, "Move Content to Recycle Bin performed by user", userId, content.Id);
return OperationStatus.Attempt.Succeed(evtMsgs);
}
///
/// 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)
{
// if moving to the recycle bin then use the proper method
if (parentId == Constants.System.RecycleBinContent)
{
MoveToRecycleBin(content, userId);
return;
}
var moves = new List>();
using (var uow = UowProvider.CreateUnitOfWork())
{
uow.WriteLock(Constants.Locks.ContentTree);
var repository = uow.CreateRepository();
var parent = parentId == Constants.System.Root ? null : GetById(parentId);
if (parentId != Constants.System.Root && (parent == null || parent.Trashed))
throw new InvalidOperationException("Parent does not exist or is trashed."); // causes rollback
if (Moving.IsRaisedEventCancelled(new MoveEventArgs(new MoveEventInfo(content, content.Path, parentId)), this))
return; // causes rollback
// if content was trashed, and since we're not moving to the recycle bin,
// indicate that the trashed status should be changed to false, else just
// leave it unchanged
var trashed = content.Trashed ? false : (bool?)null;
// if the content was trashed under another content, and so has a published version,
// it cannot move back as published but has to be unpublished first - that's for the
// root content, everything underneath will retain its published status
if (content.Trashed && content.HasPublishedVersion)
{
// however, it had been masked when being trashed, so there's no need for
// any special event here - just change its state
content.ChangePublishedState(PublishedState.Unpublishing);
}
PerformMoveLocked(repository, content, parentId, parent, userId, moves, trashed);
uow.Complete();
}
var moveInfo = moves //changes
.Select(x => new MoveEventInfo(x.Item1, x.Item2, x.Item1.ParentId))
.ToArray();
Moved.RaiseEvent(new MoveEventArgs(false, moveInfo), this);
Audit(AuditType.Move, "Move Content performed by user", userId, content.Id);
}
// MUST be called from within WriteLock
// trash indicates whether we are trashing, un-trashing, or not changing anything
private void PerformMoveLocked(IContentRepository repository,
IContent content, int parentId, IContent parent, int userId,
ICollection> moves,
bool? trash)
{
content.WriterId = userId;
content.ParentId = parentId;
// get the level delta (old pos to new pos)
var levelDelta = parent == null
? 1 - content.Level + (parentId == Constants.System.RecycleBinContent ? 1 : 0)
: parent.Level + 1 - content.Level;
var paths = new Dictionary();
moves.Add(Tuple.Create(content, content.Path)); // capture original path
// these will be updated by the repo because we changed parentId
//content.Path = (parent == null ? "-1" : parent.Path) + "," + content.Id;
//content.SortOrder = ((ContentRepository) repository).NextChildSortOrder(parentId);
//content.Level += levelDelta;
PerformMoveContentLocked(repository, content, userId, trash);
// BUT content.Path will be updated only when the UOW commits, and
// because we want it now, we have to calculate it by ourselves
//paths[content.Id] = content.Path;
paths[content.Id] = (parent == null ? (parentId == Constants.System.RecycleBinContent ? "-1,-20" : "-1") : parent.Path) + "," + content.Id;
var descendants = GetDescendants(content);
foreach (var descendant in descendants)
{
moves.Add(Tuple.Create(descendant, descendant.Path)); // capture original path
// update path and level since we do not update parentId
descendant.Path = paths[descendant.Id] = paths[descendant.ParentId] + "," + descendant.Id;
descendant.Level += levelDelta;
PerformMoveContentLocked(repository, descendant, userId, trash);
}
}
private static void PerformMoveContentLocked(IContentRepository repository, IContent content, int userId,
bool? trash)
{
if (trash.HasValue) ((ContentBase) content).Trashed = trash.Value;
content.WriterId = userId;
repository.AddOrUpdate(content);
}
///
/// Empties the Recycle Bin by deleting all that resides in the bin
///
public void EmptyRecycleBin()
{
var nodeObjectType = new Guid(Constants.ObjectTypes.Document);
var deleted = new List();
var evtMsgs = EventMessagesFactory.Get(); // todo - and then?
using (var uow = UowProvider.CreateUnitOfWork())
{
uow.WriteLock(Constants.Locks.ContentTree);
var repository = uow.CreateRepository();
// v7 EmptyingRecycleBin and EmptiedRecycleBin events are greatly simplified since
// each deleted items will have its own deleting/deleted events. so, files and such
// are managed by Delete, and not here.
// no idea what those events are for, keep a simplified version
if (EmptyingRecycleBin.IsRaisedEventCancelled(new RecycleBinEventArgs(nodeObjectType), this))
return; // causes rollback
// emptying the recycle bin means deleting whetever is in there - do it properly!
var query = repository.Query.Where(x => x.ParentId == Constants.System.RecycleBinContent);
var contents = repository.GetByQuery(query).ToArray();
foreach (var content in contents)
{
DeleteLocked(repository, content);
deleted.Add(content);
}
EmptiedRecycleBin.RaiseEvent(new RecycleBinEventArgs(nodeObjectType, true), this);
uow.Complete();
}
Audit(AuditType.Delete, "Empty Content Recycle Bin performed by user", 0, Constants.System.RecycleBinContent);
}
#endregion
#region Others
///
/// 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)
{
var copy = content.DeepCloneWithResetIdentities();
copy.ParentId = parentId;
if (Copying.IsRaisedEventCancelled(new CopyEventArgs(content, copy, parentId), this))
return null;
// fixme - relateToOriginal is ignored?!
using (var uow = UowProvider.CreateUnitOfWork())
{
uow.WriteLock(Constants.Locks.ContentTree);
var repository = uow.CreateRepository();
// a copy is .Saving and will be .Unpublished
if (copy.Published)
copy.ChangePublishedState(PublishedState.Saving);
// update the create author and last edit author
copy.CreatorId = userId;
copy.WriterId = userId;
// save
repository.AddOrUpdate(copy);
repository.AddOrUpdatePreviewXml(copy, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, c));
uow.Flush(); // ensure copy has an ID - fixme why?
if (recursive)
{
// process descendants
var copyIds = new Dictionary();
copyIds[content.Id] = copy;
foreach (var descendant in GetDescendants(content))
{
var dcopy = descendant.DeepCloneWithResetIdentities();
//dcopy.ParentId = copyIds[descendant.ParentId];
var descendantParentId = descendant.ParentId;
((Content) dcopy).SetLazyParentId(new Lazy(() => copyIds[descendantParentId].Id));
if (dcopy.Published)
dcopy.ChangePublishedState(PublishedState.Saving);
dcopy.CreatorId = userId;
dcopy.WriterId = userId;
repository.AddOrUpdate(dcopy);
repository.AddOrUpdatePreviewXml(dcopy, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, c));
copyIds[descendant.Id] = dcopy;
}
}
// fixme tag & tree issue
// tags code handling has been removed here
// - tags should be handled by the content repository
// - a copy is unpublished and therefore has no impact on tags in DB
uow.Complete();
}
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;
content.CreatorId = userId;
// need to make sure that the repository is going to save a new version
// but if we're not changing anything, the repository would not save anything
// so - make sure the property IS dirty, doing a flip-flop with an impossible value
content.WriterId = -1;
content.WriterId = userId;
using (var uow = UowProvider.CreateUnitOfWork())
{
uow.WriteLock(Constants.Locks.ContentTree);
var repository = uow.CreateRepository();
// a rolled back version is .Saving and will be .Unpublished
content.ChangePublishedState(PublishedState.Saving);
repository.AddOrUpdate(content);
repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, c));
uow.Complete();
}
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)
{
var itemsA = items.ToArray();
if (itemsA.Length == 0) return true;
if (raiseEvents && Saving.IsRaisedEventCancelled(new SaveEventArgs(itemsA), this))
return false;
var published = new List();
var saved = new List();
using (var uow = UowProvider.CreateUnitOfWork())
{
uow.WriteLock(Constants.Locks.ContentTree);
var repository = uow.CreateRepository();
var sortOrder = 0;
foreach (var content in itemsA)
{
// 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 == sortOrder)
{
sortOrder++;
continue;
}
// else update
content.SortOrder = sortOrder++;
content.WriterId = userId;
// if it's published, register it, no point running StrategyPublish
// since we're not really publishing it and it cannot be cancelled etc
if (content.Published)
published.Add(content);
else if (content.HasPublishedVersion)
published.Add(GetByVersion(content.PublishedVersionGuid));
// save
saved.Add(content);
repository.AddOrUpdate(content);
repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, c));
}
foreach (var content in published)
repository.AddOrUpdateContentXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, c));
uow.Complete();
}
if (raiseEvents)
Saved.RaiseEvent(new SaveEventArgs(saved, false), this);
if (raiseEvents && published.Any())
Published.RaiseEvent(new PublishEventArgs(published, false, false), this);
Audit(AuditType.Sort, "Sorting content performed by user", userId, 0);
return true;
}
#endregion
#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 uow = UowProvider.CreateUnitOfWork())
{
uow.ReadLock(Constants.Locks.ContentTree);
var repository = uow.CreateRepository();
var descendants = GetPublishedDescendantsLocked(repository, content);
uow.Complete();
return descendants;
}
}
internal IEnumerable GetPublishedDescendantsLocked(IContentRepository repository, IContent content)
{
var pathMatch = content.Path + ",";
var query = repository.Query.Where(x => x.Id != content.Id && x.Path.StartsWith(pathMatch) /*&& x.Trashed == false*/);
var contents = repository.GetByPublishedVersion(query);
// beware! contents contains all published version below content
// including those that are not directly published because below an unpublished content
// these must be filtered out here
var parents = new List { content.Id };
foreach (var c in contents)
{
if (parents.Contains(c.ParentId))
{
yield return c;
parents.Add(c.Id);
}
}
}
#endregion
#region Private Methods
private void Audit(AuditType type, string message, int userId, int objectId)
{
using (var uow = UowProvider.CreateUnitOfWork())
{
var repo = uow.CreateRepository();
repo.AddOrUpdate(new AuditItem(objectId, message, type, userId));
uow.Complete();
}
}
///
/// 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(nameof(content));
var evtMsgs = EventMessagesFactory.Get();
var publishedItems = new List(); // this is for events
Attempt[] attempts;
using (var uow = UowProvider.CreateUnitOfWork())
{
uow.WriteLock(Constants.Locks.ContentTree);
var repository = uow.CreateRepository();
// fail fast + use in alreadyChecked below to avoid duplicate checks
var attempt = EnsurePublishable(content, evtMsgs);
if (attempt.Success)
attempt = StrategyCanPublish(content, userId, evtMsgs);
if (attempt.Success == false)
return new[] { attempt }; // causes rollback
var contents = new List { content }; //include parent item
contents.AddRange(GetDescendants(content));
// publish using the strategy - for descendants,
// - published w/out changes: nothing to do
// - published w/changes: publish those changes
// - unpublished: publish if includeUnpublished, otherwise ignore
var alreadyChecked = new[] { content };
attempts = StrategyPublishWithChildren(contents, alreadyChecked, userId, evtMsgs, includeUnpublished).ToArray();
foreach (var status in attempts.Where(x => x.Success).Select(x => x.Result))
{
// save them all, even those that are .Success because of (.StatusType == PublishStatusType.SuccessAlreadyPublished)
// so we bump the date etc
var publishedItem = status.ContentItem;
publishedItem.WriterId = userId;
repository.AddOrUpdate(publishedItem);
repository.AddOrUpdatePreviewXml(publishedItem, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, c));
repository.AddOrUpdateContentXml(publishedItem, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, c));
publishedItems.Add(publishedItem);
}
uow.Complete();
}
Published.RaiseEvent(new PublishEventArgs(publishedItems, false, false), this);
Audit(AuditType.Publish, "Publish with Children performed by user", userId, content.Id);
return attempts;
}
///
/// 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)
{
// fixme kill omitCacheRefresh!
var evtMsgs = EventMessagesFactory.Get();
using (var uow = UowProvider.CreateUnitOfWork())
{
uow.WriteLock(Constants.Locks.ContentTree);
var repository = uow.CreateRepository();
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;
if (content.Published == false && content.HasPublishedVersion == false)
{
uow.Complete();
return Attempt.Succeed(new UnPublishStatus(UnPublishedStatusType.SuccessAlreadyUnPublished, evtMsgs, content)); // already unpublished
}
// strategy
var attempt = StrategyCanUnPublish(content, userId, evtMsgs);
if (attempt == false) return attempt; // causes rollback
attempt = StrategyUnPublish(content, true, userId, evtMsgs);
if (attempt == false) return attempt; // causes rollback
content.WriterId = userId;
repository.AddOrUpdate(content);
// fixme delete xml from database! was in _publishingStrategy.UnPublishingFinalized(content);
repository.DeleteContentXml(content);
uow.Complete();
}
UnPublished.RaiseEvent(new PublishEventArgs(content, false, false), this);
return Attempt.Succeed(new UnPublishStatus(UnPublishedStatusType.Success, evtMsgs, content));
}
///
/// 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 && Saving.IsRaisedEventCancelled(new SaveEventArgs(content), this))
return Attempt.Fail(new PublishStatus(PublishStatusType.FailedCancelledByEvent, evtMsgs, content));
var isNew = content.IsNewEntity();
var previouslyPublished = content.HasIdentity && content.HasPublishedVersion;
var status = default(Attempt);
using (var uow = UowProvider.CreateUnitOfWork())
{
uow.WriteLock(Constants.Locks.ContentTree);
var repository = uow.CreateRepository();
// fixme - EnsurePublishable vs StrategyCanPublish?
// EnsurePublishable ensures that path published is ok
// StrategyCanPublish ensures other things including valid properties
// should we merge or?!
// ensure content is publishable, and try to publish
status = EnsurePublishable(content, evtMsgs);
if (status.Success)
{
// strategy handles events, and various business rules eg release & expire
// dates, trashed status...
status = StrategyPublish(content, false, userId, evtMsgs);
}
// save - always, even if not publishing (this is SaveAndPublish)
if (content.HasIdentity == false)
content.CreatorId = userId;
content.WriterId = userId;
repository.AddOrUpdate(content);
repository.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, c));
if (content.Published)
repository.AddOrUpdateContentXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, c));
uow.Complete();
}
if (status.Success == false)
{
// fixme what about the saved event?
return status;
}
Published.RaiseEvent(new PublishEventArgs(content, false, false), this);
// if was not published and now is... descendants that were 'published' (but
// had an unpublished ancestor) are 're-published' ie not explicitely published
// but back as 'published' nevertheless
if (isNew == false && previouslyPublished == false)
{
if (HasChildren(content.Id))
{
var descendants = GetPublishedDescendants(content).ToArray();
Published.RaiseEvent(new PublishEventArgs(descendants, false, false), this);
}
}
Audit(AuditType.Publish, "Save and Publish performed by user", userId, content.Id);
return status;
}
private Attempt EnsurePublishable(IContent content, EventMessages evtMsgs)
{
// root content can be published
var checkParents = content.ParentId == Constants.System.Root;
// trashed content cannot be published
if (checkParents == false && content.ParentId != Constants.System.RecycleBinContent)
{
// ensure all ancestors are published
// because content may be new its Path may be null - start with parent
var path = content.Path ?? content.Parent().Path;
if (path != null) // if parent is also null, give up
{
var ancestorIds = path.Split(',')
.Skip(1) // remove leading "-1"
.Reverse()
.Select(int.Parse);
if (content.Path != null)
ancestorIds = ancestorIds.Skip(1); // remove trailing content.Id
if (ancestorIds.All(HasPublishedVersion))
checkParents = true;
}
}
if (checkParents == false)
{
Logger.Info($"Content '{content.Name}' with Id '{content.Id}' could not be published because its parent is not published.");
return Attempt.Fail(new PublishStatus(PublishStatusType.FailedPathNotPublished, evtMsgs, content));
}
// fixme - should we do it - are we doing it for descendants too?
if (content.IsValid() == false)
{
Logger.Info($"Content '{content.Name}' with Id '{content.Id}' could not be published because of invalid properties.");
return Attempt.Fail(new PublishStatus(PublishStatusType.FailedContentInvalid, evtMsgs, content)
{
InvalidProperties = ((ContentBase)content).LastInvalidProperties
});
}
return Attempt.Succeed(new PublishStatus(PublishStatusType.Success, evtMsgs, content));
}
#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;
///
/// Occurs before publish
///
public static event TypedEventHandler> Publishing;
///
/// Occurs after publish
///
public static event TypedEventHandler> Published;
///
/// Occurs before unpublish
///
public static event TypedEventHandler> UnPublishing;
///
/// Occurs after unpublish
///
public static event TypedEventHandler> UnPublished;
#endregion
#region Publishing Strategies
// prob. want to find nicer names?
internal Attempt StrategyCanPublish(IContent content, int userId, EventMessages evtMsgs)
{
if (Publishing.IsRaisedEventCancelled(new PublishEventArgs(content, evtMsgs), this))
{
Logger.Info($"Content '{content.Name}' with Id '{content.Id}' will not be published, the event was cancelled.");
return Attempt.Fail(new PublishStatus(PublishStatusType.FailedCancelledByEvent, evtMsgs, content));
}
// check if the content is valid
if (content.IsValid() == false)
{
Logger.Info($"Content '{content.Name}' with Id '{content.Id}' could not be published because of invalid properties.");
return Attempt.Fail(new PublishStatus(PublishStatusType.FailedContentInvalid, evtMsgs, content)
{
InvalidProperties = ((ContentBase)content).LastInvalidProperties
});
}
// check if the Content is Expired
if (content.Status == ContentStatus.Expired)
{
Logger.Info($"Content '{content.Name}' with Id '{content.Id}' has expired and could not be published.");
return Attempt.Fail(new PublishStatus(PublishStatusType.FailedHasExpired, evtMsgs, content));
}
// check if the Content is Awaiting Release
if (content.Status == ContentStatus.AwaitingRelease)
{
Logger.Info($"Content '{content.Name}' with Id '{content.Id}' is awaiting release and could not be published.");
return Attempt.Fail(new PublishStatus(PublishStatusType.FailedAwaitingRelease, evtMsgs, content));
}
// check if the Content is Trashed
if (content.Status == ContentStatus.Trashed)
{
Logger.Info($"Content '{content.Name}' with Id '{content.Id}' is trashed and could not be published.");
return Attempt.Fail(new PublishStatus(PublishStatusType.FailedIsTrashed, evtMsgs, content));
}
return Attempt.Succeed(new PublishStatus(content, evtMsgs));
}
internal Attempt StrategyPublish(IContent content, bool alreadyCheckedCanPublish, int userId, EventMessages evtMsgs)
{
var attempt = alreadyCheckedCanPublish
? Attempt.Succeed(new PublishStatus(content, evtMsgs)) // already know we can
: StrategyCanPublish(content, userId, evtMsgs); // else check
if (attempt.Success == false)
return attempt;
// change state to publishing
content.ChangePublishedState(PublishedState.Publishing);
Logger.Info($"Content '{content.Name}' with Id '{content.Id}' has been published.");
return attempt;
}
///
/// Publishes a list of content items
///
/// Contents, ordered by level ASC
/// Contents for which we've already checked CanPublish
///
///
/// Indicates whether to publish content that is completely unpublished (has no published
/// version). If false, will only publish already published content with changes. Also impacts what happens if publishing
/// fails (see remarks).
///
///
/// Navigate content & descendants top-down and for each,
/// - if it is published
/// - and unchanged, do nothing
/// - else (has changes), publish those changes
/// - if it is not published
/// - and at top-level, publish
/// - or includeUnpublished is true, publish
/// - else do nothing & skip the underlying branch
///
/// When publishing fails
/// - if content has no published version, skip the underlying branch
/// - else (has published version),
/// - if includeUnpublished is true, process the underlying branch
/// - else, do not process the underlying branch
///
internal IEnumerable> StrategyPublishWithChildren(IEnumerable contents, IEnumerable alreadyChecked, int userId, EventMessages evtMsgs, bool includeUnpublished = true)
{
var statuses = new List>();
var alreadyCheckedA = (alreadyChecked ?? Enumerable.Empty()).ToArray();
// list of ids that we exclude because they could not be published
var excude = new List();
var topLevel = -1;
foreach (var content in contents)
{
// initialize - content is ordered by level ASC
if (topLevel < 0)
topLevel = content.Level;
if (excude.Contains(content.ParentId))
{
// parent is excluded, so exclude content too
Logger.Info($"Content '{content.Name}' with Id '{content.Id}' will not be published because it's parent's publishing action failed or was cancelled.");
excude.Add(content.Id);
// status has been reported for an ancestor and that one is excluded => no status
continue;
}
if (content.Published && content.Level > topLevel) // topLevel we DO want to (re)publish
{
// newest is published already
statuses.Add(Attempt.Succeed(new PublishStatus(PublishStatusType.SuccessAlreadyPublished, evtMsgs, content)));
continue;
}
if (content.HasPublishedVersion)
{
// newest is published already but we are topLevel, or
// newest is not published, but another version is - publish newest
var r = StrategyPublish(content, alreadyCheckedA.Contains(content), userId, evtMsgs);
if (r.Success == false)
{
// we tried to publish and it failed, but it already had / still has a published version,
// the rule in remarks says that we should skip the underlying branch if includeUnpublished
// is false, else process it - not that it makes much sense, but keep it like that for now
if (includeUnpublished == false)
excude.Add(content.Id);
}
statuses.Add(r);
continue;
}
if (content.Level == topLevel || includeUnpublished)
{
// content has no published version, and we want to publish it, either
// because it is top-level or because we include unpublished.
// if publishing fails, and because content does not have a published
// version at all, ensure we do not process its descendants
var r = StrategyPublish(content, alreadyCheckedA.Contains(content), userId, evtMsgs);
if (r.Success == false)
excude.Add(content.Id);
statuses.Add(r);
continue;
}
// content has no published version, and we don't want to publish it
excude.Add(content.Id); // ignore everything below it
// content is not even considered, really => no status
}
return statuses;
}
internal Attempt StrategyCanUnPublish(IContent content, int userId, EventMessages evtMsgs)
{
// fire UnPublishing event
if (UnPublishing.IsRaisedEventCancelled(new PublishEventArgs(content, evtMsgs), this))
{
Logger.Info($"Content '{content.Name}' with Id '{content.Id}' will not be unpublished, the event was cancelled.");
return Attempt.Fail(new UnPublishStatus(UnPublishedStatusType.FailedCancelledByEvent, evtMsgs, content));
}
return Attempt.Succeed(new UnPublishStatus(content, evtMsgs));
}
internal Attempt StrategyUnPublish(IContent content, bool alreadyCheckedCanUnPublish, int userId, EventMessages evtMsgs)
{
// content should (is assumed to) be the newest version, which may not be published,
// don't know how to test this, so it's not verified
var attempt = alreadyCheckedCanUnPublish
? Attempt.Succeed(new UnPublishStatus(content, evtMsgs)) // already know we can
: StrategyCanUnPublish(content, userId, evtMsgs);
if (attempt.Success == false)
return attempt;
// if Content has a release date set to before now, it should be removed so it doesn't interrupt an unpublish
// otherwise it would remain released == published
if (content.ReleaseDate.HasValue && content.ReleaseDate.Value <= DateTime.Now)
{
content.ReleaseDate = null;
Logger.Info($"Content '{content.Name}' with Id '{content.Id}' had its release date removed, because it was unpublished.");
}
// version is published or unpublished, but content is published
// change state to unpublishing
content.ChangePublishedState(PublishedState.Unpublishing);
Logger.Info($"Content '{content.Name}' with Id '{content.Id}' has been unpublished.");
return attempt;
}
internal IEnumerable> StrategyUnPublish(IEnumerable content, int userId, EventMessages evtMsgs)
{
return content.Select(x => StrategyUnPublish(x, false, userId, evtMsgs));
}
#endregion
#region Content Types
///
/// 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.
var moves = new List>();
using (var uow = UowProvider.CreateUnitOfWork())
{
uow.WriteLock(Constants.Locks.ContentTree);
var repository = uow.CreateRepository();
// fixme what about content that has the contenttype as part of its composition?
var query = repository.Query.Where(x => x.ContentTypeId == contentTypeId);
var contents = repository.GetByQuery(query).ToArray();
if (Deleting.IsRaisedEventCancelled(new DeleteEventArgs(contents), this))
return; // causes rollback
// order by level, descending, so deepest first - that way, we cannot move
// a content of the deleted type, to the recycle bin (and then delete it...)
foreach (var content in contents.OrderByDescending(x => x.ParentId))
{
// if it's not trashed yet, and published, we should unpublish
// but... UnPublishing event makes no sense (not going to cancel?) and no need to save
// just raise the event
if (content.Trashed == false && content.HasPublishedVersion)
UnPublished.RaiseEvent(new PublishEventArgs(content, false, false), this);
// if current content has children, move them to trash
var c = content;
var childQuery = repository.Query.Where(x => x.Path.StartsWith(c.Path));
var children = repository.GetByQuery(childQuery);
foreach (var child in children.Where(x => x.ContentTypeId != contentTypeId))
{
// see MoveToRecycleBin
PerformMoveLocked(repository, child, Constants.System.RecycleBinContent, null, userId, moves, true);
}
// delete content
// triggers the deleted event (and handles the files)
DeleteLocked(repository, content);
}
uow.Complete();
}
var moveInfos = moves
.Select(x => new MoveEventInfo(x.Item1, x.Item2, x.Item1.ParentId))
.ToArray();
if (moveInfos.Length > 0)
Trashed.RaiseEvent(new MoveEventArgs(false, moveInfos), this);
Audit(AuditType.Delete, $"Delete Content of Type {contentTypeId} performed by user", userId, Constants.System.Root);
}
private IContentType GetContentType(string contentTypeAlias)
{
using (var uow = UowProvider.CreateUnitOfWork())
{
uow.ReadLock(Constants.Locks.ContentTree);
var repository = uow.CreateRepository();
var query = repository.Query.Where(x => x.Alias == contentTypeAlias);
var contentType = repository.GetByQuery(query).FirstOrDefault();
if (contentType == null)
throw new Exception($"No ContentType matching the passed in Alias: '{contentTypeAlias}' was found"); // causes rollback
uow.Complete();
return contentType;
}
}
#endregion
#region Xml - Shoud Move!
///
/// Returns the persisted content's XML structure
///
///
///
public XElement GetContentXml(int contentId)
{
using (var uow = UowProvider.CreateUnitOfWork())
{
uow.ReadLock(Constants.Locks.ContentTree);
var repository = uow.CreateRepository();
var elt = repository.GetContentXml(contentId);
uow.Complete();
return elt;
}
}
///
/// Returns the persisted content's preview XML structure
///
///
///
///
public XElement GetContentPreviewXml(int contentId, Guid version)
{
using (var uow = UowProvider.CreateUnitOfWork())
{
uow.ReadLock(Constants.Locks.ContentTree);
var repository = uow.CreateRepository();
var elt = repository.GetContentPreviewXml(contentId, version);
uow.Complete();
return elt;
}
}
///
/// 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)
{
using (var uow = UowProvider.CreateUnitOfWork())
{
uow.WriteLock(Constants.Locks.ContentTree);
var repository = uow.CreateRepository();
repository.RebuildXmlStructures(
content => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, content),
contentTypeIds: contentTypeIds.Length == 0 ? null : contentTypeIds);
uow.Complete();
}
Audit(AuditType.Publish, "ContentService.RebuildXmlStructures completed, the xml has been regenerated in the database", 0, Constants.System.Root);
}
#endregion
}
}