Merge remote-tracking branch 'origin/v9/dev' into v10/dev
# Conflicts: # build/azure-pipelines.yml # src/Umbraco.Core/Routing/DefaultUrlProvider.cs # src/Umbraco.Core/Routing/UrlProviderExtensions.cs # src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs # src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs # src/Umbraco.Infrastructure/Services/Implement/ContentService.cs # src/Umbraco.PublishedCache.NuCache/DataSource/BTree.ContentDataSerializer.cs # src/Umbraco.PublishedCache.NuCache/Persistence/NuCacheContentRepository.cs # src/Umbraco.Web.UI.Client/package-lock.json # tests/Umbraco.Tests.AcceptanceTest/package-lock.json # tests/Umbraco.Tests.AcceptanceTest/package.json # tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServiceTests.cs
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -134,6 +134,9 @@ namespace Umbraco.Cms.Core.Services.Implement
|
||||
stack.Push(c);
|
||||
}
|
||||
|
||||
var duplicatePropertyTypeAliases = new List<string>();
|
||||
var invalidPropertyGroupAliases = new List<string>();
|
||||
|
||||
foreach (var dependency in dependencies)
|
||||
{
|
||||
if (dependency.Id == compositionContentType.Id)
|
||||
@@ -143,13 +146,14 @@ namespace Umbraco.Cms.Core.Services.Implement
|
||||
if (contentTypeDependency == null)
|
||||
continue;
|
||||
|
||||
var duplicatePropertyTypeAliases = contentTypeDependency.PropertyTypes.Select(x => x.Alias).Intersect(propertyTypeAliases, StringComparer.InvariantCultureIgnoreCase).ToArray();
|
||||
var invalidPropertyGroupAliases = contentTypeDependency.PropertyGroups.Where(x => propertyGroupAliases.TryGetValue(x.Alias, out var type) && type != x.Type).Select(x => x.Alias).ToArray();
|
||||
duplicatePropertyTypeAliases.AddRange(contentTypeDependency.PropertyTypes.Select(x => x.Alias).Intersect(propertyTypeAliases, StringComparer.InvariantCultureIgnoreCase));
|
||||
invalidPropertyGroupAliases.AddRange(contentTypeDependency.PropertyGroups.Where(x => propertyGroupAliases.TryGetValue(x.Alias, out var type) && type != x.Type).Select(x => x.Alias));
|
||||
}
|
||||
|
||||
if (duplicatePropertyTypeAliases.Length == 0 && invalidPropertyGroupAliases.Length == 0)
|
||||
continue;
|
||||
if (duplicatePropertyTypeAliases.Count > 0 || invalidPropertyGroupAliases.Count > 0)
|
||||
|
||||
throw new InvalidCompositionException(compositionContentType.Alias, null, duplicatePropertyTypeAliases, invalidPropertyGroupAliases);
|
||||
{
|
||||
throw new InvalidCompositionException(compositionContentType.Alias, null, duplicatePropertyTypeAliases.Distinct().ToArray(), invalidPropertyGroupAliases.Distinct().ToArray());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Umbraco.Cms.Core.Events;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Notifications;
|
||||
using Umbraco.Cms.Core.Persistence.Repositories;
|
||||
using Umbraco.Cms.Core.Scoping;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
// ReSharper disable once CheckNamespace
|
||||
namespace Umbraco.Cms.Core.Services.Implement
|
||||
{
|
||||
internal class ContentVersionService : IContentVersionService
|
||||
{
|
||||
private readonly ILogger<ContentVersionService> _logger;
|
||||
private readonly IDocumentVersionRepository _documentVersionRepository;
|
||||
private readonly IContentVersionCleanupPolicy _contentVersionCleanupPolicy;
|
||||
private readonly IScopeProvider _scopeProvider;
|
||||
private readonly IEventMessagesFactory _eventMessagesFactory;
|
||||
private readonly IAuditRepository _auditRepository;
|
||||
private readonly ILanguageRepository _languageRepository;
|
||||
|
||||
public ContentVersionService(
|
||||
ILogger<ContentVersionService> logger,
|
||||
IDocumentVersionRepository documentVersionRepository,
|
||||
IContentVersionCleanupPolicy contentVersionCleanupPolicy,
|
||||
IScopeProvider scopeProvider,
|
||||
IEventMessagesFactory eventMessagesFactory,
|
||||
IAuditRepository auditRepository,
|
||||
ILanguageRepository languageRepository)
|
||||
{
|
||||
_logger = logger;
|
||||
_documentVersionRepository = documentVersionRepository;
|
||||
_contentVersionCleanupPolicy = contentVersionCleanupPolicy;
|
||||
_scopeProvider = scopeProvider;
|
||||
_eventMessagesFactory = eventMessagesFactory;
|
||||
_auditRepository = auditRepository;
|
||||
_languageRepository = languageRepository;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyCollection<ContentVersionMeta> PerformContentVersionCleanup(DateTime asAtDate)
|
||||
{
|
||||
// Media - ignored
|
||||
// Members - ignored
|
||||
return CleanupDocumentVersions(asAtDate);
|
||||
}
|
||||
|
||||
private IReadOnlyCollection<ContentVersionMeta> CleanupDocumentVersions(DateTime asAtDate)
|
||||
{
|
||||
List<ContentVersionMeta> versionsToDelete;
|
||||
|
||||
/* Why so many scopes?
|
||||
*
|
||||
* We could just work out the set to delete at SQL infra level which was the original plan, however we agreed that really we should fire
|
||||
* ContentService.DeletingVersions so people can hook & cancel if required.
|
||||
*
|
||||
* On first time run of cleanup on a site with a lot of history there may be a lot of historic ContentVersions to remove e.g. 200K for our.umbraco.com.
|
||||
* If we weren't supporting SQL CE we could do TVP, or use temp tables to bulk delete with joins to our list of version ids to nuke.
|
||||
* (much nicer, we can kill 100k in sub second time-frames).
|
||||
*
|
||||
* However we are supporting SQL CE, so the easiest thing to do is use the Umbraco InGroupsOf helper to create a query with 2K args of version
|
||||
* ids to delete at a time.
|
||||
*
|
||||
* This is already done at the repository level, however if we only had a single scope at service level we're still locking
|
||||
* the ContentVersions table (and other related tables) for a couple of minutes which makes the back office unusable.
|
||||
*
|
||||
* As a quick fix, we can also use InGroupsOf at service level, create a scope per group to give other connections a chance
|
||||
* to grab the locks and execute their queries.
|
||||
*
|
||||
* This makes the back office a tiny bit sluggish during first run but it is usable for loading tree and publishing content.
|
||||
*
|
||||
* There are optimizations we can do, we could add a bulk delete for SqlServerSyntaxProvider which differs in implementation
|
||||
* and fallback to this naive approach only for SQL CE, however we agreed it is not worth the effort as this is a one time pain,
|
||||
* subsequent runs shouldn't have huge numbers of versions to cleanup.
|
||||
*
|
||||
* tl;dr lots of scopes to enable other connections to use the DB whilst we work.
|
||||
*/
|
||||
using (IScope scope = _scopeProvider.CreateScope(autoComplete: true))
|
||||
{
|
||||
IReadOnlyCollection<ContentVersionMeta> allHistoricVersions = _documentVersionRepository.GetDocumentVersionsEligibleForCleanup();
|
||||
|
||||
_logger.LogDebug("Discovered {count} candidate(s) for ContentVersion cleanup", allHistoricVersions.Count);
|
||||
versionsToDelete = new List<ContentVersionMeta>(allHistoricVersions.Count);
|
||||
|
||||
IEnumerable<ContentVersionMeta> filteredContentVersions = _contentVersionCleanupPolicy.Apply(asAtDate, allHistoricVersions);
|
||||
|
||||
foreach (ContentVersionMeta version in filteredContentVersions)
|
||||
{
|
||||
EventMessages messages = _eventMessagesFactory.Get();
|
||||
|
||||
if (scope.Notifications.PublishCancelable(new ContentDeletingVersionsNotification(version.ContentId, messages, version.VersionId)))
|
||||
{
|
||||
_logger.LogDebug("Delete cancelled for ContentVersion [{versionId}]", version.VersionId);
|
||||
continue;
|
||||
}
|
||||
|
||||
versionsToDelete.Add(version);
|
||||
}
|
||||
}
|
||||
|
||||
if (!versionsToDelete.Any())
|
||||
{
|
||||
_logger.LogDebug("No remaining ContentVersions for cleanup");
|
||||
return Array.Empty<ContentVersionMeta>();
|
||||
}
|
||||
|
||||
_logger.LogDebug("Removing {count} ContentVersion(s)", versionsToDelete.Count);
|
||||
|
||||
foreach (IEnumerable<ContentVersionMeta> group in versionsToDelete.InGroupsOf(Constants.Sql.MaxParameterCount))
|
||||
{
|
||||
using (IScope scope = _scopeProvider.CreateScope(autoComplete: true))
|
||||
{
|
||||
scope.WriteLock(Constants.Locks.ContentTree);
|
||||
var groupEnumerated = group.ToList();
|
||||
_documentVersionRepository.DeleteVersions(groupEnumerated.Select(x => x.VersionId));
|
||||
|
||||
foreach (ContentVersionMeta version in groupEnumerated)
|
||||
{
|
||||
EventMessages messages = _eventMessagesFactory.Get();
|
||||
|
||||
scope.Notifications.Publish(new ContentDeletedVersionsNotification(version.ContentId, messages, version.VersionId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
using (_scopeProvider.CreateScope(autoComplete: true))
|
||||
{
|
||||
Audit(AuditType.Delete, Constants.Security.SuperUserId, -1, $"Removed {versionsToDelete.Count} ContentVersion(s) according to cleanup policy");
|
||||
}
|
||||
|
||||
return versionsToDelete;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<ContentVersionMeta> GetPagedContentVersions(int contentId, long pageIndex, int pageSize, out long totalRecords, string culture = null)
|
||||
{
|
||||
if (pageIndex < 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(pageIndex));
|
||||
}
|
||||
|
||||
if (pageSize <= 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(pageSize));
|
||||
}
|
||||
|
||||
using (IScope scope = _scopeProvider.CreateScope(autoComplete: true))
|
||||
{
|
||||
var languageId = _languageRepository.GetIdByIsoCode(culture, throwOnNotFound: true);
|
||||
scope.ReadLock(Constants.Locks.ContentTree);
|
||||
return _documentVersionRepository.GetPagedItemsByContentId(contentId, pageIndex, pageSize, out totalRecords, languageId);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetPreventCleanup(int versionId, bool preventCleanup, int userId = -1)
|
||||
{
|
||||
using (IScope scope = _scopeProvider.CreateScope(autoComplete: true))
|
||||
{
|
||||
scope.WriteLock(Constants.Locks.ContentTree);
|
||||
_documentVersionRepository.SetPreventCleanup(versionId, preventCleanup);
|
||||
|
||||
ContentVersionMeta version = _documentVersionRepository.Get(versionId);
|
||||
|
||||
AuditType auditType = preventCleanup
|
||||
? AuditType.ContentVersionPreventCleanup
|
||||
: AuditType.ContentVersionEnableCleanup;
|
||||
|
||||
var message = $"set preventCleanup = '{preventCleanup}' for version '{versionId}'";
|
||||
|
||||
Audit(auditType, userId, version.ContentId, message, $"{version.VersionDate}");
|
||||
}
|
||||
}
|
||||
|
||||
private void Audit(AuditType type, int userId, int objectId, string message = null, string parameters = null)
|
||||
{
|
||||
var entry = new AuditItem(
|
||||
objectId,
|
||||
type,
|
||||
userId,
|
||||
UmbracoObjectTypes.Document.GetName(),
|
||||
message,
|
||||
parameters);
|
||||
|
||||
_auditRepository.Save(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Persistence.Repositories;
|
||||
using Umbraco.Cms.Core.Scoping;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using ContentVersionCleanupPolicySettings = Umbraco.Cms.Core.Models.ContentVersionCleanupPolicySettings;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Services.Implement
|
||||
{
|
||||
public class DefaultContentVersionCleanupPolicy : IContentVersionCleanupPolicy
|
||||
{
|
||||
private readonly IOptions<ContentSettings> _contentSettings;
|
||||
private readonly IScopeProvider _scopeProvider;
|
||||
private readonly IDocumentVersionRepository _documentVersionRepository;
|
||||
|
||||
public DefaultContentVersionCleanupPolicy(IOptions<ContentSettings> contentSettings, IScopeProvider scopeProvider, IDocumentVersionRepository documentVersionRepository)
|
||||
{
|
||||
_contentSettings = contentSettings ?? throw new ArgumentNullException(nameof(contentSettings));
|
||||
_scopeProvider = scopeProvider ?? throw new ArgumentNullException(nameof(scopeProvider));
|
||||
_documentVersionRepository = documentVersionRepository ?? throw new ArgumentNullException(nameof(documentVersionRepository));
|
||||
}
|
||||
|
||||
public IEnumerable<ContentVersionMeta> Apply(DateTime asAtDate, IEnumerable<ContentVersionMeta> items)
|
||||
{
|
||||
// Note: Not checking global enable flag, that's handled in the scheduled job.
|
||||
// If this method is called and policy is globally disabled someone has chosen to run in code.
|
||||
|
||||
var globalPolicy = _contentSettings.Value.ContentVersionCleanupPolicy;
|
||||
|
||||
var theRest = new List<ContentVersionMeta>();
|
||||
|
||||
using(_scopeProvider.CreateScope(autoComplete: true))
|
||||
{
|
||||
var policyOverrides = _documentVersionRepository.GetCleanupPolicies()
|
||||
.ToDictionary(x => x.ContentTypeId);
|
||||
|
||||
foreach (var version in items)
|
||||
{
|
||||
var age = asAtDate - version.VersionDate;
|
||||
|
||||
var overrides = GetOverridePolicy(version, policyOverrides);
|
||||
|
||||
var keepAll = overrides?.KeepAllVersionsNewerThanDays ?? globalPolicy.KeepAllVersionsNewerThanDays!;
|
||||
var keepLatest = overrides?.KeepLatestVersionPerDayForDays ?? globalPolicy.KeepLatestVersionPerDayForDays;
|
||||
var preventCleanup = overrides?.PreventCleanup ?? false;
|
||||
|
||||
if (preventCleanup)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (age.TotalDays <= keepAll)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (age.TotalDays > keepLatest)
|
||||
{
|
||||
|
||||
yield return version;
|
||||
continue;
|
||||
}
|
||||
|
||||
theRest.Add(version);
|
||||
}
|
||||
|
||||
var grouped = theRest.GroupBy(x => new
|
||||
{
|
||||
x.ContentId,
|
||||
x.VersionDate.Date
|
||||
});
|
||||
|
||||
foreach (var group in grouped)
|
||||
{
|
||||
foreach (var version in group.OrderByDescending(x => x.VersionId).Skip(1))
|
||||
{
|
||||
yield return version;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private ContentVersionCleanupPolicySettings GetOverridePolicy(
|
||||
ContentVersionMeta version,
|
||||
IDictionary<int, ContentVersionCleanupPolicySettings> overrides)
|
||||
{
|
||||
_ = overrides.TryGetValue(version.ContentTypeId, out var value);
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,63 +8,85 @@ using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Umbraco.Cms.Core.Services.Implement
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public class LocalizedTextService : ILocalizedTextService
|
||||
{
|
||||
private readonly ILogger<LocalizedTextService> _logger;
|
||||
private readonly Lazy<LocalizedTextServiceFileSources> _fileSources;
|
||||
private IDictionary<CultureInfo, IDictionary<string, IDictionary<string, string>>> _dictionarySource => _dictionarySourceLazy.Value;
|
||||
private IDictionary<CultureInfo, IDictionary<string, string>> _noAreaDictionarySource => _noAreaDictionarySourceLazy.Value;
|
||||
private readonly Lazy<IDictionary<CultureInfo, IDictionary<string, IDictionary<string, string>>>> _dictionarySourceLazy;
|
||||
private readonly Lazy<IDictionary<CultureInfo, IDictionary<string, string>>> _noAreaDictionarySourceLazy;
|
||||
private readonly char[] _splitter = new[] { '/' };
|
||||
|
||||
private IDictionary<CultureInfo, Lazy<IDictionary<string, IDictionary<string, string>>>> _dictionarySource =>
|
||||
_dictionarySourceLazy.Value;
|
||||
|
||||
private IDictionary<CultureInfo, Lazy<IDictionary<string, string>>> _noAreaDictionarySource =>
|
||||
_noAreaDictionarySourceLazy.Value;
|
||||
|
||||
private readonly Lazy<IDictionary<CultureInfo, Lazy<IDictionary<string, IDictionary<string, string>>>>>
|
||||
_dictionarySourceLazy;
|
||||
|
||||
private readonly Lazy<IDictionary<CultureInfo, Lazy<IDictionary<string, string>>>> _noAreaDictionarySourceLazy;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes with a file sources instance
|
||||
/// </summary>
|
||||
/// <param name="fileSources"></param>
|
||||
/// <param name="logger"></param>
|
||||
public LocalizedTextService(Lazy<LocalizedTextServiceFileSources> fileSources, ILogger<LocalizedTextService> logger)
|
||||
public LocalizedTextService(Lazy<LocalizedTextServiceFileSources> fileSources,
|
||||
ILogger<LocalizedTextService> logger)
|
||||
{
|
||||
if (logger == null) throw new ArgumentNullException(nameof(logger));
|
||||
_logger = logger;
|
||||
if (fileSources == null) throw new ArgumentNullException(nameof(fileSources));
|
||||
_dictionarySourceLazy = new Lazy<IDictionary<CultureInfo, IDictionary<string, IDictionary<string, string>>>>(() => FileSourcesToAreaDictionarySources(fileSources.Value));
|
||||
_noAreaDictionarySourceLazy = new Lazy<IDictionary<CultureInfo, IDictionary<string, string>>>(() => FileSourcesToNoAreaDictionarySources(fileSources.Value));
|
||||
_dictionarySourceLazy =
|
||||
new Lazy<IDictionary<CultureInfo, Lazy<IDictionary<string, IDictionary<string, string>>>>>(() =>
|
||||
FileSourcesToAreaDictionarySources(fileSources.Value));
|
||||
_noAreaDictionarySourceLazy =
|
||||
new Lazy<IDictionary<CultureInfo, Lazy<IDictionary<string, string>>>>(() =>
|
||||
FileSourcesToNoAreaDictionarySources(fileSources.Value));
|
||||
_fileSources = fileSources;
|
||||
}
|
||||
|
||||
private IDictionary<CultureInfo, IDictionary<string, string>> FileSourcesToNoAreaDictionarySources(LocalizedTextServiceFileSources fileSources)
|
||||
private IDictionary<CultureInfo, Lazy<IDictionary<string, string>>> FileSourcesToNoAreaDictionarySources(
|
||||
LocalizedTextServiceFileSources fileSources)
|
||||
{
|
||||
var xmlSources = fileSources.GetXmlSources();
|
||||
|
||||
return XmlSourceToNoAreaDictionary(xmlSources);
|
||||
}
|
||||
|
||||
private IDictionary<CultureInfo, IDictionary<string, string>> XmlSourceToNoAreaDictionary(IDictionary<CultureInfo, Lazy<XDocument>> xmlSources)
|
||||
private IDictionary<CultureInfo, Lazy<IDictionary<string, string>>> XmlSourceToNoAreaDictionary(
|
||||
IDictionary<CultureInfo, Lazy<XDocument>> xmlSources)
|
||||
{
|
||||
var cultureNoAreaDictionary = new Dictionary<CultureInfo, IDictionary<string, string>>();
|
||||
var cultureNoAreaDictionary = new Dictionary<CultureInfo, Lazy<IDictionary<string, string>>>();
|
||||
foreach (var xmlSource in xmlSources)
|
||||
{
|
||||
var noAreaAliasValue = GetNoAreaStoredTranslations(xmlSources, xmlSource.Key);
|
||||
var noAreaAliasValue =
|
||||
new Lazy<IDictionary<string, string>>(() => GetNoAreaStoredTranslations(xmlSources, xmlSource.Key));
|
||||
cultureNoAreaDictionary.Add(xmlSource.Key, noAreaAliasValue);
|
||||
}
|
||||
|
||||
return cultureNoAreaDictionary;
|
||||
}
|
||||
|
||||
private IDictionary<CultureInfo, IDictionary<string, IDictionary<string, string>>> FileSourcesToAreaDictionarySources(LocalizedTextServiceFileSources fileSources)
|
||||
private IDictionary<CultureInfo, Lazy<IDictionary<string, IDictionary<string, string>>>>
|
||||
FileSourcesToAreaDictionarySources(LocalizedTextServiceFileSources fileSources)
|
||||
{
|
||||
var xmlSources = fileSources.GetXmlSources();
|
||||
return XmlSourcesToAreaDictionary(xmlSources);
|
||||
}
|
||||
|
||||
private IDictionary<CultureInfo, IDictionary<string, IDictionary<string, string>>> XmlSourcesToAreaDictionary(IDictionary<CultureInfo, Lazy<XDocument>> xmlSources)
|
||||
private IDictionary<CultureInfo, Lazy<IDictionary<string, IDictionary<string, string>>>>
|
||||
XmlSourcesToAreaDictionary(IDictionary<CultureInfo, Lazy<XDocument>> xmlSources)
|
||||
{
|
||||
var cultureDictionary = new Dictionary<CultureInfo, IDictionary<string, IDictionary<string, string>>>();
|
||||
var cultureDictionary =
|
||||
new Dictionary<CultureInfo, Lazy<IDictionary<string, IDictionary<string, string>>>>();
|
||||
foreach (var xmlSource in xmlSources)
|
||||
{
|
||||
var areaAliaValue = GetAreaStoredTranslations(xmlSources, xmlSource.Key);
|
||||
var areaAliaValue =
|
||||
new Lazy<IDictionary<string, IDictionary<string, string>>>(() =>
|
||||
GetAreaStoredTranslations(xmlSources, xmlSource.Key));
|
||||
cultureDictionary.Add(xmlSource.Key, areaAliaValue);
|
||||
|
||||
}
|
||||
|
||||
return cultureDictionary;
|
||||
}
|
||||
|
||||
@@ -73,45 +95,69 @@ namespace Umbraco.Cms.Core.Services.Implement
|
||||
/// </summary>
|
||||
/// <param name="source"></param>
|
||||
/// <param name="logger"></param>
|
||||
public LocalizedTextService(IDictionary<CultureInfo, Lazy<XDocument>> source, ILogger<LocalizedTextService> logger)
|
||||
public LocalizedTextService(IDictionary<CultureInfo, Lazy<XDocument>> source,
|
||||
ILogger<LocalizedTextService> logger)
|
||||
{
|
||||
if (source == null) throw new ArgumentNullException(nameof(source));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
|
||||
_dictionarySourceLazy = new Lazy<IDictionary<CultureInfo, IDictionary<string, IDictionary<string, string>>>>(() => XmlSourcesToAreaDictionary(source));
|
||||
_noAreaDictionarySourceLazy = new Lazy<IDictionary<CultureInfo, IDictionary<string, string>>>(() => XmlSourceToNoAreaDictionary(source));
|
||||
|
||||
_dictionarySourceLazy =
|
||||
new Lazy<IDictionary<CultureInfo, Lazy<IDictionary<string, IDictionary<string, string>>>>>(() =>
|
||||
XmlSourcesToAreaDictionary(source));
|
||||
_noAreaDictionarySourceLazy =
|
||||
new Lazy<IDictionary<CultureInfo, Lazy<IDictionary<string, string>>>>(() =>
|
||||
XmlSourceToNoAreaDictionary(source));
|
||||
}
|
||||
|
||||
[Obsolete("Use other ctor with IDictionary<CultureInfo, Lazy<IDictionary<string, IDictionary<string, string>>>> as input parameter.")]
|
||||
public LocalizedTextService(IDictionary<CultureInfo, IDictionary<string, IDictionary<string, string>>> source,
|
||||
ILogger<LocalizedTextService> logger) : this(source.ToDictionary(x=>x.Key, x=> new Lazy<IDictionary<string, IDictionary<string, string>>>(() => x.Value)), logger)
|
||||
{
|
||||
|
||||
}
|
||||
/// <summary>
|
||||
/// Initializes with a source of a dictionary of culture -> areas -> sub dictionary of keys/values
|
||||
/// </summary>
|
||||
/// <param name="source"></param>
|
||||
/// <param name="logger"></param>
|
||||
public LocalizedTextService(IDictionary<CultureInfo, IDictionary<string, IDictionary<string, string>>> source, ILogger<LocalizedTextService> logger)
|
||||
public LocalizedTextService(
|
||||
IDictionary<CultureInfo, Lazy<IDictionary<string, IDictionary<string, string>>>> source,
|
||||
ILogger<LocalizedTextService> logger)
|
||||
{
|
||||
var dictionarySource = source ?? throw new ArgumentNullException(nameof(source));
|
||||
_dictionarySourceLazy = new Lazy<IDictionary<CultureInfo, IDictionary<string, IDictionary<string, string>>>>(() => dictionarySource);
|
||||
_dictionarySourceLazy =
|
||||
new Lazy<IDictionary<CultureInfo, Lazy<IDictionary<string, IDictionary<string, string>>>>>(() =>
|
||||
dictionarySource);
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
var cultureNoAreaDictionary = new Dictionary<CultureInfo, IDictionary<string, string>>();
|
||||
var cultureNoAreaDictionary = new Dictionary<CultureInfo, Lazy<IDictionary<string, string>>>();
|
||||
foreach (var cultureDictionary in dictionarySource)
|
||||
{
|
||||
var areaAliaValue = GetAreaStoredTranslations(source, cultureDictionary.Key);
|
||||
var aliasValue = new Dictionary<string, string>();
|
||||
foreach (var area in areaAliaValue)
|
||||
|
||||
cultureNoAreaDictionary.Add(cultureDictionary.Key,
|
||||
new Lazy<IDictionary<string, string>>(() => GetAliasValues(areaAliaValue)));
|
||||
}
|
||||
|
||||
_noAreaDictionarySourceLazy =
|
||||
new Lazy<IDictionary<CultureInfo, Lazy<IDictionary<string, string>>>>(() => cultureNoAreaDictionary);
|
||||
}
|
||||
|
||||
private static Dictionary<string, string> GetAliasValues(
|
||||
Dictionary<string, IDictionary<string, string>> areaAliaValue)
|
||||
{
|
||||
var aliasValue = new Dictionary<string, string>();
|
||||
foreach (var area in areaAliaValue)
|
||||
{
|
||||
foreach (var alias in area.Value)
|
||||
{
|
||||
foreach (var alias in area.Value)
|
||||
if (!aliasValue.ContainsKey(alias.Key))
|
||||
{
|
||||
if (!aliasValue.ContainsKey(alias.Key))
|
||||
{
|
||||
aliasValue.Add(alias.Key, alias.Value);
|
||||
}
|
||||
aliasValue.Add(alias.Key, alias.Value);
|
||||
}
|
||||
}
|
||||
cultureNoAreaDictionary.Add(cultureDictionary.Key, aliasValue);
|
||||
}
|
||||
_noAreaDictionarySourceLazy = new Lazy<IDictionary<CultureInfo, IDictionary<string, string>>>(() => cultureNoAreaDictionary);
|
||||
|
||||
return aliasValue;
|
||||
}
|
||||
|
||||
public string Localize(string key, CultureInfo culture, IDictionary<string, string> tokens = null)
|
||||
@@ -122,12 +168,14 @@ namespace Umbraco.Cms.Core.Services.Implement
|
||||
if (string.IsNullOrEmpty(key))
|
||||
return string.Empty;
|
||||
|
||||
var keyParts = key.Split(_splitter, StringSplitOptions.RemoveEmptyEntries);
|
||||
var keyParts = key.Split(Constants.CharArrays.ForwardSlash, StringSplitOptions.RemoveEmptyEntries);
|
||||
var area = keyParts.Length > 1 ? keyParts[0] : null;
|
||||
var alias = keyParts.Length > 1 ? keyParts[1] : keyParts[0];
|
||||
return Localize(area, alias, culture, tokens);
|
||||
}
|
||||
public string Localize(string area, string alias, CultureInfo culture, IDictionary<string, string> tokens = null)
|
||||
|
||||
public string Localize(string area, string alias, CultureInfo culture,
|
||||
IDictionary<string, string> tokens = null)
|
||||
{
|
||||
if (culture == null) throw new ArgumentNullException(nameof(culture));
|
||||
|
||||
@@ -153,12 +201,15 @@ namespace Umbraco.Cms.Core.Services.Implement
|
||||
|
||||
if (_dictionarySource.ContainsKey(culture) == false)
|
||||
{
|
||||
_logger.LogWarning("The culture specified {Culture} was not found in any configured sources for this service", culture);
|
||||
_logger.LogWarning(
|
||||
"The culture specified {Culture} was not found in any configured sources for this service",
|
||||
culture);
|
||||
return new Dictionary<string, string>(0);
|
||||
}
|
||||
|
||||
IDictionary<string, string> result = new Dictionary<string, string>();
|
||||
//convert all areas + keys to a single key with a '/'
|
||||
foreach (var area in _dictionarySource[culture])
|
||||
foreach (var area in _dictionarySource[culture].Value)
|
||||
{
|
||||
foreach (var key in area.Value)
|
||||
{
|
||||
@@ -170,10 +221,12 @@ namespace Umbraco.Cms.Core.Services.Implement
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private Dictionary<string, IDictionary<string, string>> GetAreaStoredTranslations(IDictionary<CultureInfo, Lazy<XDocument>> xmlSource, CultureInfo cult)
|
||||
private IDictionary<string, IDictionary<string, string>> GetAreaStoredTranslations(
|
||||
IDictionary<CultureInfo, Lazy<XDocument>> xmlSource, CultureInfo cult)
|
||||
{
|
||||
var overallResult = new Dictionary<string, IDictionary<string, string>>(StringComparer.InvariantCulture);
|
||||
var areas = xmlSource[cult].Value.XPathSelectElements("//area");
|
||||
@@ -189,6 +242,7 @@ namespace Umbraco.Cms.Core.Services.Implement
|
||||
if (result.ContainsKey(dictionaryKey) == false)
|
||||
result.Add(dictionaryKey, key.Value);
|
||||
}
|
||||
|
||||
overallResult.Add(area.Attribute("alias").Value, result);
|
||||
}
|
||||
|
||||
@@ -199,11 +253,13 @@ namespace Umbraco.Cms.Core.Services.Implement
|
||||
var enUS = xmlSource[englishCulture].Value.XPathSelectElements("//area");
|
||||
foreach (var area in enUS)
|
||||
{
|
||||
IDictionary<string, string> result = new Dictionary<string, string>(StringComparer.InvariantCulture);
|
||||
IDictionary<string, string>
|
||||
result = new Dictionary<string, string>(StringComparer.InvariantCulture);
|
||||
if (overallResult.ContainsKey(area.Attribute("alias").Value))
|
||||
{
|
||||
result = overallResult[area.Attribute("alias").Value];
|
||||
}
|
||||
|
||||
var keys = area.XPathSelectElements("./key");
|
||||
foreach (var key in keys)
|
||||
{
|
||||
@@ -213,15 +269,19 @@ namespace Umbraco.Cms.Core.Services.Implement
|
||||
if (result.ContainsKey(dictionaryKey) == false)
|
||||
result.Add(dictionaryKey, key.Value);
|
||||
}
|
||||
|
||||
if (!overallResult.ContainsKey(area.Attribute("alias").Value))
|
||||
{
|
||||
overallResult.Add(area.Attribute("alias").Value, result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return overallResult;
|
||||
}
|
||||
private Dictionary<string, string> GetNoAreaStoredTranslations(IDictionary<CultureInfo, Lazy<XDocument>> xmlSource, CultureInfo cult)
|
||||
|
||||
private Dictionary<string, string> GetNoAreaStoredTranslations(
|
||||
IDictionary<CultureInfo, Lazy<XDocument>> xmlSource, CultureInfo cult)
|
||||
{
|
||||
var result = new Dictionary<string, string>(StringComparer.InvariantCulture);
|
||||
var keys = xmlSource[cult].Value.XPathSelectElements("//key");
|
||||
@@ -250,14 +310,18 @@ namespace Umbraco.Cms.Core.Services.Implement
|
||||
result.Add(dictionaryKey, key.Value);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
private Dictionary<string, IDictionary<string, string>> GetAreaStoredTranslations(IDictionary<CultureInfo, IDictionary<string, IDictionary<string, string>>> dictionarySource, CultureInfo cult)
|
||||
|
||||
private Dictionary<string, IDictionary<string, string>> GetAreaStoredTranslations(
|
||||
IDictionary<CultureInfo, Lazy<IDictionary<string, IDictionary<string, string>>>> dictionarySource,
|
||||
CultureInfo cult)
|
||||
{
|
||||
var overallResult = new Dictionary<string, IDictionary<string, string>>(StringComparer.InvariantCulture);
|
||||
var areaDict = dictionarySource[cult];
|
||||
|
||||
foreach (var area in areaDict)
|
||||
foreach (var area in areaDict.Value)
|
||||
{
|
||||
var result = new Dictionary<string, string>(StringComparer.InvariantCulture);
|
||||
var keys = area.Value.Keys;
|
||||
@@ -267,8 +331,10 @@ namespace Umbraco.Cms.Core.Services.Implement
|
||||
if (result.ContainsKey(key) == false)
|
||||
result.Add(key, area.Value[key]);
|
||||
}
|
||||
|
||||
overallResult.Add(area.Key, result);
|
||||
}
|
||||
|
||||
return overallResult;
|
||||
}
|
||||
|
||||
@@ -306,11 +372,14 @@ namespace Umbraco.Cms.Core.Services.Implement
|
||||
return attempt ? attempt.Result : currentCulture;
|
||||
}
|
||||
|
||||
private string GetFromDictionarySource(CultureInfo culture, string area, string key, IDictionary<string, string> tokens)
|
||||
private string GetFromDictionarySource(CultureInfo culture, string area, string key,
|
||||
IDictionary<string, string> tokens)
|
||||
{
|
||||
if (_dictionarySource.ContainsKey(culture) == false)
|
||||
{
|
||||
_logger.LogWarning("The culture specified {Culture} was not found in any configured sources for this service", culture);
|
||||
_logger.LogWarning(
|
||||
"The culture specified {Culture} was not found in any configured sources for this service",
|
||||
culture);
|
||||
return "[" + key + "]";
|
||||
}
|
||||
|
||||
@@ -318,17 +387,18 @@ namespace Umbraco.Cms.Core.Services.Implement
|
||||
string found = null;
|
||||
if (string.IsNullOrWhiteSpace(area))
|
||||
{
|
||||
_noAreaDictionarySource[culture].TryGetValue(key, out found);
|
||||
_noAreaDictionarySource[culture].Value.TryGetValue(key, out found);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_dictionarySource[culture].TryGetValue(area, out var areaDictionary))
|
||||
if (_dictionarySource[culture].Value.TryGetValue(area, out var areaDictionary))
|
||||
{
|
||||
areaDictionary.TryGetValue(key, out found);
|
||||
}
|
||||
|
||||
if (found == null)
|
||||
{
|
||||
_noAreaDictionarySource[culture].TryGetValue(key, out found);
|
||||
_noAreaDictionarySource[culture].Value.TryGetValue(key, out found);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -341,6 +411,7 @@ namespace Umbraco.Cms.Core.Services.Implement
|
||||
//NOTE: Based on how legacy works, the default text does not contain the area, just the key
|
||||
return "[" + key + "]";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses the tokens in the value
|
||||
/// </summary>
|
||||
@@ -370,20 +441,29 @@ namespace Umbraco.Cms.Core.Services.Implement
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all key/values in storage for the given culture
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public IDictionary<string, IDictionary<string, string>> GetAllStoredValuesByAreaAndAlias(CultureInfo culture)
|
||||
{
|
||||
if (culture == null) throw new ArgumentNullException("culture");
|
||||
if (culture == null)
|
||||
{
|
||||
throw new ArgumentNullException("culture");
|
||||
}
|
||||
|
||||
// TODO: Hack, see notes on ConvertToSupportedCultureWithRegionCode
|
||||
culture = ConvertToSupportedCultureWithRegionCode(culture);
|
||||
|
||||
if (_dictionarySource.ContainsKey(culture) == false)
|
||||
{
|
||||
_logger.LogWarning("The culture specified {Culture} was not found in any configured sources for this service", culture);
|
||||
_logger.LogWarning(
|
||||
"The culture specified {Culture} was not found in any configured sources for this service",
|
||||
culture);
|
||||
return new Dictionary<string, IDictionary<string, string>>(0);
|
||||
}
|
||||
|
||||
return _dictionarySource[culture];
|
||||
return _dictionarySource[culture].Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -713,7 +713,7 @@ namespace Umbraco.Cms.Core.Services.Implement
|
||||
_userGroupRepository.ReplaceGroupPermissions(groupId, permissions, entityIds);
|
||||
scope.Complete();
|
||||
|
||||
var assigned = permissions.Select(p => p.ToString(CultureInfo.InvariantCulture)).ToArray();
|
||||
var assigned = permissions?.Select(p => p.ToString(CultureInfo.InvariantCulture)).ToArray();
|
||||
var entityPermissions = entityIds.Select(x => new EntityPermission(groupId, x, assigned)).ToArray();
|
||||
scope.Notifications.Publish(new AssignedUserGroupPermissionsNotification(entityPermissions, evtMsgs));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user