2629 lines
124 KiB
C#
2629 lines
124 KiB
C#
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
|
|
{
|
|
/// <summary>
|
|
/// Represents the Content Service, which is an easy access to operations involving <see cref="IContent"/>
|
|
/// </summary>
|
|
public class ContentService : RepositoryService, IContentService, IContentServiceOperations
|
|
{
|
|
private readonly EntityXmlSerializer _entitySerializer = new EntityXmlSerializer();
|
|
private readonly IDataTypeService _dataTypeService;
|
|
private readonly IUserService _userService;
|
|
private readonly IEnumerable<IUrlSegmentProvider> _urlSegmentProviders;
|
|
private IContentTypeService _contentTypeService;
|
|
|
|
#region Constructors
|
|
|
|
public ContentService(
|
|
IDatabaseUnitOfWorkProvider provider,
|
|
ILogger logger,
|
|
IEventMessagesFactory eventMessagesFactory,
|
|
IDataTypeService dataTypeService,
|
|
IUserService userService,
|
|
IEnumerable<IUrlSegmentProvider> 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<IContentRepository>();
|
|
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<IContentRepository>();
|
|
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<IContentRepository>();
|
|
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<IContentRepository>();
|
|
var count = repo.CountDescendants(parentId, contentTypeAlias);
|
|
uow.Complete();
|
|
return count;
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Permissions
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
/// <param name="permissionSet"></param>
|
|
public void ReplaceContentPermissions(EntityPermissionSet permissionSet)
|
|
{
|
|
using (var uow = UowProvider.CreateUnitOfWork())
|
|
{
|
|
uow.WriteLock(Constants.Locks.ContentTree);
|
|
var repo = uow.CreateRepository<IContentRepository>();
|
|
repo.ReplaceContentPermissions(permissionSet);
|
|
uow.Complete();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Assigns a single permission to the current content item for the specified user ids
|
|
/// </summary>
|
|
/// <param name="entity"></param>
|
|
/// <param name="permission"></param>
|
|
/// <param name="userIds"></param>
|
|
public void AssignContentPermission(IContent entity, char permission, IEnumerable<int> userIds)
|
|
{
|
|
using (var uow = UowProvider.CreateUnitOfWork())
|
|
{
|
|
uow.WriteLock(Constants.Locks.ContentTree);
|
|
var repo = uow.CreateRepository<IContentRepository>();
|
|
repo.AssignEntityPermission(entity, permission, userIds);
|
|
uow.Complete();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the list of permissions for the content item
|
|
/// </summary>
|
|
/// <param name="content"></param>
|
|
/// <returns></returns>
|
|
public IEnumerable<EntityPermission> GetPermissionsForEntity(IContent content)
|
|
{
|
|
using (var uow = UowProvider.CreateUnitOfWork())
|
|
{
|
|
uow.ReadLock(Constants.Locks.ContentTree);
|
|
var repo = uow.CreateRepository<IContentRepository>();
|
|
var perms = repo.GetPermissionsForEntity(content.Id);
|
|
uow.Complete();
|
|
return perms;
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Create
|
|
|
|
/// <summary>
|
|
/// Creates an <see cref="IContent"/> object of a specified content type.
|
|
/// </summary>
|
|
/// <remarks>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.
|
|
/// </remarks>
|
|
/// <param name="name">The name of the content object.</param>
|
|
/// <param name="parentId">The identifier of the parent, or -1.</param>
|
|
/// <param name="contentTypeAlias">The alias of the content type.</param>
|
|
/// <param name="userId">The optional id of the user creating the content.</param>
|
|
/// <returns>The content object.</returns>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates an <see cref="IContent"/> object of a specified content type, at root.
|
|
/// </summary>
|
|
/// <remarks>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.
|
|
/// </remarks>
|
|
/// <param name="name">The name of the content object.</param>
|
|
/// <param name="contentTypeAlias">The alias of the content type.</param>
|
|
/// <param name="userId">The optional id of the user creating the content.</param>
|
|
/// <returns>The content object.</returns>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates an <see cref="IContent"/> object of a specified content type, under a parent.
|
|
/// </summary>
|
|
/// <remarks>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.
|
|
/// </remarks>
|
|
/// <param name="name">The name of the content object.</param>
|
|
/// <param name="parent">The parent content object.</param>
|
|
/// <param name="contentTypeAlias">The alias of the content type.</param>
|
|
/// <param name="userId">The optional id of the user creating the content.</param>
|
|
/// <returns>The content object.</returns>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates an <see cref="IContent"/> object of a specified content type.
|
|
/// </summary>
|
|
/// <remarks>This method returns a new, persisted, IContent with an identity.</remarks>
|
|
/// <param name="name">The name of the content object.</param>
|
|
/// <param name="parentId">The identifier of the parent, or -1.</param>
|
|
/// <param name="contentTypeAlias">The alias of the content type.</param>
|
|
/// <param name="userId">The optional id of the user creating the content.</param>
|
|
/// <returns>The content object.</returns>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates an <see cref="IContent"/> object of a specified content type, under a parent.
|
|
/// </summary>
|
|
/// <remarks>This method returns a new, persisted, IContent with an identity.</remarks>
|
|
/// <param name="name">The name of the content object.</param>
|
|
/// <param name="parent">The parent content object.</param>
|
|
/// <param name="contentTypeAlias">The alias of the content type.</param>
|
|
/// <param name="userId">The optional id of the user creating the content.</param>
|
|
/// <returns>The content object.</returns>
|
|
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<IContent>(content, content.ContentType.Alias, parent)
|
|
: new NewEventArgs<IContent>(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<IContent>(content), this))
|
|
{
|
|
content.WasCancelled = true;
|
|
return;
|
|
}
|
|
|
|
var repo = uow.CreateRepository<IContentRepository>();
|
|
repo.AddOrUpdate(content);
|
|
repo.AddOrUpdatePreviewXml(content, c => _entitySerializer.Serialize(this, _dataTypeService, _userService, _urlSegmentProviders, c));
|
|
|
|
Saved.RaiseEvent(new SaveEventArgs<IContent>(content, false), this);
|
|
}
|
|
|
|
Created.RaiseEvent(new NewEventArgs<IContent>(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
|
|
|
|
/// <summary>
|
|
/// Gets an <see cref="IContent"/> object by Id
|
|
/// </summary>
|
|
/// <param name="id">Id of the Content to retrieve</param>
|
|
/// <returns><see cref="IContent"/></returns>
|
|
public IContent GetById(int id)
|
|
{
|
|
using (var uow = UowProvider.CreateUnitOfWork())
|
|
{
|
|
uow.ReadLock(Constants.Locks.ContentTree);
|
|
var repository = uow.CreateRepository<IContentRepository>();
|
|
var content = repository.Get(id);
|
|
uow.Complete();
|
|
return content;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets an <see cref="IContent"/> object by Id
|
|
/// </summary>
|
|
/// <param name="ids">Ids of the Content to retrieve</param>
|
|
/// <returns><see cref="IContent"/></returns>
|
|
public IEnumerable<IContent> GetByIds(IEnumerable<int> ids)
|
|
{
|
|
var idsA = ids.ToArray();
|
|
if (idsA.Length == 0) return Enumerable.Empty<IContent>();
|
|
|
|
using (var uow = UowProvider.CreateUnitOfWork())
|
|
{
|
|
uow.ReadLock(Constants.Locks.ContentTree);
|
|
var repository = uow.CreateRepository<IContentRepository>();
|
|
var content = repository.GetAll(idsA);
|
|
uow.Complete();
|
|
return content;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets an <see cref="IContent"/> object by its 'UniqueId'
|
|
/// </summary>
|
|
/// <param name="key">Guid key of the Content to retrieve</param>
|
|
/// <returns><see cref="IContent"/></returns>
|
|
public IContent GetById(Guid key)
|
|
{
|
|
using (var uow = UowProvider.CreateUnitOfWork())
|
|
{
|
|
uow.ReadLock(Constants.Locks.ContentTree);
|
|
var repository = uow.CreateRepository<IContentRepository>();
|
|
var query = repository.Query.Where(x => x.Key == key);
|
|
var contents = repository.GetByQuery(query);
|
|
var content = contents.SingleOrDefault();
|
|
uow.Complete();
|
|
return content;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a collection of <see cref="IContent"/> objects by the Id of the <see cref="IContentType"/>
|
|
/// </summary>
|
|
/// <param name="id">Id of the <see cref="IContentType"/></param>
|
|
/// <returns>An Enumerable list of <see cref="IContent"/> objects</returns>
|
|
public IEnumerable<IContent> GetContentOfContentType(int id)
|
|
{
|
|
using (var uow = UowProvider.CreateUnitOfWork())
|
|
{
|
|
uow.ReadLock(Constants.Locks.ContentTree);
|
|
var repository = uow.CreateRepository<IContentRepository>();
|
|
var query = repository.Query.Where(x => x.ContentTypeId == id);
|
|
var content = repository.GetByQuery(query);
|
|
uow.Complete();
|
|
return content;
|
|
}
|
|
}
|
|
|
|
internal IEnumerable<IContent> GetPublishedContentOfContentType(int id)
|
|
{
|
|
using (var uow = UowProvider.CreateUnitOfWork())
|
|
{
|
|
uow.ReadLock(Constants.Locks.ContentTree);
|
|
var repository = uow.CreateRepository<IContentRepository>();
|
|
var query = repository.Query.Where(x => x.ContentTypeId == id);
|
|
var content = repository.GetByPublishedVersion(query);
|
|
uow.Complete();
|
|
return content;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a collection of <see cref="IContent"/> objects by Level
|
|
/// </summary>
|
|
/// <param name="level">The level to retrieve Content from</param>
|
|
/// <returns>An Enumerable list of <see cref="IContent"/> objects</returns>
|
|
/// <remarks>Contrary to most methods, this method filters out trashed content items.</remarks>
|
|
public IEnumerable<IContent> GetByLevel(int level)
|
|
{
|
|
using (var uow = UowProvider.CreateUnitOfWork())
|
|
{
|
|
uow.ReadLock(Constants.Locks.ContentTree);
|
|
var repository = uow.CreateRepository<IContentRepository>();
|
|
var query = repository.Query.Where(x => x.Level == level && x.Trashed == false);
|
|
var content = repository.GetByQuery(query);
|
|
uow.Complete();
|
|
return content;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a specific version of an <see cref="IContent"/> item.
|
|
/// </summary>
|
|
/// <param name="versionId">Id of the version to retrieve</param>
|
|
/// <returns>An <see cref="IContent"/> item</returns>
|
|
public IContent GetByVersion(Guid versionId)
|
|
{
|
|
using (var uow = UowProvider.CreateUnitOfWork())
|
|
{
|
|
uow.ReadLock(Constants.Locks.ContentTree);
|
|
var repository = uow.CreateRepository<IContentRepository>();
|
|
var content = repository.GetByVersion(versionId);
|
|
uow.Complete();
|
|
return content;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a collection of an <see cref="IContent"/> objects versions by Id
|
|
/// </summary>
|
|
/// <param name="id"></param>
|
|
/// <returns>An Enumerable list of <see cref="IContent"/> objects</returns>
|
|
public IEnumerable<IContent> GetVersions(int id)
|
|
{
|
|
using (var uow = UowProvider.CreateUnitOfWork())
|
|
{
|
|
uow.ReadLock(Constants.Locks.ContentTree);
|
|
var repository = uow.CreateRepository<IContentRepository>();
|
|
var content = repository.GetAllVersions(id);
|
|
uow.Complete();
|
|
return content;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a collection of <see cref="IContent"/> objects, which are ancestors of the current content.
|
|
/// </summary>
|
|
/// <param name="id">Id of the <see cref="IContent"/> to retrieve ancestors for</param>
|
|
/// <returns>An Enumerable list of <see cref="IContent"/> objects</returns>
|
|
public IEnumerable<IContent> GetAncestors(int id)
|
|
{
|
|
// intentionnaly not locking
|
|
var content = GetById(id);
|
|
return GetAncestors(content);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a collection of <see cref="IContent"/> objects, which are ancestors of the current content.
|
|
/// </summary>
|
|
/// <param name="content"><see cref="IContent"/> to retrieve ancestors for</param>
|
|
/// <returns>An Enumerable list of <see cref="IContent"/> objects</returns>
|
|
public IEnumerable<IContent> GetAncestors(IContent content)
|
|
{
|
|
//null check otherwise we get exceptions
|
|
if (content.Path.IsNullOrWhiteSpace()) return Enumerable.Empty<IContent>();
|
|
|
|
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<IContent>();
|
|
|
|
using (var uow = UowProvider.CreateUnitOfWork())
|
|
{
|
|
uow.ReadLock(Constants.Locks.ContentTree);
|
|
var repository = uow.CreateRepository<IContentRepository>();
|
|
var ancestors = repository.GetAll(ids);
|
|
uow.Complete();
|
|
return ancestors;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a collection of <see cref="IContent"/> objects by Parent Id
|
|
/// </summary>
|
|
/// <param name="id">Id of the Parent to retrieve Children from</param>
|
|
/// <returns>An Enumerable list of <see cref="IContent"/> objects</returns>
|
|
public IEnumerable<IContent> GetChildren(int id)
|
|
{
|
|
using (var uow = UowProvider.CreateUnitOfWork())
|
|
{
|
|
uow.ReadLock(Constants.Locks.ContentTree);
|
|
var repository = uow.CreateRepository<IContentRepository>();
|
|
var query = repository.Query.Where(x => x.ParentId == id);
|
|
var children = repository.GetByQuery(query).OrderBy(x => x.SortOrder);
|
|
uow.Complete();
|
|
return children;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a collection of published <see cref="IContent"/> objects by Parent Id
|
|
/// </summary>
|
|
/// <param name="id">Id of the Parent to retrieve Children from</param>
|
|
/// <returns>An Enumerable list of published <see cref="IContent"/> objects</returns>
|
|
public IEnumerable<IContent> GetPublishedChildren(int id)
|
|
{
|
|
using (var uow = UowProvider.CreateUnitOfWork())
|
|
{
|
|
uow.ReadLock(Constants.Locks.ContentTree);
|
|
var repository = uow.CreateRepository<IContentRepository>();
|
|
var query = repository.Query.Where(x => x.ParentId == id && x.Published);
|
|
var children = repository.GetByQuery(query).OrderBy(x => x.SortOrder);
|
|
uow.Complete();
|
|
return children;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a collection of <see cref="IContent"/> objects by Parent Id
|
|
/// </summary>
|
|
/// <param name="id">Id of the Parent to retrieve Children from</param>
|
|
/// <param name="pageIndex">Page index (zero based)</param>
|
|
/// <param name="pageSize">Page size</param>
|
|
/// <param name="totalChildren">Total records query would return without paging</param>
|
|
/// <param name="orderBy">Field to order by</param>
|
|
/// <param name="orderDirection">Direction to order by</param>
|
|
/// <param name="filter">Search text filter</param>
|
|
/// <returns>An Enumerable list of <see cref="IContent"/> objects</returns>
|
|
public IEnumerable<IContent> 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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a collection of <see cref="IContent"/> objects by Parent Id
|
|
/// </summary>
|
|
/// <param name="id">Id of the Parent to retrieve Children from</param>
|
|
/// <param name="pageIndex">Page index (zero based)</param>
|
|
/// <param name="pageSize">Page size</param>
|
|
/// <param name="totalChildren">Total records query would return without paging</param>
|
|
/// <param name="orderBy">Field to order by</param>
|
|
/// <param name="orderDirection">Direction to order by</param>
|
|
/// <param name="orderBySystemField">Flag to indicate when ordering by system field</param>
|
|
/// <param name="filter">Search text filter</param>
|
|
/// <returns>An Enumerable list of <see cref="IContent"/> objects</returns>
|
|
public IEnumerable<IContent> 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<IContentRepository>();
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a collection of <see cref="IContent"/> objects by Parent Id
|
|
/// </summary>
|
|
/// <param name="id">Id of the Parent to retrieve Descendants from</param>
|
|
/// <param name="pageIndex">Page number</param>
|
|
/// <param name="pageSize">Page size</param>
|
|
/// <param name="totalChildren">Total records query would return without paging</param>
|
|
/// <param name="orderBy">Field to order by</param>
|
|
/// <param name="orderDirection">Direction to order by</param>
|
|
/// <param name="filter">Search text filter</param>
|
|
/// <returns>An Enumerable list of <see cref="IContent"/> objects</returns>
|
|
public IEnumerable<IContent> 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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a collection of <see cref="IContent"/> objects by Parent Id
|
|
/// </summary>
|
|
/// <param name="id">Id of the Parent to retrieve Descendants from</param>
|
|
/// <param name="pageIndex">Page number</param>
|
|
/// <param name="pageSize">Page size</param>
|
|
/// <param name="totalChildren">Total records query would return without paging</param>
|
|
/// <param name="orderBy">Field to order by</param>
|
|
/// <param name="orderDirection">Direction to order by</param>
|
|
/// <param name="orderBySystemField">Flag to indicate when ordering by system field</param>
|
|
/// <param name="filter">Search text filter</param>
|
|
/// <returns>An Enumerable list of <see cref="IContent"/> objects</returns>
|
|
public IEnumerable<IContent> 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<IContentRepository>();
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a collection of <see cref="IContent"/> objects by its name or partial name
|
|
/// </summary>
|
|
/// <param name="parentId">Id of the Parent to retrieve Children from</param>
|
|
/// <param name="name">Full or partial name of the children</param>
|
|
/// <returns>An Enumerable list of <see cref="IContent"/> objects</returns>
|
|
public IEnumerable<IContent> GetChildrenByName(int parentId, string name)
|
|
{
|
|
using (var uow = UowProvider.CreateUnitOfWork())
|
|
{
|
|
uow.ReadLock(Constants.Locks.ContentTree);
|
|
var repository = uow.CreateRepository<IContentRepository>();
|
|
var query = repository.Query.Where(x => x.ParentId == parentId && x.Name.Contains(name));
|
|
var children = repository.GetByQuery(query);
|
|
uow.Complete();
|
|
return children;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a collection of <see cref="IContent"/> objects by Parent Id
|
|
/// </summary>
|
|
/// <param name="id">Id of the Parent to retrieve Descendants from</param>
|
|
/// <returns>An Enumerable list of <see cref="IContent"/> objects</returns>
|
|
public IEnumerable<IContent> GetDescendants(int id)
|
|
{
|
|
using (var uow = UowProvider.CreateUnitOfWork())
|
|
{
|
|
uow.ReadLock(Constants.Locks.ContentTree);
|
|
var repository = uow.CreateRepository<IContentRepository>();
|
|
var content = GetById(id);
|
|
if (content == null)
|
|
{
|
|
uow.Complete(); // else causes rollback
|
|
return Enumerable.Empty<IContent>();
|
|
}
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a collection of <see cref="IContent"/> objects by Parent Id
|
|
/// </summary>
|
|
/// <param name="content"><see cref="IContent"/> item to retrieve Descendants from</param>
|
|
/// <returns>An Enumerable list of <see cref="IContent"/> objects</returns>
|
|
public IEnumerable<IContent> GetDescendants(IContent content)
|
|
{
|
|
using (var uow = UowProvider.CreateUnitOfWork())
|
|
{
|
|
uow.ReadLock(Constants.Locks.ContentTree);
|
|
var repository = uow.CreateRepository<IContentRepository>();
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the parent of the current content as an <see cref="IContent"/> item.
|
|
/// </summary>
|
|
/// <param name="id">Id of the <see cref="IContent"/> to retrieve the parent from</param>
|
|
/// <returns>Parent <see cref="IContent"/> object</returns>
|
|
public IContent GetParent(int id)
|
|
{
|
|
// intentionnaly not locking
|
|
var content = GetById(id);
|
|
return GetParent(content);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the parent of the current content as an <see cref="IContent"/> item.
|
|
/// </summary>
|
|
/// <param name="content"><see cref="IContent"/> to retrieve the parent from</param>
|
|
/// <returns>Parent <see cref="IContent"/> object</returns>
|
|
public IContent GetParent(IContent content)
|
|
{
|
|
if (content.ParentId == Constants.System.Root || content.ParentId == Constants.System.RecycleBinContent)
|
|
return null;
|
|
|
|
return GetById(content.ParentId);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the published version of an <see cref="IContent"/> item
|
|
/// </summary>
|
|
/// <param name="id">Id of the <see cref="IContent"/> to retrieve version from</param>
|
|
/// <returns>An <see cref="IContent"/> item</returns>
|
|
public IContent GetPublishedVersion(int id)
|
|
{
|
|
var version = GetVersions(id);
|
|
return version.FirstOrDefault(x => x.Published);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the published version of a <see cref="IContent"/> item.
|
|
/// </summary>
|
|
/// <param name="content">The content item.</param>
|
|
/// <returns>The published version, if any; otherwise, null.</returns>
|
|
public IContent GetPublishedVersion(IContent content)
|
|
{
|
|
if (content.Published) return content;
|
|
return content.HasPublishedVersion
|
|
? GetByVersion(content.PublishedVersionGuid)
|
|
: null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a collection of <see cref="IContent"/> objects, which reside at the first level / root
|
|
/// </summary>
|
|
/// <returns>An Enumerable list of <see cref="IContent"/> objects</returns>
|
|
public IEnumerable<IContent> GetRootContent()
|
|
{
|
|
using (var uow = UowProvider.CreateUnitOfWork())
|
|
{
|
|
uow.ReadLock(Constants.Locks.ContentTree);
|
|
var repository = uow.CreateRepository<IContentRepository>();
|
|
var query = repository.Query.Where(x => x.ParentId == Constants.System.Root);
|
|
var content = repository.GetByQuery(query);
|
|
uow.Complete();
|
|
return content;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets all published content items
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
internal IEnumerable<IContent> GetAllPublished()
|
|
{
|
|
using (var uow = UowProvider.CreateUnitOfWork())
|
|
{
|
|
uow.ReadLock(Constants.Locks.ContentTree);
|
|
var repository = uow.CreateRepository<IContentRepository>();
|
|
var query = repository.Query.Where(x => x.Trashed == false);
|
|
var content = repository.GetByPublishedVersion(query);
|
|
uow.Complete();
|
|
return content;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a collection of <see cref="IContent"/> objects, which has an expiration date less than or equal to today.
|
|
/// </summary>
|
|
/// <returns>An Enumerable list of <see cref="IContent"/> objects</returns>
|
|
public IEnumerable<IContent> GetContentForExpiration()
|
|
{
|
|
using (var uow = UowProvider.CreateUnitOfWork())
|
|
{
|
|
uow.ReadLock(Constants.Locks.ContentTree);
|
|
var repository = uow.CreateRepository<IContentRepository>();
|
|
var query = repository.Query.Where(x => x.Published && x.ExpireDate <= DateTime.Now);
|
|
var content = repository.GetByQuery(query);
|
|
uow.Complete();
|
|
return content;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a collection of <see cref="IContent"/> objects, which has a release date less than or equal to today.
|
|
/// </summary>
|
|
/// <returns>An Enumerable list of <see cref="IContent"/> objects</returns>
|
|
public IEnumerable<IContent> GetContentForRelease()
|
|
{
|
|
using (var uow = UowProvider.CreateUnitOfWork())
|
|
{
|
|
uow.ReadLock(Constants.Locks.ContentTree);
|
|
var repository = uow.CreateRepository<IContentRepository>();
|
|
var query = repository.Query.Where(x => x.Published == false && x.ReleaseDate <= DateTime.Now);
|
|
var content = repository.GetByQuery(query);
|
|
uow.Complete();
|
|
return content;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a collection of an <see cref="IContent"/> objects, which resides in the Recycle Bin
|
|
/// </summary>
|
|
/// <returns>An Enumerable list of <see cref="IContent"/> objects</returns>
|
|
public IEnumerable<IContent> GetContentInRecycleBin()
|
|
{
|
|
using (var uow = UowProvider.CreateUnitOfWork())
|
|
{
|
|
uow.ReadLock(Constants.Locks.ContentTree);
|
|
var repository = uow.CreateRepository<IContentRepository>();
|
|
var query = repository.Query.Where(x => x.Path.Contains(Constants.System.RecycleBinContent.ToInvariantString()));
|
|
var content = repository.GetByQuery(query);
|
|
uow.Complete();
|
|
return content;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks whether an <see cref="IContent"/> item has any children
|
|
/// </summary>
|
|
/// <param name="id">Id of the <see cref="IContent"/></param>
|
|
/// <returns>True if the content has any children otherwise False</returns>
|
|
public bool HasChildren(int id)
|
|
{
|
|
return CountChildren(id) > 0;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks whether an <see cref="IContent"/> item has any published versions
|
|
/// </summary>
|
|
/// <param name="id">Id of the <see cref="IContent"/></param>
|
|
/// <returns>True if the content has any published version otherwise False</returns>
|
|
public bool HasPublishedVersion(int id)
|
|
{
|
|
using (var uow = UowProvider.CreateUnitOfWork())
|
|
{
|
|
uow.ReadLock(Constants.Locks.ContentTree);
|
|
var repository = uow.CreateRepository<IContentRepository>();
|
|
var query = repository.Query.Where(x => x.Published && x.Id == id && x.Trashed == false);
|
|
var count = repository.Count(query);
|
|
uow.Complete();
|
|
return count > 0;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if the passed in <see cref="IContent"/> can be published based on the anscestors publish state.
|
|
/// </summary>
|
|
/// <param name="content"><see cref="IContent"/> to check if anscestors are published</param>
|
|
/// <returns>True if the Content can be published, otherwise False</returns>
|
|
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<IContentRepository>();
|
|
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<IContentRepository>();
|
|
var isPathPublished = repo.IsPathPublished(content);
|
|
uow.Complete();
|
|
return isPathPublished;
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Save, Publish, Unpublish
|
|
|
|
/// <summary>
|
|
/// This will rebuild the xml structures for content in the database.
|
|
/// </summary>
|
|
/// <param name="userId">This is not used for anything</param>
|
|
/// <returns>True if publishing succeeded, otherwise False</returns>
|
|
/// <remarks>
|
|
/// This is used for when a document type alias or a document type property is changed, the xml will need to
|
|
/// be regenerated.
|
|
/// </remarks>
|
|
public bool RePublishAll(int userId = 0)
|
|
{
|
|
try
|
|
{
|
|
RebuildXmlStructures();
|
|
return true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.Error<ContentService>("An error occurred executing RePublishAll", ex);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// This will rebuild the xml structures for content in the database.
|
|
/// </summary>
|
|
/// <param name="contentTypeIds">
|
|
/// If specified will only rebuild the xml for the content type's specified, otherwise will update the structure
|
|
/// for all published content.
|
|
/// </param>
|
|
internal void RePublishAll(params int[] contentTypeIds)
|
|
{
|
|
try
|
|
{
|
|
RebuildXmlStructures(contentTypeIds);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.Error<ContentService>("An error occurred executing RePublishAll", ex);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Saves a single <see cref="IContent"/> object
|
|
/// </summary>
|
|
/// <param name="content">The <see cref="IContent"/> to save</param>
|
|
/// <param name="userId">Optional Id of the User saving the Content</param>
|
|
/// <param name="raiseEvents">Optional boolean indicating whether or not to raise events.</param>
|
|
public void Save(IContent content, int userId = 0, bool raiseEvents = true)
|
|
{
|
|
((IContentServiceOperations) this).Save(content, userId, raiseEvents);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Saves a single <see cref="IContent"/> object
|
|
/// </summary>
|
|
/// <param name="content">The <see cref="IContent"/> to save</param>
|
|
/// <param name="userId">Optional Id of the User saving the Content</param>
|
|
/// <param name="raiseEvents">Optional boolean indicating whether or not to raise events.</param>
|
|
Attempt<OperationStatus> IContentServiceOperations.Save(IContent content, int userId, bool raiseEvents)
|
|
{
|
|
var evtMsgs = EventMessagesFactory.Get();
|
|
|
|
if (raiseEvents && Saving.IsRaisedEventCancelled(new SaveEventArgs<IContent>(content, evtMsgs), this))
|
|
return OperationStatus.Attempt.Cancel(evtMsgs);
|
|
|
|
using (var uow = UowProvider.CreateUnitOfWork())
|
|
{
|
|
uow.WriteLock(Constants.Locks.ContentTree);
|
|
|
|
var repository = uow.CreateRepository<IContentRepository>();
|
|
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<IContent>(content, false, evtMsgs), this);
|
|
|
|
Audit(AuditType.Save, "Save Content performed by user", userId, content.Id);
|
|
|
|
return OperationStatus.Attempt.Succeed(evtMsgs);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Saves a collection of <see cref="IContent"/> objects.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// 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 <see cref="IContent"/>.
|
|
/// </remarks>
|
|
/// <param name="contents">Collection of <see cref="IContent"/> to save</param>
|
|
/// <param name="userId">Optional Id of the User saving the Content</param>
|
|
/// <param name="raiseEvents">Optional boolean indicating whether or not to raise events.</param>
|
|
public void Save(IEnumerable<IContent> contents, int userId = 0, bool raiseEvents = true)
|
|
{
|
|
((IContentServiceOperations) this).Save(contents, userId, raiseEvents);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Saves a collection of <see cref="IContent"/> objects.
|
|
/// </summary>
|
|
/// <param name="contents">Collection of <see cref="IContent"/> to save</param>
|
|
/// <param name="userId">Optional Id of the User saving the Content</param>
|
|
/// <param name="raiseEvents">Optional boolean indicating whether or not to raise events.</param>
|
|
Attempt<OperationStatus> IContentServiceOperations.Save(IEnumerable<IContent> contents, int userId, bool raiseEvents)
|
|
{
|
|
var evtMsgs = EventMessagesFactory.Get();
|
|
var contentsA = contents.ToArray();
|
|
|
|
if (raiseEvents && Saving.IsRaisedEventCancelled(new SaveEventArgs<IContent>(contentsA, evtMsgs), this))
|
|
return OperationStatus.Attempt.Cancel(evtMsgs);
|
|
|
|
using (var uow = UowProvider.CreateUnitOfWork())
|
|
{
|
|
uow.WriteLock(Constants.Locks.ContentTree);
|
|
var repository = uow.CreateRepository<IContentRepository>();
|
|
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<IContent>(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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Saves and Publishes a single <see cref="IContent"/> object
|
|
/// </summary>
|
|
/// <param name="content">The <see cref="IContent"/> to save and publish</param>
|
|
/// <param name="userId">Optional Id of the User issueing the publishing</param>
|
|
/// <param name="raiseEvents">Optional boolean indicating whether or not to raise save events.</param>
|
|
/// <returns>True if publishing succeeded, otherwise False</returns>
|
|
[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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Saves and Publishes a single <see cref="IContent"/> object
|
|
/// </summary>
|
|
/// <param name="content">The <see cref="IContent"/> to save and publish</param>
|
|
/// <param name="userId">Optional Id of the User issueing the publishing</param>
|
|
/// <param name="raiseEvents">Optional boolean indicating whether or not to raise save events.</param>
|
|
/// <returns>True if publishing succeeded, otherwise False</returns>
|
|
Attempt<PublishStatus> IContentServiceOperations.SaveAndPublish(IContent content, int userId, bool raiseEvents)
|
|
{
|
|
return SaveAndPublishDo(content, userId, raiseEvents);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Publishes a single <see cref="IContent"/> object
|
|
/// </summary>
|
|
/// <param name="content">The <see cref="IContent"/> to publish</param>
|
|
/// <param name="userId">Optional Id of the User issueing the publishing</param>
|
|
/// <returns>True if publishing succeeded, otherwise False</returns>
|
|
public bool Publish(IContent content, int userId = 0)
|
|
{
|
|
var result = SaveAndPublishDo(content, userId);
|
|
Logger.Info<ContentService>("Call was made to ContentService.Publish, use PublishWithStatus instead since that method will provide more detailed information on the outcome");
|
|
return result.Success;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Publishes a single <see cref="IContent"/> object
|
|
/// </summary>
|
|
/// <param name="content">The <see cref="IContent"/> to publish</param>
|
|
/// <param name="userId">Optional Id of the User issueing the publishing</param>
|
|
/// <returns>The published status attempt</returns>
|
|
Attempt<PublishStatus> IContentServiceOperations.Publish(IContent content, int userId)
|
|
{
|
|
return SaveAndPublishDo(content, userId);
|
|
}
|
|
|
|
/// <summary>
|
|
/// UnPublishes a single <see cref="IContent"/> object
|
|
/// </summary>
|
|
/// <param name="content">The <see cref="IContent"/> to publish</param>
|
|
/// <param name="userId">Optional Id of the User issueing the publishing</param>
|
|
/// <returns>True if unpublishing succeeded, otherwise False</returns>
|
|
public bool UnPublish(IContent content, int userId = 0)
|
|
{
|
|
return ((IContentServiceOperations)this).UnPublish(content, userId).Success;
|
|
}
|
|
|
|
/// <summary>
|
|
/// UnPublishes a single <see cref="IContent"/> object
|
|
/// </summary>
|
|
/// <param name="content">The <see cref="IContent"/> to publish</param>
|
|
/// <param name="userId">Optional Id of the User issueing the publishing</param>
|
|
/// <returns>True if unpublishing succeeded, otherwise False</returns>
|
|
Attempt<UnPublishStatus> IContentServiceOperations.UnPublish(IContent content, int userId)
|
|
{
|
|
return UnPublishDo(content, false, userId);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Saves and Publishes a single <see cref="IContent"/> object
|
|
/// </summary>
|
|
/// <param name="content">The <see cref="IContent"/> to save and publish</param>
|
|
/// <param name="userId">Optional Id of the User issueing the publishing</param>
|
|
/// <param name="raiseEvents">Optional boolean indicating whether or not to raise save events.</param>
|
|
/// <returns>True if publishing succeeded, otherwise False</returns>
|
|
public Attempt<PublishStatus> SaveAndPublishWithStatus(IContent content, int userId = 0, bool raiseEvents = true)
|
|
{
|
|
return ((IContentServiceOperations)this).SaveAndPublish(content, userId, raiseEvents);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Publishes a single <see cref="IContent"/> object
|
|
/// </summary>
|
|
/// <param name="content">The <see cref="IContent"/> to publish</param>
|
|
/// <param name="userId">Optional Id of the User issueing the publishing</param>
|
|
/// <returns>True if publishing succeeded, otherwise False</returns>
|
|
public Attempt<PublishStatus> PublishWithStatus(IContent content, int userId = 0)
|
|
{
|
|
return ((IContentServiceOperations) this).Publish(content, userId);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Publishes a <see cref="IContent"/> object and all its children
|
|
/// </summary>
|
|
/// <param name="content">The <see cref="IContent"/> to publish along with its children</param>
|
|
/// <param name="userId">Optional Id of the User issueing the publishing</param>
|
|
/// <returns>True if publishing succeeded, otherwise False</returns>
|
|
[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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Publishes a <see cref="IContent"/> object and all its children
|
|
/// </summary>
|
|
/// <param name="content">The <see cref="IContent"/> to publish along with its children</param>
|
|
/// <param name="userId">Optional Id of the User issueing the publishing</param>
|
|
/// <param name="includeUnpublished">set to true if you want to also publish children that are currently unpublished</param>
|
|
/// <returns>True if publishing succeeded, otherwise False</returns>
|
|
public IEnumerable<Attempt<PublishStatus>> PublishWithChildrenWithStatus(IContent content, int userId = 0, bool includeUnpublished = false)
|
|
{
|
|
return ((IContentServiceOperations)this).PublishWithChildren(content, userId, includeUnpublished);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Used to perform scheduled publishing/unpublishing
|
|
/// </summary>
|
|
public IEnumerable<Attempt<PublishStatus>> 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<ContentService>("Could not published the document (" + d.Id + ") based on it's scheduled release, status result: " + result.Result.StatusType, result.Exception);
|
|
}
|
|
else
|
|
{
|
|
Logger.Warn<ContentService>("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<ContentService>($"Error unpublishing node {d.Id}", ee);
|
|
throw;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Publishes a <see cref="IContent"/> object and all its children
|
|
/// </summary>
|
|
/// <param name="content">The <see cref="IContent"/> to publish along with its children</param>
|
|
/// <param name="userId">Optional Id of the User issueing the publishing</param>
|
|
/// <param name="includeUnpublished"></param>
|
|
/// <returns>The list of statuses for all published items</returns>
|
|
IEnumerable<Attempt<PublishStatus>> IContentServiceOperations.PublishWithChildren(IContent content, int userId, bool includeUnpublished)
|
|
{
|
|
return PublishWithChildrenDo(content, userId, includeUnpublished);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Delete
|
|
|
|
/// <summary>
|
|
/// Permanently deletes an <see cref="IContent"/> object as well as all of its Children.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This method will also delete associated media files, child content and possibly associated domains.
|
|
/// </remarks>
|
|
/// <remarks>Please note that this method will completely remove the Content from the database</remarks>
|
|
/// <param name="content">The <see cref="IContent"/> to delete</param>
|
|
/// <param name="userId">Optional Id of the User deleting the Content</param>
|
|
public void Delete(IContent content, int userId = 0)
|
|
{
|
|
((IContentServiceOperations)this).Delete(content, userId);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Permanently deletes an <see cref="IContent"/> object.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This method will also delete associated media files, child content and possibly associated domains.
|
|
/// </remarks>
|
|
/// <remarks>Please note that this method will completely remove the Content from the database</remarks>
|
|
/// <param name="content">The <see cref="IContent"/> to delete</param>
|
|
/// <param name="userId">Optional Id of the User deleting the Content</param>
|
|
Attempt<OperationStatus> IContentServiceOperations.Delete(IContent content, int userId)
|
|
{
|
|
var evtMsgs = EventMessagesFactory.Get();
|
|
|
|
if (Deleting.IsRaisedEventCancelled(new DeleteEventArgs<IContent>(content, evtMsgs), this))
|
|
return OperationStatus.Attempt.Cancel(evtMsgs);
|
|
|
|
using (var uow = UowProvider.CreateUnitOfWork())
|
|
{
|
|
uow.WriteLock(Constants.Locks.ContentTree);
|
|
var repository = uow.CreateRepository<IContentRepository>();
|
|
|
|
// 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<IContent>(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<IContent>();
|
|
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<IContent>(c, false); // raise event & get flagged files
|
|
Deleted.RaiseEvent(args, this);
|
|
|
|
IOHelper.DeleteFiles(args.MediaFilesToDelete, // remove flagged files
|
|
(file, e) => Logger.Error<MemberService>("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.
|
|
|
|
/// <summary>
|
|
/// Permanently deletes versions from an <see cref="IContent"/> object prior to a specific date.
|
|
/// This method will never delete the latest version of a content item.
|
|
/// </summary>
|
|
/// <param name="id">Id of the <see cref="IContent"/> object to delete versions from</param>
|
|
/// <param name="versionDate">Latest version date</param>
|
|
/// <param name="userId">Optional Id of the User deleting versions of a Content object</param>
|
|
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<IContentRepository>();
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Permanently deletes specific version(s) from an <see cref="IContent"/> object.
|
|
/// This method will never delete the latest version of a content item.
|
|
/// </summary>
|
|
/// <param name="id">Id of the <see cref="IContent"/> object to delete a version from</param>
|
|
/// <param name="versionId">Id of the version to delete</param>
|
|
/// <param name="deletePriorVersions">Boolean indicating whether to delete versions prior to the versionId</param>
|
|
/// <param name="userId">Optional Id of the User deleting versions of a Content object</param>
|
|
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<IContentRepository>();
|
|
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
|
|
|
|
/// <summary>
|
|
/// Deletes an <see cref="IContent"/> object by moving it to the Recycle Bin
|
|
/// </summary>
|
|
/// <remarks>Move an item to the Recycle Bin will result in the item being unpublished</remarks>
|
|
/// <param name="content">The <see cref="IContent"/> to delete</param>
|
|
/// <param name="userId">Optional Id of the User deleting the Content</param>
|
|
public void MoveToRecycleBin(IContent content, int userId = 0)
|
|
{
|
|
((IContentServiceOperations)this).MoveToRecycleBin(content, userId);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Deletes an <see cref="IContent"/> object by moving it to the Recycle Bin
|
|
/// </summary>
|
|
/// <remarks>Move an item to the Recycle Bin will result in the item being unpublished</remarks>
|
|
/// <param name="content">The <see cref="IContent"/> to delete</param>
|
|
/// <param name="userId">Optional Id of the User deleting the Content</param>
|
|
Attempt<OperationStatus> IContentServiceOperations.MoveToRecycleBin(IContent content, int userId)
|
|
{
|
|
var evtMsgs = EventMessagesFactory.Get();
|
|
var moves = new List<Tuple<IContent, string>>();
|
|
|
|
using (var uow = UowProvider.CreateUnitOfWork())
|
|
{
|
|
uow.WriteLock(Constants.Locks.ContentTree);
|
|
var repository = uow.CreateRepository<IContentRepository>();
|
|
|
|
var originalPath = content.Path;
|
|
if (Trashing.IsRaisedEventCancelled(new MoveEventArgs<IContent>(new MoveEventInfo<IContent>(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<IContent>(x.Item1, x.Item2, x.Item1.ParentId))
|
|
.ToArray();
|
|
|
|
Trashed.RaiseEvent(new MoveEventArgs<IContent>(false, evtMsgs, moveInfo), this);
|
|
Audit(AuditType.Move, "Move Content to Recycle Bin performed by user", userId, content.Id);
|
|
|
|
return OperationStatus.Attempt.Succeed(evtMsgs);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Moves an <see cref="IContent"/> object to a new location by changing its parent id.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// If the <see cref="IContent"/> 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.
|
|
/// </remarks>
|
|
/// <param name="content">The <see cref="IContent"/> to move</param>
|
|
/// <param name="parentId">Id of the Content's new Parent</param>
|
|
/// <param name="userId">Optional Id of the User moving the Content</param>
|
|
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<Tuple<IContent, string>>();
|
|
|
|
using (var uow = UowProvider.CreateUnitOfWork())
|
|
{
|
|
uow.WriteLock(Constants.Locks.ContentTree);
|
|
var repository = uow.CreateRepository<IContentRepository>();
|
|
|
|
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<IContent>(new MoveEventInfo<IContent>(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<IContent>(x.Item1, x.Item2, x.Item1.ParentId))
|
|
.ToArray();
|
|
|
|
Moved.RaiseEvent(new MoveEventArgs<IContent>(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<Tuple<IContent, string>> 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<int, string>();
|
|
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Empties the Recycle Bin by deleting all <see cref="IContent"/> that resides in the bin
|
|
/// </summary>
|
|
public void EmptyRecycleBin()
|
|
{
|
|
var nodeObjectType = new Guid(Constants.ObjectTypes.Document);
|
|
var deleted = new List<IContent>();
|
|
var evtMsgs = EventMessagesFactory.Get(); // todo - and then?
|
|
|
|
using (var uow = UowProvider.CreateUnitOfWork())
|
|
{
|
|
uow.WriteLock(Constants.Locks.ContentTree);
|
|
var repository = uow.CreateRepository<IContentRepository>();
|
|
|
|
// 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
|
|
|
|
/// <summary>
|
|
/// Copies an <see cref="IContent"/> 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.
|
|
/// </summary>
|
|
/// <param name="content">The <see cref="IContent"/> to copy</param>
|
|
/// <param name="parentId">Id of the Content's new Parent</param>
|
|
/// <param name="relateToOriginal">Boolean indicating whether the copy should be related to the original</param>
|
|
/// <param name="userId">Optional Id of the User copying the Content</param>
|
|
/// <returns>The newly created <see cref="IContent"/> object</returns>
|
|
public IContent Copy(IContent content, int parentId, bool relateToOriginal, int userId = 0)
|
|
{
|
|
return Copy(content, parentId, relateToOriginal, true, userId);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Copies an <see cref="IContent"/> 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.
|
|
/// </summary>
|
|
/// <param name="content">The <see cref="IContent"/> to copy</param>
|
|
/// <param name="parentId">Id of the Content's new Parent</param>
|
|
/// <param name="relateToOriginal">Boolean indicating whether the copy should be related to the original</param>
|
|
/// <param name="recursive">A value indicating whether to recursively copy children.</param>
|
|
/// <param name="userId">Optional Id of the User copying the Content</param>
|
|
/// <returns>The newly created <see cref="IContent"/> object</returns>
|
|
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<IContent>(content, copy, parentId), this))
|
|
return null;
|
|
|
|
// fixme - relateToOriginal is ignored?!
|
|
|
|
using (var uow = UowProvider.CreateUnitOfWork())
|
|
{
|
|
uow.WriteLock(Constants.Locks.ContentTree);
|
|
var repository = uow.CreateRepository<IContentRepository>();
|
|
|
|
// 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<int, IContent>();
|
|
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<int>(() => 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<IContent>(content, copy, false, parentId, relateToOriginal), this);
|
|
Audit(AuditType.Copy, "Copy Content performed by user", content.WriterId, content.Id);
|
|
return copy;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sends an <see cref="IContent"/> to Publication, which executes handlers and events for the 'Send to Publication' action.
|
|
/// </summary>
|
|
/// <param name="content">The <see cref="IContent"/> to send to publication</param>
|
|
/// <param name="userId">Optional Id of the User issueing the send to publication</param>
|
|
/// <returns>True if sending publication was succesfull otherwise false</returns>
|
|
public bool SendToPublication(IContent content, int userId = 0)
|
|
{
|
|
if (SendingToPublish.IsRaisedEventCancelled(new SendToPublishEventArgs<IContent>(content), this))
|
|
return false;
|
|
|
|
//Save before raising event
|
|
Save(content, userId);
|
|
|
|
SentToPublish.RaiseEvent(new SendToPublishEventArgs<IContent>(content, false), this);
|
|
|
|
Audit(AuditType.SendToPublish, "Send to Publish performed by user", content.WriterId, content.Id);
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Rollback an <see cref="IContent"/> object to a previous version.
|
|
/// This will create a new version, which is a copy of all the old data.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// The way data is stored actually only allows us to rollback on properties
|
|
/// and not data like Name and Alias of the Content.
|
|
/// </remarks>
|
|
/// <param name="id">Id of the <see cref="IContent"/>being rolled back</param>
|
|
/// <param name="versionId">Id of the version to rollback to</param>
|
|
/// <param name="userId">Optional Id of the User issueing the rollback of the Content</param>
|
|
/// <returns>The newly created <see cref="IContent"/> object</returns>
|
|
public IContent Rollback(int id, Guid versionId, int userId = 0)
|
|
{
|
|
var content = GetByVersion(versionId);
|
|
|
|
if (RollingBack.IsRaisedEventCancelled(new RollbackEventArgs<IContent>(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<IContentRepository>();
|
|
|
|
// 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<IContent>(content, false), this);
|
|
Audit(AuditType.RollBack, "Content rollback performed by user", content.WriterId, content.Id);
|
|
|
|
return content;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sorts a collection of <see cref="IContent"/> objects by updating the SortOrder according
|
|
/// to the ordering of items in the passed in <see cref="IEnumerable{T}"/>.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Using this method will ensure that the Published-state is maintained upon sorting
|
|
/// so the cache is updated accordingly - as needed.
|
|
/// </remarks>
|
|
/// <param name="items"></param>
|
|
/// <param name="userId"></param>
|
|
/// <param name="raiseEvents"></param>
|
|
/// <returns>True if sorting succeeded, otherwise False</returns>
|
|
public bool Sort(IEnumerable<IContent> items, int userId = 0, bool raiseEvents = true)
|
|
{
|
|
var itemsA = items.ToArray();
|
|
if (itemsA.Length == 0) return true;
|
|
|
|
if (raiseEvents && Saving.IsRaisedEventCancelled(new SaveEventArgs<IContent>(itemsA), this))
|
|
return false;
|
|
|
|
var published = new List<IContent>();
|
|
var saved = new List<IContent>();
|
|
|
|
using (var uow = UowProvider.CreateUnitOfWork())
|
|
{
|
|
uow.WriteLock(Constants.Locks.ContentTree);
|
|
var repository = uow.CreateRepository<IContentRepository>();
|
|
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<IContent>(saved, false), this);
|
|
|
|
if (raiseEvents && published.Any())
|
|
Published.RaiseEvent(new PublishEventArgs<IContent>(published, false, false), this);
|
|
|
|
Audit(AuditType.Sort, "Sorting content performed by user", userId, 0);
|
|
|
|
return true;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Internal Methods
|
|
|
|
/// <summary>
|
|
/// Gets a collection of <see cref="IContent"/> descendants by the first Parent.
|
|
/// </summary>
|
|
/// <param name="content"><see cref="IContent"/> item to retrieve Descendants from</param>
|
|
/// <returns>An Enumerable list of <see cref="IContent"/> objects</returns>
|
|
internal IEnumerable<IContent> GetPublishedDescendants(IContent content)
|
|
{
|
|
using (var uow = UowProvider.CreateUnitOfWork())
|
|
{
|
|
uow.ReadLock(Constants.Locks.ContentTree);
|
|
var repository = uow.CreateRepository<IContentRepository>();
|
|
var descendants = GetPublishedDescendantsLocked(repository, content);
|
|
uow.Complete();
|
|
return descendants;
|
|
}
|
|
}
|
|
|
|
internal IEnumerable<IContent> 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<int> { 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<IAuditRepository>();
|
|
repo.AddOrUpdate(new AuditItem(objectId, message, type, userId));
|
|
uow.Complete();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Publishes a <see cref="IContent"/> object and all its children
|
|
/// </summary>
|
|
/// <param name="content">The <see cref="IContent"/> to publish along with its children</param>
|
|
/// <param name="userId">Optional Id of the User issueing the publishing</param>
|
|
/// <param name="includeUnpublished">If set to true, this will also publish descendants that are completely unpublished, normally this will only publish children that have previously been published</param>
|
|
/// <returns>
|
|
/// 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.
|
|
/// </returns>
|
|
private IEnumerable<Attempt<PublishStatus>> 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<IContent>(); // this is for events
|
|
Attempt<PublishStatus>[] attempts;
|
|
|
|
using (var uow = UowProvider.CreateUnitOfWork())
|
|
{
|
|
uow.WriteLock(Constants.Locks.ContentTree);
|
|
var repository = uow.CreateRepository<IContentRepository>();
|
|
|
|
// 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<IContent> { 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<IContent>(publishedItems, false, false), this);
|
|
Audit(AuditType.Publish, "Publish with Children performed by user", userId, content.Id);
|
|
return attempts;
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// UnPublishes a single <see cref="IContent"/> object
|
|
/// </summary>
|
|
/// <param name="content">The <see cref="IContent"/> to publish</param>
|
|
/// <param name="omitCacheRefresh">Optional boolean to avoid having the cache refreshed when calling this Unpublish method. By default this method will update the cache.</param>
|
|
/// <param name="userId">Optional Id of the User issueing the publishing</param>
|
|
/// <returns>True if unpublishing succeeded, otherwise False</returns>
|
|
private Attempt<UnPublishStatus> 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<IContentRepository>();
|
|
|
|
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<IContent>(content, false, false), this);
|
|
return Attempt.Succeed(new UnPublishStatus(UnPublishedStatusType.Success, evtMsgs, content));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Saves and Publishes a single <see cref="IContent"/> object
|
|
/// </summary>
|
|
/// <param name="content">The <see cref="IContent"/> to save and publish</param>
|
|
/// <param name="userId">Optional Id of the User issueing the publishing</param>
|
|
/// <param name="raiseEvents">Optional boolean indicating whether or not to raise save events.</param>
|
|
/// <returns>True if publishing succeeded, otherwise False</returns>
|
|
private Attempt<PublishStatus> SaveAndPublishDo(IContent content, int userId = 0, bool raiseEvents = true)
|
|
{
|
|
var evtMsgs = EventMessagesFactory.Get();
|
|
|
|
if (raiseEvents && Saving.IsRaisedEventCancelled(new SaveEventArgs<IContent>(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<PublishStatus>);
|
|
|
|
using (var uow = UowProvider.CreateUnitOfWork())
|
|
{
|
|
uow.WriteLock(Constants.Locks.ContentTree);
|
|
var repository = uow.CreateRepository<IContentRepository>();
|
|
|
|
// 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<IContent>(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<IContent>(descendants, false, false), this);
|
|
}
|
|
}
|
|
|
|
Audit(AuditType.Publish, "Save and Publish performed by user", userId, content.Id);
|
|
return status;
|
|
}
|
|
|
|
private Attempt<PublishStatus> 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<ContentService>($"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<ContentService>($"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
|
|
|
|
/// <summary>
|
|
/// Occurs before Delete
|
|
/// </summary>
|
|
public static event TypedEventHandler<IContentService, DeleteEventArgs<IContent>> Deleting;
|
|
|
|
/// <summary>
|
|
/// Occurs after Delete
|
|
/// </summary>
|
|
public static event TypedEventHandler<IContentService, DeleteEventArgs<IContent>> Deleted;
|
|
|
|
/// <summary>
|
|
/// Occurs before Delete Versions
|
|
/// </summary>
|
|
public static event TypedEventHandler<IContentService, DeleteRevisionsEventArgs> DeletingVersions;
|
|
|
|
/// <summary>
|
|
/// Occurs after Delete Versions
|
|
/// </summary>
|
|
public static event TypedEventHandler<IContentService, DeleteRevisionsEventArgs> DeletedVersions;
|
|
|
|
/// <summary>
|
|
/// Occurs before Save
|
|
/// </summary>
|
|
public static event TypedEventHandler<IContentService, SaveEventArgs<IContent>> Saving;
|
|
|
|
/// <summary>
|
|
/// Occurs after Save
|
|
/// </summary>
|
|
public static event TypedEventHandler<IContentService, SaveEventArgs<IContent>> Saved;
|
|
|
|
/// <summary>
|
|
/// Occurs before Create
|
|
/// </summary>
|
|
[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<IContentService, NewEventArgs<IContent>> Creating;
|
|
|
|
/// <summary>
|
|
/// Occurs after Create
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// 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).
|
|
/// </remarks>
|
|
public static event TypedEventHandler<IContentService, NewEventArgs<IContent>> Created;
|
|
|
|
/// <summary>
|
|
/// Occurs before Copy
|
|
/// </summary>
|
|
public static event TypedEventHandler<IContentService, CopyEventArgs<IContent>> Copying;
|
|
|
|
/// <summary>
|
|
/// Occurs after Copy
|
|
/// </summary>
|
|
public static event TypedEventHandler<IContentService, CopyEventArgs<IContent>> Copied;
|
|
|
|
/// <summary>
|
|
/// Occurs before Content is moved to Recycle Bin
|
|
/// </summary>
|
|
public static event TypedEventHandler<IContentService, MoveEventArgs<IContent>> Trashing;
|
|
|
|
/// <summary>
|
|
/// Occurs after Content is moved to Recycle Bin
|
|
/// </summary>
|
|
public static event TypedEventHandler<IContentService, MoveEventArgs<IContent>> Trashed;
|
|
|
|
/// <summary>
|
|
/// Occurs before Move
|
|
/// </summary>
|
|
public static event TypedEventHandler<IContentService, MoveEventArgs<IContent>> Moving;
|
|
|
|
/// <summary>
|
|
/// Occurs after Move
|
|
/// </summary>
|
|
public static event TypedEventHandler<IContentService, MoveEventArgs<IContent>> Moved;
|
|
|
|
/// <summary>
|
|
/// Occurs before Rollback
|
|
/// </summary>
|
|
public static event TypedEventHandler<IContentService, RollbackEventArgs<IContent>> RollingBack;
|
|
|
|
/// <summary>
|
|
/// Occurs after Rollback
|
|
/// </summary>
|
|
public static event TypedEventHandler<IContentService, RollbackEventArgs<IContent>> RolledBack;
|
|
|
|
/// <summary>
|
|
/// Occurs before Send to Publish
|
|
/// </summary>
|
|
public static event TypedEventHandler<IContentService, SendToPublishEventArgs<IContent>> SendingToPublish;
|
|
|
|
/// <summary>
|
|
/// Occurs after Send to Publish
|
|
/// </summary>
|
|
public static event TypedEventHandler<IContentService, SendToPublishEventArgs<IContent>> SentToPublish;
|
|
|
|
/// <summary>
|
|
/// Occurs before the Recycle Bin is emptied
|
|
/// </summary>
|
|
public static event TypedEventHandler<IContentService, RecycleBinEventArgs> EmptyingRecycleBin;
|
|
|
|
/// <summary>
|
|
/// Occurs after the Recycle Bin has been Emptied
|
|
/// </summary>
|
|
public static event TypedEventHandler<IContentService, RecycleBinEventArgs> EmptiedRecycleBin;
|
|
|
|
/// <summary>
|
|
/// Occurs before publish
|
|
/// </summary>
|
|
public static event TypedEventHandler<IContentService, PublishEventArgs<IContent>> Publishing;
|
|
|
|
/// <summary>
|
|
/// Occurs after publish
|
|
/// </summary>
|
|
public static event TypedEventHandler<IContentService, PublishEventArgs<IContent>> Published;
|
|
|
|
/// <summary>
|
|
/// Occurs before unpublish
|
|
/// </summary>
|
|
public static event TypedEventHandler<IContentService, PublishEventArgs<IContent>> UnPublishing;
|
|
|
|
/// <summary>
|
|
/// Occurs after unpublish
|
|
/// </summary>
|
|
public static event TypedEventHandler<IContentService, PublishEventArgs<IContent>> UnPublished;
|
|
|
|
#endregion
|
|
|
|
#region Publishing Strategies
|
|
|
|
// prob. want to find nicer names?
|
|
|
|
internal Attempt<PublishStatus> StrategyCanPublish(IContent content, int userId, EventMessages evtMsgs)
|
|
{
|
|
if (Publishing.IsRaisedEventCancelled(new PublishEventArgs<IContent>(content, evtMsgs), this))
|
|
{
|
|
Logger.Info<ContentService>($"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<ContentService>($"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<ContentService>($"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<ContentService>($"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<ContentService>($"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<PublishStatus> 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<ContentService>($"Content '{content.Name}' with Id '{content.Id}' has been published.");
|
|
|
|
return attempt;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Publishes a list of content items
|
|
/// </summary>
|
|
/// <param name="contents">Contents, ordered by level ASC</param>
|
|
/// <param name="alreadyChecked">Contents for which we've already checked CanPublish</param>
|
|
/// <param name="userId"></param>
|
|
/// <param name="evtMsgs"></param>
|
|
/// <param name="includeUnpublished">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).</param>
|
|
/// <returns></returns>
|
|
/// <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
|
|
/// </remarks>
|
|
internal IEnumerable<Attempt<PublishStatus>> StrategyPublishWithChildren(IEnumerable<IContent> contents, IEnumerable<IContent> alreadyChecked, int userId, EventMessages evtMsgs, bool includeUnpublished = true)
|
|
{
|
|
var statuses = new List<Attempt<PublishStatus>>();
|
|
var alreadyCheckedA = (alreadyChecked ?? Enumerable.Empty<IContent>()).ToArray();
|
|
|
|
// list of ids that we exclude because they could not be published
|
|
var excude = new List<int>();
|
|
|
|
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<ContentService>($"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<UnPublishStatus> StrategyCanUnPublish(IContent content, int userId, EventMessages evtMsgs)
|
|
{
|
|
// fire UnPublishing event
|
|
if (UnPublishing.IsRaisedEventCancelled(new PublishEventArgs<IContent>(content, evtMsgs), this))
|
|
{
|
|
Logger.Info<ContentService>($"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<UnPublishStatus> 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<ContentService>($"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<ContentService>($"Content '{content.Name}' with Id '{content.Id}' has been unpublished.");
|
|
|
|
return attempt;
|
|
}
|
|
|
|
internal IEnumerable<Attempt<UnPublishStatus>> StrategyUnPublish(IEnumerable<IContent> content, int userId, EventMessages evtMsgs)
|
|
{
|
|
return content.Select(x => StrategyUnPublish(x, false, userId, evtMsgs));
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Content Types
|
|
|
|
/// <summary>
|
|
/// Deletes all content of specified type. All children of deleted content is moved to Recycle Bin.
|
|
/// </summary>
|
|
/// <remarks>This needs extra care and attention as its potentially a dangerous and extensive operation</remarks>
|
|
/// <param name="contentTypeId">Id of the <see cref="IContentType"/></param>
|
|
/// <param name="userId">Optional Id of the user issueing the delete operation</param>
|
|
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<Tuple<IContent, string>>();
|
|
|
|
using (var uow = UowProvider.CreateUnitOfWork())
|
|
{
|
|
uow.WriteLock(Constants.Locks.ContentTree);
|
|
var repository = uow.CreateRepository<IContentRepository>();
|
|
|
|
// 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<IContent>(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<IContent>(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<IContent>(x.Item1, x.Item2, x.Item1.ParentId))
|
|
.ToArray();
|
|
if (moveInfos.Length > 0)
|
|
Trashed.RaiseEvent(new MoveEventArgs<IContent>(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<IContentTypeRepository>();
|
|
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!
|
|
|
|
/// <summary>
|
|
/// Returns the persisted content's XML structure
|
|
/// </summary>
|
|
/// <param name="contentId"></param>
|
|
/// <returns></returns>
|
|
public XElement GetContentXml(int contentId)
|
|
{
|
|
using (var uow = UowProvider.CreateUnitOfWork())
|
|
{
|
|
uow.ReadLock(Constants.Locks.ContentTree);
|
|
var repository = uow.CreateRepository<IContentRepository>();
|
|
var elt = repository.GetContentXml(contentId);
|
|
uow.Complete();
|
|
return elt;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the persisted content's preview XML structure
|
|
/// </summary>
|
|
/// <param name="contentId"></param>
|
|
/// <param name="version"></param>
|
|
/// <returns></returns>
|
|
public XElement GetContentPreviewXml(int contentId, Guid version)
|
|
{
|
|
using (var uow = UowProvider.CreateUnitOfWork())
|
|
{
|
|
uow.ReadLock(Constants.Locks.ContentTree);
|
|
var repository = uow.CreateRepository<IContentRepository>();
|
|
var elt = repository.GetContentPreviewXml(contentId, version);
|
|
uow.Complete();
|
|
return elt;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Rebuilds all xml content in the cmsContentXml table for all documents
|
|
/// </summary>
|
|
/// <param name="contentTypeIds">
|
|
/// Only rebuild the xml structures for the content type ids passed in, if none then rebuilds the structures
|
|
/// for all content
|
|
/// </param>
|
|
public void RebuildXmlStructures(params int[] contentTypeIds)
|
|
{
|
|
using (var uow = UowProvider.CreateUnitOfWork())
|
|
{
|
|
uow.WriteLock(Constants.Locks.ContentTree);
|
|
var repository = uow.CreateRepository<IContentRepository>();
|
|
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
|
|
}
|
|
} |