Merge branch 'netcore/dev' into netcore/members-roles-save

# Conflicts:
#	.gitignore
#	src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberIdentityUserManagerTests.cs
#	src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs
#	src/Umbraco.Web.Common/Security/MemberManager.cs
This commit is contained in:
Emma Garland
2021-03-16 17:48:40 +00:00
202 changed files with 3478 additions and 2185 deletions

1
.gitignore vendored
View File

@@ -202,4 +202,5 @@ src/Umbraco.Tests/TEMP/
/src/Umbraco.Web.UI/config/umbracoSettings.config
/src/Umbraco.Web.UI.NetCore/Umbraco/models/*
src/Umbraco.Tests.UnitTests/umbraco/Data/TEMP/
/src/Umbraco.Web.UI.NetCore/appsettings.Local.json
src/Umbraco.Tests.Integration/DatabaseContextTests.sdf

View File

@@ -1,17 +1,17 @@
using System;
using System;
using Umbraco.Cms.Core.Events;
namespace Umbraco.Cms.Core.Cache
{
public sealed class ApplicationCacheRefresher : CacheRefresherBase<ApplicationCacheRefresher>
public sealed class ApplicationCacheRefresher : CacheRefresherBase<ApplicationCacheRefresherNotification>
{
public ApplicationCacheRefresher(AppCaches appCaches)
: base(appCaches)
{ }
public ApplicationCacheRefresher(AppCaches appCaches, IEventAggregator eventAggregator, ICacheRefresherNotificationFactory factory)
: base(appCaches, eventAggregator, factory)
{
}
#region Define
protected override ApplicationCacheRefresher This => this;
public static readonly Guid UniqueId = Guid.Parse("B15F34A1-BC1D-4F8B-8369-3222728AB4C8");
public override Guid RefresherUniqueId => UniqueId;

View File

@@ -0,0 +1,11 @@
using Umbraco.Cms.Core.Sync;
namespace Umbraco.Cms.Core.Cache
{
public class ApplicationCacheRefresherNotification : CacheRefresherNotification
{
public ApplicationCacheRefresherNotification(object messageObject, MessageType messageType) : base(messageObject, messageType)
{
}
}
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models.Entities;
using Umbraco.Cms.Core.Sync;
@@ -10,33 +10,22 @@ namespace Umbraco.Cms.Core.Cache
/// </summary>
/// <typeparam name="TInstanceType">The actual cache refresher type.</typeparam>
/// <remarks>The actual cache refresher type is used for strongly typed events.</remarks>
public abstract class CacheRefresherBase<TInstanceType> : ICacheRefresher
where TInstanceType : class, ICacheRefresher
public abstract class CacheRefresherBase<TNotification> : ICacheRefresher
where TNotification : CacheRefresherNotification
{
/// <summary>
/// Initializes a new instance of the <see cref="CacheRefresherBase{TInstanceType}"/>.
/// </summary>
/// <param name="appCaches">A cache helper.</param>
protected CacheRefresherBase(AppCaches appCaches)
protected CacheRefresherBase(AppCaches appCaches, IEventAggregator eventAggregator, ICacheRefresherNotificationFactory factory)
{
AppCaches = appCaches;
EventAggregator = eventAggregator;
NotificationFactory = factory;
}
/// <summary>
/// Triggers when the cache is updated on the server.
/// </summary>
/// <remarks>
/// Triggers on each server configured for an Umbraco project whenever a cache refresher is updated.
/// </remarks>
public static event TypedEventHandler<TInstanceType, CacheRefresherEventArgs> CacheUpdated;
#region Define
/// <summary>
/// Gets the typed 'this' for events.
/// </summary>
protected abstract TInstanceType This { get; }
/// <summary>
/// Gets the unique identifier of the refresher.
/// </summary>
@@ -47,6 +36,11 @@ namespace Umbraco.Cms.Core.Cache
/// </summary>
public abstract string Name { get; }
/// <summary>
/// Gets the <see cref="TNotificationFactory"/> for <see cref="TNotification"/>
/// </summary>
protected ICacheRefresherNotificationFactory NotificationFactory { get; }
#endregion
#region Refresher
@@ -56,7 +50,7 @@ namespace Umbraco.Cms.Core.Cache
/// </summary>
public virtual void RefreshAll()
{
OnCacheUpdated(This, new CacheRefresherEventArgs(null, MessageType.RefreshAll));
OnCacheUpdated(NotificationFactory.Create<TNotification>(null, MessageType.RefreshAll));
}
/// <summary>
@@ -65,7 +59,7 @@ namespace Umbraco.Cms.Core.Cache
/// <param name="id">The entity's identifier.</param>
public virtual void Refresh(int id)
{
OnCacheUpdated(This, new CacheRefresherEventArgs(id, MessageType.RefreshById));
OnCacheUpdated(NotificationFactory.Create<TNotification>(id, MessageType.RefreshById));
}
/// <summary>
@@ -74,7 +68,7 @@ namespace Umbraco.Cms.Core.Cache
/// <param name="id">The entity's identifier.</param>
public virtual void Refresh(Guid id)
{
OnCacheUpdated(This, new CacheRefresherEventArgs(id, MessageType.RefreshById));
OnCacheUpdated(NotificationFactory.Create<TNotification>(id, MessageType.RefreshById));
}
/// <summary>
@@ -83,7 +77,7 @@ namespace Umbraco.Cms.Core.Cache
/// <param name="id">The entity's identifier.</param>
public virtual void Remove(int id)
{
OnCacheUpdated(This, new CacheRefresherEventArgs(id, MessageType.RemoveById));
OnCacheUpdated(NotificationFactory.Create<TNotification>(id, MessageType.RemoveById));
}
#endregion
@@ -95,6 +89,8 @@ namespace Umbraco.Cms.Core.Cache
/// </summary>
protected AppCaches AppCaches { get; }
protected IEventAggregator EventAggregator { get; }
/// <summary>
/// Clears the cache for all repository entities of a specified type.
/// </summary>
@@ -110,9 +106,9 @@ namespace Umbraco.Cms.Core.Cache
/// </summary>
/// <param name="sender">The event sender.</param>
/// <param name="args">The event arguments.</param>
protected static void OnCacheUpdated(TInstanceType sender, CacheRefresherEventArgs args)
protected void OnCacheUpdated(CacheRefresherNotification notification)
{
CacheUpdated?.Invoke(sender, args);
EventAggregator.Publish(notification);
}
#endregion

View File

@@ -1,19 +0,0 @@
using System;
using Umbraco.Cms.Core.Sync;
namespace Umbraco.Cms.Core.Cache
{
/// <summary>
/// Event args for cache refresher updates
/// </summary>
public class CacheRefresherEventArgs : EventArgs
{
public CacheRefresherEventArgs(object msgObject, MessageType type)
{
MessageType = type;
MessageObject = msgObject;
}
public object MessageObject { get; private set; }
public MessageType MessageType { get; private set; }
}
}

View File

@@ -0,0 +1,22 @@
using System;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Sync;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.Cache
{
/// <summary>
/// Base class for cache refresher notifications
/// </summary>
public abstract class CacheRefresherNotification : INotification
{
public CacheRefresherNotification(object messageObject, MessageType messageType)
{
MessageObject = messageObject ?? throw new ArgumentNullException(nameof(messageObject));
MessageType = messageType;
}
public object MessageObject { get; }
public MessageType MessageType { get; }
}
}

View File

@@ -0,0 +1,23 @@
using System;
using Umbraco.Extensions;
using Umbraco.Cms.Core.Sync;
namespace Umbraco.Cms.Core.Cache
{
/// <summary>
/// A <see cref="ICacheRefresherNotificationFactory"/> that uses ActivatorUtilities to create the <see cref="CacheRefresherNotification"/> instances
/// </summary>
public sealed class CacheRefresherNotificationFactory : ICacheRefresherNotificationFactory
{
private readonly IServiceProvider _serviceProvider;
public CacheRefresherNotificationFactory(IServiceProvider serviceProvider) => _serviceProvider = serviceProvider;
/// <summary>
/// Create a <see cref="CacheRefresherNotification"/> using ActivatorUtilities
/// </summary>
/// <typeparam name="TNotification">The <see cref="CacheRefresherNotification"/> to create</typeparam>
public TNotification Create<TNotification>(object msgObject, MessageType type) where TNotification : CacheRefresherNotification
=> _serviceProvider.CreateInstance<TNotification>(new object[] { msgObject, type });
}
}

View File

@@ -1,6 +1,7 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Persistence.Repositories;
using Umbraco.Cms.Core.PublishedCache;
@@ -11,14 +12,21 @@ using Umbraco.Extensions;
namespace Umbraco.Cms.Core.Cache
{
public sealed class ContentCacheRefresher : PayloadCacheRefresherBase<ContentCacheRefresher, ContentCacheRefresher.JsonPayload>
public sealed class ContentCacheRefresher : PayloadCacheRefresherBase<ContentCacheRefresherNotification, ContentCacheRefresher.JsonPayload>
{
private readonly IPublishedSnapshotService _publishedSnapshotService;
private readonly IIdKeyMap _idKeyMap;
private readonly IDomainService _domainService;
public ContentCacheRefresher(AppCaches appCaches, IJsonSerializer serializer, IPublishedSnapshotService publishedSnapshotService, IIdKeyMap idKeyMap, IDomainService domainService)
: base(appCaches, serializer)
public ContentCacheRefresher(
AppCaches appCaches,
IJsonSerializer serializer,
IPublishedSnapshotService publishedSnapshotService,
IIdKeyMap idKeyMap,
IDomainService domainService,
IEventAggregator eventAggregator,
ICacheRefresherNotificationFactory factory)
: base(appCaches, serializer, eventAggregator, factory)
{
_publishedSnapshotService = publishedSnapshotService;
_idKeyMap = idKeyMap;
@@ -27,8 +35,6 @@ namespace Umbraco.Cms.Core.Cache
#region Define
protected override ContentCacheRefresher This => this;
public static readonly Guid UniqueId = Guid.Parse("900A4FBE-DF3C-41E6-BB77-BE896CD158EA");
public override Guid RefresherUniqueId => UniqueId;

View File

@@ -0,0 +1,11 @@
using Umbraco.Cms.Core.Sync;
namespace Umbraco.Cms.Core.Cache
{
public class ContentCacheRefresherNotification : CacheRefresherNotification
{
public ContentCacheRefresherNotification(object messageObject, MessageType messageType) : base(messageObject, messageType)
{
}
}
}

View File

@@ -1,5 +1,6 @@
using System;
using System;
using System.Linq;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.Persistence.Repositories;
@@ -11,15 +12,23 @@ using Umbraco.Extensions;
namespace Umbraco.Cms.Core.Cache
{
public sealed class ContentTypeCacheRefresher : PayloadCacheRefresherBase<ContentTypeCacheRefresher, ContentTypeCacheRefresher.JsonPayload>
public sealed class ContentTypeCacheRefresher : PayloadCacheRefresherBase<ContentTypeCacheRefresherNotification, ContentTypeCacheRefresher.JsonPayload>
{
private readonly IPublishedSnapshotService _publishedSnapshotService;
private readonly IPublishedModelFactory _publishedModelFactory;
private readonly IContentTypeCommonRepository _contentTypeCommonRepository;
private readonly IIdKeyMap _idKeyMap;
public ContentTypeCacheRefresher(AppCaches appCaches, IJsonSerializer serializer, IPublishedSnapshotService publishedSnapshotService, IPublishedModelFactory publishedModelFactory, IIdKeyMap idKeyMap, IContentTypeCommonRepository contentTypeCommonRepository)
: base(appCaches, serializer)
public ContentTypeCacheRefresher(
AppCaches appCaches,
IJsonSerializer serializer,
IPublishedSnapshotService publishedSnapshotService,
IPublishedModelFactory publishedModelFactory,
IIdKeyMap idKeyMap,
IContentTypeCommonRepository contentTypeCommonRepository,
IEventAggregator eventAggregator,
ICacheRefresherNotificationFactory factory)
: base(appCaches, serializer, eventAggregator, factory)
{
_publishedSnapshotService = publishedSnapshotService;
_publishedModelFactory = publishedModelFactory;
@@ -29,8 +38,6 @@ namespace Umbraco.Cms.Core.Cache
#region Define
protected override ContentTypeCacheRefresher This => this;
public static readonly Guid UniqueId = Guid.Parse("6902E22C-9C10-483C-91F3-66B7CAE9E2F5");
public override Guid RefresherUniqueId => UniqueId;

View File

@@ -0,0 +1,11 @@
using Umbraco.Cms.Core.Sync;
namespace Umbraco.Cms.Core.Cache
{
public class ContentTypeCacheRefresherNotification : CacheRefresherNotification
{
public ContentTypeCacheRefresherNotification(object messageObject, MessageType messageType) : base(messageObject, messageType)
{
}
}
}

View File

@@ -1,4 +1,5 @@
using System;
using System;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PropertyEditors.ValueConverters;
@@ -9,14 +10,21 @@ using Umbraco.Extensions;
namespace Umbraco.Cms.Core.Cache
{
public sealed class DataTypeCacheRefresher : PayloadCacheRefresherBase<DataTypeCacheRefresher, DataTypeCacheRefresher.JsonPayload>
public sealed class DataTypeCacheRefresher : PayloadCacheRefresherBase<DataTypeCacheRefresherNotification, DataTypeCacheRefresher.JsonPayload>
{
private readonly IPublishedSnapshotService _publishedSnapshotService;
private readonly IPublishedModelFactory _publishedModelFactory;
private readonly IIdKeyMap _idKeyMap;
public DataTypeCacheRefresher(AppCaches appCaches, IJsonSerializer serializer, IPublishedSnapshotService publishedSnapshotService, IPublishedModelFactory publishedModelFactory, IIdKeyMap idKeyMap)
: base(appCaches, serializer)
public DataTypeCacheRefresher(
AppCaches appCaches,
IJsonSerializer serializer,
IPublishedSnapshotService publishedSnapshotService,
IPublishedModelFactory publishedModelFactory,
IIdKeyMap idKeyMap,
IEventAggregator eventAggregator,
ICacheRefresherNotificationFactory factory)
: base(appCaches, serializer, eventAggregator, factory)
{
_publishedSnapshotService = publishedSnapshotService;
_publishedModelFactory = publishedModelFactory;
@@ -25,8 +33,6 @@ namespace Umbraco.Cms.Core.Cache
#region Define
protected override DataTypeCacheRefresher This => this;
public static readonly Guid UniqueId = Guid.Parse("35B16C25-A17E-45D7-BC8F-EDAB1DCC28D2");
public override Guid RefresherUniqueId => UniqueId;

View File

@@ -0,0 +1,11 @@
using Umbraco.Cms.Core.Sync;
namespace Umbraco.Cms.Core.Cache
{
public class DataTypeCacheRefresherNotification : CacheRefresherNotification
{
public DataTypeCacheRefresherNotification(object messageObject, MessageType messageType) : base(messageObject, messageType)
{
}
}
}

View File

@@ -1,18 +1,17 @@
using System;
using System;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models;
namespace Umbraco.Cms.Core.Cache
{
public sealed class DictionaryCacheRefresher : CacheRefresherBase<DictionaryCacheRefresher>
public sealed class DictionaryCacheRefresher : CacheRefresherBase<DictionaryCacheRefresherNotification>
{
public DictionaryCacheRefresher(AppCaches appCaches)
: base(appCaches)
public DictionaryCacheRefresher(AppCaches appCaches, IEventAggregator eventAggregator, ICacheRefresherNotificationFactory factory)
: base(appCaches, eventAggregator , factory)
{ }
#region Define
protected override DictionaryCacheRefresher This => this;
public static readonly Guid UniqueId = Guid.Parse("D1D7E227-F817-4816-BFE9-6C39B6152884");
public override Guid RefresherUniqueId => UniqueId;

View File

@@ -0,0 +1,11 @@
using Umbraco.Cms.Core.Sync;
namespace Umbraco.Cms.Core.Cache
{
public class DictionaryCacheRefresherNotification : CacheRefresherNotification
{
public DictionaryCacheRefresherNotification(object messageObject, MessageType messageType) : base(messageObject, messageType)
{
}
}
}

View File

@@ -1,4 +1,5 @@
using System;
using System;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Serialization;
@@ -6,20 +7,23 @@ using Umbraco.Cms.Core.Services.Changes;
namespace Umbraco.Cms.Core.Cache
{
public sealed class DomainCacheRefresher : PayloadCacheRefresherBase<DomainCacheRefresher, DomainCacheRefresher.JsonPayload>
public sealed class DomainCacheRefresher : PayloadCacheRefresherBase<DomainCacheRefresherNotification, DomainCacheRefresher.JsonPayload>
{
private readonly IPublishedSnapshotService _publishedSnapshotService;
public DomainCacheRefresher(AppCaches appCaches, IJsonSerializer serializer, IPublishedSnapshotService publishedSnapshotService)
: base(appCaches, serializer)
public DomainCacheRefresher(
AppCaches appCaches,
IJsonSerializer serializer,
IPublishedSnapshotService publishedSnapshotService,
IEventAggregator eventAggregator,
ICacheRefresherNotificationFactory factory)
: base(appCaches, serializer, eventAggregator, factory)
{
_publishedSnapshotService = publishedSnapshotService;
}
#region Define
protected override DomainCacheRefresher This => this;
public static readonly Guid UniqueId = Guid.Parse("11290A79-4B57-4C99-AD72-7748A3CF38AF");
public override Guid RefresherUniqueId => UniqueId;

View File

@@ -0,0 +1,11 @@
using Umbraco.Cms.Core.Sync;
namespace Umbraco.Cms.Core.Cache
{
public class DomainCacheRefresherNotification : CacheRefresherNotification
{
public DomainCacheRefresherNotification(object messageObject, MessageType messageType) : base(messageObject, messageType)
{
}
}
}

View File

@@ -0,0 +1,16 @@
using Umbraco.Cms.Core.Sync;
namespace Umbraco.Cms.Core.Cache
{
/// <summary>
/// Factory for creating cache refresher notification instances
/// </summary>
public interface ICacheRefresherNotificationFactory
{
/// <summary>
/// Creates a <see cref="ICacheRefresherNotification"/>
/// </summary>
/// <typeparam name="TNotification">The <see cref="ICacheRefresherNotification"/> to create</typeparam>
TNotification Create<TNotification>(object msgObject, MessageType type) where TNotification : CacheRefresherNotification;
}
}

View File

@@ -1,4 +1,5 @@
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Sync;
namespace Umbraco.Cms.Core.Cache
@@ -8,8 +9,8 @@ namespace Umbraco.Cms.Core.Cache
/// </summary>
/// <typeparam name="TInstanceType">The actual cache refresher type.</typeparam>
/// <remarks>The actual cache refresher type is used for strongly typed events.</remarks>
public abstract class JsonCacheRefresherBase<TInstanceType, TJsonPayload> : CacheRefresherBase<TInstanceType>, IJsonCacheRefresher
where TInstanceType : class, ICacheRefresher
public abstract class JsonCacheRefresherBase<TNotification, TJsonPayload> : CacheRefresherBase<TNotification>, IJsonCacheRefresher
where TNotification : CacheRefresherNotification
{
protected IJsonSerializer JsonSerializer { get; }
@@ -17,7 +18,12 @@ namespace Umbraco.Cms.Core.Cache
/// Initializes a new instance of the <see cref="JsonCacheRefresherBase{TInstanceType}"/>.
/// </summary>
/// <param name="appCaches">A cache helper.</param>
protected JsonCacheRefresherBase(AppCaches appCaches, IJsonSerializer jsonSerializer) : base(appCaches)
protected JsonCacheRefresherBase(
AppCaches appCaches,
IJsonSerializer jsonSerializer,
IEventAggregator eventAggregator,
ICacheRefresherNotificationFactory factory)
: base(appCaches, eventAggregator, factory)
{
JsonSerializer = jsonSerializer;
}
@@ -28,7 +34,7 @@ namespace Umbraco.Cms.Core.Cache
/// <param name="json">The json payload.</param>
public virtual void Refresh(string json)
{
OnCacheUpdated(This, new CacheRefresherEventArgs(json, MessageType.RefreshByJson));
OnCacheUpdated(NotificationFactory.Create<TNotification>(json, MessageType.RefreshByJson));
}
#region Json

View File

@@ -1,4 +1,5 @@
using System;
using System;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Serialization;
@@ -7,18 +8,21 @@ using static Umbraco.Cms.Core.Cache.LanguageCacheRefresher.JsonPayload;
namespace Umbraco.Cms.Core.Cache
{
public sealed class LanguageCacheRefresher : PayloadCacheRefresherBase<LanguageCacheRefresher, LanguageCacheRefresher.JsonPayload>
public sealed class LanguageCacheRefresher : PayloadCacheRefresherBase<LanguageCacheRefresherNotification, LanguageCacheRefresher.JsonPayload>
{
public LanguageCacheRefresher(AppCaches appCaches, IJsonSerializer serializer, IPublishedSnapshotService publishedSnapshotService)
: base(appCaches, serializer)
public LanguageCacheRefresher(
AppCaches appCaches,
IJsonSerializer serializer,
IPublishedSnapshotService publishedSnapshotService,
IEventAggregator eventAggregator,
ICacheRefresherNotificationFactory factory)
: base(appCaches, serializer, eventAggregator, factory)
{
_publishedSnapshotService = publishedSnapshotService;
}
#region Define
protected override LanguageCacheRefresher This => this;
public static readonly Guid UniqueId = Guid.Parse("3E0F95D8-0BE5-44B8-8394-2B8750B62654");
private readonly IPublishedSnapshotService _publishedSnapshotService;

View File

@@ -0,0 +1,11 @@
using Umbraco.Cms.Core.Sync;
namespace Umbraco.Cms.Core.Cache
{
public class LanguageCacheRefresherNotification : CacheRefresherNotification
{
public LanguageCacheRefresherNotification(object messageObject, MessageType messageType) : base(messageObject, messageType)
{
}
}
}

View File

@@ -1,23 +1,26 @@
using System;
using System;
using System.Linq;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Persistence.Repositories;
using Umbraco.Cms.Core.Serialization;
namespace Umbraco.Cms.Core.Cache
{
public sealed class MacroCacheRefresher : PayloadCacheRefresherBase<MacroCacheRefresher, MacroCacheRefresher.JsonPayload>
public sealed class MacroCacheRefresher : PayloadCacheRefresherBase<MacroCacheRefresherNotification, MacroCacheRefresher.JsonPayload>
{
public MacroCacheRefresher(AppCaches appCaches, IJsonSerializer jsonSerializer)
: base(appCaches, jsonSerializer)
public MacroCacheRefresher(
AppCaches appCaches,
IJsonSerializer jsonSerializer,
IEventAggregator eventAggregator,
ICacheRefresherNotificationFactory factory)
: base(appCaches, jsonSerializer, eventAggregator, factory)
{
}
#region Define
protected override MacroCacheRefresher This => this;
public static readonly Guid UniqueId = Guid.Parse("7B1E683C-5F34-43dd-803D-9699EA1E98CA");
public override Guid RefresherUniqueId => UniqueId;

View File

@@ -0,0 +1,11 @@
using Umbraco.Cms.Core.Sync;
namespace Umbraco.Cms.Core.Cache
{
public class MacroCacheRefresherNotification : CacheRefresherNotification
{
public MacroCacheRefresherNotification(object messageObject, MessageType messageType) : base(messageObject, messageType)
{
}
}
}

View File

@@ -1,4 +1,5 @@
using System;
using System;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Persistence.Repositories;
using Umbraco.Cms.Core.PublishedCache;
@@ -9,13 +10,13 @@ using Umbraco.Extensions;
namespace Umbraco.Cms.Core.Cache
{
public sealed class MediaCacheRefresher : PayloadCacheRefresherBase<MediaCacheRefresher, MediaCacheRefresher.JsonPayload>
public sealed class MediaCacheRefresher : PayloadCacheRefresherBase<MediaCacheRefresherNotification, MediaCacheRefresher.JsonPayload>
{
private readonly IPublishedSnapshotService _publishedSnapshotService;
private readonly IIdKeyMap _idKeyMap;
public MediaCacheRefresher(AppCaches appCaches, IJsonSerializer serializer, IPublishedSnapshotService publishedSnapshotService, IIdKeyMap idKeyMap)
: base(appCaches, serializer)
public MediaCacheRefresher(AppCaches appCaches, IJsonSerializer serializer, IPublishedSnapshotService publishedSnapshotService, IIdKeyMap idKeyMap, IEventAggregator eventAggregator, ICacheRefresherNotificationFactory factory)
: base(appCaches, serializer, eventAggregator, factory)
{
_publishedSnapshotService = publishedSnapshotService;
_idKeyMap = idKeyMap;
@@ -23,8 +24,6 @@ namespace Umbraco.Cms.Core.Cache
#region Define
protected override MediaCacheRefresher This => this;
public static readonly Guid UniqueId = Guid.Parse("B29286DD-2D40-4DDB-B325-681226589FEC");
public override Guid RefresherUniqueId => UniqueId;

View File

@@ -0,0 +1,11 @@
using Umbraco.Cms.Core.Sync;
namespace Umbraco.Cms.Core.Cache
{
public class MediaCacheRefresherNotification : CacheRefresherNotification
{
public MediaCacheRefresherNotification(object messageObject, MessageType messageType) : base(messageObject, messageType)
{
}
}
}

View File

@@ -1,6 +1,7 @@
//using Newtonsoft.Json;
using System;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Persistence.Repositories;
using Umbraco.Cms.Core.Serialization;
@@ -9,12 +10,12 @@ using Umbraco.Extensions;
namespace Umbraco.Cms.Core.Cache
{
public sealed class MemberCacheRefresher : PayloadCacheRefresherBase<MemberCacheRefresher, MemberCacheRefresher.JsonPayload>
public sealed class MemberCacheRefresher : PayloadCacheRefresherBase<MemberCacheRefresherNotification, MemberCacheRefresher.JsonPayload>
{
private readonly IIdKeyMap _idKeyMap;
public MemberCacheRefresher(AppCaches appCaches, IJsonSerializer serializer, IIdKeyMap idKeyMap)
: base(appCaches, serializer)
public MemberCacheRefresher(AppCaches appCaches, IJsonSerializer serializer, IIdKeyMap idKeyMap, IEventAggregator eventAggregator, ICacheRefresherNotificationFactory factory)
: base(appCaches, serializer, eventAggregator, factory)
{
_idKeyMap = idKeyMap;
}
@@ -36,8 +37,6 @@ namespace Umbraco.Cms.Core.Cache
#region Define
protected override MemberCacheRefresher This => this;
public static readonly Guid UniqueId = Guid.Parse("E285DF34-ACDC-4226-AE32-C0CB5CF388DA");
public override Guid RefresherUniqueId => UniqueId;

View File

@@ -0,0 +1,11 @@
using Umbraco.Cms.Core.Sync;
namespace Umbraco.Cms.Core.Cache
{
public class MemberCacheRefresherNotification : CacheRefresherNotification
{
public MemberCacheRefresherNotification(object messageObject, MessageType messageType) : base(messageObject, messageType)
{
}
}
}

View File

@@ -1,21 +1,20 @@
using System;
using System;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Serialization;
namespace Umbraco.Cms.Core.Cache
{
public sealed class MemberGroupCacheRefresher : PayloadCacheRefresherBase<MemberGroupCacheRefresher, MemberGroupCacheRefresher.JsonPayload>
public sealed class MemberGroupCacheRefresher : PayloadCacheRefresherBase<MemberGroupCacheRefresherNotification, MemberGroupCacheRefresher.JsonPayload>
{
public MemberGroupCacheRefresher(AppCaches appCaches, IJsonSerializer jsonSerializer)
: base(appCaches, jsonSerializer)
public MemberGroupCacheRefresher(AppCaches appCaches, IJsonSerializer jsonSerializer, IEventAggregator eventAggregator, ICacheRefresherNotificationFactory factory)
: base(appCaches, jsonSerializer, eventAggregator, factory)
{
}
#region Define
protected override MemberGroupCacheRefresher This => this;
public static readonly Guid UniqueId = Guid.Parse("187F236B-BD21-4C85-8A7C-29FBA3D6C00C");
public override Guid RefresherUniqueId => UniqueId;

View File

@@ -0,0 +1,11 @@
using Umbraco.Cms.Core.Sync;
namespace Umbraco.Cms.Core.Cache
{
public class MemberGroupCacheRefresherNotification : CacheRefresherNotification
{
public MemberGroupCacheRefresherNotification(object messageObject, MessageType messageType) : base(messageObject, messageType)
{
}
}
}

View File

@@ -1,4 +1,5 @@
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Sync;
namespace Umbraco.Cms.Core.Cache
@@ -9,8 +10,8 @@ namespace Umbraco.Cms.Core.Cache
/// <typeparam name="TInstanceType">The actual cache refresher type.</typeparam>
/// <typeparam name="TPayload">The payload type.</typeparam>
/// <remarks>The actual cache refresher type is used for strongly typed events.</remarks>
public abstract class PayloadCacheRefresherBase<TInstanceType, TPayload> : JsonCacheRefresherBase<TInstanceType, TPayload>, IPayloadCacheRefresher<TPayload>
where TInstanceType : class, ICacheRefresher
public abstract class PayloadCacheRefresherBase<TNotification, TPayload> : JsonCacheRefresherBase<TNotification, TPayload>, IPayloadCacheRefresher<TPayload>
where TNotification : CacheRefresherNotification
{
/// <summary>
@@ -18,7 +19,8 @@ namespace Umbraco.Cms.Core.Cache
/// </summary>
/// <param name="appCaches">A cache helper.</param>
/// <param name="serializer"></param>
protected PayloadCacheRefresherBase(AppCaches appCaches, IJsonSerializer serializer) : base(appCaches, serializer)
protected PayloadCacheRefresherBase(AppCaches appCaches, IJsonSerializer serializer, IEventAggregator eventAggregator, ICacheRefresherNotificationFactory factory)
: base(appCaches, serializer, eventAggregator, factory)
{
}
@@ -37,7 +39,7 @@ namespace Umbraco.Cms.Core.Cache
/// <param name="payloads">The payload.</param>
public virtual void Refresh(TPayload[] payloads)
{
OnCacheUpdated(This, new CacheRefresherEventArgs(payloads, MessageType.RefreshByPayload));
OnCacheUpdated(NotificationFactory.Create<TNotification>(payloads, MessageType.RefreshByPayload));
}
#endregion

View File

@@ -1,18 +1,17 @@
using System;
using System;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models;
namespace Umbraco.Cms.Core.Cache
{
public sealed class PublicAccessCacheRefresher : CacheRefresherBase<PublicAccessCacheRefresher>
public sealed class PublicAccessCacheRefresher : CacheRefresherBase<PublicAccessCacheRefresherNotification>
{
public PublicAccessCacheRefresher(AppCaches appCaches)
: base(appCaches)
public PublicAccessCacheRefresher(AppCaches appCaches, IEventAggregator eventAggregator, ICacheRefresherNotificationFactory factory)
: base(appCaches, eventAggregator, factory)
{ }
#region Define
protected override PublicAccessCacheRefresher This => this;
public static readonly Guid UniqueId = Guid.Parse("1DB08769-B104-4F8B-850E-169CAC1DF2EC");
public override Guid RefresherUniqueId => UniqueId;

View File

@@ -0,0 +1,11 @@
using Umbraco.Cms.Core.Sync;
namespace Umbraco.Cms.Core.Cache
{
public class PublicAccessCacheRefresherNotification : CacheRefresherNotification
{
public PublicAccessCacheRefresherNotification(object messageObject, MessageType messageType) : base(messageObject, messageType)
{
}
}
}

View File

@@ -1,19 +1,18 @@
using System;
using System;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Persistence.Repositories;
namespace Umbraco.Cms.Core.Cache
{
public sealed class RelationTypeCacheRefresher : CacheRefresherBase<RelationTypeCacheRefresher>
public sealed class RelationTypeCacheRefresher : CacheRefresherBase<RelationTypeCacheRefresherNotification>
{
public RelationTypeCacheRefresher(AppCaches appCaches)
: base(appCaches)
public RelationTypeCacheRefresher(AppCaches appCaches, IEventAggregator eventAggregator, ICacheRefresherNotificationFactory factory)
: base(appCaches, eventAggregator, factory)
{ }
#region Define
protected override RelationTypeCacheRefresher This => this;
public static readonly Guid UniqueId = Guid.Parse("D8375ABA-4FB3-4F86-B505-92FBA1B6F7C9");
public override Guid RefresherUniqueId => UniqueId;

View File

@@ -0,0 +1,11 @@
using Umbraco.Cms.Core.Sync;
namespace Umbraco.Cms.Core.Cache
{
public class RelationTypeCacheRefresherNotification : CacheRefresherNotification
{
public RelationTypeCacheRefresherNotification(object messageObject, MessageType messageType) : base(messageObject, messageType)
{
}
}
}

View File

@@ -1,17 +1,18 @@
using System;
using System;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Persistence.Repositories;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Core.Cache
{
public sealed class TemplateCacheRefresher : CacheRefresherBase<TemplateCacheRefresher>
public sealed class TemplateCacheRefresher : CacheRefresherBase<TemplateCacheRefresherNotification>
{
private readonly IIdKeyMap _idKeyMap;
private readonly IContentTypeCommonRepository _contentTypeCommonRepository;
public TemplateCacheRefresher(AppCaches appCaches, IIdKeyMap idKeyMap, IContentTypeCommonRepository contentTypeCommonRepository)
: base(appCaches)
public TemplateCacheRefresher(AppCaches appCaches, IIdKeyMap idKeyMap, IContentTypeCommonRepository contentTypeCommonRepository, IEventAggregator eventAggregator, ICacheRefresherNotificationFactory factory)
: base(appCaches, eventAggregator, factory)
{
_idKeyMap = idKeyMap;
_contentTypeCommonRepository = contentTypeCommonRepository;
@@ -19,8 +20,6 @@ namespace Umbraco.Cms.Core.Cache
#region Define
protected override TemplateCacheRefresher This => this;
public static readonly Guid UniqueId = Guid.Parse("DD12B6A0-14B9-46e8-8800-C154F74047C8");
public override Guid RefresherUniqueId => UniqueId;

View File

@@ -0,0 +1,11 @@
using Umbraco.Cms.Core.Sync;
namespace Umbraco.Cms.Core.Cache
{
public class TemplateCacheRefresherNotification : CacheRefresherNotification
{
public TemplateCacheRefresherNotification(object messageObject, MessageType messageType) : base(messageObject, messageType)
{
}
}
}

View File

@@ -1,19 +1,18 @@
using System;
using System;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Persistence.Repositories;
namespace Umbraco.Cms.Core.Cache
{
public sealed class UserCacheRefresher : CacheRefresherBase<UserCacheRefresher>
public sealed class UserCacheRefresher : CacheRefresherBase<UserCacheRefresherNotification>
{
public UserCacheRefresher(AppCaches appCaches)
: base(appCaches)
public UserCacheRefresher(AppCaches appCaches, IEventAggregator eventAggregator, ICacheRefresherNotificationFactory factory)
: base(appCaches, eventAggregator, factory)
{ }
#region Define
protected override UserCacheRefresher This => this;
public static readonly Guid UniqueId = Guid.Parse("E057AF6D-2EE6-41F4-8045-3694010F0AA6");
public override Guid RefresherUniqueId => UniqueId;
@@ -47,7 +46,7 @@ namespace Umbraco.Cms.Core.Cache
userCache.Result.ClearByKey(CacheKeys.UserAllContentStartNodesPrefix + id);
userCache.Result.ClearByKey(CacheKeys.UserAllMediaStartNodesPrefix + id);
}
base.Remove(id);
}

View File

@@ -0,0 +1,11 @@
using Umbraco.Cms.Core.Sync;
namespace Umbraco.Cms.Core.Cache
{
public class UserCacheRefresherNotification : CacheRefresherNotification
{
public UserCacheRefresherNotification(object messageObject, MessageType messageType) : base(messageObject, messageType)
{
}
}
}

View File

@@ -1,4 +1,5 @@
using System;
using System;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Persistence.Repositories;
@@ -10,16 +11,14 @@ namespace Umbraco.Cms.Core.Cache
/// <remarks>
/// This also needs to clear the user cache since IReadOnlyUserGroup's are attached to IUser objects
/// </remarks>
public sealed class UserGroupCacheRefresher : CacheRefresherBase<UserGroupCacheRefresher>
public sealed class UserGroupCacheRefresher : CacheRefresherBase<UserGroupCacheRefresherNotification>
{
public UserGroupCacheRefresher(AppCaches appCaches)
: base(appCaches)
public UserGroupCacheRefresher(AppCaches appCaches, IEventAggregator eventAggregator, ICacheRefresherNotificationFactory factory)
: base(appCaches, eventAggregator, factory)
{ }
#region Define
protected override UserGroupCacheRefresher This => this;
public static readonly Guid UniqueId = Guid.Parse("45178038-B232-4FE8-AA1A-F2B949C44762");
public override Guid RefresherUniqueId => UniqueId;

View File

@@ -0,0 +1,11 @@
using Umbraco.Cms.Core.Sync;
namespace Umbraco.Cms.Core.Cache
{
public class UserGroupCacheRefresherNotification : CacheRefresherNotification
{
public UserGroupCacheRefresherNotification(object messageObject, MessageType messageType) : base(messageObject, messageType)
{
}
}
}

View File

@@ -189,6 +189,7 @@ namespace Umbraco.Cms.Core.DependencyInjection
// register distributed cache
Services.AddUnique(f => new DistributedCache(f.GetRequiredService<IServerMessenger>(), f.GetRequiredService<CacheRefresherCollection>()));
Services.AddUnique<ICacheRefresherNotificationFactory, CacheRefresherNotificationFactory>();
// register the http context and umbraco context accessors
// we *should* use the HttpContextUmbracoContextAccessor, however there are cases when

View File

@@ -1,30 +0,0 @@
using System.Collections.Generic;
using Umbraco.Cms.Core.Models;
namespace Umbraco.Cms.Core.Events
{
/// <summary>
/// Represents event data for the Published event.
/// </summary>
public class ContentPublishedEventArgs : PublishEventArgs<IContent>
{
/// <summary>
/// Initializes a new instance of the <see cref="ContentPublishedEventArgs"/> class.
/// </summary>
public ContentPublishedEventArgs(IEnumerable<IContent> eventObject, bool canCancel, EventMessages eventMessages)
: base(eventObject, canCancel, eventMessages)
{ }
/// <summary>
/// Determines whether a culture has been published, during a Published event.
/// </summary>
public bool HasPublishedCulture(IContent content, string culture)
=> content.WasPropertyDirty(ContentBase.ChangeTrackingPrefix.ChangedCulture + culture);
/// <summary>
/// Determines whether a culture has been unpublished, during a Published event.
/// </summary>
public bool HasUnpublishedCulture(IContent content, string culture)
=> content.WasPropertyDirty(ContentBase.ChangeTrackingPrefix.UnpublishedCulture + culture);
}
}

View File

@@ -1,30 +0,0 @@
using System.Collections.Generic;
using Umbraco.Cms.Core.Models;
namespace Umbraco.Cms.Core.Events
{
/// <summary>
/// Represents event data for the Publishing event.
/// </summary>
public class ContentPublishingEventArgs : PublishEventArgs<IContent>
{
/// <summary>
/// Initializes a new instance of the <see cref="ContentPublishingEventArgs"/> class.
/// </summary>
public ContentPublishingEventArgs(IEnumerable<IContent> eventObject, EventMessages eventMessages)
: base(eventObject, eventMessages)
{ }
/// <summary>
/// Determines whether a culture is being published, during a Publishing event.
/// </summary>
public bool IsPublishingCulture(IContent content, string culture)
=> content.PublishCultureInfos.TryGetValue(culture, out var cultureInfo) && cultureInfo.IsDirty();
/// <summary>
/// Determines whether a culture is being unpublished, during a Publishing event.
/// </summary>
public bool IsUnpublishingCulture(IContent content, string culture)
=> content.IsPropertyDirty(ContentBase.ChangeTrackingPrefix.UnpublishedCulture + culture); //bit of a hack since we know that the content implementation tracks changes this way
}
}

View File

@@ -1,24 +0,0 @@
using System.Collections.Generic;
using Umbraco.Cms.Core.Models;
namespace Umbraco.Cms.Core.Events
{
/// <summary>
/// Represents event data for the Saved event.
/// </summary>
public class ContentSavedEventArgs : SaveEventArgs<IContent>
{
/// <summary>
/// Initializes a new instance of the <see cref="ContentSavedEventArgs"/> class.
/// </summary>
public ContentSavedEventArgs(IEnumerable<IContent> eventObject, EventMessages messages, IDictionary<string, object> additionalData)
: base(eventObject, false, messages, additionalData)
{ }
/// <summary>
/// Determines whether a culture has been saved, during a Saved event.
/// </summary>
public bool HasSavedCulture(IContent content, string culture)
=> content.WasPropertyDirty(ContentBase.ChangeTrackingPrefix.UpdatedCulture + culture);
}
}

View File

@@ -1,77 +0,0 @@
using System.Collections.Generic;
using Umbraco.Cms.Core.Models;
namespace Umbraco.Cms.Core.Events
{
/// <summary>
/// Represent event data for the Saving event.
/// </summary>
public class ContentSavingEventArgs : SaveEventArgs<IContent>
{
#region Factory Methods
/// <summary>
/// Converts <see cref="ContentSavingEventArgs"/> to <see cref="ContentSavedEventArgs"/> while preserving all args state
/// </summary>
/// <returns></returns>
public ContentSavedEventArgs ToContentSavedEventArgs()
{
return new ContentSavedEventArgs(EventObject, Messages, AdditionalData)
{
EventState = EventState
};
}
/// <summary>
/// Converts <see cref="ContentSavingEventArgs"/> to <see cref="ContentPublishedEventArgs"/> while preserving all args state
/// </summary>
/// <returns></returns>
public ContentPublishedEventArgs ToContentPublishedEventArgs()
{
return new ContentPublishedEventArgs(EventObject, false, Messages)
{
EventState = EventState,
AdditionalData = AdditionalData
};
}
/// <summary>
/// Converts <see cref="ContentSavingEventArgs"/> to <see cref="ContentPublishingEventArgs"/> while preserving all args state
/// </summary>
/// <returns></returns>
public ContentPublishingEventArgs ToContentPublishingEventArgs()
{
return new ContentPublishingEventArgs(EventObject, Messages)
{
EventState = EventState,
AdditionalData = AdditionalData
};
}
#endregion
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="ContentSavingEventArgs"/> class.
/// </summary>
public ContentSavingEventArgs(IEnumerable<IContent> eventObject, EventMessages eventMessages)
: base(eventObject, eventMessages)
{ }
/// <summary>
/// Initializes a new instance of the <see cref="ContentSavingEventArgs"/> class.
/// </summary>
public ContentSavingEventArgs(IContent eventObject, EventMessages eventMessages)
: base(eventObject, eventMessages)
{ }
#endregion
/// <summary>
/// Determines whether a culture is being saved, during a Saving event.
/// </summary>
public bool IsSavingCulture(IContent content, string culture)
=> content.CultureInfos.TryGetValue(culture, out var cultureInfo) && cultureInfo.IsDirty();
}
}

View File

@@ -1,69 +0,0 @@
using System;
namespace Umbraco.Cms.Core.Events
{
public class DeleteRevisionsEventArgs : DeleteEventArgs, IEquatable<DeleteRevisionsEventArgs>
{
public DeleteRevisionsEventArgs(int id, bool canCancel, int specificVersion = default, bool deletePriorVersions = false, DateTime dateToRetain = default)
: base(id, canCancel)
{
DeletePriorVersions = deletePriorVersions;
SpecificVersion = specificVersion;
DateToRetain = dateToRetain;
}
public DeleteRevisionsEventArgs(int id, int specificVersion = default, bool deletePriorVersions = false, DateTime dateToRetain = default)
: base(id)
{
DeletePriorVersions = deletePriorVersions;
SpecificVersion = specificVersion;
DateToRetain = dateToRetain;
}
public bool DeletePriorVersions { get; }
public int SpecificVersion { get; }
public DateTime DateToRetain { get; }
/// <summary>
/// Returns true if we are deleting a specific revision
/// </summary>
public bool IsDeletingSpecificRevision => SpecificVersion != default;
public bool Equals(DeleteRevisionsEventArgs other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return base.Equals(other) && DateToRetain.Equals(other.DateToRetain) && DeletePriorVersions == other.DeletePriorVersions && SpecificVersion.Equals(other.SpecificVersion);
}
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != GetType()) return false;
return Equals((DeleteRevisionsEventArgs) obj);
}
public override int GetHashCode()
{
unchecked
{
int hashCode = base.GetHashCode();
hashCode = (hashCode * 397) ^ DateToRetain.GetHashCode();
hashCode = (hashCode * 397) ^ DeletePriorVersions.GetHashCode();
hashCode = (hashCode * 397) ^ SpecificVersion.GetHashCode();
return hashCode;
}
}
public static bool operator ==(DeleteRevisionsEventArgs left, DeleteRevisionsEventArgs right)
{
return Equals(left, right);
}
public static bool operator !=(DeleteRevisionsEventArgs left, DeleteRevisionsEventArgs right)
{
return !Equals(left, right);
}
}
}

View File

@@ -0,0 +1,10 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
namespace Umbraco.Cms.Core.Events
{
public interface ICancelableNotification : INotification
{
bool Cancel { get; set; }
}
}

View File

@@ -0,0 +1,37 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using System.Threading.Tasks;
namespace Umbraco.Cms.Core.Events
{
public interface IScopedNotificationPublisher
{
/// <summary>
/// Publishes a cancelable notification to the notification subscribers
/// </summary>
/// <param name="notification"></param>
/// <returns>True if the notification was cancelled by a subscriber, false otherwise</returns>
bool PublishCancelable(ICancelableNotification notification);
/// <summary>
/// Publishes a cancelable notification to the notification subscribers
/// </summary>
/// <param name="notification"></param>
/// <returns>True if the notification was cancelled by a subscriber, false otherwise</returns>
Task<bool> PublishCancelableAsync(ICancelableNotification notification);
/// <summary>
/// Publishes a notification to the notification subscribers
/// </summary>
/// <param name="notification"></param>
/// <remarks>The notification is published upon successful completion of the current scope, i.e. when things have been saved/published/deleted etc.</remarks>
void Publish(INotification notification);
/// <summary>
/// Invokes publishing of all pending notifications within the current scope
/// </summary>
/// <param name="completed"></param>
void ScopeExit(bool completed);
}
}

View File

@@ -0,0 +1,9 @@
using System.Collections.Generic;
namespace Umbraco.Cms.Core.Events
{
public interface IStatefulNotification : INotification
{
IDictionary<string, object> State { get; set; }
}
}

View File

@@ -0,0 +1,17 @@
using System.Collections.Generic;
namespace Umbraco.Cms.Core.Events
{
public static class NotificationExtensions
{
public static T WithState<T>(this T notification, IDictionary<string, object> state) where T : IStatefulNotification
{
notification.State = state;
return notification;
}
public static T WithStateFrom<T, TSource>(this T notification, TSource source)
where T : IStatefulNotification where TSource : IStatefulNotification
=> notification.WithState(source.State);
}
}

View File

@@ -0,0 +1,71 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Umbraco.Cms.Core.Events
{
public class ScopedNotificationPublisher : IScopedNotificationPublisher
{
private readonly IEventAggregator _eventAggregator;
private readonly List<INotification> _notificationOnScopeCompleted;
public ScopedNotificationPublisher(IEventAggregator eventAggregator)
{
_eventAggregator = eventAggregator;
_notificationOnScopeCompleted = new List<INotification>();
}
public bool PublishCancelable(ICancelableNotification notification)
{
if (notification == null)
{
throw new ArgumentNullException(nameof(notification));
}
_eventAggregator.Publish(notification);
return notification.Cancel;
}
public async Task<bool> PublishCancelableAsync(ICancelableNotification notification)
{
if (notification == null)
{
throw new ArgumentNullException(nameof(notification));
}
await _eventAggregator.PublishAsync(notification);
return notification.Cancel;
}
public void Publish(INotification notification)
{
if (notification == null)
{
throw new ArgumentNullException(nameof(notification));
}
_notificationOnScopeCompleted.Add(notification);
}
public void ScopeExit(bool completed)
{
try
{
if (completed)
{
foreach (var notification in _notificationOnScopeCompleted)
{
_eventAggregator.Publish(notification);
}
}
}
finally
{
_notificationOnScopeCompleted.Clear();
}
}
}
}

View File

@@ -101,6 +101,12 @@ namespace Umbraco.Extensions
/// <returns>True if ClaimsIdentity</returns>
public static bool VerifyBackOfficeIdentity(this ClaimsIdentity identity, out ClaimsIdentity verifiedIdentity)
{
if (identity is null)
{
verifiedIdentity = null;
return false;
}
// Validate that all required claims exist
foreach (var claimType in RequiredBackOfficeClaimTypes)
{
@@ -112,7 +118,7 @@ namespace Umbraco.Extensions
}
}
verifiedIdentity = new ClaimsIdentity(identity.Claims, Constants.Security.BackOfficeAuthenticationType);
verifiedIdentity = identity.AuthenticationType == Constants.Security.BackOfficeAuthenticationType ? identity : new ClaimsIdentity(identity.Claims, Constants.Security.BackOfficeAuthenticationType);
return true;
}

View File

@@ -7,31 +7,53 @@ using System.Linq;
using System.Security.Claims;
using System.Security.Principal;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Security;
namespace Umbraco.Extensions
{
public static class ClaimsPrincipalExtensions
{
public static bool IsBackOfficeAuthenticationType(this ClaimsIdentity claimsIdentity)
{
if (claimsIdentity is null)
{
return false;
}
return claimsIdentity.IsAuthenticated && claimsIdentity.AuthenticationType == Constants.Security.BackOfficeAuthenticationType;
}
/// <summary>
/// This will return the current back office identity if the IPrincipal is the correct type and authenticated.
/// </summary>
/// <param name="user"></param>
/// <param name="principal"></param>
/// <returns></returns>
public static ClaimsIdentity GetUmbracoIdentity(this IPrincipal user)
public static ClaimsIdentity GetUmbracoIdentity(this IPrincipal principal)
{
// Check if the identity is a ClaimsIdentity, and that's it's authenticated and has all required claims.
if (user.Identity is ClaimsIdentity claimsIdentity
&& claimsIdentity.IsAuthenticated
&& claimsIdentity.VerifyBackOfficeIdentity(out ClaimsIdentity umbracoIdentity))
//If it's already a UmbracoBackOfficeIdentity
if (principal.Identity is ClaimsIdentity claimsIdentity
&& claimsIdentity.IsBackOfficeAuthenticationType()
&& claimsIdentity.VerifyBackOfficeIdentity(out var backOfficeIdentity))
{
if (claimsIdentity.AuthenticationType == Constants.Security.BackOfficeAuthenticationType)
{
return claimsIdentity;
}
return umbracoIdentity;
return backOfficeIdentity;
}
//Check if there's more than one identity assigned and see if it's a UmbracoBackOfficeIdentity and use that
// We can have assigned more identities if it is a preview request.
if (principal is ClaimsPrincipal claimsPrincipal )
{
claimsIdentity = claimsPrincipal.Identities.FirstOrDefault(x=>x.IsBackOfficeAuthenticationType());
if (claimsIdentity.VerifyBackOfficeIdentity(out backOfficeIdentity))
{
return backOfficeIdentity;
}
}
//Otherwise convert to a UmbracoBackOfficeIdentity if it's auth'd
if (principal.Identity is ClaimsIdentity claimsIdentity2
&& claimsIdentity2.VerifyBackOfficeIdentity(out backOfficeIdentity))
{
return backOfficeIdentity;
}
return null;
}

View File

@@ -1,4 +1,4 @@
// Copyright (c) Umbraco.
// Copyright (c) Umbraco.
// See LICENSE for more details.
using System;
@@ -126,10 +126,6 @@ namespace Umbraco.Cms.Core.Cache
() => MediaService.TreeChanged -= MediaService_TreeChanged);
// bind to content events
Bind(() => ContentService.Saved += ContentService_Saved, // needed for permissions
() => ContentService.Saved -= ContentService_Saved);
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);
@@ -182,32 +178,11 @@ namespace Umbraco.Cms.Core.Cache
{
}
/// <summary>
/// Handles cache refreshing for when content is saved (not published)
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
/// <remarks>
/// When an entity is saved we need to notify other servers about the change in order for the Examine indexes to
/// stay up-to-date for unpublished content.
/// </remarks>
private void ContentService_Saved(IContentService sender, SaveEventArgs<IContent> e)
{
}
private void ContentService_TreeChanged(IContentService sender, TreeChange<IContent>.EventArgs args)
{
_distributedCache.RefreshContentCache(args.Changes.ToArray());
}
// TODO: our weird events handling wants this for now
private void ContentService_Deleted(IContentService sender, DeleteEventArgs<IContent> e) { }
private void ContentService_Moved(IContentService sender, MoveEventArgs<IContent> e) { }
private void ContentService_Trashed(IContentService sender, MoveEventArgs<IContent> e) { }
private void ContentService_EmptiedRecycleBin(IContentService sender, RecycleBinEventArgs e) { }
private void ContentService_Published(IContentService sender, PublishEventArgs<IContent> e) { }
private void ContentService_Unpublished(IContentService sender, PublishEventArgs<IContent> e) { }
//private void ContentService_SavedBlueprint(IContentService sender, SaveEventArgs<IContent> e)
//{
// _distributedCache.RefreshUnpublishedPageCache(e.SavedEntities.ToArray());
@@ -413,13 +388,6 @@ namespace Umbraco.Cms.Core.Cache
_distributedCache.RefreshMediaCache(args.Changes.ToArray());
}
// TODO: our weird events handling wants this for now
private void MediaService_Saved(IMediaService sender, SaveEventArgs<IMedia> e) { }
private void MediaService_Deleted(IMediaService sender, DeleteEventArgs<IMedia> e) { }
private void MediaService_Moved(IMediaService sender, MoveEventArgs<IMedia> e) { }
private void MediaService_Trashed(IMediaService sender, MoveEventArgs<IMedia> e) { }
private void MediaService_EmptiedRecycleBin(IMediaService sender, RecycleBinEventArgs e) { }
#endregion
#region MemberService

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

@@ -1,4 +1,4 @@
// Copyright (c) Umbraco.
// Copyright (c) Umbraco.
// See LICENSE for more details.
using System;
@@ -13,7 +13,6 @@ 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;
@@ -22,6 +21,9 @@ using Umbraco.Extensions;
namespace Umbraco.Cms.Core.Compose
{
/// <remarks>
/// TODO: this component must be removed entirely - there is some code duplication in <see cref="UserNotificationsHandler"/> in anticipation of this component being deleted
/// </remarks>
public sealed class NotificationsComponent : IComponent
{
private readonly Notifier _notifier;
@@ -37,24 +39,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;
@@ -63,15 +47,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;
}
@@ -82,72 +57,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();
@@ -158,22 +67,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

@@ -1,5 +1,13 @@
using Umbraco.Cms.Core.Composing;
// Copyright (c) Umbraco.
// See LICENSE for more details.
using Umbraco.Cms.Core.Composing;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Core.Routing;
using Umbraco.Cms.Infrastructure.Services.Notifications;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.Compose
@@ -11,6 +19,49 @@ namespace Umbraco.Cms.Core.Compose
base.Compose(builder);
builder.Services.AddUnique<NotificationsComponent.Notifier>();
// add handlers for sending user notifications (i.e. emails)
builder.Services.AddUnique<UserNotificationsHandler.Notifier>();
builder
.AddNotificationHandler<ContentSavedNotification, UserNotificationsHandler>()
.AddNotificationHandler<ContentSortedNotification, UserNotificationsHandler>()
.AddNotificationHandler<ContentPublishedNotification, UserNotificationsHandler>()
.AddNotificationHandler<ContentMovedNotification, UserNotificationsHandler>()
.AddNotificationHandler<ContentMovedToRecycleBinNotification, UserNotificationsHandler>()
.AddNotificationHandler<ContentCopiedNotification, UserNotificationsHandler>()
.AddNotificationHandler<ContentRolledBackNotification, UserNotificationsHandler>()
.AddNotificationHandler<ContentSentToPublishNotification, UserNotificationsHandler>()
.AddNotificationHandler<ContentUnpublishedNotification, UserNotificationsHandler>();
// add handlers for building content relations
builder
.AddNotificationHandler<ContentCopiedNotification, RelateOnCopyNotificationHandler>()
.AddNotificationHandler<ContentMovedNotification, RelateOnTrashNotificationHandler>()
.AddNotificationHandler<ContentMovedToRecycleBinNotification, RelateOnTrashNotificationHandler>()
.AddNotificationHandler<MediaMovedNotification, RelateOnTrashNotificationHandler>()
.AddNotificationHandler<MediaMovedToRecycleBinNotification, RelateOnTrashNotificationHandler>();
// add notification handlers for property editors
builder
.AddNotificationHandler<ContentSavingNotification, BlockEditorPropertyHandler>()
.AddNotificationHandler<ContentCopyingNotification, BlockEditorPropertyHandler>()
.AddNotificationHandler<ContentSavingNotification, NestedContentPropertyHandler>()
.AddNotificationHandler<ContentCopyingNotification, NestedContentPropertyHandler>()
.AddNotificationHandler<ContentCopiedNotification, FileUploadPropertyEditor>()
.AddNotificationHandler<ContentDeletedNotification, FileUploadPropertyEditor>()
.AddNotificationHandler<MediaDeletedNotification, FileUploadPropertyEditor>()
.AddNotificationHandler<MediaSavingNotification, FileUploadPropertyEditor>()
.AddNotificationHandler<ContentCopiedNotification, ImageCropperPropertyEditor>()
.AddNotificationHandler<ContentDeletedNotification, ImageCropperPropertyEditor>()
.AddNotificationHandler<MediaDeletedNotification, ImageCropperPropertyEditor>()
.AddNotificationHandler<MediaSavingNotification, ImageCropperPropertyEditor>();
// add notification handlers for redirect tracking
builder
.AddNotificationHandler<ContentPublishingNotification, RedirectTrackingHandler>()
.AddNotificationHandler<ContentPublishedNotification, RedirectTrackingHandler>()
.AddNotificationHandler<ContentMovingNotification, RedirectTrackingHandler>()
.AddNotificationHandler<ContentMovedNotification, RedirectTrackingHandler>();
}
}
}

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,7 +0,0 @@
using Umbraco.Cms.Core.Composing;
namespace Umbraco.Cms.Core.Compose
{
public sealed class RelateOnTrashComposer : ComponentComposer<RelateOnTrashComponent>, ICoreComposer
{ }
}

View File

@@ -5,12 +5,12 @@ using Microsoft.Extensions.Configuration.Json;
using Microsoft.Extensions.FileProviders;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Umbraco.Cms.Core.Configuration;
namespace Umbraco.Cms.Core.Configuration
{
public class JsonConfigManipulator : IConfigManipulator
{
private static readonly object s_locker = new object();
private readonly IConfiguration _configuration;
public JsonConfigManipulator(IConfiguration configuration)
@@ -162,22 +162,26 @@ namespace Umbraco.Cms.Core.Configuration
token?.Parent?.Remove();
}
private static void SaveJson(JsonConfigurationProvider provider, JObject json)
{
if (provider.Source.FileProvider is PhysicalFileProvider physicalFileProvider)
lock (s_locker)
{
var jsonFilePath = Path.Combine(physicalFileProvider.Root, provider.Source.Path);
if (provider.Source.FileProvider is PhysicalFileProvider physicalFileProvider)
{
var jsonFilePath = Path.Combine(physicalFileProvider.Root, provider.Source.Path);
using (var sw = new StreamWriter(jsonFilePath, false))
using (var jsonTextWriter = new JsonTextWriter(sw)
{
Formatting = Formatting.Indented,
})
{
json?.WriteTo(jsonTextWriter);
using (var sw = new StreamWriter(jsonFilePath, false))
using (var jsonTextWriter = new JsonTextWriter(sw)
{
Formatting = Formatting.Indented,
})
{
json?.WriteTo(jsonTextWriter);
}
}
}
}
private static JObject GetJson(JsonConfigurationProvider provider)

View File

@@ -1,25 +1,25 @@
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core.Composing;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Core.Scoping;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Strings;
using Umbraco.Cms.Infrastructure.Examine;
using Umbraco.Cms.Infrastructure.Search;
using Umbraco.Extensions;
namespace Umbraco.Cms.Infrastructure.Search
namespace Umbraco.Cms.Infrastructure.DependencyInjection
{
/// <summary>
/// Configures and installs Examine.
/// Provides extension methods to the <see cref="IUmbracoBuilder"/> class.
/// </summary>
public sealed class ExamineComposer : ComponentComposer<ExamineComponent>, ICoreComposer
public static partial class UmbracoBuilderExtensions
{
public override void Compose(IUmbracoBuilder builder)
public static IUmbracoBuilder AddExamine(this IUmbracoBuilder builder)
{
base.Compose(builder);
// populators are not a collection: one cannot remove ours, and can only add more
// the container can inject IEnumerable<IIndexPopulator> and get them all
builder.Services.AddSingleton<IIndexPopulator, MemberIndexPopulator>();
@@ -49,6 +49,15 @@ namespace Umbraco.Cms.Infrastructure.Search
builder.Services.AddUnique<IValueSetBuilder<IMedia>, MediaValueSetBuilder>();
builder.Services.AddUnique<IValueSetBuilder<IMember>, MemberValueSetBuilder>();
builder.Services.AddUnique<BackgroundIndexRebuilder>();
builder.AddNotificationHandler<UmbracoApplicationStarting, ExamineNotificationHandler>();
builder.AddNotificationHandler<ContentCacheRefresherNotification, ExamineNotificationHandler>();
builder.AddNotificationHandler<ContentTypeCacheRefresherNotification, ExamineNotificationHandler>();
builder.AddNotificationHandler<MediaCacheRefresherNotification, ExamineNotificationHandler>();
builder.AddNotificationHandler<MemberCacheRefresherNotification, ExamineNotificationHandler>();
builder.AddNotificationHandler<LanguageCacheRefresherNotification, ExamineNotificationHandler>();
return builder;
}
}
}

View File

@@ -0,0 +1,51 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Infrastructure.Services.Notifications;
namespace Umbraco.Cms.Core.Events
{
public class RelateOnCopyNotificationHandler : INotificationHandler<ContentCopiedNotification>
{
private readonly IRelationService _relationService;
private readonly IAuditService _auditService;
public RelateOnCopyNotificationHandler(IRelationService relationService, IAuditService auditService)
{
_relationService = relationService;
_auditService = auditService;
}
public void Handle(ContentCopiedNotification notification)
{
if (notification.RelateToOriginal == false)
{
return;
}
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(notification.Original.Id, notification.Copy.Id, relationType);
_relationService.Save(relation);
_auditService.Add(
AuditType.Copy,
notification.Copy.WriterId,
notification.Copy.Id, ObjectTypes.GetName(UmbracoObjectTypes.Document),
$"Copied content with Id: '{notification.Copy.Id}' related to original content with Id: '{notification.Original.Id}'");
}
}
}

View File

@@ -1,15 +1,21 @@
using System.Linq;
using Umbraco.Cms.Core.Composing;
using Umbraco.Cms.Core.Events;
// Copyright (c) Umbraco.
// See LICENSE for more details.
using System.Linq;
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.Notifications;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.Compose
namespace Umbraco.Cms.Core.Events
{
public sealed class RelateOnTrashComponent : IComponent
// TODO: lots of duplicate code in this one, refactor
public sealed class RelateOnTrashNotificationHandler :
INotificationHandler<ContentMovedNotification>,
INotificationHandler<ContentMovedToRecycleBinNotification>,
INotificationHandler<MediaMovedNotification>,
INotificationHandler<MediaMovedToRecycleBinNotification>
{
private readonly IRelationService _relationService;
private readonly IEntityService _entityService;
@@ -17,7 +23,7 @@ namespace Umbraco.Cms.Core.Compose
private readonly IAuditService _auditService;
private readonly IScopeProvider _scopeProvider;
public RelateOnTrashComponent(
public RelateOnTrashNotificationHandler(
IRelationService relationService,
IEntityService entityService,
ILocalizedTextService textService,
@@ -31,28 +37,11 @@ namespace Umbraco.Cms.Core.Compose
_scopeProvider = scopeProvider;
}
public void Initialize()
public void Handle(ContentMovedNotification 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(Constants.System.RecycleBinContentString)))
{
const string relationTypeAlias = Cms.Core.Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteAlias;
const string relationTypeAlias = Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteAlias;
var relations = _relationService.GetByChildId(item.Entity.Id);
foreach (var relation in relations.Where(x => x.RelationType.Alias.InvariantEquals(relationTypeAlias)))
@@ -62,44 +51,29 @@ 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(ContentMovedToRecycleBinNotification notification)
{
using (var scope = _scopeProvider.CreateScope())
{
const string relationTypeAlias = Cms.Core.Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteAlias;
const string relationTypeAlias = Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteAlias;
var relationType = _relationService.GetRelationTypeByAlias(relationTypeAlias);
// check that the relation-type exists, if not, then recreate it
if (relationType == null)
{
var documentObjectType = Cms.Core.Constants.ObjectTypes.Document;
const string relationTypeName =
Cms.Core.Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteName;
var documentObjectType = Constants.ObjectTypes.Document;
const string relationTypeName = Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteName;
relationType = new RelationType(relationTypeName, relationTypeAlias, false, documentObjectType,
documentObjectType);
relationType = new RelationType(relationTypeName, relationTypeAlias, false, documentObjectType, documentObjectType);
_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
? int.Parse(originalPath[originalPath.Count - 2])
: Cms.Core.Constants.System.Root;
: Constants.System.Root;
//before we can create this relation, we need to ensure that the original parent still exists which
//may not be the case if the encompassing transaction also deleted it when this item was moved to the bin
@@ -107,17 +81,15 @@ namespace Umbraco.Cms.Core.Compose
if (_entityService.Exists(originalParentId))
{
// Add a relation for the item being deleted, so that we can know the original parent for if we need to restore later
var relation = _relationService.GetByParentAndChildId(originalParentId, item.Entity.Id, relationType) ??
new Relation(originalParentId, item.Entity.Id, relationType);
var relation = _relationService.GetByParentAndChildId(originalParentId, item.Entity.Id, relationType) ?? new Relation(originalParentId, item.Entity.Id, relationType);
_relationService.Save(relation);
_auditService.Add(AuditType.Delete,
item.Entity.WriterId,
item.Entity.Id,
ObjectTypes.GetName(UmbracoObjectTypes.Document),
string.Format(_textService.Localize(
"recycleBin/contentTrashed"),
item.Entity.Id, originalParentId));
string.Format(_textService.Localize("recycleBin/contentTrashed"), item.Entity.Id, originalParentId)
);
}
}
@@ -125,51 +97,60 @@ namespace Umbraco.Cms.Core.Compose
}
}
public void MediaService_Trashed(IMediaService sender, MoveEventArgs<IMedia> e)
public void Handle(MediaMovedNotification notification)
{
foreach (var item in notification.MoveInfoCollection.Where(x => x.OriginalPath.Contains(Constants.System.RecycleBinMediaString)))
{
const string relationTypeAlias = 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 Handle(MediaMovedToRecycleBinNotification notification)
{
using (var scope = _scopeProvider.CreateScope())
{
const string relationTypeAlias =
Cms.Core.Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteAlias;
const string relationTypeAlias = Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteAlias;
var relationType = _relationService.GetRelationTypeByAlias(relationTypeAlias);
// check that the relation-type exists, if not, then recreate it
if (relationType == null)
{
var documentObjectType = Cms.Core.Constants.ObjectTypes.Document;
const string relationTypeName =
Cms.Core.Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteName;
relationType = new RelationType(relationTypeName, relationTypeAlias, false, documentObjectType,
documentObjectType);
var documentObjectType = Constants.ObjectTypes.Document;
const string relationTypeName = Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteName;
relationType = new RelationType(relationTypeName, relationTypeAlias, false, documentObjectType, documentObjectType);
_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
? int.Parse(originalPath[originalPath.Count - 2])
: Cms.Core.Constants.System.Root;
: Constants.System.Root;
//before we can create this relation, we need to ensure that the original parent still exists which
//may not be the case if the encompassing transaction also deleted it when this item was moved to the bin
if (_entityService.Exists(originalParentId))
{
// Add a relation for the item being deleted, so that we can know the original parent for if we need to restore later
var relation =
_relationService.GetByParentAndChildId(originalParentId, item.Entity.Id, relationType) ??
new Relation(originalParentId, item.Entity.Id, relationType);
var relation = _relationService.GetByParentAndChildId(originalParentId, item.Entity.Id, relationType) ?? new Relation(originalParentId, item.Entity.Id, relationType);
_relationService.Save(relation);
_auditService.Add(AuditType.Delete,
item.Entity.CreatorId,
item.Entity.Id,
ObjectTypes.GetName(UmbracoObjectTypes.Media),
string.Format(_textService.Localize(
"recycleBin/mediaTrashed"),
item.Entity.Id, originalParentId));
string.Format(_textService.Localize("recycleBin/mediaTrashed"), item.Entity.Id, originalParentId)
);
}
}
scope.Complete();
}
}
}
}

View File

@@ -0,0 +1,215 @@
// 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.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.Notifications;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.Events
{
public sealed class UserNotificationsHandler :
INotificationHandler<ContentSavedNotification>,
INotificationHandler<ContentSortedNotification>,
INotificationHandler<ContentPublishedNotification>,
INotificationHandler<ContentMovedNotification>,
INotificationHandler<ContentMovedToRecycleBinNotification>,
INotificationHandler<ContentCopiedNotification>,
INotificationHandler<ContentRolledBackNotification>,
INotificationHandler<ContentSentToPublishNotification>,
INotificationHandler<ContentUnpublishedNotification>
{
private readonly Notifier _notifier;
private readonly ActionCollection _actions;
private readonly IContentService _contentService;
public UserNotificationsHandler(Notifier notifier, ActionCollection actions, IContentService contentService)
{
_notifier = notifier;
_actions = actions;
_contentService = contentService;
}
public void Handle(ContentSavedNotification 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(ContentSortedNotification 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(ContentPublishedNotification notification) => _notifier.Notify(_actions.GetAction<ActionPublish>(), notification.PublishedEntities.ToArray());
public void Handle(ContentMovedNotification 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(ContentMovedToRecycleBinNotification notification) => _notifier.Notify(_actions.GetAction<ActionDelete>(), notification.MoveInfoCollection.Select(m => m.Entity).ToArray());
public void Handle(ContentCopiedNotification notification) => _notifier.Notify(_actions.GetAction<ActionCopy>(), notification.Original);
public void Handle(ContentRolledBackNotification notification) => _notifier.Notify(_actions.GetAction<ActionRollback>(), notification.Entity);
public void Handle(ContentSentToPublishNotification notification) => _notifier.Notify(_actions.GetAction<ActionToPublish>(), notification.Entity);
public void Handle(ContentUnpublishedNotification 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,5 +1,4 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using Newtonsoft.Json;
@@ -66,8 +65,6 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0
public override void Migrate()
{
Debugger.Launch();
Debugger.Break();
CreateDatabaseTable();
MigrateFileContentToDB();
}

View File

@@ -12,7 +12,10 @@ using Umbraco.Extensions;
namespace Umbraco.Cms.Infrastructure.ModelsBuilder
{
// supports LiveAppData - but not PureLive
public sealed class LiveModelsProvider : INotificationHandler<UmbracoApplicationStarting>, INotificationHandler<UmbracoRequestEnd>
public sealed class LiveModelsProvider : INotificationHandler<UmbracoApplicationStarting>,
INotificationHandler<UmbracoRequestEnd>,
INotificationHandler<ContentTypeCacheRefresherNotification>,
INotificationHandler<DataTypeCacheRefresherNotification>
{
private static int s_req;
private readonly ILogger<LiveModelsProvider> _logger;
@@ -53,16 +56,6 @@ namespace Umbraco.Cms.Infrastructure.ModelsBuilder
{
return;
}
// Must register with maindom in order to function.
// If registration is not successful then events are not bound
// and we also don't generate models.
_mainDom.Register(() =>
{
// anything changes, and we want to re-generate models.
ContentTypeCacheRefresher.CacheUpdated += RequestModelsGeneration;
DataTypeCacheRefresher.CacheUpdated += RequestModelsGeneration;
});
}
// NOTE
@@ -72,8 +65,13 @@ namespace Umbraco.Cms.Infrastructure.ModelsBuilder
// need to be generated. Could be by another request. Anyway. We could
// have collisions but... you know the risk.
private void RequestModelsGeneration(object sender, EventArgs args)
private void RequestModelsGeneration()
{
if (!_mainDom.IsMainDom)
{
return;
}
_logger.LogDebug("Requested to generate models.");
Interlocked.Exchange(ref s_req, 1);
}
@@ -121,5 +119,9 @@ namespace Umbraco.Cms.Infrastructure.ModelsBuilder
GenerateModelsIfRequested();
}
}
public void Handle(ContentTypeCacheRefresherNotification notification) => RequestModelsGeneration();
public void Handle(DataTypeCacheRefresherNotification notification) => RequestModelsGeneration();
}
}

View File

@@ -11,7 +11,8 @@ namespace Umbraco.Cms.Infrastructure.ModelsBuilder
/// <summary>
/// Used to track if ModelsBuilder models are out of date/stale
/// </summary>
public sealed class OutOfDateModelsStatus : INotificationHandler<UmbracoApplicationStarting>
public sealed class OutOfDateModelsStatus : INotificationHandler<ContentTypeCacheRefresherNotification>,
INotificationHandler<DataTypeCacheRefresherNotification>
{
private readonly ModelsBuilderSettings _config;
private readonly IHostingEnvironment _hostingEnvironment;
@@ -47,22 +48,6 @@ namespace Umbraco.Cms.Infrastructure.ModelsBuilder
}
}
/// <summary>
/// Handles the <see cref="UmbracoApplicationStarting"/> notification
/// </summary>
public void Handle(UmbracoApplicationStarting notification) => Install();
private void Install()
{
// don't run if not configured
if (!IsEnabled)
{
return;
}
ContentTypeCacheRefresher.CacheUpdated += (sender, args) => Write();
DataTypeCacheRefresher.CacheUpdated += (sender, args) => Write();
}
private string GetFlagPath()
{
@@ -77,6 +62,12 @@ namespace Umbraco.Cms.Infrastructure.ModelsBuilder
private void Write()
{
// don't run if not configured
if (!IsEnabled)
{
return;
}
var path = GetFlagPath();
if (path == null || File.Exists(path))
{
@@ -101,5 +92,9 @@ namespace Umbraco.Cms.Infrastructure.ModelsBuilder
File.Delete(path);
}
public void Handle(ContentTypeCacheRefresherNotification notification) => Write();
public void Handle(DataTypeCacheRefresherNotification notification) => Write();
}
}

View File

@@ -1,36 +1,30 @@
using System;
// Copyright (c) Umbraco.
// See LICENSE for more details.
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
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,102 +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.Extensions;
namespace Umbraco.Cms.Core.PropertyEditors
{
/// <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,54 @@
// 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.Notifications;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.PropertyEditors
{
public abstract class ComplexPropertyEditorContentNotificationHandler :
INotificationHandler<ContentSavingNotification>,
INotificationHandler<ContentCopyingNotification>
{
protected abstract string EditorAlias { get; }
protected abstract string FormatPropertyValue(string rawJson, bool onlyMissingKeys);
public void Handle(ContentSavingNotification notification)
{
foreach (var entity in notification.SavedEntities)
{
var props = entity.GetPropertiesByEditor(EditorAlias);
UpdatePropertyValues(props, true);
}
}
public void Handle(ContentCopyingNotification 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.Notifications;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.PropertyEditors
@@ -24,7 +25,9 @@ namespace Umbraco.Cms.Core.PropertyEditors
"fileupload",
Group = Constants.PropertyEditors.Groups.Media,
Icon = "icon-download-alt")]
public class FileUploadPropertyEditor : DataEditor, IMediaUrlGenerator
public class FileUploadPropertyEditor : DataEditor, IMediaUrlGenerator,
INotificationHandler<ContentCopiedNotification>, INotificationHandler<ContentDeletedNotification>,
INotificationHandler<MediaDeletedNotification>, INotificationHandler<MediaSavingNotification>
{
private readonly IMediaFileSystem _mediaFileSystem;
private readonly ContentSettings _contentSettings;
@@ -32,6 +35,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 +46,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 +56,7 @@ namespace Umbraco.Cms.Core.PropertyEditors
_localizationService = localizationService;
_localizedTextService = localizedTextService;
_uploadAutoFillProperties = uploadAutoFillProperties;
_contentService = contentService;
}
/// <summary>
@@ -86,16 +92,17 @@ namespace Umbraco.Cms.Core.PropertyEditors
}
/// <summary>
/// Ensures any files associated are removed
/// The paths to all file upload property files contained within a collection of content entities
/// </summary>
/// <param name="deletedEntities"></param>
internal IEnumerable<string> ServiceDeleted(IEnumerable<ContentBase> deletedEntities)
{
return deletedEntities.SelectMany(x => x.Properties)
.Where(IsUploadField)
.SelectMany(GetFilePathsFromPropertyValues)
.Distinct();
}
/// <param name="entities"></param>
/// <remarks>
/// This method must be made private once MemberService events have been replaced by notifications
/// </remarks>
internal IEnumerable<string> ContainedFilePaths(IEnumerable<IContentBase> entities) => entities
.SelectMany(x => x.Properties)
.Where(IsUploadField)
.SelectMany(GetFilePathsFromPropertyValues)
.Distinct();
/// <summary>
/// Look through all property values stored against the property and resolve any file paths stored
@@ -119,15 +126,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>
internal void ContentServiceCopied(IContentService sender, CopyEventArgs<IContent> args)
public void Handle(ContentCopiedNotification notification)
{
// get the upload field properties with a value
var properties = args.Original.Properties.Where(IsUploadField);
var properties = notification.Original.Properties.Where(IsUploadField);
// copy files
var isUpdated = false;
@@ -137,49 +139,41 @@ namespace Umbraco.Cms.Core.PropertyEditors
foreach (var propertyValue in property.Values)
{
var propVal = property.GetValue(propertyValue.Culture, propertyValue.Segment);
if (propVal == null || !(propVal is string str) || str.IsNullOrWhiteSpace()) continue;
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);
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)
sender.Save(args.Copy);
{
_contentService.Save(notification.Copy);
}
}
/// <summary>
/// After a media has been created, auto-fill the properties.
/// </summary>
/// <param name="sender">The event sender.</param>
/// <param name="args">The event arguments.</param>
internal void MediaServiceCreated(IMediaService sender, NewEventArgs<IMedia> args)
public void Handle(ContentDeletedNotification notification) => DeleteContainedFiles(notification.DeletedEntities);
public void Handle(MediaDeletedNotification notification) => DeleteContainedFiles(notification.DeletedEntities);
private void DeleteContainedFiles(IEnumerable<IContentBase> deletedEntities)
{
AutoFillProperties(args.Entity);
var filePathsToDelete = ContainedFilePaths(deletedEntities);
_mediaFileSystem.DeleteMediaFiles(filePathsToDelete);
}
/// <summary>
/// After a media has been saved, auto-fill the properties.
/// </summary>
/// <param name="sender">The event sender.</param>
/// <param name="args">The event arguments.</param>
public void MediaServiceSaving(IMediaService sender, SaveEventArgs<IMedia> args)
public void Handle(MediaSavingNotification notification)
{
foreach (var entity in args.SavedEntities)
AutoFillProperties(entity);
}
/// <summary>
/// After a content item has been saved, auto-fill the properties.
/// </summary>
/// <param name="sender">The event sender.</param>
/// <param name="args">The event arguments.</param>
public void ContentServiceSaving(IContentService sender, SaveEventArgs<IContent> args)
{
foreach (var entity in args.SavedEntities)
foreach (var entity in notification.SavedEntities)
{
AutoFillProperties(entity);
}
}
/// <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.Notifications;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.PropertyEditors
@@ -31,7 +32,9 @@ namespace Umbraco.Cms.Core.PropertyEditors
HideLabel = false,
Group = Constants.PropertyEditors.Groups.Media,
Icon = "icon-crop")]
public class ImageCropperPropertyEditor : DataEditor, IMediaUrlGenerator
public class ImageCropperPropertyEditor : DataEditor, IMediaUrlGenerator,
INotificationHandler<ContentCopiedNotification>, INotificationHandler<ContentDeletedNotification>,
INotificationHandler<MediaDeletedNotification>, INotificationHandler<MediaSavingNotification>
{
private readonly IMediaFileSystem _mediaFileSystem;
private readonly ContentSettings _contentSettings;
@@ -39,6 +42,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 +57,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 +66,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>();
}
@@ -122,16 +128,17 @@ namespace Umbraco.Cms.Core.PropertyEditors
}
/// <summary>
/// Ensures any files associated are removed
/// The paths to all image cropper property files contained within a collection of content entities
/// </summary>
/// <param name="deletedEntities"></param>
internal IEnumerable<string> ServiceDeleted(IEnumerable<ContentBase> deletedEntities)
{
return deletedEntities.SelectMany(x => x.Properties)
.Where(IsCropperField)
.SelectMany(GetFilePathsFromPropertyValues)
.Distinct();
}
/// <param name="entities"></param>
/// <remarks>
/// This method must be made private once MemberService events have been replaced by notifications
/// </remarks>
internal IEnumerable<string> ContainedFilePaths(IEnumerable<IContentBase> entities) => entities
.SelectMany(x => x.Properties)
.Where(IsCropperField)
.SelectMany(GetFilePathsFromPropertyValues)
.Distinct();
/// <summary>
/// Look through all property values stored against the property and resolve any file paths stored
@@ -175,12 +182,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(ContentCopiedNotification 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,49 +196,40 @@ 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);
}
}
/// <summary>
/// After a media has been created, auto-fill the properties.
/// </summary>
/// <param name="sender">The event sender.</param>
/// <param name="args">The event arguments.</param>
public void MediaServiceCreated(IMediaService sender, NewEventArgs<IMedia> args)
public void Handle(ContentDeletedNotification notification) => DeleteContainedFiles(notification.DeletedEntities);
public void Handle(MediaDeletedNotification notification) => DeleteContainedFiles(notification.DeletedEntities);
private void DeleteContainedFiles(IEnumerable<IContentBase> deletedEntities)
{
AutoFillProperties(args.Entity);
var filePathsToDelete = ContainedFilePaths(deletedEntities);
_mediaFileSystem.DeleteMediaFiles(filePathsToDelete);
}
/// <summary>
/// After a media has been saved, auto-fill the properties.
/// </summary>
/// <param name="sender">The event sender.</param>
/// <param name="args">The event arguments.</param>
public void MediaServiceSaving(IMediaService sender, SaveEventArgs<IMedia> args)
public void Handle(MediaSavingNotification notification)
{
foreach (var entity in args.SavedEntities)
AutoFillProperties(entity);
}
/// <summary>
/// After a content item has been saved, auto-fill the properties.
/// </summary>
/// <param name="sender">The event sender.</param>
/// <param name="args">The event arguments.</param>
public void ContentServiceSaving(IContentService sender, SaveEventArgs<IContent> args)
{
foreach (var entity in args.SavedEntities)
foreach (var entity in notification.SavedEntities)
{
AutoFillProperties(entity);
}
}
/// <summary>

View File

@@ -1,29 +1,21 @@
using System;
// Copyright (c) Umbraco.
// See LICENSE for more details.
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
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 +70,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;
@@ -12,6 +12,7 @@ using Umbraco.Cms.Core.Services.Implement;
namespace Umbraco.Cms.Core.PropertyEditors
{
// TODO: delete this component and make the "ContainedFilePaths" methods on FileUploadPropertyEditor and ImageCropperPropertyEditor private once MemberService uses notifications instead of static events
public sealed class PropertyEditorsComponent : IComponent
{
private readonly PropertyEditorCollection _propertyEditors;
@@ -40,40 +41,14 @@ namespace Umbraco.Cms.Core.PropertyEditors
private void Initialize(FileUploadPropertyEditor fileUpload)
{
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>()));
void memberServiceDeleted(IMemberService sender, DeleteEventArgs<IMember> args) => args.MediaFilesToDelete.AddRange(fileUpload.ContainedFilePaths(args.DeletedEntities.Cast<ContentBase>()));
MemberService.Deleted += memberServiceDeleted;
_terminate.Add(() => MemberService.Deleted -= memberServiceDeleted);
}
private void Initialize(ImageCropperPropertyEditor imageCropper)
{
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>()));
void memberServiceDeleted(IMemberService sender, DeleteEventArgs<IMember> args) => args.MediaFilesToDelete.AddRange(imageCropper.ContainedFilePaths(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,42 @@
using System;
// Copyright (c) Umbraco.
// See LICENSE for more details.
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.Notifications;
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
public sealed class RedirectTrackingHandler :
INotificationHandler<ContentPublishingNotification>,
INotificationHandler<ContentPublishedNotification>,
INotificationHandler<ContentMovingNotification>,
INotificationHandler<ContentMovedNotification>
{
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)
private const string NotificationStateKey = "Umbraco.Cms.Core.Routing.RedirectTrackingHandler";
public RedirectTrackingHandler(IOptionsMonitor<WebRoutingSettings> webRoutingSettings, IPublishedSnapshotAccessor publishedSnapshotAccessor, IRedirectUrlService redirectUrlService, IVariationContextAccessor variationContextAccessor)
{
_webRoutingSettings = webRoutingSettings;
_publishedSnapshotAccessor = publishedSnapshotAccessor;
@@ -38,86 +44,53 @@ namespace Umbraco.Cms.Core.Routing
_variationContextAccessor = variationContextAccessor;
}
public void Initialize()
public void Handle(ContentPublishingNotification notification) => StoreOldRoutes(notification.PublishedEntities, notification);
public void Handle(ContentPublishedNotification notification) => CreateRedirectsForOldRoutes(notification);
public void Handle(ContentMovingNotification notification) => StoreOldRoutes(notification.MoveInfoCollection.Select(m => m.Entity), notification);
public void Handle(ContentMovedNotification notification) => CreateRedirectsForOldRoutes(notification);
private void StoreOldRoutes(IEnumerable<IContent> entities, IStatefulNotification 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(notification);
foreach (var entity in entities)
{
StoreOldRoute(entity, oldRoutes);
}
}
private void ContentService_Published(IContentService sender, ContentPublishedEventArgs args)
private void CreateRedirectsForOldRoutes(IStatefulNotification 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(notification);
CreateRedirects(oldRoutes);
}
private void ContentService_Moving(IContentService sender, MoveEventArgs<IContent> args)
private OldRoutesDictionary GetOldRoutes(IStatefulNotification notification)
{
// 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 info in args.MoveInfoCollection)
if (notification.State.ContainsKey(NotificationStateKey) == false)
{
StoreOldRoute(info.Entity, oldRoutes);
}
}
private void ContentService_Moved(IContentService sender, MoveEventArgs<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);
CreateRedirects(oldRoutes);
}
private OldRoutesDictionary GetOldRoutes(IDictionary<string, object> eventState)
{
if (! eventState.ContainsKey(_eventStateKey))
{
eventState[_eventStateKey] = new OldRoutesDictionary();
notification.State[NotificationStateKey] = new OldRoutesDictionary();
}
return eventState[_eventStateKey] as OldRoutesDictionary;
return (OldRoutesDictionary)notification.State[NotificationStateKey];
}
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 +103,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 +117,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 +151,8 @@ namespace Umbraco.Cms.Core.Routing
}
private class OldRoutesDictionary : Dictionary<ContentIdAndCulture, ContentKeyAndOldRoute>
{ }
{
}
}
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Infrastructure.Persistence;
@@ -30,6 +30,11 @@ namespace Umbraco.Cms.Core.Scoping
/// </summary>
IEventDispatcher Events { get; }
/// <summary>
/// Gets the scope notification publisher
/// </summary>
IScopedNotificationPublisher Notifications { get; }
/// <summary>
/// Gets the repositories cache mode.
/// </summary>

View File

@@ -20,6 +20,7 @@ namespace Umbraco.Cms.Core.Scoping
private readonly ScopeProvider _scopeProvider;
private readonly CoreDebugSettings _coreDebugSettings;
private readonly IMediaFileSystem _mediaFileSystem;
private readonly IEventAggregator _eventAggregator;
private readonly ILogger<Scope> _logger;
private readonly IsolationLevel _isolationLevel;
@@ -36,12 +37,15 @@ namespace Umbraco.Cms.Core.Scoping
private EventMessages _messages;
private ICompletable _fscope;
private IEventDispatcher _eventDispatcher;
// eventually this may need to be injectable - for now we'll create it explicitly and let future needs determine if it should be injectable
private IScopedNotificationPublisher _notificationPublisher;
// initializes a new scope
private Scope(
ScopeProvider scopeProvider,
CoreDebugSettings coreDebugSettings,
IMediaFileSystem mediaFileSystem,
IEventAggregator eventAggregator,
ILogger<Scope> logger,
FileSystems fileSystems,
Scope parent,
@@ -57,6 +61,7 @@ namespace Umbraco.Cms.Core.Scoping
_scopeProvider = scopeProvider;
_coreDebugSettings = coreDebugSettings;
_mediaFileSystem = mediaFileSystem;
_eventAggregator = eventAggregator;
_logger = logger;
Context = scopeContext;
@@ -70,6 +75,7 @@ namespace Umbraco.Cms.Core.Scoping
Detachable = detachable;
#if DEBUG_SCOPES
_scopeProvider.RegisterScope(this);
#endif
@@ -146,6 +152,7 @@ namespace Umbraco.Cms.Core.Scoping
ScopeProvider scopeProvider,
CoreDebugSettings coreDebugSettings,
IMediaFileSystem mediaFileSystem,
IEventAggregator eventAggregator,
ILogger<Scope> logger,
FileSystems fileSystems,
bool detachable,
@@ -156,7 +163,7 @@ namespace Umbraco.Cms.Core.Scoping
bool? scopeFileSystems = null,
bool callContext = false,
bool autoComplete = false)
: this(scopeProvider, coreDebugSettings, mediaFileSystem, logger, fileSystems, null, scopeContext, detachable, isolationLevel, repositoryCacheMode, eventDispatcher, scopeFileSystems, callContext, autoComplete)
: this(scopeProvider, coreDebugSettings, mediaFileSystem, eventAggregator, logger, fileSystems, null, scopeContext, detachable, isolationLevel, repositoryCacheMode, eventDispatcher, scopeFileSystems, callContext, autoComplete)
{ }
// initializes a new scope in a nested scopes chain, with its parent
@@ -164,6 +171,7 @@ namespace Umbraco.Cms.Core.Scoping
ScopeProvider scopeProvider,
CoreDebugSettings coreDebugSettings,
IMediaFileSystem mediaFileSystem,
IEventAggregator eventAggregator,
ILogger<Scope> logger,
FileSystems fileSystems,
Scope parent,
@@ -173,7 +181,7 @@ namespace Umbraco.Cms.Core.Scoping
bool? scopeFileSystems = null,
bool callContext = false,
bool autoComplete = false)
: this(scopeProvider, coreDebugSettings, mediaFileSystem, logger, fileSystems, parent, null, false, isolationLevel, repositoryCacheMode, eventDispatcher, scopeFileSystems, callContext, autoComplete)
: this(scopeProvider, coreDebugSettings, mediaFileSystem, eventAggregator, logger, fileSystems, parent, null, false, isolationLevel, repositoryCacheMode, eventDispatcher, scopeFileSystems, callContext, autoComplete)
{ }
public Guid InstanceId { get; } = Guid.NewGuid();
@@ -381,6 +389,16 @@ namespace Umbraco.Cms.Core.Scoping
}
}
public IScopedNotificationPublisher Notifications
{
get
{
EnsureNotDisposed();
if (ParentScope != null) return ParentScope.Notifications;
return _notificationPublisher ?? (_notificationPublisher = new ScopedNotificationPublisher(_eventAggregator));
}
}
/// <inheritdoc />
public bool Complete()
{
@@ -556,6 +574,7 @@ namespace Umbraco.Cms.Core.Scoping
if (onException == false)
{
_eventDispatcher?.ScopeExit(completed);
_notificationPublisher?.ScopeExit(completed);
}
}, () =>
{

View File

@@ -34,8 +34,9 @@ namespace Umbraco.Cms.Core.Scoping
private static readonly AsyncLocal<ConcurrentStack<IScopeContext>> s_scopeContextStack = new AsyncLocal<ConcurrentStack<IScopeContext>>();
private static readonly string s_scopeItemKey = typeof(Scope).FullName;
private static readonly string s_contextItemKey = typeof(ScopeProvider).FullName;
private readonly IEventAggregator _eventAggregator;
public ScopeProvider(IUmbracoDatabaseFactory databaseFactory, FileSystems fileSystems, IOptions<CoreDebugSettings> coreDebugSettings, IMediaFileSystem mediaFileSystem, ILogger<ScopeProvider> logger, ILoggerFactory loggerFactory, IRequestCache requestCache)
public ScopeProvider(IUmbracoDatabaseFactory databaseFactory, FileSystems fileSystems, IOptions<CoreDebugSettings> coreDebugSettings, IMediaFileSystem mediaFileSystem, ILogger<ScopeProvider> logger, ILoggerFactory loggerFactory, IRequestCache requestCache, IEventAggregator eventAggregator)
{
DatabaseFactory = databaseFactory;
_fileSystems = fileSystems;
@@ -44,6 +45,7 @@ namespace Umbraco.Cms.Core.Scoping
_logger = logger;
_loggerFactory = loggerFactory;
_requestCache = requestCache;
_eventAggregator = eventAggregator;
// take control of the FileSystems
_fileSystems.IsScoped = () => AmbientScope != null && AmbientScope.ScopedFileSystems;
}
@@ -379,7 +381,7 @@ namespace Umbraco.Cms.Core.Scoping
RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified,
IEventDispatcher eventDispatcher = null,
bool? scopeFileSystems = null)
=> new Scope(this, _coreDebugSettings, _mediaFileSystem, _loggerFactory.CreateLogger<Scope>(), _fileSystems, true, null, isolationLevel, repositoryCacheMode, eventDispatcher, scopeFileSystems);
=> new Scope(this, _coreDebugSettings, _mediaFileSystem, _eventAggregator, _loggerFactory.CreateLogger<Scope>(), _fileSystems, true, null, isolationLevel, repositoryCacheMode, eventDispatcher, scopeFileSystems);
/// <inheritdoc />
public void AttachScope(IScope other, bool callContext = false)
@@ -458,7 +460,7 @@ namespace Umbraco.Cms.Core.Scoping
{
IScopeContext ambientContext = AmbientContext;
ScopeContext newContext = ambientContext == null ? new ScopeContext() : null;
var scope = new Scope(this, _coreDebugSettings, _mediaFileSystem, _loggerFactory.CreateLogger<Scope>(), _fileSystems, false, newContext, isolationLevel, repositoryCacheMode, eventDispatcher, scopeFileSystems, callContext, autoComplete);
var scope = new Scope(this, _coreDebugSettings, _mediaFileSystem, _eventAggregator, _loggerFactory.CreateLogger<Scope>(), _fileSystems, false, newContext, isolationLevel, repositoryCacheMode, eventDispatcher, scopeFileSystems, callContext, autoComplete);
// assign only if scope creation did not throw!
PushAmbientScope(scope);
if (newContext != null)
@@ -468,7 +470,7 @@ namespace Umbraco.Cms.Core.Scoping
return scope;
}
var nested = new Scope(this, _coreDebugSettings, _mediaFileSystem, _loggerFactory.CreateLogger<Scope>(), _fileSystems, ambientScope, isolationLevel, repositoryCacheMode, eventDispatcher, scopeFileSystems, callContext, autoComplete);
var nested = new Scope(this, _coreDebugSettings, _mediaFileSystem, _eventAggregator, _loggerFactory.CreateLogger<Scope>(), _fileSystems, ambientScope, isolationLevel, repositoryCacheMode, eventDispatcher, scopeFileSystems, callContext, autoComplete);
PushAmbientScope(nested);
return nested;
}

View File

@@ -7,7 +7,7 @@ using Examine;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Composing;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Logging;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Runtime;
@@ -20,7 +20,13 @@ using Umbraco.Extensions;
namespace Umbraco.Cms.Infrastructure.Search
{
public sealed class ExamineComponent : IComponent
public sealed class ExamineNotificationHandler :
INotificationHandler<UmbracoApplicationStarting>,
INotificationHandler<ContentCacheRefresherNotification>,
INotificationHandler<ContentTypeCacheRefresherNotification>,
INotificationHandler<MediaCacheRefresherNotification>,
INotificationHandler<MemberCacheRefresherNotification>,
INotificationHandler<LanguageCacheRefresherNotification>
{
private readonly IExamineManager _examineManager;
private readonly IContentValueSetBuilder _contentValueSetBuilder;
@@ -33,18 +39,19 @@ namespace Umbraco.Cms.Infrastructure.Search
private readonly ServiceContext _services;
private readonly IMainDom _mainDom;
private readonly IProfilingLogger _profilingLogger;
private readonly ILogger<ExamineComponent> _logger;
private readonly ILogger<ExamineNotificationHandler> _logger;
private readonly IUmbracoIndexesCreator _indexCreator;
private static bool s_deactivate_handlers;
// the default enlist priority is 100
// enlist with a lower priority to ensure that anything "default" runs after us
// but greater that SafeXmlReaderWriter priority which is 60
private const int EnlistPriority = 80;
public ExamineComponent(IMainDom mainDom,
public ExamineNotificationHandler(IMainDom mainDom,
IExamineManager examineManager,
IProfilingLogger profilingLogger,
ILoggerFactory loggerFactory,
ILogger<ExamineNotificationHandler> logger,
IScopeProvider scopeProvider,
IUmbracoIndexesCreator indexCreator,
ServiceContext services,
@@ -66,16 +73,15 @@ namespace Umbraco.Cms.Infrastructure.Search
_taskHelper = taskHelper;
_mainDom = mainDom;
_profilingLogger = profilingLogger;
_logger = loggerFactory.CreateLogger<ExamineComponent>();
_logger = logger;
_indexCreator = indexCreator;
}
public void Initialize()
public void Handle(UmbracoApplicationStarting notification)
{
//let's deal with shutting down Examine with MainDom
var examineShutdownRegistered = _mainDom.Register(release: () =>
{
using (_profilingLogger.TraceDuration<ExamineComponent>("Examine shutting down"))
using (_profilingLogger.TraceDuration<ExamineNotificationHandler>("Examine shutting down"))
{
_examineManager.Dispose();
}
@@ -105,26 +111,12 @@ namespace Umbraco.Cms.Infrastructure.Search
// don't bind event handlers if we're not suppose to listen
if (registeredIndexers == 0)
{
return;
s_deactivate_handlers = true;
}
// bind to distributed cache events - this ensures that this logic occurs on ALL servers
// that are taking part in a load balanced environment.
ContentCacheRefresher.CacheUpdated += ContentCacheRefresherUpdated;
ContentTypeCacheRefresher.CacheUpdated += ContentTypeCacheRefresherUpdated;
MediaCacheRefresher.CacheUpdated += MediaCacheRefresherUpdated;
MemberCacheRefresher.CacheUpdated += MemberCacheRefresherUpdated;
LanguageCacheRefresher.CacheUpdated += LanguageCacheRefresherUpdated;
}
public void Terminate()
{
ContentCacheRefresher.CacheUpdated -= ContentCacheRefresherUpdated;
ContentTypeCacheRefresher.CacheUpdated -= ContentTypeCacheRefresherUpdated;
MediaCacheRefresher.CacheUpdated -= MediaCacheRefresherUpdated;
MemberCacheRefresher.CacheUpdated -= MemberCacheRefresherUpdated;
LanguageCacheRefresher.CacheUpdated -= LanguageCacheRefresherUpdated;
}
#region Cache refresher updated event handlers
@@ -133,8 +125,12 @@ namespace Umbraco.Cms.Infrastructure.Search
/// </summary>
/// <param name="sender"></param>
/// <param name="args"></param>
private void ContentCacheRefresherUpdated(ContentCacheRefresher sender, CacheRefresherEventArgs args)
public void Handle(ContentCacheRefresherNotification args)
{
if (s_deactivate_handlers)
{
return;
}
if (Suspendable.ExamineEvents.CanIndex == false)
{
return;
@@ -237,8 +233,13 @@ namespace Umbraco.Cms.Infrastructure.Search
}
}
private void MemberCacheRefresherUpdated(MemberCacheRefresher sender, CacheRefresherEventArgs args)
public void Handle(MemberCacheRefresherNotification args)
{
if (s_deactivate_handlers)
{
return;
}
if (Suspendable.ExamineEvents.CanIndex == false)
{
return;
@@ -300,8 +301,13 @@ namespace Umbraco.Cms.Infrastructure.Search
}
}
private void MediaCacheRefresherUpdated(MediaCacheRefresher sender, CacheRefresherEventArgs args)
public void Handle(MediaCacheRefresherNotification args)
{
if (s_deactivate_handlers)
{
return;
}
if (Suspendable.ExamineEvents.CanIndex == false)
{
return;
@@ -364,9 +370,14 @@ namespace Umbraco.Cms.Infrastructure.Search
}
}
private void LanguageCacheRefresherUpdated(LanguageCacheRefresher sender, CacheRefresherEventArgs e)
public void Handle(LanguageCacheRefresherNotification args)
{
if (!(e.MessageObject is LanguageCacheRefresher.JsonPayload[] payloads))
if (s_deactivate_handlers)
{
return;
}
if (!(args.MessageObject is LanguageCacheRefresher.JsonPayload[] payloads))
{
return;
}
@@ -393,8 +404,13 @@ namespace Umbraco.Cms.Infrastructure.Search
/// </summary>
/// <param name="sender"></param>
/// <param name="args"></param>
private void ContentTypeCacheRefresherUpdated(ContentTypeCacheRefresher sender, CacheRefresherEventArgs args)
public void Handle(ContentTypeCacheRefresherNotification args)
{
if (s_deactivate_handlers)
{
return;
}
if (Suspendable.ExamineEvents.CanIndex == false)
{
return;
@@ -668,34 +684,34 @@ namespace Umbraco.Cms.Infrastructure.Search
private class DeferedReIndexForContent : DeferedAction
{
private readonly TaskHelper _taskHelper;
private readonly ExamineComponent _examineComponent;
private readonly ExamineNotificationHandler _ExamineNotificationHandler;
private readonly IContent _content;
private readonly bool _isPublished;
public DeferedReIndexForContent(TaskHelper taskHelper, ExamineComponent examineComponent, IContent content, bool isPublished)
public DeferedReIndexForContent(TaskHelper taskHelper, ExamineNotificationHandler ExamineNotificationHandler, IContent content, bool isPublished)
{
_taskHelper = taskHelper;
_examineComponent = examineComponent;
_ExamineNotificationHandler = ExamineNotificationHandler;
_content = content;
_isPublished = isPublished;
}
public override void Execute() => Execute(_taskHelper, _examineComponent, _content, _isPublished);
public override void Execute() => Execute(_taskHelper, _ExamineNotificationHandler, _content, _isPublished);
public static void Execute(TaskHelper taskHelper, ExamineComponent examineComponent, IContent content, bool isPublished)
public static void Execute(TaskHelper taskHelper, ExamineNotificationHandler ExamineNotificationHandler, IContent content, bool isPublished)
=> taskHelper.RunBackgroundTask(() =>
{
using IScope scope = examineComponent._scopeProvider.CreateScope(autoComplete: true);
using IScope scope = ExamineNotificationHandler._scopeProvider.CreateScope(autoComplete: true);
// for content we have a different builder for published vs unpublished
// we don't want to build more value sets than is needed so we'll lazily build 2 one for published one for non-published
var builders = new Dictionary<bool, Lazy<List<ValueSet>>>
{
[true] = new Lazy<List<ValueSet>>(() => examineComponent._publishedContentValueSetBuilder.GetValueSets(content).ToList()),
[false] = new Lazy<List<ValueSet>>(() => examineComponent._contentValueSetBuilder.GetValueSets(content).ToList())
[true] = new Lazy<List<ValueSet>>(() => ExamineNotificationHandler._publishedContentValueSetBuilder.GetValueSets(content).ToList()),
[false] = new Lazy<List<ValueSet>>(() => ExamineNotificationHandler._contentValueSetBuilder.GetValueSets(content).ToList())
};
foreach (IUmbracoIndex index in examineComponent._examineManager.Indexes.OfType<IUmbracoIndex>()
foreach (IUmbracoIndex index in ExamineNotificationHandler._examineManager.Indexes.OfType<IUmbracoIndex>()
//filter the indexers
.Where(x => isPublished || !x.PublishedValuesOnly)
.Where(x => x.EnableDefaultEventHandler))
@@ -714,29 +730,29 @@ namespace Umbraco.Cms.Infrastructure.Search
private class DeferedReIndexForMedia : DeferedAction
{
private readonly TaskHelper _taskHelper;
private readonly ExamineComponent _examineComponent;
private readonly ExamineNotificationHandler _ExamineNotificationHandler;
private readonly IMedia _media;
private readonly bool _isPublished;
public DeferedReIndexForMedia(TaskHelper taskHelper, ExamineComponent examineComponent, IMedia media, bool isPublished)
public DeferedReIndexForMedia(TaskHelper taskHelper, ExamineNotificationHandler ExamineNotificationHandler, IMedia media, bool isPublished)
{
_taskHelper = taskHelper;
_examineComponent = examineComponent;
_ExamineNotificationHandler = ExamineNotificationHandler;
_media = media;
_isPublished = isPublished;
}
public override void Execute() => Execute(_taskHelper, _examineComponent, _media, _isPublished);
public override void Execute() => Execute(_taskHelper, _ExamineNotificationHandler, _media, _isPublished);
public static void Execute(TaskHelper taskHelper, ExamineComponent examineComponent, IMedia media, bool isPublished) =>
public static void Execute(TaskHelper taskHelper, ExamineNotificationHandler ExamineNotificationHandler, IMedia media, bool isPublished) =>
// perform the ValueSet lookup on a background thread
taskHelper.RunBackgroundTask(() =>
{
using IScope scope = examineComponent._scopeProvider.CreateScope(autoComplete: true);
using IScope scope = ExamineNotificationHandler._scopeProvider.CreateScope(autoComplete: true);
var valueSet = examineComponent._mediaValueSetBuilder.GetValueSets(media).ToList();
var valueSet = ExamineNotificationHandler._mediaValueSetBuilder.GetValueSets(media).ToList();
foreach (IUmbracoIndex index in examineComponent._examineManager.Indexes.OfType<IUmbracoIndex>()
foreach (IUmbracoIndex index in ExamineNotificationHandler._examineManager.Indexes.OfType<IUmbracoIndex>()
//filter the indexers
.Where(x => isPublished || !x.PublishedValuesOnly)
.Where(x => x.EnableDefaultEventHandler))
@@ -753,27 +769,27 @@ namespace Umbraco.Cms.Infrastructure.Search
/// </summary>
private class DeferedReIndexForMember : DeferedAction
{
private readonly ExamineComponent _examineComponent;
private readonly ExamineNotificationHandler _ExamineNotificationHandler;
private readonly IMember _member;
private readonly TaskHelper _taskHelper;
public DeferedReIndexForMember(TaskHelper taskHelper, ExamineComponent examineComponent, IMember member)
public DeferedReIndexForMember(TaskHelper taskHelper, ExamineNotificationHandler ExamineNotificationHandler, IMember member)
{
_examineComponent = examineComponent;
_ExamineNotificationHandler = ExamineNotificationHandler;
_member = member;
_taskHelper = taskHelper;
}
public override void Execute() => Execute(_taskHelper, _examineComponent, _member);
public override void Execute() => Execute(_taskHelper, _ExamineNotificationHandler, _member);
public static void Execute(TaskHelper taskHelper, ExamineComponent examineComponent, IMember member) =>
public static void Execute(TaskHelper taskHelper, ExamineNotificationHandler ExamineNotificationHandler, IMember member) =>
// perform the ValueSet lookup on a background thread
taskHelper.RunBackgroundTask(() =>
{
using IScope scope = examineComponent._scopeProvider.CreateScope(autoComplete: true);
using IScope scope = ExamineNotificationHandler._scopeProvider.CreateScope(autoComplete: true);
var valueSet = examineComponent._memberValueSetBuilder.GetValueSets(member).ToList();
foreach (IUmbracoIndex index in examineComponent._examineManager.Indexes.OfType<IUmbracoIndex>()
var valueSet = ExamineNotificationHandler._memberValueSetBuilder.GetValueSets(member).ToList();
foreach (IUmbracoIndex index in ExamineNotificationHandler._examineManager.Indexes.OfType<IUmbracoIndex>()
//filter the indexers
.Where(x => x.EnableDefaultEventHandler))
{
@@ -786,23 +802,23 @@ namespace Umbraco.Cms.Infrastructure.Search
private class DeferedDeleteIndex : DeferedAction
{
private readonly ExamineComponent _examineComponent;
private readonly ExamineNotificationHandler _ExamineNotificationHandler;
private readonly int _id;
private readonly bool _keepIfUnpublished;
public DeferedDeleteIndex(ExamineComponent examineComponent, int id, bool keepIfUnpublished)
public DeferedDeleteIndex(ExamineNotificationHandler ExamineNotificationHandler, int id, bool keepIfUnpublished)
{
_examineComponent = examineComponent;
_ExamineNotificationHandler = ExamineNotificationHandler;
_id = id;
_keepIfUnpublished = keepIfUnpublished;
}
public override void Execute() => Execute(_examineComponent, _id, _keepIfUnpublished);
public override void Execute() => Execute(_ExamineNotificationHandler, _id, _keepIfUnpublished);
public static void Execute(ExamineComponent examineComponent, int id, bool keepIfUnpublished)
public static void Execute(ExamineNotificationHandler ExamineNotificationHandler, int id, bool keepIfUnpublished)
{
var strId = id.ToString(CultureInfo.InvariantCulture);
foreach (var index in examineComponent._examineManager.Indexes.OfType<IUmbracoIndex>()
foreach (var index in ExamineNotificationHandler._examineManager.Indexes.OfType<IUmbracoIndex>()
.Where(x => x.PublishedValuesOnly || !keepIfUnpublished)
.Where(x => x.EnableDefaultEventHandler))
{
@@ -811,7 +827,5 @@ namespace Umbraco.Cms.Infrastructure.Search
}
}
#endregion
}
}

View File

@@ -1,3 +1,6 @@
using System.Security.Principal;
using Umbraco.Cms.Infrastructure.Security;
namespace Umbraco.Cms.Core.Security
{
/// <summary>
@@ -5,5 +8,8 @@ namespace Umbraco.Cms.Core.Security
/// </summary>
public interface IBackOfficeUserManager : IUmbracoUserManager<BackOfficeIdentityUser>
{
void NotifyForgotPasswordRequested(IPrincipal currentUser, string userId);
void NotifyForgotPasswordChanged(IPrincipal currentUser, string userId);
SignOutSuccessResult NotifyLogoutSuccess(IPrincipal currentUser, string userId);
}
}

View File

@@ -1,12 +1,9 @@
using System;
using System.Collections.Generic;
using System.Security.Claims;
using System.Security.Principal;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Umbraco.Cms.Core.Models.ContentEditing;
using Umbraco.Cms.Core.Models.Identity;
using Umbraco.Cms.Core.Models.Membership;
namespace Umbraco.Cms.Core.Security
{
@@ -379,16 +376,5 @@ namespace Umbraco.Cms.Core.Security
/// A user can only support a phone number if the BackOfficeUserStore is replaced with another that implements IUserPhoneNumberStore
/// </remarks>
Task<string> GetPhoneNumberAsync(TUser user);
// TODO: These are raised from outside the signinmanager and usermanager in the auth and user controllers,
// let's see if there's a way to avoid that and only have these called within signinmanager and usermanager
// which means we can remove these from the interface (things like invite seems like they cannot be moved)
// TODO: When we change to not having the crappy static events this will need to be revisited
void RaiseForgotPasswordRequestedEvent(IPrincipal currentUser, string userId);
void RaiseForgotPasswordChangedSuccessEvent(IPrincipal currentUser, string userId);
SignOutAuditEventArgs RaiseLogoutSuccessEvent(IPrincipal currentUser, string userId);
UserInviteEventArgs RaiseSendingUserInvite(IPrincipal currentUser, UserInvite invite, IUser createdUser);
bool HasSendingUserInviteEventHandler { get; }
}
}

View File

@@ -1,19 +0,0 @@
namespace Umbraco.Cms.Core.Security
{
/// <summary>
/// Event args used when signing out
/// </summary>
public class SignOutAuditEventArgs : IdentityAuditEventArgs
{
public SignOutAuditEventArgs(AuditEvent action, string ipAddress, string comment = null, string performingUser = Cms.Core.Constants.Security.SuperUserIdAsString, string affectedUser = Cms.Core.Constants.Security.SuperUserIdAsString)
: base(action, ipAddress, performingUser, comment, affectedUser, null)
{
}
/// <summary>
/// Allows event handlers to set a GET absolute URL to be redirected to after successful logout out of the back office. This
/// can be used for external login providers.
/// </summary>
public string SignOutRedirectUrl { get; set; }
}
}

View File

@@ -0,0 +1,7 @@
namespace Umbraco.Cms.Infrastructure.Security
{
public class SignOutSuccessResult
{
public string SignOutRedirectUrl { get; set; }
}
}

View File

@@ -1,36 +0,0 @@
using Umbraco.Cms.Core.Models.ContentEditing;
using Umbraco.Cms.Core.Models.Membership;
namespace Umbraco.Cms.Core.Security
{
public class UserInviteEventArgs : IdentityAuditEventArgs
{
public UserInviteEventArgs(string ipAddress, string performingUser, UserInvite invitedUser, IUser localUser, string comment = null)
: base(AuditEvent.SendingUserInvite, ipAddress, performingUser, comment, string.Intern(localUser.Id.ToString()), localUser.Name)
{
InvitedUser = invitedUser ?? throw new System.ArgumentNullException(nameof(invitedUser));
User = localUser;
}
/// <summary>
/// The model used to invite the user
/// </summary>
public UserInvite InvitedUser { get; }
/// <summary>
/// If event handler sets this to true it indicates that Umbraco will no try to send the invite itself
/// </summary>
public bool InviteHandled { get; set; }
/// <summary>
/// The local user that has been created that is pending the invite
/// </summary>
public IUser User { get; }
/// <summary>
/// if set to true will show the edit user button in the UI, else it will not be shown
/// </summary>
public bool ShowUserResult { get; set; }
}
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
@@ -13,6 +13,7 @@ using Umbraco.Cms.Core.Scoping;
using Umbraco.Cms.Core.Services.Changes;
using Umbraco.Cms.Core.Strings;
using Umbraco.Cms.Infrastructure.Persistence.Querying;
using Umbraco.Cms.Infrastructure.Services.Notifications;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.Services.Implement
@@ -747,8 +748,8 @@ namespace Umbraco.Cms.Core.Services.Implement
using (var scope = ScopeProvider.CreateScope())
{
var saveEventArgs = new ContentSavingEventArgs(content, evtMsgs);
if (raiseEvents && scope.Events.DispatchCancelable(Saving, this, saveEventArgs, nameof(Saving)))
var savingNotification = new ContentSavingNotification(content, evtMsgs);
if (raiseEvents && scope.Notifications.PublishCancelable(savingNotification))
{
scope.Complete();
return OperationResult.Cancel(evtMsgs);
@@ -773,7 +774,7 @@ namespace Umbraco.Cms.Core.Services.Implement
if (raiseEvents)
{
scope.Events.Dispatch(Saved, this, saveEventArgs.ToContentSavedEventArgs(), nameof(Saved));
scope.Notifications.Publish(new ContentSavedNotification(content, evtMsgs).WithStateFrom(savingNotification));
}
var changeType = TreeChangeTypes.RefreshNode;
scope.Events.Dispatch(TreeChanged, this, new TreeChange<IContent>(content, changeType).ToEventArgs());
@@ -802,8 +803,8 @@ namespace Umbraco.Cms.Core.Services.Implement
using (var scope = ScopeProvider.CreateScope())
{
var saveEventArgs = new ContentSavingEventArgs(contentsA, evtMsgs);
if (raiseEvents && scope.Events.DispatchCancelable(Saving, this, saveEventArgs, nameof(Saving)))
var savingNotification = new ContentSavingNotification(contentsA, evtMsgs);
if (raiseEvents && scope.Notifications.PublishCancelable(savingNotification))
{
scope.Complete();
return OperationResult.Cancel(evtMsgs);
@@ -823,7 +824,7 @@ namespace Umbraco.Cms.Core.Services.Implement
if (raiseEvents)
{
scope.Events.Dispatch(Saved, this, saveEventArgs.ToContentSavedEventArgs(), nameof(Saved));
scope.Notifications.Publish(new ContentSavedNotification(contentsA, evtMsgs).WithStateFrom(savingNotification));
}
scope.Events.Dispatch(TreeChanged, this, treeChanges.ToEventArgs());
Audit(AuditType.Save, userId == -1 ? 0 : userId, Cms.Core.Constants.System.Root, "Saved multiple content");
@@ -867,9 +868,11 @@ namespace Umbraco.Cms.Core.Services.Implement
var allLangs = _languageRepository.GetMany().ToList();
var saveEventArgs = new ContentSavingEventArgs(content, evtMsgs);
if (raiseEvents && scope.Events.DispatchCancelable(Saving, this, saveEventArgs, nameof(Saving)))
var savingNotification = new ContentSavingNotification(content, evtMsgs);
if (scope.Notifications.PublishCancelable(savingNotification))
{
return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, content);
}
// if culture is specific, first publish the invariant values, then publish the culture itself.
// if culture is '*', then publish them all (including variants)
@@ -881,7 +884,7 @@ namespace Umbraco.Cms.Core.Services.Implement
// we don't care about the response here, this response will be rechecked below but we need to set the culture info values now.
content.PublishCulture(impact);
var result = CommitDocumentChangesInternal(scope, content, saveEventArgs, allLangs, userId, raiseEvents);
var result = CommitDocumentChangesInternal(scope, content, evtMsgs, allLangs, savingNotification.State, userId, raiseEvents);
scope.Complete();
return result;
}
@@ -905,9 +908,12 @@ namespace Umbraco.Cms.Core.Services.Implement
var allLangs = _languageRepository.GetMany().ToList();
var evtMsgs = EventMessagesFactory.Get();
var saveEventArgs = new ContentSavingEventArgs(content, evtMsgs);
if (raiseEvents && scope.Events.DispatchCancelable(Saving, this, saveEventArgs, nameof(Saving)))
var savingNotification = new ContentSavingNotification(content, evtMsgs);
if (raiseEvents && scope.Notifications.PublishCancelable(savingNotification))
{
return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, content);
}
var varies = content.ContentType.VariesByCulture();
@@ -927,7 +933,7 @@ namespace Umbraco.Cms.Core.Services.Implement
foreach (var impact in impacts)
content.PublishCulture(impact);
var result = CommitDocumentChangesInternal(scope, content, saveEventArgs, allLangs, userId, raiseEvents);
var result = CommitDocumentChangesInternal(scope, content, evtMsgs, allLangs, savingNotification.State, userId, raiseEvents);
scope.Complete();
return result;
}
@@ -969,9 +975,11 @@ namespace Umbraco.Cms.Core.Services.Implement
var allLangs = _languageRepository.GetMany().ToList();
var saveEventArgs = new ContentSavingEventArgs(content, evtMsgs);
if (scope.Events.DispatchCancelable(Saving, this, saveEventArgs, nameof(Saving)))
var savingNotification = new ContentSavingNotification(content, evtMsgs);
if (scope.Notifications.PublishCancelable(savingNotification))
{
return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, content);
}
// all cultures = unpublish whole
if (culture == "*" || (!content.ContentType.VariesByCulture() && culture == null))
@@ -982,7 +990,7 @@ namespace Umbraco.Cms.Core.Services.Implement
// to be non-routable so that when it's re-published all variants were as they were.
content.PublishedState = PublishedState.Unpublishing;
var result = CommitDocumentChangesInternal(scope, content, saveEventArgs, allLangs, userId);
var result = CommitDocumentChangesInternal(scope, content, evtMsgs, allLangs, savingNotification.State, userId);
scope.Complete();
return result;
}
@@ -996,7 +1004,7 @@ namespace Umbraco.Cms.Core.Services.Implement
var removed = content.UnpublishCulture(culture);
//save and publish any changes
var result = CommitDocumentChangesInternal(scope, content, saveEventArgs, allLangs, userId);
var result = CommitDocumentChangesInternal(scope, content, evtMsgs, allLangs, savingNotification.State, userId);
scope.Complete();
@@ -1039,13 +1047,15 @@ namespace Umbraco.Cms.Core.Services.Implement
scope.WriteLock(Cms.Core.Constants.Locks.ContentTree);
var saveEventArgs = new ContentSavingEventArgs(content, evtMsgs);
if (raiseEvents && scope.Events.DispatchCancelable(Saving, this, saveEventArgs, nameof(Saving)))
var savingNotification = new ContentSavingNotification(content, evtMsgs);
if (scope.Notifications.PublishCancelable(savingNotification))
{
return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, content);
}
var allLangs = _languageRepository.GetMany().ToList();
var result = CommitDocumentChangesInternal(scope, content, saveEventArgs, allLangs, userId, raiseEvents);
var result = CommitDocumentChangesInternal(scope, content, evtMsgs, allLangs, savingNotification.State, userId, raiseEvents);
scope.Complete();
return result;
}
@@ -1056,7 +1066,7 @@ namespace Umbraco.Cms.Core.Services.Implement
/// </summary>
/// <param name="scope"></param>
/// <param name="content"></param>
/// <param name="saveEventArgs"></param>
/// <param name="notificationState"></param>
/// <param name="userId"></param>
/// <param name="raiseEvents"></param>
/// <param name="branchOne"></param>
@@ -1069,15 +1079,14 @@ namespace Umbraco.Cms.Core.Services.Implement
/// </para>
/// </remarks>
private PublishResult CommitDocumentChangesInternal(IScope scope, IContent content,
ContentSavingEventArgs saveEventArgs, IReadOnlyCollection<ILanguage> allLangs,
EventMessages evtMsgs, IReadOnlyCollection<ILanguage> allLangs,
IDictionary<string, object> notificationState,
int userId = Cms.Core.Constants.Security.SuperUserId,
bool raiseEvents = true, bool branchOne = false, bool branchRoot = false)
{
if (scope == null) throw new ArgumentNullException(nameof(scope));
if (content == null) throw new ArgumentNullException(nameof(content));
if (saveEventArgs == null) throw new ArgumentNullException(nameof(saveEventArgs));
var evtMsgs = saveEventArgs.Messages;
if (evtMsgs == null) throw new ArgumentNullException(nameof(evtMsgs));
PublishResult publishResult = null;
PublishResult unpublishResult = null;
@@ -1125,7 +1134,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, notificationState);
if (publishResult.Success)
{
// note: StrategyPublish flips the PublishedState to Publishing!
@@ -1210,7 +1219,7 @@ namespace Umbraco.Cms.Core.Services.Implement
// raise the Saved event, always
if (raiseEvents)
{
scope.Events.Dispatch(Saved, this, saveEventArgs.ToContentSavedEventArgs(), nameof(Saved));
scope.Notifications.Publish(new ContentSavedNotification(content, evtMsgs).WithState(notificationState));
}
if (unpublishing) // we have tried to unpublish - won't happen in a branch
@@ -1218,7 +1227,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");
scope.Notifications.Publish(new ContentUnpublishedNotification(content, evtMsgs).WithState(notificationState));
scope.Events.Dispatch(TreeChanged, this, new TreeChange<IContent>(content, TreeChangeTypes.RefreshBranch).ToEventArgs());
if (culturesUnpublishing != null)
@@ -1273,7 +1282,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));
scope.Notifications.Publish(new ContentPublishedNotification(content, evtMsgs).WithState(notificationState));
}
// it was not published and now is... descendants that were 'published' (but
@@ -1282,7 +1291,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");
scope.Notifications.Publish(new ContentPublishedNotification(descendants, evtMsgs).WithState(notificationState));
}
switch (publishResult.Result)
@@ -1375,8 +1384,8 @@ namespace Umbraco.Cms.Core.Services.Implement
if (pendingCultures.Count == 0)
continue; //shouldn't happen but no point in processing this document if there's nothing there
var saveEventArgs = new ContentSavingEventArgs(d, evtMsgs);
if (scope.Events.DispatchCancelable(Saving, this, saveEventArgs, nameof(Saving)))
var savingNotification = new ContentSavingNotification(d, evtMsgs);
if (scope.Notifications.PublishCancelable(savingNotification))
{
results.Add(new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, d));
continue;
@@ -1390,7 +1399,7 @@ namespace Umbraco.Cms.Core.Services.Implement
d.UnpublishCulture(c);
}
var result = CommitDocumentChangesInternal(scope, d, saveEventArgs, allLangs.Value, d.WriterId);
var result = CommitDocumentChangesInternal(scope, d, evtMsgs, allLangs.Value, savingNotification.State, d.WriterId);
if (result.Success == false)
_logger.LogError(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result);
results.Add(result);
@@ -1436,11 +1445,11 @@ namespace Umbraco.Cms.Core.Services.Implement
if (pendingCultures.Count == 0)
continue; //shouldn't happen but no point in processing this document if there's nothing there
var saveEventArgs = new ContentSavingEventArgs(d, evtMsgs);
if (scope.Events.DispatchCancelable(Saving, this, saveEventArgs, nameof(Saving)))
var savingNotification = new ContentSavingNotification(d, evtMsgs);
if (scope.Notifications.PublishCancelable(savingNotification))
{
results.Add(new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, d));
continue; // this document is canceled move next
continue;
}
var publishing = true;
@@ -1470,7 +1479,7 @@ namespace Umbraco.Cms.Core.Services.Implement
else if (!publishing)
result = new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, d);
else
result = CommitDocumentChangesInternal(scope, d, saveEventArgs, allLangs.Value, d.WriterId);
result = CommitDocumentChangesInternal(scope, d, evtMsgs, allLangs.Value, savingNotification.State, d.WriterId);
if (result.Success == false)
_logger.LogError(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result);
@@ -1694,7 +1703,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));
scope.Notifications.Publish(new ContentPublishedNotification(publishedDocuments, evtMsgs));
scope.Complete();
}
@@ -1718,9 +1727,11 @@ namespace Umbraco.Cms.Core.Services.Implement
if (culturesToPublish.Count == 0) // empty = already published
return new PublishResult(PublishResultType.SuccessPublishAlready, evtMsgs, document);
var saveEventArgs = new ContentSavingEventArgs(document, evtMsgs);
if (scope.Events.DispatchCancelable(Saving, this, saveEventArgs, nameof(Saving)))
var savingNotification = new ContentSavingNotification(document, evtMsgs);
if (scope.Notifications.PublishCancelable(savingNotification))
{
return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, document);
}
// publish & check if values are valid
if (!publishCultures(document, culturesToPublish, allLangs))
@@ -1729,7 +1740,7 @@ namespace Umbraco.Cms.Core.Services.Implement
return new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, document);
}
var result = CommitDocumentChangesInternal(scope, document, saveEventArgs, allLangs, userId, branchOne: true, branchRoot: isRoot);
var result = CommitDocumentChangesInternal(scope, document, evtMsgs, allLangs, savingNotification.State, userId, branchOne: true, branchRoot: isRoot);
if (result.Success)
publishedDocuments.Add(document);
return result;
@@ -1746,8 +1757,7 @@ namespace Umbraco.Cms.Core.Services.Implement
using (var scope = ScopeProvider.CreateScope())
{
var deleteEventArgs = new DeleteEventArgs<IContent>(content, evtMsgs);
if (scope.Events.DispatchCancelable(Deleting, this, deleteEventArgs, nameof(Deleting)))
if (scope.Notifications.PublishCancelable(new ContentDeletingNotification(content, evtMsgs)))
{
scope.Complete();
return OperationResult.Cancel(evtMsgs);
@@ -1759,9 +1769,11 @@ 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));
{
scope.Notifications.Publish(new ContentUnpublishedNotification(content, evtMsgs));
}
DeleteLocked(scope, content);
DeleteLocked(scope, content, evtMsgs);
scope.Events.Dispatch(TreeChanged, this, new TreeChange<IContent>(content, TreeChangeTypes.Remove).ToEventArgs());
Audit(AuditType.Delete, userId, content.Id);
@@ -1772,13 +1784,12 @@ namespace Umbraco.Cms.Core.Services.Implement
return OperationResult.Succeed(evtMsgs);
}
private void DeleteLocked(IScope scope, IContent content)
private void DeleteLocked(IScope scope, IContent content, EventMessages evtMsgs)
{
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));
scope.Notifications.Publish(new ContentDeletedNotification(c, evtMsgs));
// media files deleted by QueuingEventDispatcher
}
@@ -1809,10 +1820,12 @@ 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 deletingVersionsNotification = new ContentDeletingVersionsNotification(id, evtMsgs, dateToRetain: versionDate);
if (scope.Notifications.PublishCancelable(deletingVersionsNotification))
{
scope.Complete();
return;
@@ -1821,8 +1834,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);
scope.Notifications.Publish(new ContentDeletedVersionsNotification(id, evtMsgs, dateToRetain: versionDate).WithStateFrom(deletingVersionsNotification));
Audit(AuditType.Delete, userId, Cms.Core.Constants.System.Root, "Delete (by version date)");
scope.Complete();
@@ -1839,9 +1851,12 @@ 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 deletingVersionsNotification = new ContentDeletingVersionsNotification(id, evtMsgs, specificVersion: versionId);
if (scope.Notifications.PublishCancelable(deletingVersionsNotification))
{
scope.Complete();
return;
@@ -1858,7 +1873,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));
scope.Notifications.Publish(new ContentDeletedVersionsNotification(id, evtMsgs, specificVersion: versionId).WithStateFrom(deletingVersionsNotification));
Audit(AuditType.Delete, userId, Cms.Core.Constants.System.Root, "Delete (by version)");
scope.Complete();
@@ -1881,8 +1896,9 @@ 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 movingToRecycleBinNotification = new ContentMovingToRecycleBinNotification(moveEventInfo, evtMsgs);
if (scope.Notifications.PublishCancelable(movingToRecycleBinNotification))
{
scope.Complete();
return OperationResult.Cancel(evtMsgs); // causes rollback
@@ -1901,9 +1917,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));
scope.Notifications.Publish(new ContentMovedToRecycleBinNotification(moveInfo, evtMsgs).WithStateFrom(movingToRecycleBinNotification));
Audit(AuditType.Move, userId, content.Id, "Moved to recycle bin");
scope.Complete();
@@ -1932,6 +1946,8 @@ namespace Umbraco.Cms.Core.Services.Implement
return;
}
var evtMsgs = EventMessagesFactory.Get();
var moves = new List<(IContent, string)>();
using (var scope = ScopeProvider.CreateScope())
@@ -1943,8 +1959,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 movingNotification = new ContentMovingNotification(moveEventInfo, evtMsgs);
if (scope.Notifications.PublishCancelable(movingNotification))
{
scope.Complete();
return; // causes rollback
@@ -1973,9 +1990,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));
scope.Notifications.Publish(new ContentMovedNotification(moveInfo, evtMsgs).WithStateFrom(movingNotification));
Audit(AuditType.Move, userId, content.Id);
scope.Complete();
@@ -2047,7 +2063,6 @@ namespace Umbraco.Cms.Core.Services.Implement
/// </summary>
public OperationResult EmptyRecycleBin(int userId = Cms.Core.Constants.Security.SuperUserId)
{
var nodeObjectType = Cms.Core.Constants.ObjectTypes.Document;
var deleted = new List<IContent>();
var evtMsgs = EventMessagesFactory.Get();
@@ -2060,8 +2075,8 @@ 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 emptyingRecycleBinNotification = new ContentEmptyingRecycleBinNotification(evtMsgs);
if (scope.Notifications.PublishCancelable(emptyingRecycleBinNotification))
{
scope.Complete();
return OperationResult.Cancel(evtMsgs);
@@ -2072,13 +2087,11 @@ namespace Umbraco.Cms.Core.Services.Implement
var contents = _documentRepository.Get(query).ToArray();
foreach (var content in contents)
{
DeleteLocked(scope, content);
DeleteLocked(scope, content, evtMsgs);
deleted.Add(content);
}
recycleBinEventArgs.CanCancel = false;
recycleBinEventArgs.RecycleBinEmptiedSuccessfully = true; // oh my?!
scope.Events.Dispatch(EmptiedRecycleBin, this, recycleBinEventArgs);
scope.Notifications.Publish(new ContentEmptiedRecycleBinNotification(evtMsgs).WithStateFrom(emptyingRecycleBinNotification));
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");
@@ -2118,13 +2131,14 @@ 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))
if (scope.Notifications.PublishCancelable(new ContentCopyingNotification(content, copy, parentId, evtMsgs)))
{
scope.Complete();
return null;
@@ -2179,8 +2193,10 @@ 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)))
if (scope.Notifications.PublishCancelable(new ContentCopyingNotification(descendant, descendantCopy, parentId, evtMsgs)))
{
continue;
}
// a copy is not published (but not really unpublishing either)
// update the create author and last edit author
@@ -2204,7 +2220,9 @@ 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));
{
scope.Notifications.Publish(new ContentCopiedNotification(x.Item1, x.Item2, parentId, relateToOriginal, evtMsgs));
}
Audit(AuditType.Copy, userId, content.Id);
scope.Complete();
@@ -2221,10 +2239,12 @@ 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 sendingToPublishNotification = new ContentSendingToPublishNotification(content, evtMsgs);
if (scope.Notifications.PublishCancelable(sendingToPublishNotification))
{
scope.Complete();
return false;
@@ -2249,8 +2269,7 @@ namespace Umbraco.Cms.Core.Services.Implement
if (!saveResult.Success)
return saveResult.Success;
sendToPublishEventArgs.CanCancel = false;
scope.Events.Dispatch(SentToPublish, this, sendToPublishEventArgs);
scope.Notifications.Publish(new ContentSentToPublishNotification(content, evtMsgs).WithStateFrom(sendingToPublishNotification));
if (culturesChanging != null)
Audit(AuditType.SendToPublishVariant, userId, content.Id, $"Send To Publish for cultures: {culturesChanging}", culturesChanging);
@@ -2322,16 +2341,21 @@ namespace Umbraco.Cms.Core.Services.Implement
private OperationResult Sort(IScope scope, IContent[] itemsA, int userId, EventMessages evtMsgs, bool raiseEvents)
{
var saveEventArgs = new ContentSavingEventArgs(itemsA, evtMsgs);
var sortingNotification = new ContentSortingNotification(itemsA, evtMsgs);
var savingNotification = new ContentSavingNotification(itemsA, evtMsgs);
if (raiseEvents)
{
//raise cancelable sorting event
if (scope.Events.DispatchCancelable(Saving, this, saveEventArgs, nameof(Sorting)))
// raise cancelable sorting event
if (scope.Notifications.PublishCancelable(sortingNotification))
{
return OperationResult.Cancel(evtMsgs);
}
//raise saving event (this one cannot be canceled)
saveEventArgs.CanCancel = false;
scope.Events.Dispatch(Saving, this, saveEventArgs, nameof(Saving));
// raise cancelable saving event
if (scope.Notifications.PublishCancelable(savingNotification))
{
return OperationResult.Cancel(evtMsgs);
}
}
var published = new List<IContent>();
@@ -2364,16 +2388,17 @@ namespace Umbraco.Cms.Core.Services.Implement
if (raiseEvents)
{
var savedEventsArgs = saveEventArgs.ToContentSavedEventArgs();
//first saved, then sorted
scope.Events.Dispatch(Saved, this, savedEventsArgs, nameof(Saved));
scope.Events.Dispatch(Sorted, this, savedEventsArgs, nameof(Sorted));
scope.Notifications.Publish(new ContentSavedNotification(itemsA, evtMsgs).WithStateFrom(savingNotification));
scope.Notifications.Publish(new ContentSortedNotification(itemsA, evtMsgs).WithStateFrom(sortingNotification));
}
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");
{
scope.Notifications.Publish(new ContentPublishedNotification(published, evtMsgs));
}
Audit(AuditType.Sort, userId, 0, "Sorting content performed by user");
return OperationResult.Succeed(evtMsgs);
@@ -2453,140 +2478,13 @@ 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 before Save
/// </summary>
public static event TypedEventHandler<IContentService, ContentSavingEventArgs> Saving;
/// <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;
/// <remarks>
/// This event needs to be rewritten using notifications instead
/// </remarks>
internal static event TypedEventHandler<IContentService, TreeChange<IContent>.EventArgs> TreeChanged;
#endregion
@@ -2601,14 +2499,14 @@ 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>
/// <param name="notificationState"></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, IDictionary<string, object> notificationState)
{
// raise Publishing event
if (scope.Events.DispatchCancelable(Publishing, this, savingEventArgs.ToContentPublishingEventArgs()))
// raise Publishing notification
if (scope.Notifications.PublishCancelable(new ContentPublishingNotification(content, evtMsgs).WithState(notificationState)))
{
_logger.LogInformation("Document {ContentName} (id={ContentId}) cannot be published: {Reason}", content.Name, content.Id, "publishing was cancelled");
return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, content);
@@ -2767,8 +2665,8 @@ 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
if (scope.Notifications.PublishCancelable(new ContentUnpublishingNotification(content, evtMsgs)))
{
_logger.LogInformation("Document {ContentName} (id={ContentId}) cannot be unpublished: unpublishing was cancelled.", content.Name, content.Id);
return new PublishResult(PublishResultType.FailedUnpublishCancelledByEvent, evtMsgs, content);
@@ -2837,6 +2735,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,
@@ -2849,7 +2748,7 @@ 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)))
if (scope.Notifications.PublishCancelable(new ContentDeletingNotification(contents, evtMsgs)))
{
scope.Complete();
return;
@@ -2863,7 +2762,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));
{
scope.Notifications.Publish(new ContentUnpublishedNotification(content, evtMsgs));
}
// if current content has children, move them to trash
var c = content;
@@ -2878,7 +2779,7 @@ namespace Umbraco.Cms.Core.Services.Implement
// delete content
// triggers the deleted event (and handles the files)
DeleteLocked(scope, content);
DeleteLocked(scope, content, evtMsgs);
changes.Add(new TreeChange<IContent>(content, TreeChangeTypes.Remove));
}
@@ -2886,7 +2787,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));
{
scope.Notifications.Publish(new ContentMovedToRecycleBinNotification(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)}");
@@ -2963,6 +2866,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;
@@ -2983,7 +2888,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");
scope.Notifications.Publish(new ContentSavedBlueprintNotification(content, evtMsgs));
scope.Complete();
}
@@ -2991,11 +2896,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));
scope.Notifications.Publish(new ContentDeletedBlueprintNotification(content, evtMsgs));
scope.Complete();
}
}
@@ -3065,6 +2972,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);
@@ -3085,7 +2994,7 @@ namespace Umbraco.Cms.Core.Services.Implement
_documentBlueprintRepository.Delete(blueprint);
}
scope.Events.Dispatch(DeletedBlueprint, this, new DeleteEventArgs<IContent>(blueprints), nameof(DeletedBlueprint));
scope.Notifications.Publish(new ContentDeletedBlueprintNotification(blueprints, evtMsgs));
scope.Complete();
}
}
@@ -3120,10 +3029,8 @@ 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 rollingBackNotification = new ContentRollingBackNotification(content, evtMsgs);
if (scope.Notifications.PublishCancelable(rollingBackNotification))
{
scope.Complete();
return OperationResult.Cancel(evtMsgs);
@@ -3143,9 +3050,7 @@ namespace Umbraco.Cms.Core.Services.Implement
}
else
{
//Emit RolledBack event aka after
rollbackEventArgs.CanCancel = false;
scope.Events.Dispatch(RolledBack, this, rollbackEventArgs);
scope.Notifications.Publish(new ContentRolledBackNotification(content, evtMsgs).WithStateFrom(rollingBackNotification));
//Logging & Audit message
_logger.LogInformation("User '{UserId}' rolled back content '{ContentId}' to version '{VersionId}'", userId, id, versionId);

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
@@ -13,6 +13,7 @@ using Umbraco.Cms.Core.Scoping;
using Umbraco.Cms.Core.Services.Changes;
using Umbraco.Cms.Core.Strings;
using Umbraco.Cms.Infrastructure.Persistence.Querying;
using Umbraco.Cms.Infrastructure.Services.Notifications;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.Services.Implement
@@ -289,19 +290,21 @@ namespace Umbraco.Cms.Core.Services.Implement
private void CreateMedia(IScope scope, Core.Models.Media media, IMedia parent, int userId, bool withIdentity)
{
var evtMsgs = EventMessagesFactory.Get();
media.CreatorId = userId;
if (withIdentity)
{
// if saving is cancelled, media remains without an identity
var saveEventArgs = new SaveEventArgs<IMedia>(media);
if (Saving.IsRaisedEventCancelled(saveEventArgs, this))
var savingNotification = new MediaSavingNotification(media, evtMsgs);
if (scope.Notifications.PublishCancelable(savingNotification))
{
return;
}
_mediaRepository.Save(media);
saveEventArgs.CanCancel = false;
scope.Events.Dispatch(Saved, this, saveEventArgs);
scope.Notifications.Publish(new MediaSavedNotification(media, evtMsgs).WithStateFrom(savingNotification));
scope.Events.Dispatch(TreeChanged, this, new TreeChange<IMedia>(media, TreeChangeTypes.RefreshNode).ToEventArgs());
}
@@ -659,8 +662,8 @@ namespace Umbraco.Cms.Core.Services.Implement
using (var scope = ScopeProvider.CreateScope())
{
var saveEventArgs = new SaveEventArgs<IMedia>(media, evtMsgs);
if (raiseEvents && scope.Events.DispatchCancelable(Saving, this, saveEventArgs))
var savingNotification = new MediaSavingNotification(media, evtMsgs);
if (raiseEvents && scope.Notifications.PublishCancelable(savingNotification))
{
scope.Complete();
return OperationResult.Attempt.Cancel(evtMsgs);
@@ -682,8 +685,7 @@ namespace Umbraco.Cms.Core.Services.Implement
_mediaRepository.Save(media);
if (raiseEvents)
{
saveEventArgs.CanCancel = false;
scope.Events.Dispatch(Saved, this, saveEventArgs);
scope.Notifications.Publish(new MediaSavedNotification(media, evtMsgs).WithStateFrom(savingNotification));
}
var changeType = TreeChangeTypes.RefreshNode;
scope.Events.Dispatch(TreeChanged, this, new TreeChange<IMedia>(media, changeType).ToEventArgs());
@@ -708,8 +710,8 @@ namespace Umbraco.Cms.Core.Services.Implement
using (var scope = ScopeProvider.CreateScope())
{
var saveEventArgs = new SaveEventArgs<IMedia>(mediasA, evtMsgs);
if (raiseEvents && scope.Events.DispatchCancelable(Saving, this, new SaveEventArgs<IMedia>(mediasA, evtMsgs)))
var savingNotification = new MediaSavingNotification(mediasA, evtMsgs);
if (raiseEvents && scope.Notifications.PublishCancelable(savingNotification))
{
scope.Complete();
return OperationResult.Attempt.Cancel(evtMsgs);
@@ -727,8 +729,7 @@ namespace Umbraco.Cms.Core.Services.Implement
if (raiseEvents)
{
saveEventArgs.CanCancel = false;
scope.Events.Dispatch(Saved, this, saveEventArgs);
scope.Notifications.Publish(new MediaSavedNotification(mediasA, evtMsgs).WithStateFrom(savingNotification));
}
scope.Events.Dispatch(TreeChanged, this, treeChanges.ToEventArgs());
Audit(AuditType.Save, userId == -1 ? 0 : userId, Cms.Core.Constants.System.Root, "Bulk save media");
@@ -754,7 +755,7 @@ namespace Umbraco.Cms.Core.Services.Implement
using (var scope = ScopeProvider.CreateScope())
{
if (scope.Events.DispatchCancelable(Deleting, this, new DeleteEventArgs<IMedia>(media, evtMsgs)))
if (scope.Notifications.PublishCancelable(new MediaDeletingNotification(media, evtMsgs)))
{
scope.Complete();
return OperationResult.Attempt.Cancel(evtMsgs);
@@ -762,7 +763,7 @@ namespace Umbraco.Cms.Core.Services.Implement
scope.WriteLock(Cms.Core.Constants.Locks.MediaTree);
DeleteLocked(scope, media);
DeleteLocked(scope, media, evtMsgs);
scope.Events.Dispatch(TreeChanged, this, new TreeChange<IMedia>(media, TreeChangeTypes.Remove).ToEventArgs());
Audit(AuditType.Delete, userId, media.Id);
@@ -773,13 +774,12 @@ namespace Umbraco.Cms.Core.Services.Implement
return OperationResult.Attempt.Succeed(evtMsgs);
}
private void DeleteLocked(IScope scope, IMedia media)
private void DeleteLocked(IScope scope, IMedia media, EventMessages evtMsgs)
{
void DoDelete(IMedia c)
{
_mediaRepository.Delete(c);
var args = new DeleteEventArgs<IMedia>(c, false); // raise event & get flagged files
scope.Events.Dispatch(Deleted, this, args);
scope.Notifications.Publish(new MediaDeletedNotification(c, evtMsgs));
// media files deleted by QueuingEventDispatcher
}
@@ -815,36 +815,24 @@ namespace Umbraco.Cms.Core.Services.Implement
{
DeleteVersions(scope, true, id, versionDate, userId);
scope.Complete();
//if (uow.Events.DispatchCancelable(DeletingVersions, this, new DeleteRevisionsEventArgs(id, dateToRetain: versionDate)))
//{
// uow.Complete();
// return;
//}
//uow.WriteLock(Constants.Locks.MediaTree);
//var repository = uow.CreateRepository<IMediaRepository>();
//repository.DeleteVersions(id, versionDate);
//uow.Events.Dispatch(DeletedVersions, this, new DeleteRevisionsEventArgs(id, false, dateToRetain: versionDate));
//Audit(uow, AuditType.Delete, "Delete Media by version date, userId, Constants.System.Root);
//uow.Complete();
}
}
private void DeleteVersions(IScope scope, bool wlock, int id, DateTime versionDate, int userId = Cms.Core.Constants.Security.SuperUserId)
{
var args = new DeleteRevisionsEventArgs(id, dateToRetain: versionDate);
if (scope.Events.DispatchCancelable(DeletingVersions, this, args))
var evtMsgs = EventMessagesFactory.Get();
var deletingVersionsNotification = new MediaDeletingVersionsNotification(id, evtMsgs, dateToRetain: versionDate);
if (scope.Notifications.PublishCancelable(deletingVersionsNotification))
{
return;
}
if (wlock)
scope.WriteLock(Cms.Core.Constants.Locks.MediaTree);
_mediaRepository.DeleteVersions(id, versionDate);
args.CanCancel = false;
scope.Events.Dispatch(DeletedVersions, this, args);
scope.Notifications.Publish(new MediaDeletedVersionsNotification(id, evtMsgs, dateToRetain: versionDate).WithStateFrom(deletingVersionsNotification));
Audit(AuditType.Delete, userId, Cms.Core.Constants.System.Root, "Delete Media by version date");
}
@@ -858,10 +846,12 @@ namespace Umbraco.Cms.Core.Services.Implement
/// <param name="userId">Optional Id of the User deleting versions of a Media 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())
{
var args = new DeleteRevisionsEventArgs(id, /*specificVersion:*/ versionId);
if (scope.Events.DispatchCancelable(DeletingVersions, this, args))
var deletingVersionsNotification = new MediaDeletingVersionsNotification(id, evtMsgs, specificVersion: versionId);
if (scope.Notifications.PublishCancelable(deletingVersionsNotification))
{
scope.Complete();
return;
@@ -879,8 +869,7 @@ namespace Umbraco.Cms.Core.Services.Implement
_mediaRepository.DeleteVersion(versionId);
args.CanCancel = false;
scope.Events.Dispatch(DeletedVersions, this, args);
scope.Notifications.Publish(new MediaDeletedVersionsNotification(id, evtMsgs, specificVersion: versionId).WithStateFrom(deletingVersionsNotification));
Audit(AuditType.Delete, userId, Cms.Core.Constants.System.Root, "Delete Media by version");
scope.Complete();
@@ -911,8 +900,9 @@ namespace Umbraco.Cms.Core.Services.Implement
var originalPath = media.Path;
var moveEventInfo = new MoveEventInfo<IMedia>(media, originalPath, Cms.Core.Constants.System.RecycleBinMedia);
var moveEventArgs = new MoveEventArgs<IMedia>(true, evtMsgs, moveEventInfo);
if (scope.Events.DispatchCancelable(Trashing, this, moveEventArgs, nameof(Trashing)))
var movingToRecycleBinNotification = new MediaMovingToRecycleBinNotification(moveEventInfo, evtMsgs);
if (scope.Notifications.PublishCancelable(movingToRecycleBinNotification))
{
scope.Complete();
return OperationResult.Attempt.Cancel(evtMsgs);
@@ -921,11 +911,8 @@ namespace Umbraco.Cms.Core.Services.Implement
PerformMoveLocked(media, Cms.Core.Constants.System.RecycleBinMedia, null, userId, moves, true);
scope.Events.Dispatch(TreeChanged, this, new TreeChange<IMedia>(media, TreeChangeTypes.RefreshBranch).ToEventArgs());
var moveInfo = moves.Select(x => new MoveEventInfo<IMedia>(x.Item1, x.Item2, x.Item1.ParentId))
.ToArray();
moveEventArgs.MoveInfoCollection = moveInfo;
moveEventArgs.CanCancel = false;
scope.Events.Dispatch(Trashed, this, moveEventArgs, nameof(Trashed));
var moveInfo = moves.Select(x => new MoveEventInfo<IMedia>(x.Item1, x.Item2, x.Item1.ParentId)).ToArray();
scope.Notifications.Publish(new MediaMovedToRecycleBinNotification(moveInfo, evtMsgs).WithStateFrom(movingToRecycleBinNotification));
Audit(AuditType.Move, userId, media.Id, "Move Media to recycle bin");
scope.Complete();
@@ -962,8 +949,8 @@ namespace Umbraco.Cms.Core.Services.Implement
throw new InvalidOperationException("Parent does not exist or is trashed."); // causes rollback
var moveEventInfo = new MoveEventInfo<IMedia>(media, media.Path, parentId);
var moveEventArgs = new MoveEventArgs<IMedia>(true, evtMsgs, moveEventInfo);
if (scope.Events.DispatchCancelable(Moving, this, moveEventArgs, nameof(Moving)))
var movingNotification = new MediaMovingNotification(moveEventInfo, evtMsgs);
if (scope.Notifications.PublishCancelable(movingNotification))
{
scope.Complete();
return OperationResult.Attempt.Cancel(evtMsgs);
@@ -979,9 +966,7 @@ namespace Umbraco.Cms.Core.Services.Implement
var moveInfo = moves //changes
.Select(x => new MoveEventInfo<IMedia>(x.Item1, x.Item2, x.Item1.ParentId))
.ToArray();
moveEventArgs.MoveInfoCollection = moveInfo;
moveEventArgs.CanCancel = false;
scope.Events.Dispatch(Moved, this, moveEventArgs, nameof(Moved));
scope.Notifications.Publish(new MediaMovedNotification(moveInfo, evtMsgs).WithStateFrom(movingNotification));
Audit(AuditType.Move, userId, media.Id);
scope.Complete();
}
@@ -1050,7 +1035,6 @@ namespace Umbraco.Cms.Core.Services.Implement
/// <param name="userId">Optional Id of the User emptying the Recycle Bin</param>
public OperationResult EmptyRecycleBin(int userId = Cms.Core.Constants.Security.SuperUserId)
{
var nodeObjectType = Cms.Core.Constants.ObjectTypes.Media;
var deleted = new List<IMedia>();
var evtMsgs = EventMessagesFactory.Get(); // TODO: and then?
@@ -1063,23 +1047,22 @@ namespace Umbraco.Cms.Core.Services.Implement
// 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.
var args = new RecycleBinEventArgs(nodeObjectType, evtMsgs);
if (scope.Events.DispatchCancelable(EmptyingRecycleBin, this, args))
var emptyingRecycleBinNotification = new MediaEmptyingRecycleBinNotification(evtMsgs);
if (scope.Notifications.PublishCancelable(emptyingRecycleBinNotification))
{
scope.Complete();
return OperationResult.Cancel(evtMsgs);
}
// emptying the recycle bin means deleting whatever is in there - do it properly!
var query = Query<IMedia>().Where(x => x.ParentId == Cms.Core.Constants.System.RecycleBinMedia);
var medias = _mediaRepository.Get(query).ToArray();
foreach (var media in medias)
{
DeleteLocked(scope, media);
DeleteLocked(scope, media, evtMsgs);
deleted.Add(media);
}
args.CanCancel = false;
scope.Events.Dispatch(EmptiedRecycleBin, this, args);
scope.Notifications.Publish(new MediaEmptiedRecycleBinNotification(new EventMessages()).WithStateFrom(emptyingRecycleBinNotification));
scope.Events.Dispatch(TreeChanged, this, deleted.Select(x => new TreeChange<IMedia>(x, TreeChangeTypes.Remove)).ToEventArgs());
Audit(AuditType.Delete, userId, Cms.Core.Constants.System.RecycleBinMedia, "Empty Media recycle bin");
scope.Complete();
@@ -1105,10 +1088,12 @@ namespace Umbraco.Cms.Core.Services.Implement
var itemsA = items.ToArray();
if (itemsA.Length == 0) return true;
var evtMsgs = EventMessagesFactory.Get();
using (var scope = ScopeProvider.CreateScope())
{
var args = new SaveEventArgs<IMedia>(itemsA);
if (raiseEvents && scope.Events.DispatchCancelable(Saving, this, args))
var savingNotification = new MediaSavingNotification(itemsA, evtMsgs);
if (raiseEvents && scope.Notifications.PublishCancelable(savingNotification))
{
scope.Complete();
return false;
@@ -1137,8 +1122,7 @@ namespace Umbraco.Cms.Core.Services.Implement
if (raiseEvents)
{
args.CanCancel = false;
scope.Events.Dispatch(Saved, this, args);
scope.Notifications.Publish(new MediaSavedNotification(itemsA, evtMsgs).WithStateFrom(savingNotification));
}
scope.Events.Dispatch(TreeChanged, this, saved.Select(x => new TreeChange<IMedia>(x, TreeChangeTypes.RefreshNode)).ToEventArgs());
Audit(AuditType.Sort, userId, 0);
@@ -1216,70 +1200,13 @@ namespace Umbraco.Cms.Core.Services.Implement
#region Event Handlers
/// <summary>
/// Occurs before Delete
/// </summary>
public static event TypedEventHandler<IMediaService, DeleteEventArgs<IMedia>> Deleting;
/// <summary>
/// Occurs after Delete
/// </summary>
public static event TypedEventHandler<IMediaService, DeleteEventArgs<IMedia>> Deleted;
/// <summary>
/// Occurs before Delete Versions
/// </summary>
public static event TypedEventHandler<IMediaService, DeleteRevisionsEventArgs> DeletingVersions;
/// <summary>
/// Occurs after Delete Versions
/// </summary>
public static event TypedEventHandler<IMediaService, DeleteRevisionsEventArgs> DeletedVersions;
/// <summary>
/// Occurs before Save
/// </summary>
public static event TypedEventHandler<IMediaService, SaveEventArgs<IMedia>> Saving;
/// <summary>
/// Occurs after Save
/// </summary>
public static event TypedEventHandler<IMediaService, SaveEventArgs<IMedia>> Saved;
/// <summary>
/// Occurs before Media is moved to Recycle Bin
/// </summary>
public static event TypedEventHandler<IMediaService, MoveEventArgs<IMedia>> Trashing;
/// <summary>
/// Occurs after Media is moved to Recycle Bin
/// </summary>
public static event TypedEventHandler<IMediaService, MoveEventArgs<IMedia>> Trashed;
/// <summary>
/// Occurs before Move
/// </summary>
public static event TypedEventHandler<IMediaService, MoveEventArgs<IMedia>> Moving;
/// <summary>
/// Occurs after Move
/// </summary>
public static event TypedEventHandler<IMediaService, MoveEventArgs<IMedia>> Moved;
/// <summary>
/// Occurs before the Recycle Bin is emptied
/// </summary>
public static event TypedEventHandler<IMediaService, RecycleBinEventArgs> EmptyingRecycleBin;
/// <summary>
/// Occurs after the Recycle Bin has been Emptied
/// </summary>
public static event TypedEventHandler<IMediaService, RecycleBinEventArgs> EmptiedRecycleBin;
/// <summary>
/// Occurs after change.
/// </summary>
public static event TypedEventHandler<IMediaService, TreeChange<IMedia>.EventArgs> TreeChanged;
/// <remarks>
/// This event needs to be rewritten using notifications instead
/// </remarks>
internal static event TypedEventHandler<IMediaService, TreeChange<IMedia>.EventArgs> TreeChanged;
#endregion
@@ -1307,6 +1234,7 @@ namespace Umbraco.Cms.Core.Services.Implement
var changes = new List<TreeChange<IMedia>>();
var moves = new List<(IMedia, string)>();
var mediaTypeIdsA = mediaTypeIds.ToArray();
var evtMsgs = EventMessagesFactory.Get();
using (var scope = ScopeProvider.CreateScope())
{
@@ -1315,7 +1243,7 @@ namespace Umbraco.Cms.Core.Services.Implement
var query = Query<IMedia>().WhereIn(x => x.ContentTypeId, mediaTypeIdsA);
var medias = _mediaRepository.Get(query).ToArray();
if (scope.Events.DispatchCancelable(Deleting, this, new DeleteEventArgs<IMedia>(medias)))
if (scope.Notifications.PublishCancelable(new MediaDeletingNotification(medias, evtMsgs)))
{
scope.Complete();
return;
@@ -1338,14 +1266,16 @@ namespace Umbraco.Cms.Core.Services.Implement
// delete media
// triggers the deleted event (and handles the files)
DeleteLocked(scope, media);
DeleteLocked(scope, media, evtMsgs);
changes.Add(new TreeChange<IMedia>(media, TreeChangeTypes.Remove));
}
var moveInfos = moves.Select(x => new MoveEventInfo<IMedia>(x.Item1, x.Item2, x.Item1.ParentId))
.ToArray();
if (moveInfos.Length > 0)
scope.Events.Dispatch(Trashed, this, new MoveEventArgs<IMedia>(false, moveInfos), nameof(Trashed));
{
scope.Notifications.Publish(new MediaMovedToRecycleBinNotification(moveInfos, evtMsgs));
}
scope.Events.Dispatch(TreeChanged, this, changes.ToEventArgs());
Audit(AuditType.Delete, userId, Cms.Core.Constants.System.Root, $"Delete Media of types {string.Join(",", mediaTypeIdsA)}");

View File

@@ -0,0 +1,18 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using System.Collections.Generic;
using Umbraco.Cms.Core.Events;
namespace Umbraco.Cms.Infrastructure.Services.Notifications
{
public abstract class CancelableEnumerableObjectNotification<T> : CancelableObjectNotification<IEnumerable<T>>
{
protected CancelableEnumerableObjectNotification(T target, EventMessages messages) : base(new [] {target}, messages)
{
}
protected CancelableEnumerableObjectNotification(IEnumerable<T> target, EventMessages messages) : base(target, messages)
{
}
}
}

View File

@@ -0,0 +1,23 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using Umbraco.Cms.Core.Events;
namespace Umbraco.Cms.Infrastructure.Services.Notifications
{
public abstract class CancelableObjectNotification<T> : ObjectNotification<T>, ICancelableNotification where T : class
{
protected CancelableObjectNotification(T target, EventMessages messages) : base(target, messages)
{
}
public bool Cancel { get; set; }
public void CancelOperation(EventMessage cancelationMessage)
{
Cancel = true;
cancelationMessage.IsDefaultEventMessage = true;
Messages.Add(cancelationMessage);
}
}
}

View File

@@ -0,0 +1,16 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models;
namespace Umbraco.Cms.Infrastructure.Services.Notifications
{
public sealed class ContentCopiedNotification : CopiedNotification<IContent>
{
public ContentCopiedNotification(IContent original, IContent copy, int parentId, bool relateToOriginal, EventMessages messages)
: base(original, copy, parentId, relateToOriginal, messages)
{
}
}
}

View File

@@ -0,0 +1,16 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models;
namespace Umbraco.Cms.Infrastructure.Services.Notifications
{
public sealed class ContentCopyingNotification : CopyingNotification<IContent>
{
public ContentCopyingNotification(IContent original, IContent copy, int parentId, EventMessages messages)
: base(original, copy, parentId, messages)
{
}
}
}

View File

@@ -0,0 +1,22 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using System.Collections.Generic;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models;
namespace Umbraco.Cms.Infrastructure.Services.Notifications
{
public sealed class ContentDeletedBlueprintNotification : EnumerableObjectNotification<IContent>
{
public ContentDeletedBlueprintNotification(IContent target, EventMessages messages) : base(target, messages)
{
}
public ContentDeletedBlueprintNotification(IEnumerable<IContent> target, EventMessages messages) : base(target, messages)
{
}
public IEnumerable<IContent> DeletedBlueprints => Target;
}
}

View File

@@ -0,0 +1,15 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models;
namespace Umbraco.Cms.Infrastructure.Services.Notifications
{
public sealed class ContentDeletedNotification : DeletedNotification<IContent>
{
public ContentDeletedNotification(IContent target, EventMessages messages) : base(target, messages)
{
}
}
}

View File

@@ -0,0 +1,16 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using System;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models;
namespace Umbraco.Cms.Infrastructure.Services.Notifications
{
public sealed class ContentDeletedVersionsNotification : DeletedVersionsNotification<IContent>
{
public ContentDeletedVersionsNotification(int id, EventMessages messages, int specificVersion = default, bool deletePriorVersions = false, DateTime dateToRetain = default) : base(id, messages, specificVersion, deletePriorVersions, dateToRetain)
{
}
}
}

View File

@@ -0,0 +1,20 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using System.Collections.Generic;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models;
namespace Umbraco.Cms.Infrastructure.Services.Notifications
{
public sealed class ContentDeletingNotification : DeletingNotification<IContent>
{
public ContentDeletingNotification(IContent target, EventMessages messages) : base(target, messages)
{
}
public ContentDeletingNotification(IEnumerable<IContent> target, EventMessages messages) : base(target, messages)
{
}
}
}

View File

@@ -0,0 +1,16 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using System;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models;
namespace Umbraco.Cms.Infrastructure.Services.Notifications
{
public sealed class ContentDeletingVersionsNotification : DeletingVersionsNotification<IContent>
{
public ContentDeletingVersionsNotification(int id, EventMessages messages, int specificVersion = default, bool deletePriorVersions = false, DateTime dateToRetain = default) : base(id, messages, specificVersion, deletePriorVersions, dateToRetain)
{
}
}
}

View File

@@ -0,0 +1,15 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models;
namespace Umbraco.Cms.Infrastructure.Services.Notifications
{
public sealed class ContentEmptiedRecycleBinNotification : EmptiedRecycleBinNotification<IContent>
{
public ContentEmptiedRecycleBinNotification(EventMessages messages) : base(messages)
{
}
}
}

View File

@@ -0,0 +1,15 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models;
namespace Umbraco.Cms.Infrastructure.Services.Notifications
{
public sealed class ContentEmptyingRecycleBinNotification : EmptyingRecycleBinNotification<IContent>
{
public ContentEmptyingRecycleBinNotification(EventMessages messages) : base(messages)
{
}
}
}

Some files were not shown because too many files have changed in this diff Show More