* Sanitize dictionary overview and export actions * Amend dictionary services with async and attempt pattern + isolate temporary file handling in its own service. * Update OpenAPI schema to match new dictionary bulk actions * Implement move API for dictionary items. * Add unit tests for dictionary item move * Fix merge * Update OpenAPI json after merge
319 lines
13 KiB
C#
319 lines
13 KiB
C#
using Microsoft.Extensions.Logging;
|
|
using Umbraco.Cms.Core.Events;
|
|
using Umbraco.Cms.Core.Models;
|
|
using Umbraco.Cms.Core.Notifications;
|
|
using Umbraco.Cms.Core.Persistence.Querying;
|
|
using Umbraco.Cms.Core.Persistence.Repositories;
|
|
using Umbraco.Cms.Core.Scoping;
|
|
using Umbraco.Cms.Core.Services.OperationStatus;
|
|
|
|
namespace Umbraco.Cms.Core.Services;
|
|
|
|
internal sealed class DictionaryItemService : RepositoryService, IDictionaryItemService
|
|
{
|
|
private readonly IDictionaryRepository _dictionaryRepository;
|
|
private readonly IAuditRepository _auditRepository;
|
|
private readonly ILanguageService _languageService;
|
|
|
|
public DictionaryItemService(
|
|
ICoreScopeProvider provider,
|
|
ILoggerFactory loggerFactory,
|
|
IEventMessagesFactory eventMessagesFactory,
|
|
IDictionaryRepository dictionaryRepository,
|
|
IAuditRepository auditRepository,
|
|
ILanguageService languageService)
|
|
: base(provider, loggerFactory, eventMessagesFactory)
|
|
{
|
|
_dictionaryRepository = dictionaryRepository;
|
|
_auditRepository = auditRepository;
|
|
_languageService = languageService;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task<IDictionaryItem?> GetAsync(Guid id)
|
|
{
|
|
using (ScopeProvider.CreateCoreScope(autoComplete: true))
|
|
{
|
|
IDictionaryItem? item = _dictionaryRepository.Get(id);
|
|
return await Task.FromResult(item);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task<IDictionaryItem?> GetAsync(string key)
|
|
{
|
|
using (ScopeProvider.CreateCoreScope(autoComplete: true))
|
|
{
|
|
IDictionaryItem? item = _dictionaryRepository.Get(key);
|
|
return await Task.FromResult(item);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task<IEnumerable<IDictionaryItem>> GetManyAsync(params Guid[] ids)
|
|
{
|
|
using (ScopeProvider.CreateCoreScope(autoComplete: true))
|
|
{
|
|
IEnumerable<IDictionaryItem> items = _dictionaryRepository.GetMany(ids).ToArray();
|
|
return await Task.FromResult(items);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task<IEnumerable<IDictionaryItem>> GetManyAsync(params string[] keys)
|
|
{
|
|
using (ScopeProvider.CreateCoreScope(autoComplete: true))
|
|
{
|
|
IEnumerable<IDictionaryItem> items = _dictionaryRepository.GetManyByKeys(keys).ToArray();
|
|
return await Task.FromResult(items);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task<IEnumerable<IDictionaryItem>> GetChildrenAsync(Guid parentId)
|
|
=> await GetByQueryAsync(Query<IDictionaryItem>().Where(x => x.ParentId == parentId));
|
|
|
|
/// <inheritdoc />
|
|
public async Task<IEnumerable<IDictionaryItem>> GetDescendantsAsync(Guid? parentId)
|
|
{
|
|
using (ScopeProvider.CreateCoreScope(autoComplete: true))
|
|
{
|
|
IDictionaryItem[] items = _dictionaryRepository.GetDictionaryItemDescendants(parentId).ToArray();
|
|
return await Task.FromResult(items);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public async Task<IEnumerable<IDictionaryItem>> GetAtRootAsync()
|
|
=> await GetByQueryAsync(Query<IDictionaryItem>().Where(x => x.ParentId == null));
|
|
|
|
/// <inheritdoc/>
|
|
public async Task<bool> ExistsAsync(string key)
|
|
{
|
|
using (ScopeProvider.CreateCoreScope(autoComplete: true))
|
|
{
|
|
IDictionaryItem? item = _dictionaryRepository.Get(key);
|
|
return await Task.FromResult(item != null);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public async Task<Attempt<IDictionaryItem, DictionaryItemOperationStatus>> CreateAsync(IDictionaryItem dictionaryItem, int userId = Constants.Security.SuperUserId)
|
|
{
|
|
if (dictionaryItem.Id != 0)
|
|
{
|
|
return Attempt.FailWithStatus(DictionaryItemOperationStatus.InvalidId, dictionaryItem);
|
|
}
|
|
|
|
return await SaveAsync(
|
|
dictionaryItem,
|
|
() =>
|
|
{
|
|
if (_dictionaryRepository.Get(dictionaryItem.Key) != null)
|
|
{
|
|
return DictionaryItemOperationStatus.DuplicateKey;
|
|
}
|
|
|
|
return DictionaryItemOperationStatus.Success;
|
|
},
|
|
AuditType.New,
|
|
"Create DictionaryItem",
|
|
userId);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task<Attempt<IDictionaryItem, DictionaryItemOperationStatus>> UpdateAsync(
|
|
IDictionaryItem dictionaryItem, int userId = Constants.Security.SuperUserId)
|
|
=> await SaveAsync(
|
|
dictionaryItem,
|
|
() =>
|
|
{
|
|
// is there an item to update?
|
|
if (_dictionaryRepository.Exists(dictionaryItem.Id) == false)
|
|
{
|
|
return DictionaryItemOperationStatus.ItemNotFound;
|
|
}
|
|
|
|
return DictionaryItemOperationStatus.Success;
|
|
},
|
|
AuditType.Save,
|
|
"Update DictionaryItem",
|
|
userId);
|
|
|
|
/// <inheritdoc />
|
|
public async Task<Attempt<IDictionaryItem?, DictionaryItemOperationStatus>> DeleteAsync(Guid id, int userId = Constants.Security.SuperUserId)
|
|
{
|
|
using (ICoreScope scope = ScopeProvider.CreateCoreScope())
|
|
{
|
|
IDictionaryItem? dictionaryItem = _dictionaryRepository.Get(id);
|
|
if (dictionaryItem == null)
|
|
{
|
|
return Attempt.FailWithStatus<IDictionaryItem?, DictionaryItemOperationStatus>(DictionaryItemOperationStatus.ItemNotFound, null);
|
|
}
|
|
|
|
EventMessages eventMessages = EventMessagesFactory.Get();
|
|
var deletingNotification = new DictionaryItemDeletingNotification(dictionaryItem, eventMessages);
|
|
if (await scope.Notifications.PublishCancelableAsync(deletingNotification))
|
|
{
|
|
scope.Complete();
|
|
return Attempt.FailWithStatus<IDictionaryItem?, DictionaryItemOperationStatus>(DictionaryItemOperationStatus.CancelledByNotification, dictionaryItem);
|
|
}
|
|
|
|
_dictionaryRepository.Delete(dictionaryItem);
|
|
scope.Notifications.Publish(
|
|
new DictionaryItemDeletedNotification(dictionaryItem, eventMessages)
|
|
.WithStateFrom(deletingNotification));
|
|
|
|
Audit(AuditType.Delete, "Delete DictionaryItem", userId, dictionaryItem.Id, nameof(DictionaryItem));
|
|
|
|
scope.Complete();
|
|
return await Task.FromResult(Attempt.SucceedWithStatus<IDictionaryItem?, DictionaryItemOperationStatus>(DictionaryItemOperationStatus.Success, dictionaryItem));
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public async Task<Attempt<IDictionaryItem, DictionaryItemOperationStatus>> MoveAsync(
|
|
IDictionaryItem dictionaryItem,
|
|
Guid? parentId,
|
|
int userId = Constants.Security.SuperUserId)
|
|
{
|
|
// same parent? then just ignore this operation, assume success.
|
|
if (dictionaryItem.ParentId == parentId)
|
|
{
|
|
return Attempt.SucceedWithStatus(DictionaryItemOperationStatus.Success, dictionaryItem);
|
|
}
|
|
|
|
// cannot move a dictionary item underneath itself
|
|
if (dictionaryItem.Key == parentId)
|
|
{
|
|
return Attempt.FailWithStatus(DictionaryItemOperationStatus.InvalidParent, dictionaryItem);
|
|
}
|
|
|
|
using (ICoreScope scope = ScopeProvider.CreateCoreScope())
|
|
{
|
|
IDictionaryItem? parent = parentId.HasValue ? _dictionaryRepository.Get(parentId.Value) : null;
|
|
|
|
// validate parent if applicable
|
|
if (parentId.HasValue && parent == null)
|
|
{
|
|
return Attempt.FailWithStatus(DictionaryItemOperationStatus.ParentNotFound, dictionaryItem);
|
|
}
|
|
|
|
// ensure we don't move a dictionary item underneath one of its own descendants
|
|
if (parent != null)
|
|
{
|
|
IEnumerable<IDictionaryItem> descendants = _dictionaryRepository.GetDictionaryItemDescendants(dictionaryItem.Key);
|
|
if (descendants.Any(item => item.Key == parent.Key))
|
|
{
|
|
return Attempt.FailWithStatus(DictionaryItemOperationStatus.InvalidParent, dictionaryItem);
|
|
}
|
|
}
|
|
|
|
dictionaryItem.ParentId = parentId;
|
|
|
|
EventMessages eventMessages = EventMessagesFactory.Get();
|
|
var moveEventInfo = new MoveEventInfo<IDictionaryItem>(dictionaryItem, string.Empty, parent?.Id ?? Constants.System.Root);
|
|
var movingNotification = new DictionaryItemMovingNotification(moveEventInfo, eventMessages);
|
|
if (await scope.Notifications.PublishCancelableAsync(movingNotification))
|
|
{
|
|
scope.Complete();
|
|
return Attempt.FailWithStatus(DictionaryItemOperationStatus.CancelledByNotification, dictionaryItem);
|
|
}
|
|
|
|
_dictionaryRepository.Save(dictionaryItem);
|
|
|
|
scope.Notifications.Publish(
|
|
new DictionaryItemMovedNotification(moveEventInfo, eventMessages).WithStateFrom(movingNotification));
|
|
|
|
Audit(AuditType.Move, "Move DictionaryItem", userId, dictionaryItem.Id, nameof(DictionaryItem));
|
|
scope.Complete();
|
|
|
|
return await Task.FromResult(Attempt.SucceedWithStatus(DictionaryItemOperationStatus.Success, dictionaryItem));
|
|
}
|
|
}
|
|
|
|
private async Task<Attempt<IDictionaryItem, DictionaryItemOperationStatus>> SaveAsync(
|
|
IDictionaryItem dictionaryItem,
|
|
Func<DictionaryItemOperationStatus> operationValidation,
|
|
AuditType auditType,
|
|
string auditMessage,
|
|
int userId)
|
|
{
|
|
using (ICoreScope scope = ScopeProvider.CreateCoreScope())
|
|
{
|
|
DictionaryItemOperationStatus status = operationValidation();
|
|
if (status != DictionaryItemOperationStatus.Success)
|
|
{
|
|
return Attempt.FailWithStatus(status, dictionaryItem);
|
|
}
|
|
|
|
// validate the parent
|
|
if (HasValidParent(dictionaryItem) == false)
|
|
{
|
|
return Attempt.FailWithStatus(DictionaryItemOperationStatus.ParentNotFound, dictionaryItem);
|
|
}
|
|
|
|
// do we have an item key collision (item keys must be unique)?
|
|
if (HasItemKeyCollision(dictionaryItem))
|
|
{
|
|
return Attempt.FailWithStatus(DictionaryItemOperationStatus.DuplicateItemKey, dictionaryItem);
|
|
}
|
|
|
|
// ensure valid languages for all translations
|
|
ILanguage[] allLanguages = (await _languageService.GetAllAsync()).ToArray();
|
|
RemoveInvalidTranslations(dictionaryItem, allLanguages);
|
|
|
|
EventMessages eventMessages = EventMessagesFactory.Get();
|
|
var savingNotification = new DictionaryItemSavingNotification(dictionaryItem, eventMessages);
|
|
if (await scope.Notifications.PublishCancelableAsync(savingNotification))
|
|
{
|
|
scope.Complete();
|
|
return Attempt.FailWithStatus(DictionaryItemOperationStatus.CancelledByNotification, dictionaryItem);
|
|
}
|
|
|
|
_dictionaryRepository.Save(dictionaryItem);
|
|
|
|
scope.Notifications.Publish(
|
|
new DictionaryItemSavedNotification(dictionaryItem, eventMessages).WithStateFrom(savingNotification));
|
|
|
|
Audit(auditType, auditMessage, userId, dictionaryItem.Id, nameof(DictionaryItem));
|
|
scope.Complete();
|
|
|
|
return await Task.FromResult(Attempt.SucceedWithStatus(DictionaryItemOperationStatus.Success, dictionaryItem));
|
|
}
|
|
}
|
|
|
|
private async Task<IEnumerable<IDictionaryItem>> GetByQueryAsync(IQuery<IDictionaryItem> query)
|
|
{
|
|
using (ScopeProvider.CreateCoreScope(autoComplete: true))
|
|
{
|
|
IDictionaryItem[] items = _dictionaryRepository.Get(query).ToArray();
|
|
return await Task.FromResult(items);
|
|
}
|
|
}
|
|
|
|
private void Audit(AuditType type, string message, int userId, int objectId, string? entityType) =>
|
|
_auditRepository.Save(new AuditItem(objectId, type, userId, entityType, message));
|
|
|
|
private bool HasValidParent(IDictionaryItem dictionaryItem)
|
|
=> dictionaryItem.ParentId.HasValue == false || _dictionaryRepository.Get(dictionaryItem.ParentId.Value) != null;
|
|
|
|
private void RemoveInvalidTranslations(IDictionaryItem dictionaryItem, IEnumerable<ILanguage> allLanguages)
|
|
{
|
|
IDictionaryTranslation[] translationsAsArray = dictionaryItem.Translations.ToArray();
|
|
if (translationsAsArray.Any() == false)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var allLanguageIsoCodes = allLanguages.Select(language => language.IsoCode).ToArray();
|
|
dictionaryItem.Translations = translationsAsArray.Where(translation => allLanguageIsoCodes.Contains(translation.LanguageIsoCode)).ToArray();
|
|
}
|
|
|
|
private bool HasItemKeyCollision(IDictionaryItem dictionaryItem)
|
|
{
|
|
IDictionaryItem? itemKeyCollision = _dictionaryRepository.Get(dictionaryItem.ItemKey);
|
|
return itemKeyCollision != null && itemKeyCollision.Key != dictionaryItem.Key;
|
|
}
|
|
}
|