Remove a bunch more events from ContentService

This commit is contained in:
Kenn Jacobsen
2021-03-01 17:05:59 +01:00
parent 87f8039497
commit ab2b35904f
19 changed files with 849 additions and 895 deletions

View File

@@ -126,8 +126,6 @@ namespace Umbraco.Cms.Core.Cache
() => MediaService.TreeChanged -= MediaService_TreeChanged);
// bind to content events
Bind(() => ContentService.Copied += ContentService_Copied, // needed for permissions
() => ContentService.Copied -= ContentService_Copied);
Bind(() => ContentService.TreeChanged += ContentService_TreeChanged,// handles all content changes
() => ContentService.TreeChanged -= ContentService_TreeChanged);

View File

@@ -1,10 +0,0 @@
using Umbraco.Cms.Core.Composing;
namespace Umbraco.Cms.Core.Compose
{
/// <summary>
/// A composer for Block editors to run a component
/// </summary>
public class BlockEditorComposer : ComponentComposer<BlockEditorComponent>, ICoreComposer
{ }
}

View File

@@ -1,10 +0,0 @@
using Umbraco.Cms.Core.Composing;
namespace Umbraco.Cms.Core.Compose
{
/// <summary>
/// A composer for nested content to run a component
/// </summary>
public class NestedContentPropertyComposer : ComponentComposer<NestedContentPropertyComponent>, ICoreComposer
{ }
}

View File

@@ -13,167 +13,15 @@ using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Hosting;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Entities;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.Implement;
using Umbraco.Cms.Infrastructure.Services;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.Compose
{
public sealed class NotificationsHandler :
INotificationHandler<SavedNotification<IContent>>,
INotificationHandler<SortedNotification<IContent>>
{
private readonly Notifier _notifier;
private readonly ActionCollection _actions;
private readonly IContentService _contentService;
public void Handle(SavedNotification<IContent> notification)
{
var newEntities = new List<IContent>();
var updatedEntities = new List<IContent>();
//need to determine if this is updating or if it is new
foreach (var entity in notification.SavedEntities)
{
var dirty = (IRememberBeingDirty)entity;
if (dirty.WasPropertyDirty("Id"))
{
//it's new
newEntities.Add(entity);
}
else
{
//it's updating
updatedEntities.Add(entity);
}
}
_notifier.Notify(_actions.GetAction<ActionNew>(), newEntities.ToArray());
_notifier.Notify(_actions.GetAction<ActionUpdate>(), updatedEntities.ToArray());
}
public void Handle(SortedNotification<IContent> notification)
{
var parentId = notification.SortedEntities.Select(x => x.ParentId).Distinct().ToList();
if (parentId.Count != 1)
return; // this shouldn't happen, for sorting all entities will have the same parent id
// in this case there's nothing to report since if the root is sorted we can't report on a fake entity.
// this is how it was in v7, we can't report on root changes because you can't subscribe to root changes.
if (parentId[0] <= 0)
return;
var parent = _contentService.GetById(parentId[0]);
if (parent == null)
return; // this shouldn't happen
_notifier.Notify(_actions.GetAction<ActionSort>(), new[] { parent });
}
/// <summary>
/// This class is used to send the notifications
/// </summary>
public sealed class Notifier
{
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
private readonly IHostingEnvironment _hostingEnvironment;
private readonly INotificationService _notificationService;
private readonly IUserService _userService;
private readonly ILocalizedTextService _textService;
private readonly GlobalSettings _globalSettings;
private readonly ILogger<Notifier> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="Notifier"/> class.
/// </summary>
public Notifier(
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
IHostingEnvironment hostingEnvironment,
INotificationService notificationService,
IUserService userService,
ILocalizedTextService textService,
IOptions<GlobalSettings> globalSettings,
ILogger<Notifier> logger)
{
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
_hostingEnvironment = hostingEnvironment;
_notificationService = notificationService;
_userService = userService;
_textService = textService;
_globalSettings = globalSettings.Value;
_logger = logger;
}
public void Notify(IAction action, params IContent[] entities)
{
var user = _backOfficeSecurityAccessor?.BackOfficeSecurity?.CurrentUser;
//if there is no current user, then use the admin
if (user == null)
{
_logger.LogDebug("There is no current Umbraco user logged in, the notifications will be sent from the administrator");
user = _userService.GetUserById(Constants.Security.SuperUserId);
if (user == null)
{
_logger.LogWarning("Notifications can not be sent, no admin user with id {SuperUserId} could be resolved", Constants.Security.SuperUserId);
return;
}
}
SendNotification(user, entities, action, _hostingEnvironment.ApplicationMainUrl);
}
private void SendNotification(IUser sender, IEnumerable<IContent> entities, IAction action, Uri siteUri)
{
if (sender == null)
throw new ArgumentNullException(nameof(sender));
if (siteUri == null)
{
_logger.LogWarning("Notifications can not be sent, no site URL is set (might be during boot process?)");
return;
}
//group by the content type variation since the emails will be different
foreach (var contentVariantGroup in entities.GroupBy(x => x.ContentType.Variations))
{
_notificationService.SendNotifications(
sender,
contentVariantGroup,
action.Letter.ToString(CultureInfo.InvariantCulture),
_textService.Localize("actions", action.Alias),
siteUri,
((IUser user, NotificationEmailSubjectParams subject) x)
=> _textService.Localize(
"notifications/mailSubject",
x.user.GetUserCulture(_textService, _globalSettings),
new[] { x.subject.SiteUrl, x.subject.Action, x.subject.ItemName }),
((IUser user, NotificationEmailBodyParams body, bool isHtml) x)
=> _textService.Localize(
x.isHtml ? "notifications/mailBodyHtml" : "notifications/mailBody",
x.user.GetUserCulture(_textService, _globalSettings),
new[]
{
x.body.RecipientName,
x.body.Action,
x.body.ItemName,
x.body.EditedUser,
x.body.SiteUrl,
x.body.ItemId,
//format the summary depending on if it's variant or not
contentVariantGroup.Key == ContentVariation.Culture
? (x.isHtml ? _textService.Localize("notifications/mailBodyVariantHtmlSummary", new[]{ x.body.Summary }) : _textService.Localize("notifications/mailBodyVariantSummary", new []{ x.body.Summary }))
: x.body.Summary,
x.body.ItemUrl
}));
}
}
}
}
// TODO: this component must be removed entirely - there is some code duplication in NotificationsHandler in anticipation of this component being deleted
public sealed class NotificationsComponent : IComponent
{
private readonly Notifier _notifier;
@@ -189,24 +37,6 @@ namespace Umbraco.Cms.Core.Compose
public void Initialize()
{
//Send notifications for the send to publish action
ContentService.SentToPublish += ContentService_SentToPublish;
//Send notifications for the published action
ContentService.Published += ContentService_Published;
////Send notifications for the saved action
//ContentService.Sorted += ContentService_Sorted;
////Send notifications for the update and created actions
//ContentService.Saved += ContentService_Saved;
//Send notifications for the unpublish action
ContentService.Unpublished += ContentService_Unpublished;
//Send notifications for the move/move to recycle bin and restore actions
ContentService.Moved += ContentService_Moved;
//Send notifications for the delete action when content is moved to the recycle bin
ContentService.Trashed += ContentService_Trashed;
//Send notifications for the copy action
ContentService.Copied += ContentService_Copied;
//Send notifications for the rollback action
ContentService.RolledBack += ContentService_RolledBack;
//Send notifications for the public access changed action
PublicAccessService.Saved += PublicAccessService_Saved;
@@ -215,15 +45,6 @@ namespace Umbraco.Cms.Core.Compose
public void Terminate()
{
ContentService.SentToPublish -= ContentService_SentToPublish;
ContentService.Published -= ContentService_Published;
//ContentService.Sorted -= ContentService_Sorted;
//ContentService.Saved -= ContentService_Saved;
ContentService.Unpublished -= ContentService_Unpublished;
ContentService.Moved -= ContentService_Moved;
ContentService.Trashed -= ContentService_Trashed;
ContentService.Copied -= ContentService_Copied;
ContentService.RolledBack -= ContentService_RolledBack;
PublicAccessService.Saved -= PublicAccessService_Saved;
UserService.UserGroupPermissionsAssigned -= UserService_UserGroupPermissionsAssigned;
}
@@ -234,72 +55,6 @@ namespace Umbraco.Cms.Core.Compose
private void PublicAccessService_Saved(IPublicAccessService sender, SaveEventArgs<PublicAccessEntry> args)
=> PublicAccessServiceSaved(args, _contentService);
private void ContentService_RolledBack(IContentService sender, RollbackEventArgs<IContent> args)
=> _notifier.Notify(_actions.GetAction<ActionRollback>(), args.Entity);
private void ContentService_Copied(IContentService sender, CopyEventArgs<IContent> args)
=> _notifier.Notify(_actions.GetAction<ActionCopy>(), args.Original);
private void ContentService_Trashed(IContentService sender, MoveEventArgs<IContent> args)
=> _notifier.Notify(_actions.GetAction<ActionDelete>(), args.MoveInfoCollection.Select(m => m.Entity).ToArray());
private void ContentService_Moved(IContentService sender, MoveEventArgs<IContent> args)
=> ContentServiceMoved(args);
private void ContentService_Unpublished(IContentService sender, PublishEventArgs<IContent> args)
=> _notifier.Notify(_actions.GetAction<ActionUnpublish>(), args.PublishedEntities.ToArray());
private void ContentService_Saved(IContentService sender, ContentSavedEventArgs args)
=> ContentServiceSaved(args);
private void ContentService_Sorted(IContentService sender, SaveEventArgs<IContent> args)
=> ContentServiceSorted(sender, args);
private void ContentService_Published(IContentService sender, ContentPublishedEventArgs args)
=> _notifier.Notify(_actions.GetAction<ActionPublish>(), args.PublishedEntities.ToArray());
private void ContentService_SentToPublish(IContentService sender, SendToPublishEventArgs<IContent> args)
=> _notifier.Notify(_actions.GetAction<ActionToPublish>(), args.Entity);
private void ContentServiceSorted(IContentService sender, SaveEventArgs<IContent> args)
{
var parentId = args.SavedEntities.Select(x => x.ParentId).Distinct().ToList();
if (parentId.Count != 1) return; // this shouldn't happen, for sorting all entities will have the same parent id
// in this case there's nothing to report since if the root is sorted we can't report on a fake entity.
// this is how it was in v7, we can't report on root changes because you can't subscribe to root changes.
if (parentId[0] <= 0) return;
var parent = sender.GetById(parentId[0]);
if (parent == null) return; // this shouldn't happen
_notifier.Notify(_actions.GetAction<ActionSort>(), new[] { parent });
}
private void ContentServiceSaved(SaveEventArgs<IContent> args)
{
var newEntities = new List<IContent>();
var updatedEntities = new List<IContent>();
//need to determine if this is updating or if it is new
foreach (var entity in args.SavedEntities)
{
var dirty = (IRememberBeingDirty)entity;
if (dirty.WasPropertyDirty("Id"))
{
//it's new
newEntities.Add(entity);
}
else
{
//it's updating
updatedEntities.Add(entity);
}
}
_notifier.Notify(_actions.GetAction<ActionNew>(), newEntities.ToArray());
_notifier.Notify(_actions.GetAction<ActionUpdate>(), updatedEntities.ToArray());
}
private void UserServiceUserGroupPermissionsAssigned(SaveEventArgs<EntityPermission> args, IContentService contentService)
{
var entities = contentService.GetByIds(args.SavedEntities.Select(e => e.EntityId)).ToArray();
@@ -310,22 +65,6 @@ namespace Umbraco.Cms.Core.Compose
_notifier.Notify(_actions.GetAction<ActionRights>(), entities);
}
private void ContentServiceMoved(MoveEventArgs<IContent> args)
{
// notify about the move for all moved items
_notifier.Notify(_actions.GetAction<ActionMove>(), args.MoveInfoCollection.Select(m => m.Entity).ToArray());
// for any items being moved from the recycle bin (restored), explicitly notify about that too
var restoredEntities = args.MoveInfoCollection
.Where(m => m.OriginalPath.Contains(Constants.System.RecycleBinContentString))
.Select(m => m.Entity)
.ToArray();
if (restoredEntities.Any())
{
_notifier.Notify(_actions.GetAction<ActionRestore>(), restoredEntities);
}
}
private void PublicAccessServiceSaved(SaveEventArgs<PublicAccessEntry> args, IContentService contentService)
{
var entities = contentService.GetByIds(args.SavedEntities.Select(e => e.ProtectedNodeId)).ToArray();

View File

@@ -0,0 +1,217 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Actions;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Hosting;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Entities;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Infrastructure.Services;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.Compose
{
// TODO: insert these notification handlers in core composition
public sealed class NotificationsHandler :
INotificationHandler<SavedNotification<IContent>>,
INotificationHandler<SortedNotification<IContent>>,
INotificationHandler<PublishedNotification<IContent>>,
INotificationHandler<MovedNotification<IContent>>,
INotificationHandler<TrashedNotification<IContent>>,
INotificationHandler<CopiedNotification<IContent>>,
INotificationHandler<RolledBackNotification<IContent>>,
INotificationHandler<SentToPublishNotification<IContent>>,
INotificationHandler<UnpublishedNotification<IContent>>
{
private readonly Notifier _notifier;
private readonly ActionCollection _actions;
private readonly IContentService _contentService;
public NotificationsHandler(Notifier notifier, ActionCollection actions, IContentService contentService)
{
_notifier = notifier;
_actions = actions;
_contentService = contentService;
}
public void Handle(SavedNotification<IContent> notification)
{
var newEntities = new List<IContent>();
var updatedEntities = new List<IContent>();
//need to determine if this is updating or if it is new
foreach (var entity in notification.SavedEntities)
{
var dirty = (IRememberBeingDirty)entity;
if (dirty.WasPropertyDirty("Id"))
{
//it's new
newEntities.Add(entity);
}
else
{
//it's updating
updatedEntities.Add(entity);
}
}
_notifier.Notify(_actions.GetAction<ActionNew>(), newEntities.ToArray());
_notifier.Notify(_actions.GetAction<ActionUpdate>(), updatedEntities.ToArray());
}
public void Handle(SortedNotification<IContent> notification)
{
var parentId = notification.SortedEntities.Select(x => x.ParentId).Distinct().ToList();
if (parentId.Count != 1)
return; // this shouldn't happen, for sorting all entities will have the same parent id
// in this case there's nothing to report since if the root is sorted we can't report on a fake entity.
// this is how it was in v7, we can't report on root changes because you can't subscribe to root changes.
if (parentId[0] <= 0)
return;
var parent = _contentService.GetById(parentId[0]);
if (parent == null)
return; // this shouldn't happen
_notifier.Notify(_actions.GetAction<ActionSort>(), new[] { parent });
}
public void Handle(PublishedNotification<IContent> notification) => _notifier.Notify(_actions.GetAction<ActionPublish>(), notification.PublishedEntities.ToArray());
public void Handle(MovedNotification<IContent> notification)
{
// notify about the move for all moved items
_notifier.Notify(_actions.GetAction<ActionMove>(), notification.MoveInfoCollection.Select(m => m.Entity).ToArray());
// for any items being moved from the recycle bin (restored), explicitly notify about that too
var restoredEntities = notification.MoveInfoCollection
.Where(m => m.OriginalPath.Contains(Constants.System.RecycleBinContentString))
.Select(m => m.Entity)
.ToArray();
if (restoredEntities.Any())
{
_notifier.Notify(_actions.GetAction<ActionRestore>(), restoredEntities);
}
}
public void Handle(TrashedNotification<IContent> notification) => _notifier.Notify(_actions.GetAction<ActionDelete>(), notification.MoveInfoCollection.Select(m => m.Entity).ToArray());
public void Handle(CopiedNotification<IContent> notification) => _notifier.Notify(_actions.GetAction<ActionCopy>(), notification.Original);
public void Handle(RolledBackNotification<IContent> notification) => _notifier.Notify(_actions.GetAction<ActionRollback>(), notification.Entity);
public void Handle(SentToPublishNotification<IContent> notification) => _notifier.Notify(_actions.GetAction<ActionToPublish>(), notification.Entity);
public void Handle(UnpublishedNotification<IContent> notification) => _notifier.Notify(_actions.GetAction<ActionUnpublish>(), notification.UnpublishedEntities.ToArray());
/// <summary>
/// This class is used to send the notifications
/// </summary>
public sealed class Notifier
{
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
private readonly IHostingEnvironment _hostingEnvironment;
private readonly INotificationService _notificationService;
private readonly IUserService _userService;
private readonly ILocalizedTextService _textService;
private readonly GlobalSettings _globalSettings;
private readonly ILogger<Notifier> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="Notifier"/> class.
/// </summary>
public Notifier(
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
IHostingEnvironment hostingEnvironment,
INotificationService notificationService,
IUserService userService,
ILocalizedTextService textService,
IOptions<GlobalSettings> globalSettings,
ILogger<Notifier> logger)
{
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
_hostingEnvironment = hostingEnvironment;
_notificationService = notificationService;
_userService = userService;
_textService = textService;
_globalSettings = globalSettings.Value;
_logger = logger;
}
public void Notify(IAction action, params IContent[] entities)
{
var user = _backOfficeSecurityAccessor?.BackOfficeSecurity?.CurrentUser;
//if there is no current user, then use the admin
if (user == null)
{
_logger.LogDebug("There is no current Umbraco user logged in, the notifications will be sent from the administrator");
user = _userService.GetUserById(Constants.Security.SuperUserId);
if (user == null)
{
_logger.LogWarning("Notifications can not be sent, no admin user with id {SuperUserId} could be resolved", Constants.Security.SuperUserId);
return;
}
}
SendNotification(user, entities, action, _hostingEnvironment.ApplicationMainUrl);
}
private void SendNotification(IUser sender, IEnumerable<IContent> entities, IAction action, Uri siteUri)
{
if (sender == null)
throw new ArgumentNullException(nameof(sender));
if (siteUri == null)
{
_logger.LogWarning("Notifications can not be sent, no site URL is set (might be during boot process?)");
return;
}
//group by the content type variation since the emails will be different
foreach (var contentVariantGroup in entities.GroupBy(x => x.ContentType.Variations))
{
_notificationService.SendNotifications(
sender,
contentVariantGroup,
action.Letter.ToString(CultureInfo.InvariantCulture),
_textService.Localize("actions", action.Alias),
siteUri,
((IUser user, NotificationEmailSubjectParams subject) x)
=> _textService.Localize(
"notifications/mailSubject",
x.user.GetUserCulture(_textService, _globalSettings),
new[] { x.subject.SiteUrl, x.subject.Action, x.subject.ItemName }),
((IUser user, NotificationEmailBodyParams body, bool isHtml) x)
=> _textService.Localize(
x.isHtml ? "notifications/mailBodyHtml" : "notifications/mailBody",
x.user.GetUserCulture(_textService, _globalSettings),
new[]
{
x.body.RecipientName,
x.body.Action,
x.body.ItemName,
x.body.EditedUser,
x.body.SiteUrl,
x.body.ItemId,
//format the summary depending on if it's variant or not
contentVariantGroup.Key == ContentVariation.Culture
? (x.isHtml ? _textService.Localize("notifications/mailBodyVariantHtmlSummary", new[]{ x.body.Summary }) : _textService.Localize("notifications/mailBodyVariantSummary", new []{ x.body.Summary }))
: x.body.Summary,
x.body.ItemUrl
}));
}
}
}
}
}

View File

@@ -1,59 +0,0 @@
using Umbraco.Cms.Core.Composing;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.Implement;
namespace Umbraco.Cms.Core.Compose
{
// TODO: This should just exist in the content service/repo!
public sealed class RelateOnCopyComponent : IComponent
{
private readonly IRelationService _relationService;
private readonly IAuditService _auditService;
public RelateOnCopyComponent(IRelationService relationService, IAuditService auditService)
{
_relationService = relationService;
_auditService = auditService;
}
public void Initialize()
{
ContentService.Copied += ContentServiceCopied;
}
public void Terminate()
{
ContentService.Copied -= ContentServiceCopied;
}
private void ContentServiceCopied(IContentService sender, CopyEventArgs<IContent> e)
{
if (e.RelateToOriginal == false) return;
var relationType = _relationService.GetRelationTypeByAlias(Cms.Core.Constants.Conventions.RelationTypes.RelateDocumentOnCopyAlias);
if (relationType == null)
{
relationType = new RelationType(Cms.Core.Constants.Conventions.RelationTypes.RelateDocumentOnCopyAlias,
Cms.Core.Constants.Conventions.RelationTypes.RelateDocumentOnCopyName,
true,
Cms.Core.Constants.ObjectTypes.Document,
Cms.Core.Constants.ObjectTypes.Document);
_relationService.Save(relationType);
}
var relation = new Relation(e.Original.Id, e.Copy.Id, relationType);
_relationService.Save(relation);
_auditService.Add(
AuditType.Copy,
e.Copy.WriterId,
e.Copy.Id, ObjectTypes.GetName(UmbracoObjectTypes.Document),
$"Copied content with Id: '{e.Copy.Id}' related to original content with Id: '{e.Original.Id}'");
}
}
}

View File

@@ -1,7 +0,0 @@
using Umbraco.Cms.Core.Composing;
namespace Umbraco.Cms.Core.Compose
{
public sealed class RelateOnCopyComposer : ComponentComposer<RelateOnCopyComponent>, ICoreComposer
{ }
}

View File

@@ -1,15 +1,20 @@
using System.Linq;
using System.Linq;
using Umbraco.Cms.Core.Composing;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Scoping;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.Implement;
using Umbraco.Cms.Infrastructure.Services;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.Compose
{
public sealed class RelateOnTrashComponent : IComponent
// TODO: insert these notification handlers in core composition
// TODO: lots of duplicate code in this one, refactor
public sealed class RelateOnTrashHandler :
INotificationHandler<MovedNotification<IContent>>,
INotificationHandler<TrashedNotification<IContent>>
{
private readonly IRelationService _relationService;
private readonly IEntityService _entityService;
@@ -17,7 +22,7 @@ namespace Umbraco.Cms.Core.Compose
private readonly IAuditService _auditService;
private readonly IScopeProvider _scopeProvider;
public RelateOnTrashComponent(
public RelateOnTrashHandler(
IRelationService relationService,
IEntityService entityService,
ILocalizedTextService textService,
@@ -31,27 +36,10 @@ namespace Umbraco.Cms.Core.Compose
_scopeProvider = scopeProvider;
}
public void Initialize()
public void Handle(MovedNotification<IContent> notification)
{
ContentService.Moved += ContentService_Moved;
ContentService.Trashed += ContentService_Trashed;
MediaService.Moved += MediaService_Moved;
MediaService.Trashed += MediaService_Trashed;
}
public void Terminate()
{
ContentService.Moved -= ContentService_Moved;
ContentService.Trashed -= ContentService_Trashed;
MediaService.Moved -= MediaService_Moved;
MediaService.Trashed -= MediaService_Trashed;
}
private void ContentService_Moved(IContentService sender, MoveEventArgs<IContent> e)
{
foreach (var item in e.MoveInfoCollection.Where(x => x.OriginalPath.Contains(Cms.Core.Constants.System.RecycleBinContentString)))
foreach (var item in notification.MoveInfoCollection.Where(x => x.OriginalPath.Contains(Cms.Core.Constants.System.RecycleBinContentString)))
{
const string relationTypeAlias = Cms.Core.Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteAlias;
var relations = _relationService.GetByChildId(item.Entity.Id);
@@ -62,20 +50,7 @@ namespace Umbraco.Cms.Core.Compose
}
}
private void MediaService_Moved(IMediaService sender, MoveEventArgs<IMedia> e)
{
foreach (var item in e.MoveInfoCollection.Where(x => x.OriginalPath.Contains(Cms.Core.Constants.System.RecycleBinMediaString)))
{
const string relationTypeAlias = Cms.Core.Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteAlias;
var relations = _relationService.GetByChildId(item.Entity.Id);
foreach (var relation in relations.Where(x => x.RelationType.Alias.InvariantEquals(relationTypeAlias)))
{
_relationService.Delete(relation);
}
}
}
private void ContentService_Trashed(IContentService sender, MoveEventArgs<IContent> e)
public void Handle(TrashedNotification<IContent> notification)
{
using (var scope = _scopeProvider.CreateScope())
{
@@ -94,7 +69,7 @@ namespace Umbraco.Cms.Core.Compose
_relationService.Save(relationType);
}
foreach (var item in e.MoveInfoCollection)
foreach (var item in notification.MoveInfoCollection)
{
var originalPath = item.OriginalPath.ToDelimitedList();
var originalParentId = originalPath.Count > 2
@@ -124,6 +99,55 @@ namespace Umbraco.Cms.Core.Compose
scope.Complete();
}
}
}
public sealed class RelateOnTrashComponent : IComponent
{
private readonly IRelationService _relationService;
private readonly IEntityService _entityService;
private readonly ILocalizedTextService _textService;
private readonly IAuditService _auditService;
private readonly IScopeProvider _scopeProvider;
public RelateOnTrashComponent(
IRelationService relationService,
IEntityService entityService,
ILocalizedTextService textService,
IAuditService auditService,
IScopeProvider scopeProvider)
{
_relationService = relationService;
_entityService = entityService;
_textService = textService;
_auditService = auditService;
_scopeProvider = scopeProvider;
}
public void Initialize()
{
MediaService.Moved += MediaService_Moved;
MediaService.Trashed += MediaService_Trashed;
}
public void Terminate()
{
MediaService.Moved -= MediaService_Moved;
MediaService.Trashed -= MediaService_Trashed;
}
private void MediaService_Moved(IMediaService sender, MoveEventArgs<IMedia> e)
{
foreach (var item in e.MoveInfoCollection.Where(x => x.OriginalPath.Contains(Cms.Core.Constants.System.RecycleBinMediaString)))
{
const string relationTypeAlias = Cms.Core.Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteAlias;
var relations = _relationService.GetByChildId(item.Entity.Id);
foreach (var relation in relations.Where(x => x.RelationType.Alias.InvariantEquals(relationTypeAlias)))
{
_relationService.Delete(relation);
}
}
}
public void MediaService_Trashed(IMediaService sender, MoveEventArgs<IMedia> e)
{

View File

@@ -1,36 +1,28 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Umbraco.Cms.Core.Composing;
using Umbraco.Cms.Core.Models.Blocks;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.Compose
namespace Umbraco.Cms.Core.PropertyEditors
{
/// <summary>
/// A component for Block editors used to bind to events
/// A handler for Block editors used to bind to notifications
/// </summary>
public class BlockEditorComponent : IComponent
// TODO: insert these notification handlers in core composition
public class BlockEditorPropertyHandler : ComplexPropertyEditorContentNotificationHandler
{
private ComplexPropertyEditorContentEventHandler _handler;
private readonly BlockListEditorDataConverter _converter = new BlockListEditorDataConverter();
public void Initialize()
{
_handler = new ComplexPropertyEditorContentEventHandler(
Constants.PropertyEditors.Aliases.BlockList,
ReplaceBlockListUdis);
}
protected override string EditorAlias => Constants.PropertyEditors.Aliases.BlockList;
public void Terminate() => _handler?.Dispose();
private string ReplaceBlockListUdis(string rawJson, bool onlyMissingUdis)
protected override string FormatPropertyValue(string rawJson, bool onlyMissingKeys)
{
// the block editor doesn't ever have missing UDIs so when this is true there's nothing to process
if (onlyMissingUdis) return rawJson;
if (onlyMissingKeys)
return rawJson;
return ReplaceBlockListUdis(rawJson, null);
}

View File

@@ -1,145 +0,0 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using System;
using System.Collections.Generic;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.Implement;
using Umbraco.Cms.Infrastructure.Services;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.PropertyEditors
{
// TODO: add copying handling
public abstract class ComplexPropertyEditorContentNotificationHandler
: INotificationHandler<SavingNotification<IContent>>
{
private readonly string _editorAlias;
private readonly Func<string, bool, string> _formatPropertyValue;
protected ComplexPropertyEditorContentNotificationHandler(string editorAlias, Func<string, bool, string> formatPropertyValue)
{
_editorAlias = editorAlias;
_formatPropertyValue = formatPropertyValue;
}
public void Handle(SavingNotification<IContent> notification)
{
foreach (var entity in notification.SavedEntities)
{
var props = entity.GetPropertiesByEditor(_editorAlias);
UpdatePropertyValues(props, true);
}
}
private void UpdatePropertyValues(IEnumerable<IProperty> props, bool onlyMissingKeys)
{
foreach (var prop in props)
{
// A Property may have one or more values due to cultures
var propVals = prop.Values;
foreach (var cultureVal in propVals)
{
// Remove keys from published value & any nested properties
var updatedPublishedVal = _formatPropertyValue(cultureVal.PublishedValue?.ToString(), onlyMissingKeys);
cultureVal.PublishedValue = updatedPublishedVal;
// Remove keys from edited/draft value & any nested properties
var updatedEditedVal = _formatPropertyValue(cultureVal.EditedValue?.ToString(), onlyMissingKeys);
cultureVal.EditedValue = updatedEditedVal;
}
}
}
}
/// <summary>
/// Utility class for dealing with <see cref="ContentService"/> Copying/Saving events for complex editors
/// </summary>
public class ComplexPropertyEditorContentEventHandler : IDisposable
{
private readonly string _editorAlias;
private readonly Func<string, bool, string> _formatPropertyValue;
private bool _disposedValue;
public ComplexPropertyEditorContentEventHandler(string editorAlias,
Func<string, bool, string> formatPropertyValue)
{
_editorAlias = editorAlias;
_formatPropertyValue = formatPropertyValue;
ContentService.Copying += ContentService_Copying;
//ContentService.Saving += ContentService_Saving;
}
/// <summary>
/// <see cref="ContentService"/> Copying event handler
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void ContentService_Copying(IContentService sender, CopyEventArgs<IContent> e)
{
var props = e.Copy.GetPropertiesByEditor(_editorAlias);
UpdatePropertyValues(props, false);
}
/// <summary>
/// <see cref="ContentService"/> Saving event handler
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void ContentService_Saving(IContentService sender, ContentSavingEventArgs e)
{
foreach (var entity in e.SavedEntities)
{
var props = entity.GetPropertiesByEditor(_editorAlias);
UpdatePropertyValues(props, true);
}
}
private void UpdatePropertyValues(IEnumerable<IProperty> props, bool onlyMissingKeys)
{
foreach (var prop in props)
{
// A Property may have one or more values due to cultures
var propVals = prop.Values;
foreach (var cultureVal in propVals)
{
// Remove keys from published value & any nested properties
var updatedPublishedVal = _formatPropertyValue(cultureVal.PublishedValue?.ToString(), onlyMissingKeys);
cultureVal.PublishedValue = updatedPublishedVal;
// Remove keys from edited/draft value & any nested properties
var updatedEditedVal = _formatPropertyValue(cultureVal.EditedValue?.ToString(), onlyMissingKeys);
cultureVal.EditedValue = updatedEditedVal;
}
}
}
/// <summary>
/// Unbinds from events
/// </summary>
/// <param name="disposing"></param>
protected virtual void Dispose(bool disposing)
{
if (!_disposedValue)
{
if (disposing)
{
ContentService.Copying -= ContentService_Copying;
//ContentService.Saving -= ContentService_Saving;
}
_disposedValue = true;
}
}
/// <summary>
/// Unbinds from events
/// </summary>
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}
}

View File

@@ -0,0 +1,55 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using System.Collections.Generic;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Infrastructure.Services;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.PropertyEditors
{
// TODO: insert these notification handlers in core composition
public abstract class ComplexPropertyEditorContentNotificationHandler :
INotificationHandler<SavingNotification<IContent>>,
INotificationHandler<CopyingNotification<IContent>>
{
protected abstract string EditorAlias { get; }
protected abstract string FormatPropertyValue(string rawJson, bool onlyMissingKeys);
public void Handle(SavingNotification<IContent> notification)
{
foreach (var entity in notification.SavedEntities)
{
var props = entity.GetPropertiesByEditor(EditorAlias);
UpdatePropertyValues(props, true);
}
}
public void Handle(CopyingNotification<IContent> notification)
{
var props = notification.Copy.GetPropertiesByEditor(EditorAlias);
UpdatePropertyValues(props, false);
}
private void UpdatePropertyValues(IEnumerable<IProperty> props, bool onlyMissingKeys)
{
foreach (var prop in props)
{
// A Property may have one or more values due to cultures
var propVals = prop.Values;
foreach (var cultureVal in propVals)
{
// Remove keys from published value & any nested properties
var updatedPublishedVal = FormatPropertyValue(cultureVal.PublishedValue?.ToString(), onlyMissingKeys);
cultureVal.PublishedValue = updatedPublishedVal;
// Remove keys from edited/draft value & any nested properties
var updatedEditedVal = FormatPropertyValue(cultureVal.EditedValue?.ToString(), onlyMissingKeys);
cultureVal.EditedValue = updatedEditedVal;
}
}
}
}
}

View File

@@ -1,4 +1,4 @@
// Copyright (c) Umbraco.
// Copyright (c) Umbraco.
// See LICENSE for more details.
using System;
@@ -14,6 +14,7 @@ using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Strings;
using Umbraco.Cms.Infrastructure.Services;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.PropertyEditors
@@ -24,7 +25,8 @@ namespace Umbraco.Cms.Core.PropertyEditors
"fileupload",
Group = Constants.PropertyEditors.Groups.Media,
Icon = "icon-download-alt")]
public class FileUploadPropertyEditor : DataEditor, IMediaUrlGenerator
// TODO: insert these notification handlers in core composition
public class FileUploadPropertyEditor : DataEditor, IMediaUrlGenerator, INotificationHandler<CopiedNotification<IContent>>, INotificationHandler<DeletedNotification<IContent>>
{
private readonly IMediaFileSystem _mediaFileSystem;
private readonly ContentSettings _contentSettings;
@@ -32,6 +34,7 @@ namespace Umbraco.Cms.Core.PropertyEditors
private readonly IDataTypeService _dataTypeService;
private readonly ILocalizationService _localizationService;
private readonly ILocalizedTextService _localizedTextService;
private readonly IContentService _contentService;
public FileUploadPropertyEditor(
ILoggerFactory loggerFactory,
@@ -42,7 +45,8 @@ namespace Umbraco.Cms.Core.PropertyEditors
ILocalizedTextService localizedTextService,
IShortStringHelper shortStringHelper,
UploadAutoFillProperties uploadAutoFillProperties,
IJsonSerializer jsonSerializer)
IJsonSerializer jsonSerializer,
IContentService contentService)
: base(loggerFactory, dataTypeService, localizationService, localizedTextService, shortStringHelper, jsonSerializer)
{
_mediaFileSystem = mediaFileSystem ?? throw new ArgumentNullException(nameof(mediaFileSystem));
@@ -51,6 +55,7 @@ namespace Umbraco.Cms.Core.PropertyEditors
_localizationService = localizationService;
_localizedTextService = localizedTextService;
_uploadAutoFillProperties = uploadAutoFillProperties;
_contentService = contentService;
}
/// <summary>
@@ -119,37 +124,6 @@ namespace Umbraco.Cms.Core.PropertyEditors
}
}
/// <summary>
/// After a content has been copied, also copy uploaded files.
/// </summary>
/// <param name="sender">The event sender.</param>
/// <param name="args">The event arguments.</param>
internal void ContentServiceCopied(IContentService sender, CopyEventArgs<IContent> args)
{
// get the upload field properties with a value
var properties = args.Original.Properties.Where(IsUploadField);
// copy files
var isUpdated = false;
foreach (var property in properties)
{
//copy each of the property values (variants, segments) to the destination
foreach (var propertyValue in property.Values)
{
var propVal = property.GetValue(propertyValue.Culture, propertyValue.Segment);
if (propVal == null || !(propVal is string str) || str.IsNullOrWhiteSpace()) continue;
var sourcePath = _mediaFileSystem.GetRelativePath(str);
var copyPath = _mediaFileSystem.CopyFile(args.Copy, property.PropertyType, sourcePath);
args.Copy.SetValue(property.Alias, _mediaFileSystem.GetUrl(copyPath), propertyValue.Culture, propertyValue.Segment);
isUpdated = true;
}
}
// if updated, re-save the copy with the updated value
if (isUpdated)
sender.Save(args.Copy);
}
/// <summary>
/// After a media has been created, auto-fill the properties.
/// </summary>
@@ -182,6 +156,40 @@ namespace Umbraco.Cms.Core.PropertyEditors
AutoFillProperties(entity);
}
public void Handle(CopiedNotification<IContent> notification)
{
// get the upload field properties with a value
var properties = notification.Original.Properties.Where(IsUploadField);
// copy files
var isUpdated = false;
foreach (var property in properties)
{
//copy each of the property values (variants, segments) to the destination
foreach (var propertyValue in property.Values)
{
var propVal = property.GetValue(propertyValue.Culture, propertyValue.Segment);
if (propVal == null || !(propVal is string str) || str.IsNullOrWhiteSpace())
{
continue;
}
var sourcePath = _mediaFileSystem.GetRelativePath(str);
var copyPath = _mediaFileSystem.CopyFile(notification.Copy, property.PropertyType, sourcePath);
notification.Copy.SetValue(property.Alias, _mediaFileSystem.GetUrl(copyPath), propertyValue.Culture, propertyValue.Segment);
isUpdated = true;
}
}
// if updated, re-save the copy with the updated value
if (isUpdated)
{
_contentService.Save(notification.Copy);
}
}
public void Handle(DeletedNotification<IContent> notification) => notification.MediaFilesToDelete.AddRange(ServiceDeleted(notification.DeletedEntities.OfType<ContentBase>()));
/// <summary>
/// Auto-fill properties (or clear).
/// </summary>

View File

@@ -1,4 +1,4 @@
// Copyright (c) Umbraco.
// Copyright (c) Umbraco.
// See LICENSE for more details.
using System;
@@ -16,6 +16,7 @@ using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Strings;
using Umbraco.Cms.Infrastructure.Services;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.PropertyEditors
@@ -31,7 +32,8 @@ namespace Umbraco.Cms.Core.PropertyEditors
HideLabel = false,
Group = Constants.PropertyEditors.Groups.Media,
Icon = "icon-crop")]
public class ImageCropperPropertyEditor : DataEditor, IMediaUrlGenerator
// TODO: insert these notification handlers in core composition
public class ImageCropperPropertyEditor : DataEditor, IMediaUrlGenerator, INotificationHandler<CopiedNotification<IContent>>, INotificationHandler<DeletedNotification<IContent>>
{
private readonly IMediaFileSystem _mediaFileSystem;
private readonly ContentSettings _contentSettings;
@@ -39,6 +41,7 @@ namespace Umbraco.Cms.Core.PropertyEditors
private readonly IIOHelper _ioHelper;
private readonly UploadAutoFillProperties _autoFillProperties;
private readonly ILogger<ImageCropperPropertyEditor> _logger;
private readonly IContentService _contentService;
/// <summary>
/// Initializes a new instance of the <see cref="ImageCropperPropertyEditor"/> class.
@@ -53,7 +56,8 @@ namespace Umbraco.Cms.Core.PropertyEditors
IShortStringHelper shortStringHelper,
ILocalizedTextService localizedTextService,
UploadAutoFillProperties uploadAutoFillProperties,
IJsonSerializer jsonSerializer)
IJsonSerializer jsonSerializer,
IContentService contentService)
: base(loggerFactory, dataTypeService, localizationService, localizedTextService, shortStringHelper, jsonSerializer)
{
_mediaFileSystem = mediaFileSystem ?? throw new ArgumentNullException(nameof(mediaFileSystem));
@@ -61,6 +65,7 @@ namespace Umbraco.Cms.Core.PropertyEditors
_dataTypeService = dataTypeService ?? throw new ArgumentNullException(nameof(dataTypeService));
_ioHelper = ioHelper ?? throw new ArgumentNullException(nameof(ioHelper));
_autoFillProperties = uploadAutoFillProperties ?? throw new ArgumentNullException(nameof(uploadAutoFillProperties));
_contentService = contentService;
_logger = loggerFactory.CreateLogger<ImageCropperPropertyEditor>();
}
@@ -175,12 +180,10 @@ namespace Umbraco.Cms.Core.PropertyEditors
/// <summary>
/// After a content has been copied, also copy uploaded files.
/// </summary>
/// <param name="sender">The event sender.</param>
/// <param name="args">The event arguments.</param>
public void ContentServiceCopied(IContentService sender, CopyEventArgs<IContent> args)
public void Handle(CopiedNotification<IContent> notification)
{
// get the image cropper field properties
var properties = args.Original.Properties.Where(IsCropperField);
var properties = notification.Original.Properties.Where(IsCropperField);
// copy files
var isUpdated = false;
@@ -191,19 +194,27 @@ namespace Umbraco.Cms.Core.PropertyEditors
{
var propVal = property.GetValue(propertyValue.Culture, propertyValue.Segment);
var src = GetFileSrcFromPropertyValue(propVal, out var jo);
if (src == null) continue;
if (src == null)
{
continue;
}
var sourcePath = _mediaFileSystem.GetRelativePath(src);
var copyPath = _mediaFileSystem.CopyFile(args.Copy, property.PropertyType, sourcePath);
var copyPath = _mediaFileSystem.CopyFile(notification.Copy, property.PropertyType, sourcePath);
jo["src"] = _mediaFileSystem.GetUrl(copyPath);
args.Copy.SetValue(property.Alias, jo.ToString(), propertyValue.Culture, propertyValue.Segment);
notification.Copy.SetValue(property.Alias, jo.ToString(), propertyValue.Culture, propertyValue.Segment);
isUpdated = true;
}
}
// if updated, re-save the copy with the updated value
if (isUpdated)
sender.Save(args.Copy);
{
_contentService.Save(notification.Copy);
}
}
public void Handle(DeletedNotification<IContent> notification) => notification.MediaFilesToDelete.AddRange(ServiceDeleted(notification.DeletedEntities.OfType<ContentBase>()));
/// <summary>
/// After a media has been created, auto-fill the properties.
/// </summary>

View File

@@ -1,29 +1,19 @@
using System;
using System;
using System.Linq;
using Newtonsoft.Json.Linq;
using Umbraco.Cms.Core.Composing;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.Compose
namespace Umbraco.Cms.Core.PropertyEditors
{
/// <summary>
/// A component for NestedContent used to bind to events
/// A handler for NestedContent used to bind to notifications
/// </summary>
public class NestedContentPropertyComponent : IComponent
// TODO: insert these notification handlers in core composition
public class NestedContentPropertyHandler : ComplexPropertyEditorContentNotificationHandler
{
private ComplexPropertyEditorContentEventHandler _handler;
protected override string EditorAlias => Constants.PropertyEditors.Aliases.NestedContent;
public void Initialize()
{
_handler = new ComplexPropertyEditorContentEventHandler(
Constants.PropertyEditors.Aliases.NestedContent,
CreateNestedContentKeys);
}
public void Terminate() => _handler?.Dispose();
private string CreateNestedContentKeys(string rawJson, bool onlyMissingKeys) => CreateNestedContentKeys(rawJson, onlyMissingKeys, null);
protected override string FormatPropertyValue(string rawJson, bool onlyMissingKeys) => CreateNestedContentKeys(rawJson, onlyMissingKeys, null);
// internal for tests
internal string CreateNestedContentKeys(string rawJson, bool onlyMissingKeys, Func<Guid> createGuid = null)
@@ -78,6 +68,5 @@ namespace Umbraco.Cms.Core.Compose
}
}
}
}
}

View File

@@ -1,4 +1,4 @@
// Copyright (c) Umbraco.
// Copyright (c) Umbraco.
// See LICENSE for more details.
using System;
@@ -42,17 +42,11 @@ namespace Umbraco.Cms.Core.PropertyEditors
{
MediaService.Saving += fileUpload.MediaServiceSaving;
_terminate.Add(() => MediaService.Saving -= fileUpload.MediaServiceSaving);
ContentService.Copied += fileUpload.ContentServiceCopied;
_terminate.Add(() => ContentService.Copied -= fileUpload.ContentServiceCopied);
void mediaServiceDeleted(IMediaService sender, DeleteEventArgs<IMedia> args) => args.MediaFilesToDelete.AddRange(fileUpload.ServiceDeleted(args.DeletedEntities.Cast<ContentBase>()));
MediaService.Deleted += mediaServiceDeleted;
_terminate.Add(() => MediaService.Deleted -= mediaServiceDeleted);
void contentServiceDeleted(IContentService sender, DeleteEventArgs<IContent> args) => args.MediaFilesToDelete.AddRange(fileUpload.ServiceDeleted(args.DeletedEntities.Cast<ContentBase>()));
ContentService.Deleted += contentServiceDeleted;
_terminate.Add(() => ContentService.Deleted -= contentServiceDeleted);
void memberServiceDeleted(IMemberService sender, DeleteEventArgs<IMember> args) => args.MediaFilesToDelete.AddRange(fileUpload.ServiceDeleted(args.DeletedEntities.Cast<ContentBase>()));
MemberService.Deleted += memberServiceDeleted;
_terminate.Add(() => MemberService.Deleted -= memberServiceDeleted);
@@ -62,17 +56,11 @@ namespace Umbraco.Cms.Core.PropertyEditors
{
MediaService.Saving += imageCropper.MediaServiceSaving;
_terminate.Add(() => MediaService.Saving -= imageCropper.MediaServiceSaving);
ContentService.Copied += imageCropper.ContentServiceCopied;
_terminate.Add(() => ContentService.Copied -= imageCropper.ContentServiceCopied);
void mediaServiceDeleted(IMediaService sender, DeleteEventArgs<IMedia> args) => args.MediaFilesToDelete.AddRange(imageCropper.ServiceDeleted(args.DeletedEntities.Cast<ContentBase>()));
MediaService.Deleted += mediaServiceDeleted;
_terminate.Add(() => MediaService.Deleted -= mediaServiceDeleted);
void contentServiceDeleted(IContentService sender, DeleteEventArgs<IContent> args) => args.MediaFilesToDelete.AddRange(imageCropper.ServiceDeleted(args.DeletedEntities.Cast<ContentBase>()));
ContentService.Deleted += contentServiceDeleted;
_terminate.Add(() => ContentService.Deleted -= contentServiceDeleted);
void memberServiceDeleted(IMemberService sender, DeleteEventArgs<IMember> args) => args.MediaFilesToDelete.AddRange(imageCropper.ServiceDeleted(args.DeletedEntities.Cast<ContentBase>()));
MemberService.Deleted += memberServiceDeleted;
_terminate.Add(() => MemberService.Deleted -= memberServiceDeleted);

View File

@@ -1,15 +0,0 @@
using Umbraco.Cms.Core.Composing;
namespace Umbraco.Cms.Core.Routing
{
/// <summary>
/// Implements an Application Event Handler for managing redirect URLs tracking.
/// </summary>
/// <remarks>
/// <para>when content is renamed or moved, we want to create a permanent 301 redirect from it's old URL</para>
/// <para>not managing domains because we don't know how to do it - changing domains => must create a higher level strategy using rewriting rules probably</para>
/// <para>recycle bin = moving to and from does nothing: to = the node is gone, where would we redirect? from = same</para>
/// </remarks>
public class RedirectTrackingComposer : ComponentComposer<RedirectTrackingComponent>, ICoreComposer
{ }
}

View File

@@ -1,36 +1,38 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Composing;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.Implement;
using Umbraco.Cms.Infrastructure.Services;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.Routing
{
/// Implements an Application Event Handler for managing redirect URLs tracking.
/// Implements a notification handler for managing redirect URLs tracking.
/// <para>when content is renamed or moved, we want to create a permanent 301 redirect from it's old URL</para>
/// <para>
/// not managing domains because we don't know how to do it - changing domains => must create a higher level
/// strategy using rewriting rules probably
/// </para>
/// <para>recycle bin = moving to and from does nothing: to = the node is gone, where would we redirect? from = same</para>
public sealed class RedirectTrackingComponent : IComponent
// TODO: insert these notification handlers in core composition
public sealed class RedirectTrackingHandler :
INotificationHandler<PublishingNotification<IContent>>,
INotificationHandler<PublishedNotification<IContent>>,
INotificationHandler<MovingNotification<IContent>>,
INotificationHandler<MovedNotification<IContent>>
{
private const string _eventStateKey = "Umbraco.Web.Redirects.RedirectTrackingEventHandler";
private readonly IOptionsMonitor<WebRoutingSettings> _webRoutingSettings;
private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor;
private readonly IRedirectUrlService _redirectUrlService;
private readonly IVariationContextAccessor _variationContextAccessor;
public RedirectTrackingComponent(IOptionsMonitor<WebRoutingSettings> webRoutingSettings, IPublishedSnapshotAccessor publishedSnapshotAccessor, IRedirectUrlService redirectUrlService, IVariationContextAccessor variationContextAccessor)
public RedirectTrackingHandler(IOptionsMonitor<WebRoutingSettings> webRoutingSettings, IPublishedSnapshotAccessor publishedSnapshotAccessor, IRedirectUrlService redirectUrlService, IVariationContextAccessor variationContextAccessor)
{
_webRoutingSettings = webRoutingSettings;
_publishedSnapshotAccessor = publishedSnapshotAccessor;
@@ -38,86 +40,62 @@ namespace Umbraco.Cms.Core.Routing
_variationContextAccessor = variationContextAccessor;
}
public void Initialize()
public void Handle(PublishingNotification<IContent> notification)
{
ContentService.Publishing += ContentService_Publishing;
ContentService.Published += ContentService_Published;
ContentService.Moving += ContentService_Moving;
ContentService.Moved += ContentService_Moved;
// don't let the notification handlers kick in if Redirect Tracking is turned off in the config
if (_webRoutingSettings.CurrentValue.DisableRedirectUrlTracking)
return;
// kill all redirects once a content is deleted
//ContentService.Deleted += ContentService_Deleted;
// BUT, doing it here would prevent content deletion due to FK
// so the rows are actually deleted by the ContentRepository (see GetDeleteClauses)
// rolled back items have to be published, so publishing will take care of that
}
public void Terminate()
{
ContentService.Publishing -= ContentService_Publishing;
ContentService.Published -= ContentService_Published;
ContentService.Moving -= ContentService_Moving;
ContentService.Moved -= ContentService_Moved;
}
private void ContentService_Publishing(IContentService sender, PublishEventArgs<IContent> args)
{
// don't let the event handlers kick in if Redirect Tracking is turned off in the config
if (_webRoutingSettings.CurrentValue.DisableRedirectUrlTracking) return;
var oldRoutes = GetOldRoutes(args.EventState);
foreach (var entity in args.PublishedEntities)
var oldRoutes = GetOldRoutes();
foreach (var entity in notification.PublishedEntities)
{
StoreOldRoute(entity, oldRoutes);
}
}
private void ContentService_Published(IContentService sender, ContentPublishedEventArgs args)
public void Handle(PublishedNotification<IContent> notification)
{
// don't let the event handlers kick in if Redirect Tracking is turned off in the config
if (_webRoutingSettings.CurrentValue.DisableRedirectUrlTracking) return;
// don't let the notification handlers kick in if Redirect Tracking is turned off in the config
if (_webRoutingSettings.CurrentValue.DisableRedirectUrlTracking)
return;
var oldRoutes = GetOldRoutes(args.EventState);
var oldRoutes = GetOldRoutes();
CreateRedirects(oldRoutes);
}
private void ContentService_Moving(IContentService sender, MoveEventArgs<IContent> args)
public void Handle(MovingNotification<IContent> notification)
{
// don't let the event handlers kick in if Redirect Tracking is turned off in the config
if (_webRoutingSettings.CurrentValue.DisableRedirectUrlTracking) return;
// don't let the notification handlers kick in if Redirect Tracking is turned off in the config
if (_webRoutingSettings.CurrentValue.DisableRedirectUrlTracking)
return;
var oldRoutes = GetOldRoutes(args.EventState);
foreach (var info in args.MoveInfoCollection)
var oldRoutes = GetOldRoutes();
foreach (var info in notification.MoveInfoCollection)
{
StoreOldRoute(info.Entity, oldRoutes);
}
}
private void ContentService_Moved(IContentService sender, MoveEventArgs<IContent> args)
// TODO refactor (this is duplicate code, see published notification handling above)
public void Handle(MovedNotification<IContent> notification)
{
// don't let the event handlers kick in if Redirect Tracking is turned off in the config
if (_webRoutingSettings.CurrentValue.DisableRedirectUrlTracking) return;
// don't let the notification handlers kick in if Redirect Tracking is turned off in the config
if (_webRoutingSettings.CurrentValue.DisableRedirectUrlTracking)
return;
var oldRoutes = GetOldRoutes(args.EventState);
var oldRoutes = GetOldRoutes();
CreateRedirects(oldRoutes);
}
private OldRoutesDictionary GetOldRoutes(IDictionary<string, object> eventState)
{
if (! eventState.ContainsKey(_eventStateKey))
{
eventState[_eventStateKey] = new OldRoutesDictionary();
}
return eventState[_eventStateKey] as OldRoutesDictionary;
}
// TODO: figure out how to do notification state / temp state
private OldRoutesDictionary GetOldRoutes() => new OldRoutesDictionary();
private void StoreOldRoute(IContent entity, OldRoutesDictionary oldRoutes)
{
var contentCache = _publishedSnapshotAccessor.PublishedSnapshot.Content;
var entityContent = contentCache.GetById(entity.Id);
if (entityContent == null) return;
if (entityContent == null)
return;
// get the default affected cultures by going up the tree until we find the first culture variant entity (default to no cultures)
var defaultCultures = entityContent.AncestorsOrSelf()?.FirstOrDefault(a => a.Cultures.Any())?.Cultures.Keys.ToArray()
@@ -130,7 +108,8 @@ namespace Umbraco.Cms.Core.Routing
foreach (var culture in cultures)
{
var route = contentCache.GetRouteById(x.Id, culture);
if (IsNotRoute(route)) continue;
if (IsNotRoute(route))
continue;
oldRoutes[new ContentIdAndCulture(x.Id, culture)] = new ContentKeyAndOldRoute(x.Key, route);
}
}
@@ -143,7 +122,8 @@ namespace Umbraco.Cms.Core.Routing
foreach (var oldRoute in oldRoutes)
{
var newRoute = contentCache.GetRouteById(oldRoute.Key.ContentId, oldRoute.Key.Culture);
if (IsNotRoute(newRoute) || oldRoute.Value.OldRoute == newRoute) continue;
if (IsNotRoute(newRoute) || oldRoute.Value.OldRoute == newRoute)
continue;
_redirectUrlService.Register(oldRoute.Value.OldRoute, oldRoute.Value.ContentKey, oldRoute.Key.Culture);
}
}
@@ -176,6 +156,8 @@ namespace Umbraco.Cms.Core.Routing
}
private class OldRoutesDictionary : Dictionary<ContentIdAndCulture, ContentKeyAndOldRoute>
{ }
{
}
}
}

View File

@@ -1,11 +1,16 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using System;
using System.Collections.Generic;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Events;
namespace Umbraco.Cms.Infrastructure.Services
{
// TODO split this file into several small classes and move to another namespace
public interface ICancelableNotification
public interface ICancelableNotification : INotification
{
bool Cancel { get; set; }
}
@@ -34,7 +39,7 @@ namespace Umbraco.Cms.Infrastructure.Services
}
}
public abstract class CancelableObjectNotification<T> : ObjectNotification<T> where T : class
public abstract class CancelableObjectNotification<T> : ObjectNotification<T>, ICancelableNotification where T : class
{
protected CancelableObjectNotification(T target, EventMessages messages) : base(target, messages)
{
@@ -66,16 +71,33 @@ namespace Umbraco.Cms.Infrastructure.Services
{
}
public DeletingNotification(IEnumerable<T> target, EventMessages messages) : base(target, messages)
{
}
public IEnumerable<T> DeletedEntities => Target;
}
public class DeletedNotification<T> : EnumerableObjectNotification<T>
{
public DeletedNotification(T target, EventMessages messages) : base(target, messages)
public DeletedNotification(T target, EventMessages messages) : base(target, messages) => MediaFilesToDelete = new List<string>();
public IEnumerable<T> DeletedEntities => Target;
public List<string> MediaFilesToDelete { get; }
}
public class DeletedBlueprintNotification<T> : EnumerableObjectNotification<T>
{
public DeletedBlueprintNotification(T target, EventMessages messages) : base(target, messages)
{
}
public IEnumerable<T> DeletedEntities => Target;
public DeletedBlueprintNotification(IEnumerable<T> target, EventMessages messages) : base(target, messages)
{
}
public IEnumerable<T> DeletedBlueprints => Target;
}
public class SortingNotification<T> : CancelableEnumerableObjectNotification<T>
@@ -121,4 +143,243 @@ namespace Umbraco.Cms.Infrastructure.Services
public IEnumerable<T> SavedEntities => Target;
}
public class SavedBlueprintNotification<T> : ObjectNotification<T> where T : class
{
public SavedBlueprintNotification(T target, EventMessages messages) : base(target, messages)
{
}
public T SavedBlueprint => Target;
}
public class PublishingNotification<T> : CancelableEnumerableObjectNotification<T>
{
public PublishingNotification(T target, EventMessages messages) : base(target, messages)
{
}
public PublishingNotification(IEnumerable<T> target, EventMessages messages) : base(target, messages)
{
}
public IEnumerable<T> PublishedEntities => Target;
}
public class PublishedNotification<T> : EnumerableObjectNotification<T>
{
public PublishedNotification(T target, EventMessages messages) : base(target, messages)
{
}
public PublishedNotification(IEnumerable<T> target, EventMessages messages) : base(target, messages)
{
}
public IEnumerable<T> PublishedEntities => Target;
}
public class MovingNotification<T> : CancelableObjectNotification<IEnumerable<MoveEventInfo<T>>>
{
public MovingNotification(MoveEventInfo<T> target, EventMessages messages) : base(new[] {target}, messages)
{
}
public MovingNotification(IEnumerable<MoveEventInfo<T>> target, EventMessages messages) : base(target, messages)
{
}
public IEnumerable<MoveEventInfo<T>> MoveInfoCollection => Target;
}
public class MovedNotification<T> : ObjectNotification<IEnumerable<MoveEventInfo<T>>>
{
public MovedNotification(MoveEventInfo<T> target, EventMessages messages) : base(new[] { target }, messages)
{
}
public MovedNotification(IEnumerable<MoveEventInfo<T>> target, EventMessages messages) : base(target, messages)
{
}
public IEnumerable<MoveEventInfo<T>> MoveInfoCollection => Target;
}
public class TrashingNotification<T> : CancelableObjectNotification<IEnumerable<MoveEventInfo<T>>>
{
public TrashingNotification(MoveEventInfo<T> target, EventMessages messages) : base(new[] { target }, messages)
{
}
public TrashingNotification(IEnumerable<MoveEventInfo<T>> target, EventMessages messages) : base(target, messages)
{
}
public IEnumerable<MoveEventInfo<T>> MoveInfoCollection => Target;
}
public class TrashedNotification<T> : ObjectNotification<IEnumerable<MoveEventInfo<T>>>
{
public TrashedNotification(MoveEventInfo<T> target, EventMessages messages) : base(new[] { target }, messages)
{
}
public TrashedNotification(IEnumerable<MoveEventInfo<T>> target, EventMessages messages) : base(target, messages)
{
}
public IEnumerable<MoveEventInfo<T>> MoveInfoCollection => Target;
}
public class CopyingNotification<T> : CancelableObjectNotification<T> where T : class
{
public CopyingNotification(T original, T copy, int parentId, EventMessages messages) : base(original, messages)
{
Copy = copy;
ParentId = parentId;
}
public T Original => Target;
public T Copy { get; }
public int ParentId { get; }
}
public class CopiedNotification<T> : ObjectNotification<T> where T : class
{
public CopiedNotification(T original, T copy, int parentId, EventMessages messages) : base(original, messages)
{
Copy = copy;
ParentId = parentId;
}
public T Original => Target;
public T Copy { get; }
public int ParentId { get; }
}
public class RollingBackNotification<T> : CancelableObjectNotification<T> where T : class
{
public RollingBackNotification(T target, EventMessages messages) : base(target, messages)
{
}
public T Entity => Target;
}
public class RolledBackNotification<T> : ObjectNotification<T> where T : class
{
public RolledBackNotification(T target, EventMessages messages) : base(target, messages)
{
}
public T Entity => Target;
}
public class SendingToPublishNotification<T> : CancelableObjectNotification<T> where T : class
{
public SendingToPublishNotification(T target, EventMessages messages) : base(target, messages)
{
}
public T Entity => Target;
}
public class SentToPublishNotification<T> : ObjectNotification<T> where T : class
{
public SentToPublishNotification(T target, EventMessages messages) : base(target, messages)
{
}
public T Entity => Target;
}
public class UnpublishingNotification<T> : CancelableEnumerableObjectNotification<T>
{
public UnpublishingNotification(T target, EventMessages messages) : base(target, messages)
{
}
public UnpublishingNotification(IEnumerable<T> target, EventMessages messages) : base(target, messages)
{
}
public IEnumerable<T> UnpublishedEntities => Target;
}
public class UnpublishedNotification<T> : EnumerableObjectNotification<T>
{
public UnpublishedNotification(T target, EventMessages messages) : base(target, messages)
{
}
public UnpublishedNotification(IEnumerable<T> target, EventMessages messages) : base(target, messages)
{
}
public IEnumerable<T> UnpublishedEntities => Target;
}
public class EmptiedRecycleBinNotification : INotification
{
public EmptiedRecycleBinNotification(Guid nodeObjectType, EventMessages messages)
{
NodeObjectType = nodeObjectType;
Messages = messages;
}
public Guid NodeObjectType { get; }
public EventMessages Messages { get; }
public bool IsContentRecycleBin => NodeObjectType == Constants.ObjectTypes.Document;
public bool IsMediaRecycleBin => NodeObjectType == Constants.ObjectTypes.Media;
}
public class EmptyingRecycleBinNotification : EmptiedRecycleBinNotification, ICancelableNotification
{
public EmptyingRecycleBinNotification(Guid nodeObjectType, EventMessages messages)
: base(nodeObjectType, messages)
{
}
public bool Cancel { get; set; }
}
public class DeletedVersionsNotification : INotification
{
public DeletedVersionsNotification(int id, EventMessages messages, int specificVersion = default, bool deletePriorVersions = false, DateTime dateToRetain = default)
{
Id = id;
Messages = messages;
SpecificVersion = specificVersion;
DeletePriorVersions = deletePriorVersions;
DateToRetain = dateToRetain;
}
public int Id { get; }
public EventMessages Messages { get; }
public int SpecificVersion { get; }
public bool DeletePriorVersions { get; }
public DateTime DateToRetain { get; }
}
public class DeletingVersionsNotification : DeletedVersionsNotification, ICancelableNotification
{
public DeletingVersionsNotification(int id, EventMessages messages, int specificVersion = default, bool deletePriorVersions = false, DateTime dateToRetain = default)
: base(id, messages, specificVersion, deletePriorVersions, dateToRetain)
{
}
public bool Cancel { get; set; }
}
}

View File

@@ -34,6 +34,7 @@ namespace Umbraco.Cms.Core.Services.Implement
private readonly ILogger<ContentService> _logger;
private IQuery<IContent> _queryNotTrashed;
private readonly IEventAggregator _eventAggregator;
private readonly IRelationService _relationService;
#region Constructors
@@ -41,7 +42,7 @@ namespace Umbraco.Cms.Core.Services.Implement
IEventMessagesFactory eventMessagesFactory,
IDocumentRepository documentRepository, IEntityRepository entityRepository, IAuditRepository auditRepository,
IContentTypeRepository contentTypeRepository, IDocumentBlueprintRepository documentBlueprintRepository, ILanguageRepository languageRepository,
Lazy<IPropertyValidationService> propertyValidationService, IShortStringHelper shortStringHelper, IEventAggregator eventAggregator)
Lazy<IPropertyValidationService> propertyValidationService, IShortStringHelper shortStringHelper, IEventAggregator eventAggregator, IRelationService relationService)
: base(provider, loggerFactory, eventMessagesFactory)
{
_documentRepository = documentRepository;
@@ -53,6 +54,7 @@ namespace Umbraco.Cms.Core.Services.Implement
_propertyValidationService = propertyValidationService;
_shortStringHelper = shortStringHelper;
_eventAggregator = eventAggregator;
_relationService = relationService;
_logger = loggerFactory.CreateLogger<ContentService>();
}
@@ -1150,7 +1152,7 @@ namespace Umbraco.Cms.Core.Services.Implement
: null;
// ensure that the document can be published, and publish handling events, business rules, etc
publishResult = StrategyCanPublish(scope, content, /*checkPath:*/ (!branchOne || branchRoot), culturesPublishing, culturesUnpublishing, evtMsgs, saveEventArgs, allLangs);
publishResult = StrategyCanPublish(scope, content, /*checkPath:*/ (!branchOne || branchRoot), culturesPublishing, culturesUnpublishing, evtMsgs, allLangs);
if (publishResult.Success)
{
// note: StrategyPublish flips the PublishedState to Publishing!
@@ -1243,7 +1245,7 @@ namespace Umbraco.Cms.Core.Services.Implement
if (unpublishResult.Success) // and succeeded, trigger events
{
// events and audit
scope.Events.Dispatch(Unpublished, this, new PublishEventArgs<IContent>(content, false, false), "Unpublished");
_eventAggregator.Publish(new UnpublishedNotification<IContent>(content, evtMsgs));
scope.Events.Dispatch(TreeChanged, this, new TreeChange<IContent>(content, TreeChangeTypes.RefreshBranch).ToEventArgs());
if (culturesUnpublishing != null)
@@ -1298,7 +1300,7 @@ namespace Umbraco.Cms.Core.Services.Implement
if (!branchOne) // for branches, handled by SaveAndPublishBranch
{
scope.Events.Dispatch(TreeChanged, this, new TreeChange<IContent>(content, changeType).ToEventArgs());
scope.Events.Dispatch(Published, this, saveEventArgs.ToContentPublishedEventArgs(), nameof(Published));
_eventAggregator.Publish(new PublishingNotification<IContent>(content, evtMsgs));
}
// it was not published and now is... descendants that were 'published' (but
@@ -1307,7 +1309,7 @@ namespace Umbraco.Cms.Core.Services.Implement
if (!branchOne && isNew == false && previouslyPublished == false && HasChildren(content.Id))
{
var descendants = GetPublishedDescendantsLocked(content).ToArray();
scope.Events.Dispatch(Published, this, new ContentPublishedEventArgs(descendants, false, evtMsgs), "Published");
_eventAggregator.Publish(new PublishedNotification<IContent>(descendants, evtMsgs));
}
switch (publishResult.Result)
@@ -1721,7 +1723,7 @@ namespace Umbraco.Cms.Core.Services.Implement
// trigger events for the entire branch
// (SaveAndPublishBranchOne does *not* do it)
scope.Events.Dispatch(TreeChanged, this, new TreeChange<IContent>(document, TreeChangeTypes.RefreshBranch).ToEventArgs());
scope.Events.Dispatch(Published, this, new ContentPublishedEventArgs(publishedDocuments, false, evtMsgs), nameof(Published));
_eventAggregator.Publish(new PublishedNotification<IContent>(publishedDocuments, evtMsgs));
scope.Complete();
}
@@ -1776,11 +1778,9 @@ namespace Umbraco.Cms.Core.Services.Implement
using (var scope = ScopeProvider.CreateScope())
{
var deleteNotification = new DeletingNotification<IContent>(content, evtMsgs);
_eventAggregator.Publish(deleteNotification);
var deleteEventArgs = new DeleteEventArgs<IContent>(content, evtMsgs);
if (scope.Events.DispatchCancelable(Deleting, this, deleteEventArgs, nameof(Deleting)))
var notification = new DeletingNotification<IContent>(content, evtMsgs);
_eventAggregator.Publish(notification);
if (notification.Cancel)
{
scope.Complete();
return OperationResult.Cancel(evtMsgs);
@@ -1792,7 +1792,9 @@ namespace Umbraco.Cms.Core.Services.Implement
// but... Unpublishing event makes no sense (not going to cancel?) and no need to save
// just raise the event
if (content.Trashed == false && content.Published)
scope.Events.Dispatch(Unpublished, this, new PublishEventArgs<IContent>(content, false, false), nameof(Unpublished));
{
_eventAggregator.Publish(new UnpublishedNotification<IContent>(content, evtMsgs));
}
DeleteLocked(scope, content);
@@ -1807,11 +1809,12 @@ namespace Umbraco.Cms.Core.Services.Implement
private void DeleteLocked(IScope scope, IContent content)
{
var evtMsgs = EventMessagesFactory.Get();
void DoDelete(IContent c)
{
_documentRepository.Delete(c);
var args = new DeleteEventArgs<IContent>(c, false); // raise event & get flagged files
scope.Events.Dispatch(Deleted, this, args, nameof(Deleted));
_eventAggregator.Publish(new DeletedNotification<IContent>(c, evtMsgs));
// media files deleted by QueuingEventDispatcher
}
@@ -1842,10 +1845,13 @@ namespace Umbraco.Cms.Core.Services.Implement
/// <param name="userId">Optional Id of the User deleting versions of a Content object</param>
public void DeleteVersions(int id, DateTime versionDate, int userId = Cms.Core.Constants.Security.SuperUserId)
{
var evtMsgs = EventMessagesFactory.Get();
using (var scope = ScopeProvider.CreateScope())
{
var deleteRevisionsEventArgs = new DeleteRevisionsEventArgs(id, dateToRetain: versionDate);
if (scope.Events.DispatchCancelable(DeletingVersions, this, deleteRevisionsEventArgs))
var notification = new DeletingVersionsNotification(id, evtMsgs, dateToRetain: versionDate);
_eventAggregator.Publish(notification);
if (notification.Cancel)
{
scope.Complete();
return;
@@ -1854,8 +1860,7 @@ namespace Umbraco.Cms.Core.Services.Implement
scope.WriteLock(Cms.Core.Constants.Locks.ContentTree);
_documentRepository.DeleteVersions(id, versionDate);
deleteRevisionsEventArgs.CanCancel = false;
scope.Events.Dispatch(DeletedVersions, this, deleteRevisionsEventArgs);
_eventAggregator.Publish(new DeletedVersionsNotification(id, evtMsgs, dateToRetain: versionDate));
Audit(AuditType.Delete, userId, Cms.Core.Constants.System.Root, "Delete (by version date)");
scope.Complete();
@@ -1872,9 +1877,13 @@ namespace Umbraco.Cms.Core.Services.Implement
/// <param name="userId">Optional Id of the User deleting versions of a Content object</param>
public void DeleteVersion(int id, int versionId, bool deletePriorVersions, int userId = Cms.Core.Constants.Security.SuperUserId)
{
var evtMsgs = EventMessagesFactory.Get();
using (var scope = ScopeProvider.CreateScope())
{
if (scope.Events.DispatchCancelable(DeletingVersions, this, new DeleteRevisionsEventArgs(id, /*specificVersion:*/ versionId)))
var notification = new DeletingVersionsNotification(id, evtMsgs, specificVersion: versionId);
_eventAggregator.Publish(notification);
if (notification.Cancel)
{
scope.Complete();
return;
@@ -1891,7 +1900,7 @@ namespace Umbraco.Cms.Core.Services.Implement
if (c.VersionId != versionId && c.PublishedVersionId != versionId) // don't delete the current or published version
_documentRepository.DeleteVersion(versionId);
scope.Events.Dispatch(DeletedVersions, this, new DeleteRevisionsEventArgs(id, false,/* specificVersion:*/ versionId));
_eventAggregator.Publish(new DeletedVersionsNotification(id, evtMsgs, specificVersion: versionId));
Audit(AuditType.Delete, userId, Cms.Core.Constants.System.Root, "Delete (by version)");
scope.Complete();
@@ -1914,8 +1923,10 @@ namespace Umbraco.Cms.Core.Services.Implement
var originalPath = content.Path;
var moveEventInfo = new MoveEventInfo<IContent>(content, originalPath, Cms.Core.Constants.System.RecycleBinContent);
var moveEventArgs = new MoveEventArgs<IContent>(evtMsgs, moveEventInfo);
if (scope.Events.DispatchCancelable(Trashing, this, moveEventArgs, nameof(Trashing)))
var notification = new TrashingNotification<IContent>(moveEventInfo, evtMsgs);
_eventAggregator.Publish(notification);
if (notification.Cancel)
{
scope.Complete();
return OperationResult.Cancel(evtMsgs); // causes rollback
@@ -1934,9 +1945,7 @@ namespace Umbraco.Cms.Core.Services.Implement
.Select(x => new MoveEventInfo<IContent>(x.Item1, x.Item2, x.Item1.ParentId))
.ToArray();
moveEventArgs.CanCancel = false;
moveEventArgs.MoveInfoCollection = moveInfo;
scope.Events.Dispatch(Trashed, this, moveEventArgs, nameof(Trashed));
_eventAggregator.Publish(new TrashedNotification<IContent>(moveInfo, evtMsgs));
Audit(AuditType.Move, userId, content.Id, "Moved to recycle bin");
scope.Complete();
@@ -1965,6 +1974,8 @@ namespace Umbraco.Cms.Core.Services.Implement
return;
}
var evtMsgs = EventMessagesFactory.Get();
var moves = new List<(IContent, string)>();
using (var scope = ScopeProvider.CreateScope())
@@ -1976,8 +1987,9 @@ namespace Umbraco.Cms.Core.Services.Implement
throw new InvalidOperationException("Parent does not exist or is trashed."); // causes rollback
var moveEventInfo = new MoveEventInfo<IContent>(content, content.Path, parentId);
var moveEventArgs = new MoveEventArgs<IContent>(moveEventInfo);
if (scope.Events.DispatchCancelable(Moving, this, moveEventArgs, nameof(Moving)))
var notification = new MovingNotification<IContent>(moveEventInfo, evtMsgs);
_eventAggregator.Publish(notification);
if (notification.Cancel)
{
scope.Complete();
return; // causes rollback
@@ -2006,9 +2018,8 @@ namespace Umbraco.Cms.Core.Services.Implement
.Select(x => new MoveEventInfo<IContent>(x.Item1, x.Item2, x.Item1.ParentId))
.ToArray();
moveEventArgs.MoveInfoCollection = moveInfo;
moveEventArgs.CanCancel = false;
scope.Events.Dispatch(Moved, this, moveEventArgs, nameof(Moved));
_eventAggregator.Publish(new MovedNotification<IContent>(moveInfo, evtMsgs));
Audit(AuditType.Move, userId, content.Id);
scope.Complete();
@@ -2093,8 +2104,9 @@ namespace Umbraco.Cms.Core.Services.Implement
// are managed by Delete, and not here.
// no idea what those events are for, keep a simplified version
var recycleBinEventArgs = new RecycleBinEventArgs(nodeObjectType, evtMsgs);
if (scope.Events.DispatchCancelable(EmptyingRecycleBin, this, recycleBinEventArgs))
var notification = new EmptyingRecycleBinNotification(nodeObjectType, evtMsgs);
_eventAggregator.Publish(notification);
if (notification.Cancel)
{
scope.Complete();
return OperationResult.Cancel(evtMsgs);
@@ -2109,9 +2121,7 @@ namespace Umbraco.Cms.Core.Services.Implement
deleted.Add(content);
}
recycleBinEventArgs.CanCancel = false;
recycleBinEventArgs.RecycleBinEmptiedSuccessfully = true; // oh my?!
scope.Events.Dispatch(EmptiedRecycleBin, this, recycleBinEventArgs);
_eventAggregator.Publish(new EmptiedRecycleBinNotification(nodeObjectType, evtMsgs));
scope.Events.Dispatch(TreeChanged, this, deleted.Select(x => new TreeChange<IContent>(x, TreeChangeTypes.Remove)).ToEventArgs());
Audit(AuditType.Delete, userId, Cms.Core.Constants.System.RecycleBinContent, "Recycle bin emptied");
@@ -2151,13 +2161,16 @@ namespace Umbraco.Cms.Core.Services.Implement
/// <returns>The newly created <see cref="IContent"/> object</returns>
public IContent Copy(IContent content, int parentId, bool relateToOriginal, bool recursive, int userId = Cms.Core.Constants.Security.SuperUserId)
{
var evtMsgs = EventMessagesFactory.Get();
var copy = content.DeepCloneWithResetIdentities();
copy.ParentId = parentId;
using (var scope = ScopeProvider.CreateScope())
{
var copyEventArgs = new CopyEventArgs<IContent>(content, copy, true, parentId, relateToOriginal);
if (scope.Events.DispatchCancelable(Copying, this, copyEventArgs))
var notification = new CopyingNotification<IContent>(content, copy, parentId, evtMsgs);
_eventAggregator.Publish(notification);
if (notification.Cancel)
{
scope.Complete();
return null;
@@ -2212,8 +2225,12 @@ namespace Umbraco.Cms.Core.Services.Implement
var descendantCopy = descendant.DeepCloneWithResetIdentities();
descendantCopy.ParentId = parentId;
if (scope.Events.DispatchCancelable(Copying, this, new CopyEventArgs<IContent>(descendant, descendantCopy, parentId)))
notification = new CopyingNotification<IContent>(descendant, descendantCopy, parentId, evtMsgs);
_eventAggregator.Publish(notification);
if (notification.Cancel)
{
continue;
}
// a copy is not published (but not really unpublishing either)
// update the create author and last edit author
@@ -2237,7 +2254,14 @@ namespace Umbraco.Cms.Core.Services.Implement
scope.Events.Dispatch(TreeChanged, this, new TreeChange<IContent>(copy, TreeChangeTypes.RefreshBranch).ToEventArgs());
foreach (var x in copies)
scope.Events.Dispatch(Copied, this, new CopyEventArgs<IContent>(x.Item1, x.Item2, false, x.Item2.ParentId, relateToOriginal));
{
if (relateToOriginal)
{
RelateOnCopy(x.Item1, x.Item2);
}
_eventAggregator.Publish(new CopiedNotification<IContent>(x.Item1, x.Item2, parentId, evtMsgs));
}
Audit(AuditType.Copy, userId, content.Id);
scope.Complete();
@@ -2246,6 +2270,26 @@ namespace Umbraco.Cms.Core.Services.Implement
return copy;
}
private void RelateOnCopy(IContent original, IContent copy)
{
var relationType = _relationService.GetRelationTypeByAlias(Constants.Conventions.RelationTypes.RelateDocumentOnCopyAlias);
if (relationType == null)
{
relationType = new RelationType(Constants.Conventions.RelationTypes.RelateDocumentOnCopyAlias,
Constants.Conventions.RelationTypes.RelateDocumentOnCopyName,
true,
Constants.ObjectTypes.Document,
Constants.ObjectTypes.Document);
_relationService.Save(relationType);
}
var relation = new Relation(original.Id, copy.Id, relationType);
_relationService.Save(relation);
Audit(AuditType.Copy, copy.WriterId, copy.Id, $"Copied content with Id: '{copy.Id}' related to original content with Id: '{original.Id}'");
}
/// <summary>
/// Sends an <see cref="IContent"/> to Publication, which executes handlers and events for the 'Send to Publication' action.
/// </summary>
@@ -2254,10 +2298,13 @@ namespace Umbraco.Cms.Core.Services.Implement
/// <returns>True if sending publication was successful otherwise false</returns>
public bool SendToPublication(IContent content, int userId = Cms.Core.Constants.Security.SuperUserId)
{
var evtMsgs = EventMessagesFactory.Get();
using (var scope = ScopeProvider.CreateScope())
{
var sendToPublishEventArgs = new SendToPublishEventArgs<IContent>(content);
if (scope.Events.DispatchCancelable(SendingToPublish, this, sendToPublishEventArgs))
var notification = new SendingToPublishNotification<IContent>(content, evtMsgs);
_eventAggregator.Publish(notification);
if (notification.Cancel)
{
scope.Complete();
return false;
@@ -2282,8 +2329,7 @@ namespace Umbraco.Cms.Core.Services.Implement
if (!saveResult.Success)
return saveResult.Success;
sendToPublishEventArgs.CanCancel = false;
scope.Events.Dispatch(SentToPublish, this, sendToPublishEventArgs);
_eventAggregator.Publish(new SentToPublishNotification<IContent>(content, evtMsgs));
if (culturesChanging != null)
Audit(AuditType.SendToPublishVariant, userId, content.Id, $"Send To Publish for cultures: {culturesChanging}", culturesChanging);
@@ -2412,7 +2458,9 @@ namespace Umbraco.Cms.Core.Services.Implement
scope.Events.Dispatch(TreeChanged, this, saved.Select(x => new TreeChange<IContent>(x, TreeChangeTypes.RefreshNode)).ToEventArgs());
if (raiseEvents && published.Any())
scope.Events.Dispatch(Published, this, new ContentPublishedEventArgs(published, false, evtMsgs), "Published");
{
_eventAggregator.Publish(new PublishedNotification<IContent>(published, evtMsgs));
}
Audit(AuditType.Sort, userId, 0, "Sorting content performed by user");
return OperationResult.Succeed(evtMsgs);
@@ -2492,136 +2540,11 @@ namespace Umbraco.Cms.Core.Services.Implement
#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 Sorting
/// </summary>
public static event TypedEventHandler<IContentService, SaveEventArgs<IContent>> Sorting;
/// <summary>
/// Occurs after Sorting
/// </summary>
public static event TypedEventHandler<IContentService, SaveEventArgs<IContent>> Sorted;
/// <summary>
/// Occurs after Save
/// </summary>
public static event TypedEventHandler<IContentService, ContentSavedEventArgs> Saved;
/// <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, ContentPublishingEventArgs> Publishing;
/// <summary>
/// Occurs after publish
/// </summary>
public static event TypedEventHandler<IContentService, ContentPublishedEventArgs> 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;
/// <summary>
/// Occurs after change.
/// </summary>
public static event TypedEventHandler<IContentService, TreeChange<IContent>.EventArgs> TreeChanged;
/// <summary>
/// Occurs after a blueprint has been saved.
/// </summary>
public static event TypedEventHandler<IContentService, SaveEventArgs<IContent>> SavedBlueprint;
/// <summary>
/// Occurs after a blueprint has been deleted.
/// </summary>
public static event TypedEventHandler<IContentService, DeleteEventArgs<IContent>> DeletedBlueprint;
#endregion
#region Publishing Strategies
@@ -2635,14 +2558,15 @@ namespace Umbraco.Cms.Core.Services.Implement
/// <param name="culturesUnpublishing"></param>
/// <param name="evtMsgs"></param>
/// <param name="culturesPublishing"></param>
/// <param name="savingEventArgs"></param>
/// <param name="allLangs"></param>
/// <returns></returns>
private PublishResult StrategyCanPublish(IScope scope, IContent content, bool checkPath, IReadOnlyList<string> culturesPublishing,
IReadOnlyCollection<string> culturesUnpublishing, EventMessages evtMsgs, ContentSavingEventArgs savingEventArgs,
IReadOnlyCollection<ILanguage> allLangs)
IReadOnlyCollection<string> culturesUnpublishing, EventMessages evtMsgs, IReadOnlyCollection<ILanguage> allLangs)
{
// raise Publishing event
if (scope.Events.DispatchCancelable(Publishing, this, savingEventArgs.ToContentPublishingEventArgs()))
// raise Publishing notification
var notification = new PublishingNotification<IContent>(content, evtMsgs);
_eventAggregator.Publish(notification);
if (notification.Cancel)
{
_logger.LogInformation("Document {ContentName} (id={ContentId}) cannot be published: {Reason}", content.Name, content.Id, "publishing was cancelled");
return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, content);
@@ -2801,8 +2725,10 @@ namespace Umbraco.Cms.Core.Services.Implement
/// <returns></returns>
private PublishResult StrategyCanUnpublish(IScope scope, IContent content, EventMessages evtMsgs)
{
// raise Unpublishing event
if (scope.Events.DispatchCancelable(Unpublishing, this, new PublishEventArgs<IContent>(content, evtMsgs)))
// raise Unpublishing notification
var notification = new UnpublishingNotification<IContent>(content, evtMsgs);
_eventAggregator.Publish(notification);
if (notification.Cancel)
{
_logger.LogInformation("Document {ContentName} (id={ContentId}) cannot be unpublished: unpublishing was cancelled.", content.Name, content.Id);
return new PublishResult(PublishResultType.FailedUnpublishCancelledByEvent, evtMsgs, content);
@@ -2871,6 +2797,7 @@ namespace Umbraco.Cms.Core.Services.Implement
var changes = new List<TreeChange<IContent>>();
var moves = new List<(IContent, string)>();
var contentTypeIdsA = contentTypeIds.ToArray();
var evtMsgs = EventMessagesFactory.Get();
// using an immediate uow here because we keep making changes with
// PerformMoveLocked and DeleteLocked that must be applied immediately,
@@ -2883,7 +2810,9 @@ namespace Umbraco.Cms.Core.Services.Implement
var query = Query<IContent>().WhereIn(x => x.ContentTypeId, contentTypeIdsA);
var contents = _documentRepository.Get(query).ToArray();
if (scope.Events.DispatchCancelable(Deleting, this, new DeleteEventArgs<IContent>(contents), nameof(Deleting)))
var notification = new DeletingNotification<IContent>(contents, evtMsgs);
_eventAggregator.Publish(notification);
if (notification.Cancel)
{
scope.Complete();
return;
@@ -2897,7 +2826,9 @@ namespace Umbraco.Cms.Core.Services.Implement
// but... Unpublishing event makes no sense (not going to cancel?) and no need to save
// just raise the event
if (content.Trashed == false && content.Published)
scope.Events.Dispatch(Unpublished, this, new PublishEventArgs<IContent>(content, false, false), nameof(Unpublished));
{
_eventAggregator.Publish(new UnpublishedNotification<IContent>(content, evtMsgs));
}
// if current content has children, move them to trash
var c = content;
@@ -2920,7 +2851,9 @@ namespace Umbraco.Cms.Core.Services.Implement
.Select(x => new MoveEventInfo<IContent>(x.Item1, x.Item2, x.Item1.ParentId))
.ToArray();
if (moveInfos.Length > 0)
scope.Events.Dispatch(Trashed, this, new MoveEventArgs<IContent>(false, moveInfos), nameof(Trashed));
{
_eventAggregator.Publish(new TrashedNotification<IContent>(moveInfos, evtMsgs));
}
scope.Events.Dispatch(TreeChanged, this, changes.ToEventArgs());
Audit(AuditType.Delete, userId, Cms.Core.Constants.System.Root, $"Delete content of type {string.Join(",", contentTypeIdsA)}");
@@ -2997,6 +2930,8 @@ namespace Umbraco.Cms.Core.Services.Implement
public void SaveBlueprint(IContent content, int userId = Cms.Core.Constants.Security.SuperUserId)
{
var evtMsgs = EventMessagesFactory.Get();
//always ensure the blueprint is at the root
if (content.ParentId != -1)
content.ParentId = -1;
@@ -3017,7 +2952,7 @@ namespace Umbraco.Cms.Core.Services.Implement
Audit(AuditType.Save, Cms.Core.Constants.Security.SuperUserId, content.Id, $"Saved content template: {content.Name}");
scope.Events.Dispatch(SavedBlueprint, this, new SaveEventArgs<IContent>(content), "SavedBlueprint");
_eventAggregator.Publish(new SavedBlueprintNotification<IContent>(content, evtMsgs));
scope.Complete();
}
@@ -3025,11 +2960,13 @@ namespace Umbraco.Cms.Core.Services.Implement
public void DeleteBlueprint(IContent content, int userId = Cms.Core.Constants.Security.SuperUserId)
{
var evtMsgs = EventMessagesFactory.Get();
using (var scope = ScopeProvider.CreateScope())
{
scope.WriteLock(Cms.Core.Constants.Locks.ContentTree);
_documentBlueprintRepository.Delete(content);
scope.Events.Dispatch(DeletedBlueprint, this, new DeleteEventArgs<IContent>(content), nameof(DeletedBlueprint));
_eventAggregator.Publish(new DeletedBlueprintNotification<IContent>(content, evtMsgs));
scope.Complete();
}
}
@@ -3099,6 +3036,8 @@ namespace Umbraco.Cms.Core.Services.Implement
public void DeleteBlueprintsOfTypes(IEnumerable<int> contentTypeIds, int userId = Cms.Core.Constants.Security.SuperUserId)
{
var evtMsgs = EventMessagesFactory.Get();
using (var scope = ScopeProvider.CreateScope())
{
scope.WriteLock(Cms.Core.Constants.Locks.ContentTree);
@@ -3119,7 +3058,7 @@ namespace Umbraco.Cms.Core.Services.Implement
_documentBlueprintRepository.Delete(blueprint);
}
scope.Events.Dispatch(DeletedBlueprint, this, new DeleteEventArgs<IContent>(blueprints), nameof(DeletedBlueprint));
_eventAggregator.Publish(new DeletedBlueprintNotification<IContent>(blueprints, evtMsgs));
scope.Complete();
}
}
@@ -3154,10 +3093,9 @@ namespace Umbraco.Cms.Core.Services.Implement
using (var scope = ScopeProvider.CreateScope())
{
var rollbackEventArgs = new RollbackEventArgs<IContent>(content);
//Emit RollingBack event aka before
if (scope.Events.DispatchCancelable(RollingBack, this, rollbackEventArgs))
var notification = new RollingBackNotification<IContent>(content, evtMsgs);
_eventAggregator.Publish(notification);
if (notification.Cancel)
{
scope.Complete();
return OperationResult.Cancel(evtMsgs);
@@ -3177,9 +3115,7 @@ namespace Umbraco.Cms.Core.Services.Implement
}
else
{
//Emit RolledBack event aka after
rollbackEventArgs.CanCancel = false;
scope.Events.Dispatch(RolledBack, this, rollbackEventArgs);
_eventAggregator.Publish(new RolledBackNotification<IContent>(content, evtMsgs));
//Logging & Audit message
_logger.LogInformation("User '{UserId}' rolled back content '{ContentId}' to version '{VersionId}'", userId, id, versionId);