diff --git a/.github/ISSUE_TEMPLATE/01_bug_report.yml b/.github/ISSUE_TEMPLATE/01_bug_report.yml index 04d1a0e04c..800ac53e68 100644 --- a/.github/ISSUE_TEMPLATE/01_bug_report.yml +++ b/.github/ISSUE_TEMPLATE/01_bug_report.yml @@ -6,7 +6,7 @@ body: - type: input id: "version" attributes: - label: "Which Umbraco version are you using?" + label: "Which *exact* Umbraco version are you using? For example: 8.13.1 - don't just write v8" description: "Use the help icon in the Umbraco backoffice to find the version you're using" validations: required: true diff --git a/src/Umbraco.Core/Configuration/ContentDashboardSettings.cs b/src/Umbraco.Core/Configuration/ContentDashboardSettings.cs new file mode 100644 index 0000000000..6840d974aa --- /dev/null +++ b/src/Umbraco.Core/Configuration/ContentDashboardSettings.cs @@ -0,0 +1,23 @@ + +namespace Umbraco.Core.Dashboards +{ + public class ContentDashboardSettings + { + private const string DefaultContentDashboardPath = "cms"; + + /// + /// Gets a value indicating whether the content dashboard should be available to all users. + /// + /// + /// true if the dashboard is visible for all user groups; otherwise, false + /// and the default access rules for that dashboard will be in use. + /// + public bool AllowContentDashboardAccessToAllUsers { get; set; } = true; + + /// + /// Gets the path to use when constructing the URL for retrieving data for the content dashboard. + /// + /// The URL path. + public string ContentDashboardPath { get; set; } = DefaultContentDashboardPath; + } +} diff --git a/src/Umbraco.Core/Constants-SqlTemplates.cs b/src/Umbraco.Core/Constants-SqlTemplates.cs index c6bee5ca4f..a2fe501ab3 100644 --- a/src/Umbraco.Core/Constants-SqlTemplates.cs +++ b/src/Umbraco.Core/Constants-SqlTemplates.cs @@ -1,4 +1,4 @@ -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core { public static partial class Constants { @@ -30,7 +30,7 @@ namespace Umbraco.Cms.Core public const string WhereNodeId = "Umbraco.Web.PublishedCache.NuCache.DataSource.WhereNodeId"; public const string WhereNodeIdX = "Umbraco.Web.PublishedCache.NuCache.DataSource.WhereNodeIdX"; public const string SourcesSelectUmbracoNodeJoin = "Umbraco.Web.PublishedCache.NuCache.DataSource.SourcesSelectUmbracoNodeJoin"; - public const string ContentSourcesSelect = "Umbraco.Web.PublishedCache.NuCache.DataSource.ContentSourcesSelect"; + public const string ContentSourcesSelect = "Umbraco.Web.PublishedCache.NuCache.DataSource.ContentSourcesSelect"; public const string ContentSourcesCount = "Umbraco.Web.PublishedCache.NuCache.DataSource.ContentSourcesCount"; public const string MediaSourcesSelect = "Umbraco.Web.PublishedCache.NuCache.DataSource.MediaSourcesSelect"; public const string MediaSourcesCount = "Umbraco.Web.PublishedCache.NuCache.DataSource.MediaSourcesCount"; diff --git a/src/Umbraco.Core/Events/UserNotificationsHandler.cs b/src/Umbraco.Core/Events/UserNotificationsHandler.cs index f5172d3e28..45be01ce65 100644 --- a/src/Umbraco.Core/Events/UserNotificationsHandler.cs +++ b/src/Umbraco.Core/Events/UserNotificationsHandler.cs @@ -188,12 +188,12 @@ namespace Umbraco.Cms.Core.Events siteUri, ((IUser user, NotificationEmailSubjectParams subject) x) => _textService.Localize( - "notifications/mailSubject", + "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", + "notifications", x.isHtml ? "mailBodyHtml" : "mailBody", x.user.GetUserCulture(_textService, _globalSettings), new[] { @@ -205,7 +205,7 @@ namespace Umbraco.Cms.Core.Events 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.isHtml ? _textService.Localize("notifications", "mailBodyVariantHtmlSummary", new[]{ x.body.Summary }) : _textService.Localize("notifications","mailBodyVariantSummary", new []{ x.body.Summary })) : x.body.Summary, x.body.ItemUrl })); diff --git a/src/Umbraco.Core/Extensions/PublishedPropertyExtension.cs b/src/Umbraco.Core/Extensions/PublishedPropertyExtension.cs index c559fc9a74..a36942862a 100644 --- a/src/Umbraco.Core/Extensions/PublishedPropertyExtension.cs +++ b/src/Umbraco.Core/Extensions/PublishedPropertyExtension.cs @@ -31,10 +31,17 @@ namespace Umbraco.Extensions { // we have a value // try to cast or convert it - var value = property.GetValue(culture, segment); - if (value is T valueAsT) return valueAsT; + var value = property.GetValue(culture, segment); + if (value is T valueAsT) + { + return valueAsT; + } + var valueConverted = value.TryConvertTo(); - if (valueConverted) return valueConverted.Result; + if (valueConverted) + { + return valueConverted.Result; + } // cannot cast nor convert the value, nothing we can return but 'default' // note: we don't want to fallback in that case - would make little sense @@ -43,14 +50,23 @@ namespace Umbraco.Extensions // we don't have a value, try fallback if (publishedValueFallback.TryGetValue(property, culture, segment, fallback, defaultValue, out var fallbackValue)) + { return fallbackValue; + } // we don't have a value - neither direct nor fallback // give a chance to the converter to return something (eg empty enumerable) var noValue = property.GetValue(culture, segment); - if (noValue is T noValueAsT) return noValueAsT; + if (noValue is T noValueAsT) + { + return noValueAsT; + } + var noValueConverted = noValue.TryConvertTo(); - if (noValueConverted) return noValueConverted.Result; + if (noValueConverted) + { + return noValueConverted.Result; + } // cannot cast noValue nor convert it, nothing we can return but 'default' return default; diff --git a/src/Umbraco.Core/HealthChecks/Checks/AbstractSettingsCheck.cs b/src/Umbraco.Core/HealthChecks/Checks/AbstractSettingsCheck.cs index 99b2ef2f97..7123255b0d 100644 --- a/src/Umbraco.Core/HealthChecks/Checks/AbstractSettingsCheck.cs +++ b/src/Umbraco.Core/HealthChecks/Checks/AbstractSettingsCheck.cs @@ -1,4 +1,4 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. using System; @@ -54,7 +54,7 @@ namespace Umbraco.Cms.Core.HealthChecks.Checks /// /// Gets the message for when the check has succeeded. /// - public virtual string CheckSuccessMessage => LocalizedTextService.Localize("healthcheck/checkSuccessMessage", new[] { CurrentValue, Values.First(v => v.IsRecommended).Value, ItemPath }); + public virtual string CheckSuccessMessage => LocalizedTextService.Localize("healthcheck", "checkSuccessMessage", new[] { CurrentValue, Values.First(v => v.IsRecommended).Value, ItemPath }); /// /// Gets the message for when the check has failed. @@ -62,10 +62,10 @@ namespace Umbraco.Cms.Core.HealthChecks.Checks public virtual string CheckErrorMessage => ValueComparisonType == ValueComparisonType.ShouldEqual ? LocalizedTextService.Localize( - "healthcheck/checkErrorMessageDifferentExpectedValue", + "healthcheck", "checkErrorMessageDifferentExpectedValue", new[] { CurrentValue, Values.First(v => v.IsRecommended).Value, ItemPath }) : LocalizedTextService.Localize( - "healthcheck/checkErrorMessageUnexpectedValue", + "healthcheck", "checkErrorMessageUnexpectedValue", new[] { CurrentValue, Values.First(v => v.IsRecommended).Value, ItemPath }); /// diff --git a/src/Umbraco.Core/HealthChecks/Checks/Configuration/MacroErrorsCheck.cs b/src/Umbraco.Core/HealthChecks/Checks/Configuration/MacroErrorsCheck.cs index 7359a60341..2ded5a0659 100644 --- a/src/Umbraco.Core/HealthChecks/Checks/Configuration/MacroErrorsCheck.cs +++ b/src/Umbraco.Core/HealthChecks/Checks/Configuration/MacroErrorsCheck.cs @@ -78,7 +78,7 @@ namespace Umbraco.Cms.Core.HealthChecks.Checks.Configuration /// public override string CheckSuccessMessage => _textService.Localize( - "healthcheck/macroErrorModeCheckSuccessMessage", + "healthcheck","macroErrorModeCheckSuccessMessage", new[] { CurrentValue, Values.First(v => v.IsRecommended).Value }); /// @@ -86,7 +86,7 @@ namespace Umbraco.Cms.Core.HealthChecks.Checks.Configuration /// public override string CheckErrorMessage => _textService.Localize( - "healthcheck/macroErrorModeCheckErrorMessage", + "healthcheck","macroErrorModeCheckErrorMessage", new[] { CurrentValue, Values.First(v => v.IsRecommended).Value }); } } diff --git a/src/Umbraco.Core/HealthChecks/Checks/Configuration/NotificationEmailCheck.cs b/src/Umbraco.Core/HealthChecks/Checks/Configuration/NotificationEmailCheck.cs index b139cad710..8d76f1cf4e 100644 --- a/src/Umbraco.Core/HealthChecks/Checks/Configuration/NotificationEmailCheck.cs +++ b/src/Umbraco.Core/HealthChecks/Checks/Configuration/NotificationEmailCheck.cs @@ -1,4 +1,4 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. using System.Collections.Generic; @@ -50,11 +50,11 @@ namespace Umbraco.Cms.Core.HealthChecks.Checks.Configuration /// public override string CheckSuccessMessage => - LocalizedTextService.Localize("healthcheck/notificationEmailsCheckSuccessMessage", + LocalizedTextService.Localize("healthcheck","notificationEmailsCheckSuccessMessage", new[] { CurrentValue ?? "<null>" }); /// - public override string CheckErrorMessage => LocalizedTextService.Localize("healthcheck/notificationEmailsCheckErrorMessage", new[] { DefaultFromEmail }); + public override string CheckErrorMessage => LocalizedTextService.Localize("healthcheck","notificationEmailsCheckErrorMessage", new[] { DefaultFromEmail }); /// public override string ReadMoreLink => Constants.HealthChecks.DocumentationLinks.Configuration.NotificationEmailCheck; diff --git a/src/Umbraco.Core/HealthChecks/Checks/LiveEnvironment/CompilationDebugCheck.cs b/src/Umbraco.Core/HealthChecks/Checks/LiveEnvironment/CompilationDebugCheck.cs index ff37807f27..d28c3ca8f5 100644 --- a/src/Umbraco.Core/HealthChecks/Checks/LiveEnvironment/CompilationDebugCheck.cs +++ b/src/Umbraco.Core/HealthChecks/Checks/LiveEnvironment/CompilationDebugCheck.cs @@ -51,9 +51,9 @@ namespace Umbraco.Cms.Core.HealthChecks.Checks.LiveEnvironment public override string CurrentValue => _hostingSettings.CurrentValue.Debug.ToString(); /// - public override string CheckSuccessMessage => LocalizedTextService.Localize("healthcheck/compilationDebugCheckSuccessMessage"); + public override string CheckSuccessMessage => LocalizedTextService.Localize("healthcheck","compilationDebugCheckSuccessMessage"); /// - public override string CheckErrorMessage => LocalizedTextService.Localize("healthcheck/compilationDebugCheckErrorMessage"); + public override string CheckErrorMessage => LocalizedTextService.Localize("healthcheck","compilationDebugCheckErrorMessage"); } } diff --git a/src/Umbraco.Core/HealthChecks/Checks/Security/BaseHttpHeaderCheck.cs b/src/Umbraco.Core/HealthChecks/Checks/Security/BaseHttpHeaderCheck.cs index f9dccbc585..eeb291c41f 100644 --- a/src/Umbraco.Core/HealthChecks/Checks/Security/BaseHttpHeaderCheck.cs +++ b/src/Umbraco.Core/HealthChecks/Checks/Security/BaseHttpHeaderCheck.cs @@ -95,12 +95,12 @@ namespace Umbraco.Cms.Core.HealthChecks.Checks.Security } message = success - ? LocalizedTextService.Localize($"healthcheck/{_localizedTextPrefix}CheckHeaderFound") - : LocalizedTextService.Localize($"healthcheck/{_localizedTextPrefix}CheckHeaderNotFound"); + ? LocalizedTextService.Localize($"healthcheck", $"{_localizedTextPrefix}CheckHeaderFound") + : LocalizedTextService.Localize($"healthcheck", $"{_localizedTextPrefix}CheckHeaderNotFound"); } catch (Exception ex) { - message = LocalizedTextService.Localize("healthcheck/healthCheckInvalidUrl", new[] { url.ToString(), ex.Message }); + message = LocalizedTextService.Localize("healthcheck","healthCheckInvalidUrl", new[] { url.ToString(), ex.Message }); } return diff --git a/src/Umbraco.Core/HealthChecks/Checks/Security/ExcessiveHeadersCheck.cs b/src/Umbraco.Core/HealthChecks/Checks/Security/ExcessiveHeadersCheck.cs index 825ff09a69..377fbff718 100644 --- a/src/Umbraco.Core/HealthChecks/Checks/Security/ExcessiveHeadersCheck.cs +++ b/src/Umbraco.Core/HealthChecks/Checks/Security/ExcessiveHeadersCheck.cs @@ -68,12 +68,12 @@ namespace Umbraco.Cms.Core.HealthChecks.Checks.Security .ToArray(); success = headersFound.Any() == false; message = success - ? _textService.Localize("healthcheck/excessiveHeadersNotFound") - : _textService.Localize("healthcheck/excessiveHeadersFound", new[] { string.Join(", ", headersFound) }); + ? _textService.Localize("healthcheck","excessiveHeadersNotFound") + : _textService.Localize("healthcheck","excessiveHeadersFound", new[] { string.Join(", ", headersFound) }); } catch (Exception ex) { - message = _textService.Localize("healthcheck/healthCheckInvalidUrl", new[] { url.ToString(), ex.Message }); + message = _textService.Localize("healthcheck","healthCheckInvalidUrl", new[] { url.ToString(), ex.Message }); } return diff --git a/src/Umbraco.Core/HealthChecks/Checks/Security/HttpsCheck.cs b/src/Umbraco.Core/HealthChecks/Checks/Security/HttpsCheck.cs index 01638366d1..2e3ef9f5c8 100644 --- a/src/Umbraco.Core/HealthChecks/Checks/Security/HttpsCheck.cs +++ b/src/Umbraco.Core/HealthChecks/Checks/Security/HttpsCheck.cs @@ -102,23 +102,23 @@ namespace Umbraco.Cms.Core.HealthChecks.Checks.Security if (s_certificateDaysToExpiry <= 0) { result = StatusResultType.Error; - message = _textService.Localize("healthcheck/httpsCheckExpiredCertificate"); + message = _textService.Localize("healthcheck","httpsCheckExpiredCertificate"); } else if (s_certificateDaysToExpiry < numberOfDaysForExpiryWarning) { result = StatusResultType.Warning; - message = _textService.Localize("healthcheck/httpsCheckExpiringCertificate", new[] { s_certificateDaysToExpiry.ToString() }); + message = _textService.Localize("healthcheck","httpsCheckExpiringCertificate", new[] { s_certificateDaysToExpiry.ToString() }); } else { result = StatusResultType.Success; - message = _textService.Localize("healthcheck/httpsCheckValidCertificate"); + message = _textService.Localize("healthcheck","httpsCheckValidCertificate"); } } else { result = StatusResultType.Error; - message = _textService.Localize("healthcheck/healthCheckInvalidUrl", new[] { url, response.ReasonPhrase }); + message = _textService.Localize("healthcheck","healthCheckInvalidUrl", new[] { url, response.ReasonPhrase }); } } catch (Exception ex) @@ -127,12 +127,12 @@ namespace Umbraco.Cms.Core.HealthChecks.Checks.Security if (exception != null) { message = exception.Status == WebExceptionStatus.TrustFailure - ? _textService.Localize("healthcheck/httpsCheckInvalidCertificate", new[] { exception.Message }) - : _textService.Localize("healthcheck/healthCheckInvalidUrl", new[] { url, exception.Message }); + ? _textService.Localize("healthcheck","httpsCheckInvalidCertificate", new[] { exception.Message }) + : _textService.Localize("healthcheck","healthCheckInvalidUrl", new[] { url, exception.Message }); } else { - message = _textService.Localize("healthcheck/healthCheckInvalidUrl", new[] { url, ex.Message }); + message = _textService.Localize("healthcheck","healthCheckInvalidUrl", new[] { url, ex.Message }); } result = StatusResultType.Error; @@ -152,7 +152,7 @@ namespace Umbraco.Cms.Core.HealthChecks.Checks.Security Uri uri = _hostingEnvironment.ApplicationMainUrl; var success = uri.Scheme == "https"; - return Task.FromResult(new HealthCheckStatus(_textService.Localize("healthcheck/httpsCheckIsCurrentSchemeHttps", new[] { success ? string.Empty : "not" })) + return Task.FromResult(new HealthCheckStatus(_textService.Localize("healthcheck","httpsCheckIsCurrentSchemeHttps", new[] { success ? string.Empty : "not" })) { ResultType = success ? StatusResultType.Success : StatusResultType.Error, ReadMoreLink = success ? null : Constants.HealthChecks.DocumentationLinks.Security.HttpsCheck.CheckIfCurrentSchemeIsHttps @@ -168,13 +168,13 @@ namespace Umbraco.Cms.Core.HealthChecks.Checks.Security StatusResultType resultType; if (uri.Scheme != "https") { - resultMessage = _textService.Localize("healthcheck/httpsCheckConfigurationRectifyNotPossible"); + resultMessage = _textService.Localize("healthcheck","httpsCheckConfigurationRectifyNotPossible"); resultType = StatusResultType.Info; } else { resultMessage = _textService.Localize( - "healthcheck/httpsCheckConfigurationCheckResult", + "healthcheck","httpsCheckConfigurationCheckResult", new[] { httpsSettingEnabled.ToString(), httpsSettingEnabled ? string.Empty : "not" }); resultType = httpsSettingEnabled ? StatusResultType.Success : StatusResultType.Error; } diff --git a/src/Umbraco.Core/HealthChecks/Checks/Services/SmtpCheck.cs b/src/Umbraco.Core/HealthChecks/Checks/Services/SmtpCheck.cs index 618b44b9b3..bc9f7fcaba 100644 --- a/src/Umbraco.Core/HealthChecks/Checks/Services/SmtpCheck.cs +++ b/src/Umbraco.Core/HealthChecks/Checks/Services/SmtpCheck.cs @@ -56,21 +56,21 @@ namespace Umbraco.Cms.Core.HealthChecks.Checks.Services string message; if (smtpSettings == null) { - message = _textService.Localize("healthcheck/smtpMailSettingsNotFound"); + message = _textService.Localize("healthcheck", "smtpMailSettingsNotFound"); } else { if (string.IsNullOrEmpty(smtpSettings.Host)) { - message = _textService.Localize("healthcheck/smtpMailSettingsHostNotConfigured"); + message = _textService.Localize("healthcheck", "smtpMailSettingsHostNotConfigured"); } else { success = CanMakeSmtpConnection(smtpSettings.Host, smtpSettings.Port); message = success - ? _textService.Localize("healthcheck/smtpMailSettingsConnectionSuccess") + ? _textService.Localize("healthcheck", "smtpMailSettingsConnectionSuccess") : _textService.Localize( - "healthcheck/smtpMailSettingsConnectionFail", + "healthcheck", "smtpMailSettingsConnectionFail", new[] { smtpSettings.Host, smtpSettings.Port.ToString() }); } } diff --git a/src/Umbraco.Core/HealthChecks/NotificationMethods/EmailNotificationMethod.cs b/src/Umbraco.Core/HealthChecks/NotificationMethods/EmailNotificationMethod.cs index 6e679ddbb1..0811feeb7d 100644 --- a/src/Umbraco.Core/HealthChecks/NotificationMethods/EmailNotificationMethod.cs +++ b/src/Umbraco.Core/HealthChecks/NotificationMethods/EmailNotificationMethod.cs @@ -59,7 +59,7 @@ namespace Umbraco.Cms.Core.HealthChecks.NotificationMethods return; } - var message = _textService.Localize("healthcheck/scheduledHealthCheckEmailBody", new[] + var message = _textService.Localize("healthcheck","scheduledHealthCheckEmailBody", new[] { DateTime.Now.ToShortDateString(), DateTime.Now.ToShortTimeString(), @@ -70,7 +70,7 @@ namespace Umbraco.Cms.Core.HealthChecks.NotificationMethods // you can identify the site that these results are for. var host = _hostingEnvironment.ApplicationMainUrl?.ToString(); - var subject = _textService.Localize("healthcheck/scheduledHealthCheckEmailSubject", new[] { host }); + var subject = _textService.Localize("healthcheck","scheduledHealthCheckEmailSubject", new[] { host }); var mailMessage = CreateMailMessage(subject, message); diff --git a/src/Umbraco.Core/Models/Mapping/CommonMapper.cs b/src/Umbraco.Core/Models/Mapping/CommonMapper.cs index b6a220e04c..3cfcc89085 100644 --- a/src/Umbraco.Core/Models/Mapping/CommonMapper.cs +++ b/src/Umbraco.Core/Models/Mapping/CommonMapper.cs @@ -54,7 +54,7 @@ namespace Umbraco.Cms.Core.Models.Mapping // localize content app names foreach (var app in apps) { - var localizedAppName = _localizedTextService.Localize($"apps/{app.Alias}"); + var localizedAppName = _localizedTextService.Localize("apps", app.Alias); if (localizedAppName.Equals($"[{app.Alias}]", StringComparison.OrdinalIgnoreCase) == false) { app.Name = localizedAppName; diff --git a/src/Umbraco.Core/Models/Mapping/ContentVariantMapper.cs b/src/Umbraco.Core/Models/Mapping/ContentVariantMapper.cs index ddc7add7ed..8aaa515dcd 100644 --- a/src/Umbraco.Core/Models/Mapping/ContentVariantMapper.cs +++ b/src/Umbraco.Core/Models/Mapping/ContentVariantMapper.cs @@ -150,7 +150,7 @@ namespace Umbraco.Cms.Core.Models.Mapping if(!isCultureVariant && !isSegmentVariant) { - return _localizedTextService.Localize("general/default"); + return _localizedTextService.Localize("general", "default"); } var parts = new List(); diff --git a/src/Umbraco.Core/Models/Mapping/MemberTabsAndPropertiesMapper.cs b/src/Umbraco.Core/Models/Mapping/MemberTabsAndPropertiesMapper.cs index 0d3a5b7536..8de419bd0e 100644 --- a/src/Umbraco.Core/Models/Mapping/MemberTabsAndPropertiesMapper.cs +++ b/src/Umbraco.Core/Models/Mapping/MemberTabsAndPropertiesMapper.cs @@ -72,7 +72,7 @@ namespace Umbraco.Cms.Core.Models.Mapping if (isLockedOutProperty?.Value != null && isLockedOutProperty.Value.ToString() != "1") { isLockedOutProperty.View = "readonlyvalue"; - isLockedOutProperty.Value = _localizedTextService.Localize("general/no"); + isLockedOutProperty.Value = _localizedTextService.Localize("general", "no"); } if (_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser != null @@ -108,14 +108,14 @@ namespace Umbraco.Cms.Core.Models.Mapping new ContentPropertyDisplay { Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}id", - Label = _localizedTextService.Localize("general/id"), + Label = _localizedTextService.Localize("general","id"), Value = new List {member.Id.ToString(), member.Key.ToString()}, View = "idwithguid" }, new ContentPropertyDisplay { Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}doctype", - Label = _localizedTextService.Localize("content/membertype"), + Label = _localizedTextService.Localize("content","membertype"), Value = _localizedTextService.UmbracoDictionaryTranslate(CultureDictionary, member.ContentType.Name), View = _propertyEditorCollection[Constants.PropertyEditors.Aliases.Label].GetValueEditor().View }, @@ -123,7 +123,7 @@ namespace Umbraco.Cms.Core.Models.Mapping new ContentPropertyDisplay { Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}email", - Label = _localizedTextService.Localize("general/email"), + Label = _localizedTextService.Localize("general","email"), Value = member.Email, View = "email", Validation = {Mandatory = true} @@ -131,8 +131,7 @@ namespace Umbraco.Cms.Core.Models.Mapping new ContentPropertyDisplay { Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}password", - Label = _localizedTextService.Localize("password"), - + Label = _localizedTextService.Localize(null,"password"), Value = new Dictionary { // TODO: why ignoreCase, what are we doing here?! @@ -146,7 +145,7 @@ namespace Umbraco.Cms.Core.Models.Mapping new ContentPropertyDisplay { Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}membergroup", - Label = _localizedTextService.Localize("content/membergroup"), + Label = _localizedTextService.Localize("content","membergroup"), Value = GetMemberGroupValue(member.Username), View = "membergroups", Config = new Dictionary {{"IsRequired", true}} @@ -222,7 +221,7 @@ namespace Umbraco.Cms.Core.Models.Mapping var prop = new ContentPropertyDisplay { Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}login", - Label = localizedText.Localize("login"), + Label = localizedText.Localize(null,"login"), Value = member.Username }; diff --git a/src/Umbraco.Core/Models/Mapping/SectionMapDefinition.cs b/src/Umbraco.Core/Models/Mapping/SectionMapDefinition.cs index af1bafed4c..b7bdbccd26 100644 --- a/src/Umbraco.Core/Models/Mapping/SectionMapDefinition.cs +++ b/src/Umbraco.Core/Models/Mapping/SectionMapDefinition.cs @@ -35,7 +35,7 @@ namespace Umbraco.Cms.Core.Models.Mapping private void Map(ISection source, Section target, MapperContext context) { target.Alias = source.Alias; - target.Name = _textService.Localize("sections/" + source.Alias); + target.Name = _textService.Localize("sections", source.Alias); } // Umbraco.Code.MapAll diff --git a/src/Umbraco.Core/Models/Mapping/TabsAndPropertiesMapper.cs b/src/Umbraco.Core/Models/Mapping/TabsAndPropertiesMapper.cs index 3716767b3d..583f921e3f 100644 --- a/src/Umbraco.Core/Models/Mapping/TabsAndPropertiesMapper.cs +++ b/src/Umbraco.Core/Models/Mapping/TabsAndPropertiesMapper.cs @@ -59,7 +59,7 @@ namespace Umbraco.Cms.Core.Models.Mapping tabs.Add(new Tab { Id = 0, - Label = LocalizedTextService.Localize("general/properties"), + Label = LocalizedTextService.Localize("general", "properties"), Alias = "Generic properties", Properties = genericproperties }); diff --git a/src/Umbraco.Core/Models/Mapping/UserMapDefinition.cs b/src/Umbraco.Core/Models/Mapping/UserMapDefinition.cs index 4a19fbe530..f66e3a6b23 100644 --- a/src/Umbraco.Core/Models/Mapping/UserMapDefinition.cs +++ b/src/Umbraco.Core/Models/Mapping/UserMapDefinition.cs @@ -284,8 +284,8 @@ namespace Umbraco.Cms.Core.Models.Mapping { target.AvailableCultures = _textService.GetSupportedCultures().ToDictionary(x => x.Name, x => x.DisplayName); target.Avatars = source.GetUserAvatarUrls(_appCaches.RuntimeCache, _mediaFileManager, _imageUrlGenerator); - target.CalculatedStartContentIds = GetStartNodes(source.CalculateContentStartNodeIds(_entityService, _appCaches), UmbracoObjectTypes.Document, "content/contentRoot", context); - target.CalculatedStartMediaIds = GetStartNodes(source.CalculateMediaStartNodeIds(_entityService, _appCaches), UmbracoObjectTypes.Media, "media/mediaRoot", context); + target.CalculatedStartContentIds = GetStartNodes(source.CalculateContentStartNodeIds(_entityService,_appCaches), UmbracoObjectTypes.Document, "content","contentRoot", context); + target.CalculatedStartMediaIds = GetStartNodes(source.CalculateMediaStartNodeIds(_entityService, _appCaches), UmbracoObjectTypes.Media, "media","mediaRoot", context); target.CreateDate = source.CreateDate; target.Culture = source.GetUserCulture(_textService, _globalSettings).ToString(); target.Email = source.Email; @@ -300,8 +300,8 @@ namespace Umbraco.Cms.Core.Models.Mapping target.Navigation = CreateUserEditorNavigation(); target.ParentId = -1; target.Path = "-1," + source.Id; - target.StartContentIds = GetStartNodes(source.StartContentIds.ToArray(), UmbracoObjectTypes.Document, "content/contentRoot", context); - target.StartMediaIds = GetStartNodes(source.StartMediaIds.ToArray(), UmbracoObjectTypes.Media, "media/mediaRoot", context); + target.StartContentIds = GetStartNodes(source.StartContentIds.ToArray(), UmbracoObjectTypes.Document, "content","contentRoot", context); + target.StartMediaIds = GetStartNodes(source.StartMediaIds.ToArray(), UmbracoObjectTypes.Media, "media","mediaRoot", context); target.UpdateDate = source.UpdateDate; target.UserGroups = context.MapEnumerable(source.Groups); target.Username = source.Username; @@ -358,12 +358,12 @@ namespace Umbraco.Cms.Core.Models.Mapping if (sourceStartMediaId > 0) target.MediaStartNode = context.Map(_entityService.Get(sourceStartMediaId.Value, UmbracoObjectTypes.Media)); else if (sourceStartMediaId == -1) - target.MediaStartNode = CreateRootNode(_textService.Localize("media/mediaRoot")); + target.MediaStartNode = CreateRootNode(_textService.Localize("media", "mediaRoot")); if (sourceStartContentId > 0) target.ContentStartNode = context.Map(_entityService.Get(sourceStartContentId.Value, UmbracoObjectTypes.Document)); else if (sourceStartContentId == -1) - target.ContentStartNode = CreateRootNode(_textService.Localize("content/contentRoot")); + target.ContentStartNode = CreateRootNode(_textService.Localize("content", "contentRoot")); if (target.Icon.IsNullOrWhiteSpace()) target.Icon = Constants.Icons.UserGroup; @@ -375,10 +375,10 @@ namespace Umbraco.Cms.Core.Models.Mapping => new Permission { Category = action.Category.IsNullOrWhiteSpace() - ? _textService.Localize($"actionCategories/{Constants.Conventions.PermissionCategories.OtherCategory}") - : _textService.Localize($"actionCategories/{action.Category}"), - Name = _textService.Localize($"actions/{action.Alias}"), - Description = _textService.Localize($"actionDescriptions/{action.Alias}"), + ? _textService.Localize("actionCategories",Constants.Conventions.PermissionCategories.OtherCategory) + : _textService.Localize("actionCategories", action.Category), + Name = _textService.Localize("actions", action.Alias), + Description = _textService.Localize("actionDescriptions", action.Alias), Icon = action.Icon, Checked = source.Permissions != null && source.Permissions.Contains(action.Letter.ToString(CultureInfo.InvariantCulture)), PermissionCode = action.Letter.ToString(CultureInfo.InvariantCulture) @@ -394,14 +394,14 @@ namespace Umbraco.Cms.Core.Models.Mapping private static string MapContentTypeIcon(IEntitySlim entity) => entity is IContentEntitySlim contentEntity ? contentEntity.ContentTypeIcon : null; - private IEnumerable GetStartNodes(int[] startNodeIds, UmbracoObjectTypes objectType, string localizedKey, MapperContext context) + private IEnumerable GetStartNodes(int[] startNodeIds, UmbracoObjectTypes objectType, string localizedArea,string localizedAlias, MapperContext context) { if (startNodeIds.Length <= 0) return Enumerable.Empty(); var startNodes = new List(); if (startNodeIds.Contains(-1)) - startNodes.Add(CreateRootNode(_textService.Localize(localizedKey))); + startNodes.Add(CreateRootNode(_textService.Localize(localizedArea, localizedAlias))); var mediaItems = _entityService.GetAll(objectType, startNodeIds); startNodes.AddRange(context.MapEnumerable(mediaItems)); @@ -417,7 +417,7 @@ namespace Umbraco.Cms.Core.Models.Mapping Active = true, Alias = "details", Icon = "icon-umb-users", - Name = _textService.Localize("general/user"), + Name = _textService.Localize("general","user"), View = "views/users/views/user/details.html" } }; diff --git a/src/Umbraco.Core/Models/Trees/MenuItem.cs b/src/Umbraco.Core/Models/Trees/MenuItem.cs index 4c275709aa..8407530548 100644 --- a/src/Umbraco.Core/Models/Trees/MenuItem.cs +++ b/src/Umbraco.Core/Models/Trees/MenuItem.cs @@ -32,12 +32,9 @@ namespace Umbraco.Cms.Core.Models.Trees public MenuItem(string alias, ILocalizedTextService textService) : this() { - var values = textService.GetAllStoredValues(Thread.CurrentThread.CurrentUICulture); - values.TryGetValue($"visuallyHiddenTexts/{alias}_description", out var textDescription); - Alias = alias; - Name = textService.Localize($"actions/{Alias}"); - TextDescription = textDescription; + Name = textService.Localize("actions", Alias); + TextDescription = textService.Localize("visuallyHiddenTexts", alias + "_description", Thread.CurrentThread.CurrentUICulture); } /// diff --git a/src/Umbraco.Core/PropertyEditors/IPropertyCacheCompression.cs b/src/Umbraco.Core/PropertyEditors/IPropertyCacheCompression.cs index 817bc5aeae..f0973f3157 100644 --- a/src/Umbraco.Core/PropertyEditors/IPropertyCacheCompression.cs +++ b/src/Umbraco.Core/PropertyEditors/IPropertyCacheCompression.cs @@ -6,7 +6,7 @@ namespace Umbraco.Cms.Core.PropertyEditors /// Determines if a property type's value should be compressed in memory /// /// - /// + /// /// public interface IPropertyCacheCompression { diff --git a/src/Umbraco.Core/PropertyEditors/MediaPickerConfiguration.cs b/src/Umbraco.Core/PropertyEditors/MediaPickerConfiguration.cs index 967347e83d..06faedea0d 100644 --- a/src/Umbraco.Core/PropertyEditors/MediaPickerConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/MediaPickerConfiguration.cs @@ -5,9 +5,6 @@ /// public class MediaPickerConfiguration : IIgnoreUserStartNodesConfig { - [ConfigurationField("notice", "You can NOT change the property editor", "obsoletemediapickernotice")] - public bool Notice { get; set; } - [ConfigurationField("multiPicker", "Pick multiple items", "boolean")] public bool Multiple { get; set; } diff --git a/src/Umbraco.Core/PropertyEditors/PropertyCacheCompression.cs b/src/Umbraco.Core/PropertyEditors/PropertyCacheCompression.cs index 3664be6101..5216e3158f 100644 --- a/src/Umbraco.Core/PropertyEditors/PropertyCacheCompression.cs +++ b/src/Umbraco.Core/PropertyEditors/PropertyCacheCompression.cs @@ -2,8 +2,10 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.PropertyEditors; -namespace Umbraco.Cms.Core.PropertyEditors + +namespace Umbraco.Core.PropertyEditors { /// diff --git a/src/Umbraco.Core/Routing/UrlProviderExtensions.cs b/src/Umbraco.Core/Routing/UrlProviderExtensions.cs index 6dfdc89583..71ec01d0b5 100644 --- a/src/Umbraco.Core/Routing/UrlProviderExtensions.cs +++ b/src/Umbraco.Core/Routing/UrlProviderExtensions.cs @@ -48,7 +48,7 @@ namespace Umbraco.Extensions if (content.Published == false) { - result.Add(UrlInfo.Message(textService.Localize("content/itemNotPublished"))); + result.Add(UrlInfo.Message(textService.Localize("content", "itemNotPublished"))); return result; } @@ -145,7 +145,7 @@ namespace Umbraco.Extensions // deal with exceptions case "#ex": - result.Add(UrlInfo.Message(textService.Localize("content/getUrlException"), culture)); + result.Add(UrlInfo.Message(textService.Localize("content", "getUrlException"), culture)); break; // got a URL, deal with collisions, add URL @@ -182,17 +182,17 @@ namespace Umbraco.Extensions if (parent == null) { // oops, internal error - return UrlInfo.Message(textService.Localize("content/parentNotPublishedAnomaly"), culture); + return UrlInfo.Message(textService.Localize("content", "parentNotPublishedAnomaly"), culture); } else if (!parent.Published) { // totally not published - return UrlInfo.Message(textService.Localize("content/parentNotPublished", new[] { parent.Name }), culture); + return UrlInfo.Message(textService.Localize("content", "parentNotPublished", new[] { parent.Name }), culture); } else { // culture not published - return UrlInfo.Message(textService.Localize("content/parentCultureNotPublished", new[] {parent.Name}), culture); + return UrlInfo.Message(textService.Localize("content", "parentCultureNotPublished", new[] {parent.Name}), culture); } } @@ -221,7 +221,7 @@ namespace Umbraco.Extensions logger.LogDebug(logMsg, url, uri, culture); } - var urlInfo = UrlInfo.Message(textService.Localize("content/routeErrorCannotRoute"), culture); + var urlInfo = UrlInfo.Message(textService.Localize("content", "routeErrorCannotRoute"), culture); return Attempt.Succeed(urlInfo); } @@ -243,7 +243,7 @@ namespace Umbraco.Extensions l.Reverse(); var s = "/" + string.Join("/", l) + " (id=" + pcr.PublishedContent.Id + ")"; - var urlInfo = UrlInfo.Message(textService.Localize("content/routeError", new[] { s }), culture); + var urlInfo = UrlInfo.Message(textService.Localize("content", "routeError", new[] { s }), culture); return Attempt.Succeed(urlInfo); } diff --git a/src/Umbraco.Core/Services/IContentService.cs b/src/Umbraco.Core/Services/IContentService.cs index c7b930bc6e..cef8a8c6d6 100644 --- a/src/Umbraco.Core/Services/IContentService.cs +++ b/src/Umbraco.Core/Services/IContentService.cs @@ -490,6 +490,11 @@ namespace Umbraco.Cms.Core.Services /// IContent Create(string name, int parentId, string documentTypeAlias, int userId = Constants.Security.SuperUserId); + /// + /// Creates a document + /// + IContent Create(string name, int parentId, IContentType contentType, int userId = Constants.Security.SuperUserId); + /// /// Creates a document. /// diff --git a/src/Umbraco.Core/Services/ILocalizedTextService.cs b/src/Umbraco.Core/Services/ILocalizedTextService.cs index fd23dd6f78..51c1c97c9b 100644 --- a/src/Umbraco.Core/Services/ILocalizedTextService.cs +++ b/src/Umbraco.Core/Services/ILocalizedTextService.cs @@ -3,6 +3,10 @@ using System.Globalization; namespace Umbraco.Cms.Core.Services { + // TODO: This needs to be merged into one interface in v9, but better yet + // the Localize method should just the based on area + alias and we should remove + // the one with the 'key' (the concatenated area/alias) to ensure that we never use that again. + /// /// The entry point to localize any key in the text storage source for a given culture /// @@ -15,11 +19,19 @@ namespace Umbraco.Cms.Core.Services /// /// Localize a key with variables /// - /// + /// + /// /// /// This can be null /// - string Localize(string key, CultureInfo culture, IDictionary tokens = null); + string Localize(string area, string alias, CultureInfo culture, IDictionary tokens = null); + + + /// + /// Returns all key/values in storage for the given culture + /// + /// + IDictionary> GetAllStoredValuesByAreaAndAlias(CultureInfo culture); /// /// Returns all key/values in storage for the given culture diff --git a/src/Umbraco.Core/Services/LocalizedTextServiceExtensions.cs b/src/Umbraco.Core/Services/LocalizedTextServiceExtensions.cs index 598cebc2c0..dc20774142 100644 --- a/src/Umbraco.Core/Services/LocalizedTextServiceExtensions.cs +++ b/src/Umbraco.Core/Services/LocalizedTextServiceExtensions.cs @@ -1,6 +1,7 @@ // Copyright (c) Umbraco. // See LICENSE for more details. +using System; using System.Collections.Generic; using System.Globalization; using System.Linq; @@ -15,88 +16,81 @@ namespace Umbraco.Extensions /// public static class LocalizedTextServiceExtensions { - public static string Localize(this ILocalizedTextService manager, string area, T key) - where T: System.Enum - { - var fullKey = string.Join("/", area, key); - return manager.Localize(fullKey, Thread.CurrentThread.CurrentUICulture); - } + public static string Localize(this ILocalizedTextService manager, string area, T key) + where T: System.Enum => + manager.Localize(area, key.ToString(), Thread.CurrentThread.CurrentUICulture); - public static string Localize(this ILocalizedTextService manager, string area, string key) - { - var fullKey = string.Join("/", area, key); - return manager.Localize(fullKey, Thread.CurrentThread.CurrentUICulture); - } + public static string Localize(this ILocalizedTextService manager, string area, string alias) + => manager.Localize(area, alias, Thread.CurrentThread.CurrentUICulture); /// /// Localize using the current thread culture /// /// - /// + /// + /// /// /// - public static string Localize(this ILocalizedTextService manager, string key, string[] tokens) - { - return manager.Localize(key, Thread.CurrentThread.CurrentUICulture, tokens); - } + public static string Localize(this ILocalizedTextService manager, string area, string alias, string[] tokens) + => manager.Localize(area, alias, Thread.CurrentThread.CurrentUICulture, ConvertToDictionaryVars(tokens)); /// /// Localize using the current thread culture /// /// - /// + /// + /// /// /// - public static string Localize(this ILocalizedTextService manager, string key, IDictionary tokens = null) - { - return manager.Localize(key, Thread.CurrentThread.CurrentUICulture, tokens); - } + public static string Localize(this ILocalizedTextService manager, string area, string alias, IDictionary tokens = null) + => manager.Localize(area, alias, Thread.CurrentThread.CurrentUICulture, tokens); /// /// Localize a key without any variables /// /// - /// + /// + /// /// /// /// - public static string Localize(this ILocalizedTextService manager, string key, CultureInfo culture, string[] tokens) - { - return manager.Localize(key, culture, ConvertToDictionaryVars(tokens)); - } + public static string Localize(this ILocalizedTextService manager, string area, string alias, CultureInfo culture, string[] tokens) + => manager.Localize(area, alias, Thread.CurrentThread.CurrentUICulture, tokens); - /// - /// Convert an array of strings to a dictionary of indices -> values - /// - /// - /// - internal static IDictionary ConvertToDictionaryVars(string[] variables) - { - if (variables == null) return null; - if (variables.Any() == false) return null; + /// + /// Convert an array of strings to a dictionary of indices -> values + /// + /// + /// + internal static IDictionary ConvertToDictionaryVars(string[] variables) + { + if (variables == null) return null; + if (variables.Any() == false) return null; - return variables.Select((s, i) => new { index = i.ToString(CultureInfo.InvariantCulture), value = s }) - .ToDictionary(keyvals => keyvals.index, keyvals => keyvals.value); - } + return variables.Select((s, i) => new { index = i.ToString(CultureInfo.InvariantCulture), value = s }) + .ToDictionary(keyvals => keyvals.index, keyvals => keyvals.value); + } - public static string UmbracoDictionaryTranslate(this ILocalizedTextService manager, ICultureDictionary cultureDictionary, string text) - { - if (text == null) - return null; + public static string UmbracoDictionaryTranslate(this ILocalizedTextService manager, ICultureDictionary cultureDictionary, string text) + { + if (text == null) + return null; - if (text.StartsWith("#") == false) - return text; + if (text.StartsWith("#") == false) + return text; - text = text.Substring(1); - var value = cultureDictionary[text]; - if (value.IsNullOrWhiteSpace() == false) - { - return value; - } + text = text.Substring(1); + var value = cultureDictionary[text]; + if (value.IsNullOrWhiteSpace() == false) + { + return value; + } - value = manager.Localize(text.Replace('_', '/')); - return value.StartsWith("[") ? text : value; - } + var areaAndKey = text.Split('_'); + + value = manager.Localize(areaAndKey[0], areaAndKey[1]); + return value.StartsWith("[") ? text : value; + } } } diff --git a/src/Umbraco.Core/Trees/MenuItemList.cs b/src/Umbraco.Core/Trees/MenuItemList.cs index d2468f724b..7588fe778a 100644 --- a/src/Umbraco.Core/Trees/MenuItemList.cs +++ b/src/Umbraco.Core/Trees/MenuItemList.cs @@ -59,7 +59,7 @@ namespace Umbraco.Cms.Core.Trees var values = textService.GetAllStoredValues(Thread.CurrentThread.CurrentUICulture); values.TryGetValue($"visuallyHiddenTexts/{item.Alias}Description", out var textDescription); - var menuItem = new MenuItem(item, textService.Localize($"actions/{item.Alias}")) + var menuItem = new MenuItem(item, textService.Localize($"actions", item.Alias)) { SeparatorBefore = hasSeparator, OpensDialog = opensDialog, diff --git a/src/Umbraco.Core/Trees/Tree.cs b/src/Umbraco.Core/Trees/Tree.cs index a0286090ec..a3b3d42b14 100644 --- a/src/Umbraco.Core/Trees/Tree.cs +++ b/src/Umbraco.Core/Trees/Tree.cs @@ -54,7 +54,7 @@ namespace Umbraco.Cms.Core.Trees var label = $"[{tree.TreeAlias}]"; // try to look up a the localized tree header matching the tree alias - var localizedLabel = textService.Localize("treeHeaders/" + tree.TreeAlias); + var localizedLabel = textService.Localize("treeHeader", tree.TreeAlias); // if the localizedLabel returns [alias] then return the title if it's defined if (localizedLabel != null && localizedLabel.Equals(label, StringComparison.InvariantCultureIgnoreCase)) diff --git a/src/Umbraco.Examine.Lucene/BackOfficeExamineSearcher.cs b/src/Umbraco.Examine.Lucene/BackOfficeExamineSearcher.cs index 0a99c4d6ef..27fdf1f2f7 100644 --- a/src/Umbraco.Examine.Lucene/BackOfficeExamineSearcher.cs +++ b/src/Umbraco.Examine.Lucene/BackOfficeExamineSearcher.cs @@ -8,6 +8,7 @@ using System.Text; using System.Text.RegularExpressions; using Examine; using Examine.Search; +using Examine.Search; using Lucene.Net.QueryParsers.Classic; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; @@ -61,6 +62,15 @@ namespace Umbraco.Cms.Infrastructure.Examine var indexName = Constants.UmbracoIndexes.InternalIndexName; var fields = _treeSearcherFields.GetBackOfficeFields().ToList(); + ISet fieldsToLoad = new HashSet(_treeSearcherFields.GetBackOfficeFieldsToLoad()); + + // TODO: WE should try to allow passing in a lucene raw query, however we will still need to do some manual string + // manipulation for things like start paths, member types, etc... + //if (Examine.ExamineExtensions.TryParseLuceneQuery(query)) + //{ + + //} + //special GUID check since if a user searches on one specifically we need to escape it if (Guid.TryParse(query, out var g)) { @@ -75,6 +85,11 @@ namespace Umbraco.Cms.Infrastructure.Examine indexName = Constants.UmbracoIndexes.MembersIndexName; type = "member"; fields.AddRange(_treeSearcherFields.GetBackOfficeMembersFields()); + foreach(var field in _treeSearcherFields.GetBackOfficeMembersFieldsToLoad()) + { + fieldsToLoad.Add(field); + } + if (searchFrom != null && searchFrom != Constants.Conventions.MemberTypes.AllMembersListId && searchFrom.Trim() != "-1") { sb.Append("+__NodeTypeAlias:"); @@ -85,6 +100,11 @@ namespace Umbraco.Cms.Infrastructure.Examine case UmbracoEntityTypes.Media: type = "media"; fields.AddRange(_treeSearcherFields.GetBackOfficeMediaFields()); + foreach (var field in _treeSearcherFields.GetBackOfficeMediaFieldsToLoad()) + { + fieldsToLoad.Add(field); + } + var allMediaStartNodes = currentUser != null ? currentUser.CalculateMediaStartNodeIds(_entityService, _appCaches) : Array.Empty(); @@ -93,6 +113,10 @@ namespace Umbraco.Cms.Infrastructure.Examine case UmbracoEntityTypes.Document: type = "content"; fields.AddRange(_treeSearcherFields.GetBackOfficeDocumentFields()); + foreach (var field in _treeSearcherFields.GetBackOfficeDocumentFieldsToLoad()) + { + fieldsToLoad.Add(field); + } var allContentStartNodes = currentUser != null ? currentUser.CalculateContentStartNodeIds(_entityService, _appCaches) : Array.Empty(); @@ -113,7 +137,9 @@ namespace Umbraco.Cms.Infrastructure.Examine var result = index.Searcher .CreateQuery() - .NativeQuery(sb.ToString()) + .NativeQuery(sb.ToString()) + .SelectFields(fieldsToLoad) + //only return the number of items specified to read up to the amount of records to fill from 0 -> the number of items on the page requested .Execute(QueryOptions.SkipTake(Convert.ToInt32(pageSize * pageIndex), pageSize)); totalFound = result.TotalItemCount; diff --git a/src/Umbraco.Examine.Lucene/UmbracoContentIndex.cs b/src/Umbraco.Examine.Lucene/UmbracoContentIndex.cs index b3852254af..56d987830a 100644 --- a/src/Umbraco.Examine.Lucene/UmbracoContentIndex.cs +++ b/src/Umbraco.Examine.Lucene/UmbracoContentIndex.cs @@ -19,7 +19,7 @@ namespace Umbraco.Cms.Infrastructure.Examine public class UmbracoContentIndex : UmbracoExamineIndex, IUmbracoContentIndex, IDisposable { private readonly ILogger _logger; - + private readonly ISet _idOnlyFieldSet = new HashSet { "id" }; public UmbracoContentIndex( ILoggerFactory loggerFactory, string name, @@ -133,7 +133,8 @@ namespace Umbraco.Cms.Infrastructure.Examine var rawQuery = $"{UmbracoExamineFieldNames.IndexPathFieldName}:{descendantPath}"; var c = Searcher.CreateQuery(); var filtered = c.NativeQuery(rawQuery); - var results = filtered.Execute(); + var selectedFields = filtered.SelectFields(_idOnlyFieldSet); + var results = selectedFields.Execute(); _logger. LogDebug("DeleteFromIndex with query: {Query} (found {TotalItems} results)", rawQuery, results.TotalItemCount); diff --git a/src/Umbraco.Infrastructure/Events/RelateOnTrashNotificationHandler.cs b/src/Umbraco.Infrastructure/Events/RelateOnTrashNotificationHandler.cs index 67c1dbda06..df0f074ea3 100644 --- a/src/Umbraco.Infrastructure/Events/RelateOnTrashNotificationHandler.cs +++ b/src/Umbraco.Infrastructure/Events/RelateOnTrashNotificationHandler.cs @@ -88,7 +88,7 @@ namespace Umbraco.Cms.Core.Events 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) ); } } @@ -143,7 +143,7 @@ namespace Umbraco.Cms.Core.Events 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) ); } } diff --git a/src/Umbraco.Infrastructure/Examine/GenericIndexDiagnostics.cs b/src/Umbraco.Infrastructure/Examine/GenericIndexDiagnostics.cs index 2ff01c51dc..36d2e60acb 100644 --- a/src/Umbraco.Infrastructure/Examine/GenericIndexDiagnostics.cs +++ b/src/Umbraco.Infrastructure/Examine/GenericIndexDiagnostics.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Examine; +using Examine.Search; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Composing; using Umbraco.Extensions; @@ -18,6 +19,7 @@ namespace Umbraco.Cms.Infrastructure.Examine private readonly IIndex _index; private static readonly string[] s_ignoreProperties = { "Description" }; + private readonly ISet _idOnlyFieldSet = new HashSet { "id" }; public GenericIndexDiagnostics(IIndex index) => _index = index; public int DocumentCount => -1; //unknown @@ -31,7 +33,7 @@ namespace Umbraco.Cms.Infrastructure.Examine try { - var result = _index.Searcher.Search("test"); + var result = _index.Searcher.CreateQuery().ManagedQuery("test").SelectFields(_idOnlyFieldSet).Execute(new QueryOptions(0, 1)); return Attempt.Succeed(); //if we can search we'll assume it's healthy } catch (Exception e) diff --git a/src/Umbraco.Infrastructure/Examine/IUmbracoTreeSearcherFields.cs b/src/Umbraco.Infrastructure/Examine/IUmbracoTreeSearcherFields.cs index 6d0d3f8efb..fe135a82b7 100644 --- a/src/Umbraco.Infrastructure/Examine/IUmbracoTreeSearcherFields.cs +++ b/src/Umbraco.Infrastructure/Examine/IUmbracoTreeSearcherFields.cs @@ -8,20 +8,28 @@ namespace Umbraco.Cms.Infrastructure.Examine public interface IUmbracoTreeSearcherFields { /// - /// Propagate list of searchable fields for all node types + /// The default index fields that are searched on in the back office search for umbraco content entities. /// IEnumerable GetBackOfficeFields(); + /// - /// Propagate list of searchable fields for Members + /// The additional index fields that are searched on in the back office for member entities. /// IEnumerable GetBackOfficeMembersFields(); + /// - /// Propagate list of searchable fields for Media + /// The additional index fields that are searched on in the back office for media entities. /// IEnumerable GetBackOfficeMediaFields(); + /// - /// Propagate list of searchable fields for Documents + /// The additional index fields that are searched on in the back office for document entities. /// IEnumerable GetBackOfficeDocumentFields(); + + ISet GetBackOfficeFieldsToLoad(); + ISet GetBackOfficeMembersFieldsToLoad(); + ISet GetBackOfficeDocumentFieldsToLoad(); + ISet GetBackOfficeMediaFieldsToLoad(); } } diff --git a/src/Umbraco.Infrastructure/Examine/UmbracoExamineFieldNames.cs b/src/Umbraco.Infrastructure/Examine/UmbracoExamineFieldNames.cs index 90f1ddf634..72e914c584 100644 --- a/src/Umbraco.Infrastructure/Examine/UmbracoExamineFieldNames.cs +++ b/src/Umbraco.Infrastructure/Examine/UmbracoExamineFieldNames.cs @@ -21,5 +21,8 @@ namespace Umbraco.Cms.Infrastructure.Examine public const string VariesByCultureFieldName = ExamineFieldNames.SpecialFieldPrefix + "VariesByCulture"; public const string NodeNameFieldName = "nodeName"; + public const string ItemIdFieldName ="__NodeId"; + public const string CategoryFieldName = "__IndexType"; + public const string ItemTypeFieldName = "__NodeTypeAlias"; } } diff --git a/src/Umbraco.Infrastructure/Extensions/MediaPicker3ConfigurationExtensions.cs b/src/Umbraco.Infrastructure/Extensions/MediaPicker3ConfigurationExtensions.cs new file mode 100644 index 0000000000..fe4aa541a0 --- /dev/null +++ b/src/Umbraco.Infrastructure/Extensions/MediaPicker3ConfigurationExtensions.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using System.Linq; +using Umbraco.Cms.Core.PropertyEditors; +using Umbraco.Cms.Core.PropertyEditors.ValueConverters; + +namespace Umbraco.Extensions +{ + public static class MediaPicker3ConfigurationExtensions + { + /// + /// Applies the configuration to ensure only valid crops are kept and have the correct width/height. + /// + /// The configuration. + public static void ApplyConfiguration(this ImageCropperValue imageCropperValue, MediaPicker3Configuration configuration) + { + var crops = new List(); + + var configuredCrops = configuration?.Crops; + if (configuredCrops != null) + { + foreach (var configuredCrop in configuredCrops) + { + var crop = imageCropperValue.Crops?.FirstOrDefault(x => x.Alias == configuredCrop.Alias); + + crops.Add(new ImageCropperValue.ImageCropperCrop + { + Alias = configuredCrop.Alias, + Width = configuredCrop.Width, + Height = configuredCrop.Height, + Coordinates = crop?.Coordinates + }); + } + } + + imageCropperValue.Crops = crops; + + if (configuration?.EnableLocalFocalPoint == false) + { + imageCropperValue.FocalPoint = null; + } + } + } +} diff --git a/src/Umbraco.Infrastructure/IPublishedContentQuery.cs b/src/Umbraco.Infrastructure/IPublishedContentQuery.cs index e487873ab2..24bd7ba44c 100644 --- a/src/Umbraco.Infrastructure/IPublishedContentQuery.cs +++ b/src/Umbraco.Infrastructure/IPublishedContentQuery.cs @@ -35,6 +35,29 @@ namespace Umbraco.Cms.Core IEnumerable Media(IEnumerable ids); IEnumerable MediaAtRoot(); + /// + /// Searches content. + /// + /// The term to search. + /// The amount of results to skip. + /// The amount of results to take/return. + /// The total amount of records. + /// The culture (defaults to a culture insensitive search). + /// The name of the index to search (defaults to ). + /// The fields to load in the results of the search (defaults to all fields loaded). + /// + /// The search results. + /// + /// + /// + /// When the is not specified or is *, all cultures are searched. + /// To search for only invariant documents and fields use null. + /// When searching on a specific culture, all culture specific fields are searched for the provided culture and all invariant fields for all documents. + /// + /// While enumerating results, the ambient culture is changed to be the searched culture. + /// + IEnumerable Search(string term, int skip, int take, out long totalRecords, string culture = "*", string indexName = Constants.UmbracoIndexes.ExternalIndexName, ISet loadedFields = null); + /// /// Searches content. /// diff --git a/src/Umbraco.Infrastructure/Models/Mapping/EntityMapDefinition.cs b/src/Umbraco.Infrastructure/Models/Mapping/EntityMapDefinition.cs index 9d511eb877..b34257ec8c 100644 --- a/src/Umbraco.Infrastructure/Models/Mapping/EntityMapDefinition.cs +++ b/src/Umbraco.Infrastructure/Models/Mapping/EntityMapDefinition.cs @@ -177,7 +177,7 @@ namespace Umbraco.Cms.Core.Models.Mapping target.Name = source.Values.ContainsKey(UmbracoExamineFieldNames.NodeNameFieldName) ? source.Values[UmbracoExamineFieldNames.NodeNameFieldName] : "[no name]"; - var culture = context.GetCulture(); + var culture = context.GetCulture()?.ToLowerInvariant(); if(culture.IsNullOrWhiteSpace() == false) { target.Name = source.Values.ContainsKey($"nodeName_{culture}") ? source.Values[$"nodeName_{culture}"] : target.Name; diff --git a/src/Umbraco.Infrastructure/Models/MediaWithCrops.cs b/src/Umbraco.Infrastructure/Models/MediaWithCrops.cs index cc80a34310..14cc8e31f8 100644 --- a/src/Umbraco.Infrastructure/Models/MediaWithCrops.cs +++ b/src/Umbraco.Infrastructure/Models/MediaWithCrops.cs @@ -1,17 +1,79 @@ - - using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PropertyEditors.ValueConverters; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Core.Models { /// - /// Model used in Razor Views for rendering + /// Represents a media item with local crops. /// - public class MediaWithCrops + /// + public class MediaWithCrops : PublishedContentWrapped { - public IPublishedContent MediaItem { get; set; } - public ImageCropperValue LocalCrops { get; set; } + /// + /// Gets the content/media item. + /// + /// + /// The content/media item. + /// + public IPublishedContent Content => Unwrap(); + + /// + /// Gets the local crops. + /// + /// + /// The local crops. + /// + public ImageCropperValue LocalCrops { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The content. + /// The published value fallback. + /// The local crops. + public MediaWithCrops(IPublishedContent content, IPublishedValueFallback publishedValueFallback, ImageCropperValue localCrops) + : base(content, publishedValueFallback) + { + LocalCrops = localCrops; + } + } + + /// + /// Represents a media item with local crops. + /// + /// The type of the media item. + /// + public class MediaWithCrops : MediaWithCrops + where T : IPublishedContent + { + /// + /// Gets the media item. + /// + /// + /// The media item. + /// + public new T Content { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The content. + /// The published value fallback. + /// The local crops. + public MediaWithCrops(T content,IPublishedValueFallback publishedValueFallback, ImageCropperValue localCrops) + : base(content, publishedValueFallback, localCrops) + { + Content = content; + } + + /// + /// Performs an implicit conversion from to . + /// + /// The media with crops. + /// + /// The result of the conversion. + /// + public static implicit operator T(MediaWithCrops mediaWithCrops) => mediaWithCrops.Content; } } diff --git a/src/Umbraco.Infrastructure/Persistence/UmbracoDatabase.cs b/src/Umbraco.Infrastructure/Persistence/UmbracoDatabase.cs index d95b5feaf7..090e1d4a50 100644 --- a/src/Umbraco.Infrastructure/Persistence/UmbracoDatabase.cs +++ b/src/Umbraco.Infrastructure/Persistence/UmbracoDatabase.cs @@ -14,6 +14,7 @@ using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Persistence { + /// /// Extends NPoco Database for Umbraco. /// diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyEditor.cs index 6828b3938f..49cc6b2902 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyEditor.cs @@ -265,7 +265,7 @@ namespace Umbraco.Cms.Core.PropertyEditors || (blockEditorData != null && validationLimit.Min.HasValue && blockEditorData.Layout.Count() < validationLimit.Min)) { yield return new ValidationResult( - _textService.Localize("validation/entriesShort", new[] + _textService.Localize("validation", "entriesShort", new[] { validationLimit.Min.ToString(), (validationLimit.Min - blockEditorData.Layout.Count()).ToString() @@ -276,7 +276,7 @@ namespace Umbraco.Cms.Core.PropertyEditors if (blockEditorData != null && validationLimit.Max.HasValue && blockEditorData.Layout.Count() > validationLimit.Max) { yield return new ValidationResult( - _textService.Localize("validation/entriesExceed", new[] + _textService.Localize("validation", "entriesExceed", new[] { validationLimit.Max.ToString(), (blockEditorData.Layout.Count() - validationLimit.Max).ToString() diff --git a/src/Umbraco.Infrastructure/PropertyEditors/DropDownFlexibleConfigurationEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/DropDownFlexibleConfigurationEditor.cs index cc755f7ed1..5538546ece 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/DropDownFlexibleConfigurationEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/DropDownFlexibleConfigurationEditor.cs @@ -17,7 +17,7 @@ namespace Umbraco.Cms.Core.PropertyEditors var items = Fields.First(x => x.Key == "items"); // customize the items field - items.Name = textService.Localize("editdatatype/addPrevalue"); + items.Name = textService.Localize("editdatatype", "addPrevalue"); items.Validators.Add(new ValueListUniqueValueValidator()); } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ImageCropperConfiguration.cs b/src/Umbraco.Infrastructure/PropertyEditors/ImageCropperConfiguration.cs index 1ffa38d94d..987b275ee1 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ImageCropperConfiguration.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ImageCropperConfiguration.cs @@ -1,7 +1,10 @@ // Copyright (c) Umbraco. // See LICENSE for more details. +using System.Collections.Generic; +using System.Linq; using System.Runtime.Serialization; +using Umbraco.Cms.Core.PropertyEditors.ValueConverters; namespace Umbraco.Cms.Core.PropertyEditors { @@ -26,4 +29,35 @@ namespace Umbraco.Cms.Core.PropertyEditors public int Height { get; set; } } } + + internal static class ImageCropperConfigurationExtensions + { + /// + /// Applies the configuration to ensure only valid crops are kept and have the correct width/height. + /// + /// The configuration. + public static void ApplyConfiguration(this ImageCropperValue imageCropperValue, ImageCropperConfiguration configuration) + { + var crops = new List(); + + var configuredCrops = configuration?.Crops; + if (configuredCrops != null) + { + foreach (var configuredCrop in configuredCrops) + { + var crop = imageCropperValue.Crops?.FirstOrDefault(x => x.Alias == configuredCrop.Alias); + + crops.Add(new ImageCropperValue.ImageCropperCrop + { + Alias = configuredCrop.Alias, + Width = configuredCrop.Width, + Height = configuredCrop.Height, + Coordinates = crop?.Coordinates + }); + } + } + + imageCropperValue.Crops = crops; + } + } } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/MediaPicker3PropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/MediaPicker3PropertyEditor.cs index 0b80116c2e..45aa507a54 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/MediaPicker3PropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/MediaPicker3PropertyEditor.cs @@ -8,6 +8,11 @@ using Umbraco.Cms.Core.PropertyEditors.ValueConverters; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; +using Newtonsoft.Json; +using System; +using System.Linq; +using System.Runtime.Serialization; +using Umbraco.Extensions; namespace Umbraco.Cms.Core.PropertyEditors { @@ -26,6 +31,9 @@ namespace Umbraco.Cms.Core.PropertyEditors { private readonly IIOHelper _ioHelper; + /// + /// Initializes a new instance of the class. + /// public MediaPicker3PropertyEditor( IDataValueEditorFactory dataValueEditorFactory, IIOHelper ioHelper, @@ -35,9 +43,11 @@ namespace Umbraco.Cms.Core.PropertyEditors _ioHelper = ioHelper; } + /// protected override IConfigurationEditor CreateConfigurationEditor() => new MediaPicker3ConfigurationEditor(_ioHelper); + /// protected override IDataValueEditor CreateValueEditor() => DataValueEditorFactory.Create(Attribute); internal class MediaPicker3PropertyValueEditor : DataValueEditor, IDataValueReference @@ -55,6 +65,13 @@ namespace Umbraco.Cms.Core.PropertyEditors _jsonSerializer = jsonSerializer; } + public override object ToEditor(IProperty property, string culture = null, string segment = null) + { + var value = property.GetValue(culture, segment); + + return Deserialize(_jsonSerializer, value); + } + /// /// Note: no FromEditor() and ToEditor() methods /// We do not want to transform the way the data is stored in the DB and would like to keep a raw JSON string @@ -62,19 +79,70 @@ namespace Umbraco.Cms.Core.PropertyEditors public IEnumerable GetReferences(object value) { - var rawJson = value == null ? string.Empty : value is string str ? str : value.ToString(); - - if (string.IsNullOrWhiteSpace(rawJson)) - yield break; - - var mediaWithCropsDtos = _jsonSerializer.Deserialize(rawJson); - - foreach (var mediaWithCropsDto in mediaWithCropsDtos) + foreach (var dto in Deserialize(_jsonSerializer, value)) { - yield return new UmbracoEntityReference(GuidUdi.Create(Constants.UdiEntityType.Media, mediaWithCropsDto.MediaKey)); + yield return new UmbracoEntityReference(Udi.Create(Constants.UdiEntityType.Media, dto.MediaKey)); } } + internal static IEnumerable Deserialize(IJsonSerializer jsonSerializer,object value) + { + var rawJson = value is string str ? str : value?.ToString(); + if (string.IsNullOrWhiteSpace(rawJson)) + { + yield break; + } + + if (!rawJson.DetectIsJson()) + { + // Old comma seperated UDI format + foreach (var udiStr in rawJson.Split(Constants.CharArrays.Comma)) + { + if (UdiParser.TryParse(udiStr, out GuidUdi udi)) + { + yield return new MediaWithCropsDto + { + Key = Guid.NewGuid(), + MediaKey = udi.Guid, + Crops = Enumerable.Empty(), + FocalPoint = new ImageCropperValue.ImageCropperFocalPoint + { + Left = 0.5m, + Top = 0.5m + } + }; + } + } + } + else + { + // New JSON format + foreach (var dto in jsonSerializer.Deserialize>(rawJson)) + { + yield return dto; + } + } + } + + + /// + /// Model/DTO that represents the JSON that the MediaPicker3 stores. + /// + [DataContract] + internal class MediaWithCropsDto + { + [DataMember(Name = "key")] + public Guid Key { get; set; } + + [DataMember(Name = "mediaKey")] + public Guid MediaKey { get; set; } + + [DataMember(Name = "crops")] + public IEnumerable Crops { get; set; } + + [DataMember(Name = "focalPoint")] + public ImageCropperValue.ImageCropperFocalPoint FocalPoint { get; set; } + } } } } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyEditor.cs index 452c27c096..80ed34e6e1 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyEditor.cs @@ -158,6 +158,7 @@ namespace Umbraco.Cms.Core.PropertyEditors public override object ToEditor(IProperty property, string culture = null, string segment = null) { var val = property.GetValue(culture, segment); + var valEditors = new Dictionary(); var rows = _nestedContentValues.GetPropertyValues(val); @@ -186,8 +187,15 @@ namespace Umbraco.Cms.Core.PropertyEditors continue; } - var tempConfig = _dataTypeService.GetDataType(prop.Value.PropertyType.DataTypeId).Configuration; - var valEditor = propEditor.GetValueEditor(tempConfig); + var dataTypeId = prop.Value.PropertyType.DataTypeId; + if (!valEditors.TryGetValue(dataTypeId, out var valEditor)) + { + var tempConfig = _dataTypeService.GetDataType(dataTypeId).Configuration; + valEditor = propEditor.GetValueEditor(tempConfig); + + valEditors.Add(dataTypeId, valEditor); + } + var convValue = valEditor.ToEditor(tempProp); // update the raw value since this is what will get serialized out diff --git a/src/Umbraco.Infrastructure/PropertyEditors/UploadFileTypeValidator.cs b/src/Umbraco.Infrastructure/PropertyEditors/UploadFileTypeValidator.cs index afa4f48249..61597bc47b 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/UploadFileTypeValidator.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/UploadFileTypeValidator.cs @@ -50,7 +50,7 @@ namespace Umbraco.Cms.Core.PropertyEditors { //we only store a single value for this editor so the 'member' or 'field' // we'll associate this error with will simply be called 'value' - yield return new ValidationResult(_localizedTextService.Localize("errors/dissallowedMediaType"), new[] { "value" }); + yield return new ValidationResult(_localizedTextService.Localize("errors", "dissallowedMediaType"), new[] { "value" }); } } } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs index f1995c6732..98879fb0c3 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs @@ -116,6 +116,7 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters settingsData = null; } + // TODO: This should be optimized/cached, as calling Activator.CreateInstance is slow var layoutType = typeof(BlockListItem<,>).MakeGenericType(contentData.GetType(), settingsData?.GetType() ?? typeof(IPublishedElement)); var layoutRef = (BlockListItem)Activator.CreateInstance(layoutType, contentGuidUdi, contentData, settingGuidUdi, settingsData); diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/ImageCropperValue.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/ImageCropperValue.cs index c61289bcc9..32101d6cf7 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/ImageCropperValue.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/ImageCropperValue.cs @@ -130,7 +130,7 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters /// Determines whether the value has a specified crop. /// public bool HasCrop(string alias) - => Crops.Any(x => x.Alias == alias); + => Crops != null && Crops.Any(x => x.Alias == alias); /// /// Determines whether the value has a source image. @@ -138,46 +138,35 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters public bool HasImage() => !string.IsNullOrWhiteSpace(Src); - /// - /// Applies a configuration. - /// - /// Ensures that all crops defined in the configuration exists in the value. - public void ApplyConfiguration(ImageCropperConfiguration configuration) + public ImageCropperValue Merge(ImageCropperValue imageCropperValue) { - // merge the crop values - the alias + width + height comes from - // configuration, but each crop can store its own coordinates - - var configuredCrops = configuration?.Crops; - if (configuredCrops == null) return; - - //Use Crops if it's not null, otherwise create a new list var crops = Crops?.ToList() ?? new List(); - foreach (var configuredCrop in configuredCrops) + var incomingCrops = imageCropperValue?.Crops; + if (incomingCrops != null) { - var crop = crops.FirstOrDefault(x => x.Alias == configuredCrop.Alias); - if (crop != null) + foreach (var incomingCrop in incomingCrops) { - // found, apply the height & width - crop.Width = configuredCrop.Width; - crop.Height = configuredCrop.Height; - } - else - { - // not found, add - crops.Add(new ImageCropperCrop + var crop = crops.FirstOrDefault(x => x.Alias == incomingCrop.Alias); + if (crop == null) { - Alias = configuredCrop.Alias, - Width = configuredCrop.Width, - Height = configuredCrop.Height - }); + // Add incoming crop + crops.Add(incomingCrop); + } + else if (crop.Coordinates == null) + { + // Use incoming crop coordinates + crop.Coordinates = incomingCrop.Coordinates; + } } } - // assume we don't have to remove the crops in value, that - // are not part of configuration anymore? - - Crops = crops; + return new ImageCropperValue() + { + Src = !string.IsNullOrWhiteSpace(Src) ? Src : imageCropperValue?.Src, + Crops = crops, + FocalPoint = FocalPoint ?? imageCropperValue?.FocalPoint + }; } #region IEquatable diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/MediaPickerWithCropsValueConverter.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/MediaPickerWithCropsValueConverter.cs index 1e661866b1..0aa6fca5a9 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/MediaPickerWithCropsValueConverter.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/MediaPickerWithCropsValueConverter.cs @@ -1,13 +1,11 @@ - -using System; -using System.Collections; +using System; using System.Collections.Generic; -using System.Runtime.Serialization; -using Newtonsoft.Json; -using Umbraco.Cms.Core.Models; +using System.Linq; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Routing; +using Umbraco.Cms.Core.Serialization; +using Umbraco.Core.Models; using Umbraco.Extensions; namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters @@ -15,110 +13,85 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters [DefaultPropertyValueConverter] public class MediaPickerWithCropsValueConverter : PropertyValueConverterBase { - private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; private readonly IPublishedUrlProvider _publishedUrlProvider; + private readonly IJsonSerializer _jsonSerializer; public MediaPickerWithCropsValueConverter( IPublishedSnapshotAccessor publishedSnapshotAccessor, - IPublishedUrlProvider publishedUrlProvider) + IPublishedUrlProvider publishedUrlProvider, + IJsonSerializer jsonSerializer) { _publishedSnapshotAccessor = publishedSnapshotAccessor ?? throw new ArgumentNullException(nameof(publishedSnapshotAccessor)); _publishedUrlProvider = publishedUrlProvider; + _jsonSerializer = jsonSerializer; } + public override bool IsConverter(IPublishedPropertyType propertyType) => propertyType.EditorAlias.Equals(Core.Constants.PropertyEditors.Aliases.MediaPicker3); + + public override bool? IsValue(object value, PropertyValueLevel level) + { + var isValue = base.IsValue(value, level); + if (isValue != false && level == PropertyValueLevel.Source) + { + // Empty JSON array is not a value + isValue = value?.ToString() != "[]"; + } + + return isValue; + } + + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) + => IsMultipleDataType(propertyType.DataType) + ? typeof(IEnumerable) + : typeof(MediaWithCrops); + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Snapshot; - /// - /// Enusre this property value convertor is for the New Media Picker with Crops aka MediaPicker 3 - /// - public override bool IsConverter(IPublishedPropertyType propertyType) => propertyType.EditorAlias.Equals(Core.Constants.PropertyEditors.Aliases.MediaPicker3); - - /// - /// Check if the raw JSON value is not an empty array - /// - public override bool? IsValue(object value, PropertyValueLevel level) => value?.ToString() != "[]"; - - /// - /// What C# model type does the raw JSON return for Models & Views - /// - public override Type GetPropertyValueType(IPublishedPropertyType propertyType) - { - // Check do we want to return IPublishedContent collection still or a NEW model ? - var isMultiple = IsMultipleDataType(propertyType.DataType); - return isMultiple - ? typeof(IEnumerable) - : typeof(MediaWithCrops); - } - - public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview) => source?.ToString(); - public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) { - var mediaItems = new List(); var isMultiple = IsMultipleDataType(propertyType.DataType); - if (inter == null) + if (string.IsNullOrEmpty(inter?.ToString())) { - return isMultiple ? mediaItems: null; + // Short-circuit on empty value + return isMultiple ? Enumerable.Empty() : null; } - var dtos = JsonConvert.DeserializeObject>(inter.ToString()); + var mediaItems = new List(); + var dtos = MediaPicker3PropertyEditor.MediaPicker3PropertyValueEditor.Deserialize(_jsonSerializer, inter); + var configuration = propertyType.DataType.ConfigurationAs(); - foreach(var media in dtos) + foreach (var dto in dtos) { - var item = _publishedSnapshotAccessor.PublishedSnapshot.Media.GetById(media.MediaKey); - if (item != null) + var mediaItem = _publishedSnapshotAccessor.PublishedSnapshot.Media.GetById(preview, dto.MediaKey); + if (mediaItem != null) { - mediaItems.Add(new MediaWithCrops + var localCrops = new ImageCropperValue { - MediaItem = item, - LocalCrops = new ImageCropperValue - { - Crops = media.Crops, - FocalPoint = media.FocalPoint, - Src = item.Url(_publishedUrlProvider) - } - }); + Crops = dto.Crops, + FocalPoint = dto.FocalPoint, + Src = mediaItem.Url(_publishedUrlProvider) + }; + + localCrops.ApplyConfiguration(configuration); + + // TODO: This should be optimized/cached, as calling Activator.CreateInstance is slow + var mediaWithCropsType = typeof(MediaWithCrops<>).MakeGenericType(mediaItem.GetType()); + var mediaWithCrops = (MediaWithCrops)Activator.CreateInstance(mediaWithCropsType, mediaItem, localCrops); + + mediaItems.Add(mediaWithCrops); + + if (!isMultiple) + { + // Short-circuit on single item + break; + } } } - return isMultiple ? mediaItems : FirstOrDefault(mediaItems); + return isMultiple ? mediaItems : mediaItems.FirstOrDefault(); } - /// - /// Is the media picker configured to pick multiple media items - /// - /// - /// - private bool IsMultipleDataType(PublishedDataType dataType) - { - var config = dataType.ConfigurationAs(); - return config.Multiple; - } - - private object FirstOrDefault(IList mediaItems) - { - return mediaItems.Count == 0 ? null : mediaItems[0]; - } - - - /// - /// Model/DTO that represents the JSON that the MediaPicker3 stores - /// - [DataContract] - internal class MediaWithCropsDto - { - [DataMember(Name = "key")] - public Guid Key { get; set; } - - [DataMember(Name = "mediaKey")] - public Guid MediaKey { get; set; } - - [DataMember(Name = "crops")] - public IEnumerable Crops { get; set; } - - [DataMember(Name = "focalPoint")] - public ImageCropperValue.ImageCropperFocalPoint FocalPoint { get; set; } - } + private bool IsMultipleDataType(PublishedDataType dataType) => dataType.ConfigurationAs().Multiple; } } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueListConfigurationEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueListConfigurationEditor.cs index 1acd039b93..6b81329557 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ValueListConfigurationEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueListConfigurationEditor.cs @@ -23,7 +23,7 @@ namespace Umbraco.Cms.Core.PropertyEditors var items = Fields.First(x => x.Key == "items"); // customize the items field - items.Name = textService.Localize("editdatatype/addPrevalue"); + items.Name = textService.Localize("editdatatype", "addPrevalue"); items.Validators.Add(new ValueListUniqueValueValidator()); } diff --git a/src/Umbraco.Infrastructure/PublishedContentQuery.cs b/src/Umbraco.Infrastructure/PublishedContentQuery.cs index 47b98d8dc0..14298cbb4e 100644 --- a/src/Umbraco.Infrastructure/PublishedContentQuery.cs +++ b/src/Umbraco.Infrastructure/PublishedContentQuery.cs @@ -2,7 +2,6 @@ using System; using System.Collections; using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using System.Xml.XPath; using Examine; using Examine.Search; @@ -237,6 +236,10 @@ namespace Umbraco.Cms.Infrastructure /// public IEnumerable Search(string term, int skip, int take, out long totalRecords, string culture = "*", string indexName = Constants.UmbracoIndexes.ExternalIndexName) + => Search(term, skip, take, out totalRecords, culture, indexName, null); + + /// + public IEnumerable Search(string term, int skip, int take, out long totalRecords, string culture = "*", string indexName = Constants.UmbracoIndexes.ExternalIndexName, ISet loadedFields = null) { if (skip < 0) { @@ -284,6 +287,10 @@ namespace Umbraco.Cms.Infrastructure .ToArray(); // Get all index fields suffixed with the culture name supplied queryExecutor = query.ManagedQuery(term, fields); } + if (loadedFields != null && queryExecutor is IBooleanOperation booleanOperation) + { + queryExecutor = booleanOperation.SelectFields(loadedFields); + } var results = skip == 0 && take == 0 ? queryExecutor.Execute() diff --git a/src/Umbraco.Infrastructure/Search/UmbracoTreeSearcherFields.cs b/src/Umbraco.Infrastructure/Search/UmbracoTreeSearcherFields.cs index aa11c1ad54..26b638a436 100644 --- a/src/Umbraco.Infrastructure/Search/UmbracoTreeSearcherFields.cs +++ b/src/Umbraco.Infrastructure/Search/UmbracoTreeSearcherFields.cs @@ -1,31 +1,61 @@ + using System.Collections.Generic; using System.Linq; +using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Examine; namespace Umbraco.Cms.Infrastructure.Search { public class UmbracoTreeSearcherFields : IUmbracoTreeSearcherFields { - private IReadOnlyList _backOfficeFields = new List {"id", "__NodeId", "__Key"}; - public IEnumerable GetBackOfficeFields() - { - return _backOfficeFields; - } - - - private IReadOnlyList _backOfficeMembersFields = new List {"email", "loginName"}; - public IEnumerable GetBackOfficeMembersFields() - { - return _backOfficeMembersFields; - } + private IReadOnlyList _backOfficeFields = new List {"id", UmbracoExamineFieldNames.ItemIdFieldName, UmbracoExamineFieldNames.NodeKeyFieldName}; + private readonly ISet _backOfficeFieldsToLoad = new HashSet { "id", UmbracoExamineFieldNames.ItemIdFieldName, UmbracoExamineFieldNames.NodeKeyFieldName, "nodeName", UmbracoExamineFieldNames.IconFieldName, UmbracoExamineFieldNames.CategoryFieldName, "parentID", UmbracoExamineFieldNames.ItemTypeFieldName }; private IReadOnlyList _backOfficeMediaFields = new List { UmbracoExamineFieldNames.UmbracoFileFieldName }; - public IEnumerable GetBackOfficeMediaFields() + private readonly ISet _backOfficeMediaFieldsToLoad = new HashSet { UmbracoExamineFieldNames.UmbracoFileFieldName }; + private IReadOnlyList _backOfficeMembersFields = new List { "email", "loginName" }; + private readonly ISet _backOfficeMembersFieldsToLoad = new HashSet { "email", "loginName" }; + private readonly ISet _backOfficeDocumentFieldsToLoad = new HashSet { UmbracoExamineFieldNames.VariesByCultureFieldName }; + private readonly ILocalizationService _localizationService; + + public UmbracoTreeSearcherFields(ILocalizationService localizationService) { - return _backOfficeMediaFields; + _localizationService = localizationService; } - public IEnumerable GetBackOfficeDocumentFields() + + /// + public IEnumerable GetBackOfficeFields() => _backOfficeFields; + + /// + public IEnumerable GetBackOfficeMembersFields() => _backOfficeMembersFields; + + /// + public IEnumerable GetBackOfficeMediaFields() => _backOfficeMediaFields; + + /// + public IEnumerable GetBackOfficeDocumentFields() => Enumerable.Empty(); + + /// + public ISet GetBackOfficeFieldsToLoad() => _backOfficeFieldsToLoad; + + /// + public ISet GetBackOfficeMembersFieldsToLoad() => _backOfficeMembersFieldsToLoad; + + /// + public ISet GetBackOfficeMediaFieldsToLoad() => _backOfficeMediaFieldsToLoad; + + /// + public ISet GetBackOfficeDocumentFieldsToLoad() { - return Enumerable.Empty(); + var fields = _backOfficeDocumentFieldsToLoad; + + // We need to load all nodeName_* fields but we won't know those up front so need to get + // all langs (this is cached) + foreach(var field in _localizationService.GetAllLanguages().Select(x => "nodeName_" + x.IsoCode.ToLowerInvariant())) + { + fields.Add(field); + } + + return fields; } } } diff --git a/src/Umbraco.Infrastructure/Security/BackOfficeErrorDescriber.cs b/src/Umbraco.Infrastructure/Security/BackOfficeErrorDescriber.cs index c0021bc967..5f09a7a524 100644 --- a/src/Umbraco.Infrastructure/Security/BackOfficeErrorDescriber.cs +++ b/src/Umbraco.Infrastructure/Security/BackOfficeErrorDescriber.cs @@ -17,43 +17,43 @@ namespace Umbraco.Cms.Core.Security public override IdentityError DuplicateRoleName(string role) => new IdentityError { Code = nameof(DuplicateRoleName), - Description = _textService.Localize("validation/duplicateUserGroupName", new[] { role }) + Description = _textService.Localize("validation", "duplicateUserGroupName", new[] { role }) }; public override IdentityError InvalidRoleName(string role) => new IdentityError { Code = nameof(InvalidRoleName), - Description = _textService.Localize("validation/invalidUserGroupName") + Description = _textService.Localize("validation", "invalidUserGroupName") }; public override IdentityError LoginAlreadyAssociated() => new IdentityError { Code = nameof(LoginAlreadyAssociated), - Description = _textService.Localize("user/duplicateLogin") + Description = _textService.Localize("user", "duplicateLogin") }; public override IdentityError UserAlreadyHasPassword() => new IdentityError { Code = nameof(UserAlreadyHasPassword), - Description = _textService.Localize("user/userHasPassword") + Description = _textService.Localize("user", "userHasPassword") }; public override IdentityError UserAlreadyInRole(string role) => new IdentityError { Code = nameof(UserAlreadyInRole), - Description = _textService.Localize("user/userHasGroup", new[] { role }) + Description = _textService.Localize("user", "userHasGroup", new[] { role }) }; public override IdentityError UserLockoutNotEnabled() => new IdentityError { Code = nameof(UserLockoutNotEnabled), - Description = _textService.Localize("user/userLockoutNotEnabled") + Description = _textService.Localize("user", "userLockoutNotEnabled") }; public override IdentityError UserNotInRole(string role) => new IdentityError { Code = nameof(UserNotInRole), - Description = _textService.Localize("user/userNotInGroup", new[] { role }) + Description = _textService.Localize("user", "userNotInGroup", new[] { role }) }; } @@ -67,43 +67,43 @@ namespace Umbraco.Cms.Core.Security public override IdentityError DuplicateRoleName(string role) => new IdentityError { Code = nameof(DuplicateRoleName), - Description = _textService.Localize("validation/duplicateMemberGroupName", new[] { role }) + Description = _textService.Localize("validation", "duplicateMemberGroupName", new[] { role }) }; public override IdentityError InvalidRoleName(string role) => new IdentityError { Code = nameof(InvalidRoleName), - Description = _textService.Localize("validation/invalidMemberGroupName") + Description = _textService.Localize("validation", "invalidMemberGroupName") }; public override IdentityError LoginAlreadyAssociated() => new IdentityError { Code = nameof(LoginAlreadyAssociated), - Description = _textService.Localize("member/duplicateMemberLogin") + Description = _textService.Localize("member", "duplicateMemberLogin") }; public override IdentityError UserAlreadyHasPassword() => new IdentityError { Code = nameof(UserAlreadyHasPassword), - Description = _textService.Localize("member/memberHasPassword") + Description = _textService.Localize("member", "memberHasPassword") }; public override IdentityError UserAlreadyInRole(string role) => new IdentityError { Code = nameof(UserAlreadyInRole), - Description = _textService.Localize("member/memberHasGroup", new[] { role }) + Description = _textService.Localize("member", "memberHasGroup", new[] { role }) }; public override IdentityError UserLockoutNotEnabled() => new IdentityError { Code = nameof(UserLockoutNotEnabled), - Description = _textService.Localize("member/memberLockoutNotEnabled") + Description = _textService.Localize("member", "memberLockoutNotEnabled") }; public override IdentityError UserNotInRole(string role) => new IdentityError { Code = nameof(UserNotInRole), - Description = _textService.Localize("member/memberNotInGroup", new[] { role }) + Description = _textService.Localize("member", "memberNotInGroup", new[] { role }) }; } } diff --git a/src/Umbraco.Infrastructure/Security/UmbracoErrorDescriberBase.cs b/src/Umbraco.Infrastructure/Security/UmbracoErrorDescriberBase.cs index 0214811274..b5ff881249 100644 --- a/src/Umbraco.Infrastructure/Security/UmbracoErrorDescriberBase.cs +++ b/src/Umbraco.Infrastructure/Security/UmbracoErrorDescriberBase.cs @@ -13,91 +13,91 @@ namespace Umbraco.Cms.Core.Security public override IdentityError ConcurrencyFailure() => new IdentityError { Code = nameof(ConcurrencyFailure), - Description = _textService.Localize("errors/concurrencyError") + Description = _textService.Localize("errors", "concurrencyError") }; public override IdentityError DefaultError() => new IdentityError { Code = nameof(DefaultError), - Description = _textService.Localize("errors/defaultError") + Description = _textService.Localize("errors", "defaultError") }; public override IdentityError DuplicateEmail(string email) => new IdentityError { Code = nameof(DuplicateEmail), - Description = _textService.Localize("validation/duplicateEmail", new[] { email }) + Description = _textService.Localize("validation", "duplicateEmail", new[] { email }) }; public override IdentityError DuplicateUserName(string userName) => new IdentityError { Code = nameof(DuplicateUserName), - Description = _textService.Localize("validation/duplicateUsername", new[] { userName }) + Description = _textService.Localize("validation", "duplicateUsername", new[] { userName }) }; public override IdentityError InvalidEmail(string email) => new IdentityError { Code = nameof(InvalidEmail), - Description = _textService.Localize("validation/invalidEmail") + Description = _textService.Localize("validation", "invalidEmail") }; public override IdentityError InvalidToken() => new IdentityError { Code = nameof(InvalidToken), - Description = _textService.Localize("validation/invalidToken") + Description = _textService.Localize("validation", "invalidToken") }; public override IdentityError InvalidUserName(string userName) => new IdentityError { Code = nameof(InvalidUserName), - Description = _textService.Localize("validation/invalidUsername") + Description = _textService.Localize("validation", "invalidUsername") }; public override IdentityError PasswordMismatch() => new IdentityError { Code = nameof(PasswordMismatch), - Description = _textService.Localize("user/passwordMismatch") + Description = _textService.Localize("user", "passwordMismatch") }; public override IdentityError PasswordRequiresDigit() => new IdentityError { Code = nameof(PasswordRequiresDigit), - Description = _textService.Localize("user/passwordRequiresDigit") + Description = _textService.Localize("user", "passwordRequiresDigit") }; public override IdentityError PasswordRequiresLower() => new IdentityError { Code = nameof(PasswordRequiresLower), - Description = _textService.Localize("user/passwordRequiresLower") + Description = _textService.Localize("user", "passwordRequiresLower") }; public override IdentityError PasswordRequiresNonAlphanumeric() => new IdentityError { Code = nameof(PasswordRequiresNonAlphanumeric), - Description = _textService.Localize("user/passwordRequiresNonAlphanumeric") + Description = _textService.Localize("user", "passwordRequiresNonAlphanumeric") }; public override IdentityError PasswordRequiresUniqueChars(int uniqueChars) => new IdentityError { Code = nameof(PasswordRequiresUniqueChars), - Description = _textService.Localize("user/passwordRequiresUniqueChars", new[] { uniqueChars.ToString() }) + Description = _textService.Localize("user", "passwordRequiresUniqueChars", new[] { uniqueChars.ToString() }) }; public override IdentityError PasswordRequiresUpper() => new IdentityError { Code = nameof(PasswordRequiresUpper), - Description = _textService.Localize("user/passwordRequiresUpper") + Description = _textService.Localize("user", "passwordRequiresUpper") }; public override IdentityError PasswordTooShort(int length) => new IdentityError { Code = nameof(PasswordTooShort), - Description = _textService.Localize("user/passwordTooShort", new[] { length.ToString() }) + Description = _textService.Localize("user", "passwordTooShort", new[] { length.ToString() }) }; public override IdentityError RecoveryCodeRedemptionFailed() => new IdentityError { Code = nameof(RecoveryCodeRedemptionFailed), - Description = _textService.Localize("login/resetCodeExpired") + Description = _textService.Localize("login", "resetCodeExpired") }; } } diff --git a/src/Umbraco.Infrastructure/Services/Implement/ContentService.cs b/src/Umbraco.Infrastructure/Services/Implement/ContentService.cs index 85bc2c7f4d..3d4c6dfbbe 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/ContentService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/ContentService.cs @@ -194,11 +194,34 @@ namespace Umbraco.Cms.Core.Services.Implement // TODO: what about culture? var contentType = GetContentType(contentTypeAlias); - if (contentType == null) - throw new ArgumentException("No content type with that alias.", nameof(contentTypeAlias)); + return Create(name, parentId, contentType, userId); + } + + /// + /// Creates an object of a specified content type. + /// + /// This method simply returns a new, non-persisted, IContent without any identity. It + /// is intended as a shortcut to creating new content objects that does not invoke a save + /// operation against the database. + /// + /// The name of the content object. + /// The identifier of the parent, or -1. + /// The content type of the content + /// The optional id of the user creating the content. + /// The content object. + public IContent Create(string name, int parentId, IContentType contentType, + int userId = Constants.Security.SuperUserId) + { + if (contentType is null) + { + throw new ArgumentException("Content type must be specified", nameof(contentType)); + } + var parent = parentId > 0 ? GetById(parentId) : null; - if (parentId > 0 && parent == null) + if (parentId > 0 && parent is null) + { throw new ArgumentException("No content with that id.", nameof(parentId)); + } var content = new Content(name, parentId, contentType, userId); diff --git a/src/Umbraco.Infrastructure/Services/Implement/LocalizedTextService.cs b/src/Umbraco.Infrastructure/Services/Implement/LocalizedTextService.cs index b8588ba969..68fa4c37a5 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/LocalizedTextService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/LocalizedTextService.cs @@ -5,19 +5,18 @@ using System.Linq; using System.Xml.Linq; using System.Xml.XPath; using Microsoft.Extensions.Logging; -using Umbraco.Extensions; namespace Umbraco.Cms.Core.Services.Implement { - // TODO: Convert all of this over to Niels K's localization framework one day - public class LocalizedTextService : ILocalizedTextService { private readonly ILogger _logger; private readonly Lazy _fileSources; - private readonly IDictionary>> _dictionarySource; - private readonly IDictionary> _xmlSource; - + private IDictionary>> _dictionarySource => _dictionarySourceLazy.Value; + private IDictionary> _noAreaDictionarySource => _noAreaDictionarySourceLazy.Value; + private readonly Lazy>>> _dictionarySourceLazy; + private readonly Lazy>> _noAreaDictionarySourceLazy; + private readonly char[] _splitter = new[] { '/' }; /// /// Initializes with a file sources instance /// @@ -25,12 +24,50 @@ namespace Umbraco.Cms.Core.Services.Implement /// public LocalizedTextService(Lazy fileSources, ILogger logger) { - if (logger == null) throw new ArgumentNullException("logger"); + if (logger == null) throw new ArgumentNullException(nameof(logger)); _logger = logger; - if (fileSources == null) throw new ArgumentNullException("fileSources"); + if (fileSources == null) throw new ArgumentNullException(nameof(fileSources)); + _dictionarySourceLazy = new Lazy>>>(() => FileSourcesToAreaDictionarySources(fileSources.Value)); + _noAreaDictionarySourceLazy = new Lazy>>(() => FileSourcesToNoAreaDictionarySources(fileSources.Value)); _fileSources = fileSources; } + private IDictionary> FileSourcesToNoAreaDictionarySources(LocalizedTextServiceFileSources fileSources) + { + var xmlSources = fileSources.GetXmlSources(); + + return XmlSourceToNoAreaDictionary(xmlSources); + } + + private IDictionary> XmlSourceToNoAreaDictionary(IDictionary> xmlSources) + { + var cultureNoAreaDictionary = new Dictionary>(); + foreach (var xmlSource in xmlSources) + { + var noAreaAliasValue = GetNoAreaStoredTranslations(xmlSources, xmlSource.Key); + cultureNoAreaDictionary.Add(xmlSource.Key, noAreaAliasValue); + } + return cultureNoAreaDictionary; + } + + private IDictionary>> FileSourcesToAreaDictionarySources(LocalizedTextServiceFileSources fileSources) + { + var xmlSources = fileSources.GetXmlSources(); + return XmlSourcesToAreaDictionary(xmlSources); + } + + private IDictionary>> XmlSourcesToAreaDictionary(IDictionary> xmlSources) + { + var cultureDictionary = new Dictionary>>(); + foreach (var xmlSource in xmlSources) + { + var areaAliaValue = GetAreaStoredTranslations(xmlSources, xmlSource.Key); + cultureDictionary.Add(xmlSource.Key, areaAliaValue); + + } + return cultureDictionary; + } + /// /// Initializes with an XML source /// @@ -38,12 +75,15 @@ namespace Umbraco.Cms.Core.Services.Implement /// public LocalizedTextService(IDictionary> source, ILogger logger) { - if (source == null) throw new ArgumentNullException("source"); - if (logger == null) throw new ArgumentNullException("logger"); - _xmlSource = source; - _logger = logger; + if (source == null) throw new ArgumentNullException(nameof(source)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + + _dictionarySourceLazy = new Lazy>>>(() => XmlSourcesToAreaDictionary(source)); + _noAreaDictionarySourceLazy = new Lazy>>(() => XmlSourceToNoAreaDictionary(source)); + } + /// /// Initializes with a source of a dictionary of culture -> areas -> sub dictionary of keys/values /// @@ -51,37 +91,54 @@ namespace Umbraco.Cms.Core.Services.Implement /// public LocalizedTextService(IDictionary>> source, ILogger logger) { - _dictionarySource = source ?? throw new ArgumentNullException(nameof(source)); + var dictionarySource = source ?? throw new ArgumentNullException(nameof(source)); + _dictionarySourceLazy = new Lazy>>>(() => dictionarySource); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + var cultureNoAreaDictionary = new Dictionary>(); + foreach (var cultureDictionary in dictionarySource) + { + var areaAliaValue = GetAreaStoredTranslations(source, cultureDictionary.Key); + var aliasValue = new Dictionary(); + foreach (var area in areaAliaValue) + { + foreach (var alias in area.Value) + { + if (!aliasValue.ContainsKey(alias.Key)) + { + aliasValue.Add(alias.Key, alias.Value); + } + } + } + cultureNoAreaDictionary.Add(cultureDictionary.Key, aliasValue); + } + _noAreaDictionarySourceLazy = new Lazy>>(() => cultureNoAreaDictionary); } public string Localize(string key, CultureInfo culture, IDictionary tokens = null) { if (culture == null) throw new ArgumentNullException(nameof(culture)); - // TODO: Hack, see notes on ConvertToSupportedCultureWithRegionCode - culture = ConvertToSupportedCultureWithRegionCode(culture); - //This is what the legacy ui service did if (string.IsNullOrEmpty(key)) return string.Empty; - var keyParts = key.Split(new[] {'/'}, StringSplitOptions.RemoveEmptyEntries); + var keyParts = key.Split(_splitter, 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 tokens = null) + { + if (culture == null) throw new ArgumentNullException(nameof(culture)); - var xmlSource = _xmlSource ?? (_fileSources != null - ? _fileSources.Value.GetXmlSources() - : null); + //This is what the legacy ui service did + if (string.IsNullOrEmpty(alias)) + return string.Empty; - if (xmlSource != null) - { - return GetFromXmlSource(xmlSource, culture, area, alias, tokens); - } - else - { - return GetFromDictionarySource(culture, area, alias, tokens); - } + // TODO: Hack, see notes on ConvertToSupportedCultureWithRegionCode + culture = ConvertToSupportedCultureWithRegionCode(culture); + + return GetFromDictionarySource(culture, area, alias, tokens); } /// @@ -89,76 +146,105 @@ namespace Umbraco.Cms.Core.Services.Implement /// public IDictionary GetAllStoredValues(CultureInfo culture) { - if (culture == null) throw new ArgumentNullException("culture"); + if (culture == null) throw new ArgumentNullException(nameof(culture)); // TODO: Hack, see notes on ConvertToSupportedCultureWithRegionCode culture = ConvertToSupportedCultureWithRegionCode(culture); - var result = new Dictionary(); - - var xmlSource = _xmlSource ?? (_fileSources != null - ? _fileSources.Value.GetXmlSources() - : null); - - if (xmlSource != null) + if (_dictionarySource.ContainsKey(culture) == false) { - if (xmlSource.ContainsKey(culture) == false) + _logger.LogWarning("The culture specified {Culture} was not found in any configured sources for this service", culture); + return new Dictionary(0); + } + IDictionary result = new Dictionary(); + //convert all areas + keys to a single key with a '/' + foreach (var area in _dictionarySource[culture]) + { + foreach (var key in area.Value) { - _logger.LogWarning("The culture specified {Culture} was not found in any configured sources for this service", culture); - return result; - } - - // convert all areas + keys to a single key with a '/' - result = GetStoredTranslations(xmlSource, culture); - - // merge with the English file in case there's keys in there that don't exist in the local file - var englishCulture = CultureInfo.GetCultureInfo("en-US"); - if (culture.Equals(englishCulture) == false) - { - var englishResults = GetStoredTranslations(xmlSource, englishCulture); - foreach (var englishResult in englishResults.Where(englishResult => result.ContainsKey(englishResult.Key) == false)) + var dictionaryKey = string.Format("{0}/{1}", area.Key, key.Key); + //i don't think it's possible to have duplicates because we're dealing with a dictionary in the first place, but we'll double check here just in case. + if (result.ContainsKey(dictionaryKey) == false) { - result.Add(englishResult.Key, englishResult.Value); + result.Add(dictionaryKey, key.Value); } } } - else - { - if (_dictionarySource.ContainsKey(culture) == false) - { - _logger.LogWarning("The culture specified {Culture} was not found in any configured sources for this service", culture); - return result; - } - - // convert all areas + keys to a single key with a '/' - foreach (var area in _dictionarySource[culture]) - { - foreach (var key in area.Value) - { - var dictionaryKey = string.Format("{0}/{1}", area.Key, key.Key); - // i don't think it's possible to have duplicates because we're dealing with a dictionary in the first place, but we'll double check here just in case. - if (result.ContainsKey(dictionaryKey) == false) - { - result.Add(dictionaryKey, key.Value); - } - } - } - } - return result; } - private Dictionary GetStoredTranslations(IDictionary> xmlSource, CultureInfo cult) + private Dictionary> GetAreaStoredTranslations(IDictionary> xmlSource, CultureInfo cult) { - var result = new Dictionary(); + var overallResult = new Dictionary>(StringComparer.InvariantCulture); var areas = xmlSource[cult].Value.XPathSelectElements("//area"); foreach (var area in areas) { + var result = new Dictionary(StringComparer.InvariantCulture); var keys = area.XPathSelectElements("./key"); foreach (var key in keys) { - var dictionaryKey = string.Format("{0}/{1}", (string)area.Attribute("alias"), - (string)key.Attribute("alias")); + var dictionaryKey = + (string)key.Attribute("alias"); + //there could be duplicates if the language file isn't formatted nicely - which is probably the case for quite a few lang files + if (result.ContainsKey(dictionaryKey) == false) + result.Add(dictionaryKey, key.Value); + } + overallResult.Add(area.Attribute("alias").Value, result); + } + + //Merge English Dictionary + var englishCulture = new CultureInfo("en-US"); + if (!cult.Equals(englishCulture)) + { + var enUS = xmlSource[englishCulture].Value.XPathSelectElements("//area"); + foreach (var area in enUS) + { + IDictionary result = new Dictionary(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) + { + var dictionaryKey = + (string)key.Attribute("alias"); + //there could be duplicates if the language file isn't formatted nicely - which is probably the case for quite a few lang files + 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 GetNoAreaStoredTranslations(IDictionary> xmlSource, CultureInfo cult) + { + var result = new Dictionary(StringComparer.InvariantCulture); + var keys = xmlSource[cult].Value.XPathSelectElements("//key"); + + foreach (var key in keys) + { + var dictionaryKey = + (string)key.Attribute("alias"); + //there could be duplicates if the language file isn't formatted nicely - which is probably the case for quite a few lang files + if (result.ContainsKey(dictionaryKey) == false) + result.Add(dictionaryKey, key.Value); + } + + //Merge English Dictionary + var englishCulture = new CultureInfo("en-US"); + if (!cult.Equals(englishCulture)) + { + var keysEn = xmlSource[englishCulture].Value.XPathSelectElements("//key"); + + foreach (var key in keys) + { + var dictionaryKey = + (string)key.Attribute("alias"); //there could be duplicates if the language file isn't formatted nicely - which is probably the case for quite a few lang files if (result.ContainsKey(dictionaryKey) == false) result.Add(dictionaryKey, key.Value); @@ -166,6 +252,25 @@ namespace Umbraco.Cms.Core.Services.Implement } return result; } + private Dictionary> GetAreaStoredTranslations(IDictionary>> dictionarySource, CultureInfo cult) + { + var overallResult = new Dictionary>(StringComparer.InvariantCulture); + var areaDict = dictionarySource[cult]; + + foreach (var area in areaDict) + { + var result = new Dictionary(StringComparer.InvariantCulture); + var keys = area.Value.Keys; + foreach (var key in keys) + { + //there could be duplicates if the language file isn't formatted nicely - which is probably the case for quite a few lang files + if (result.ContainsKey(key) == false) + result.Add(key, area.Value[key]); + } + overallResult.Add(area.Key, result); + } + return overallResult; + } /// /// Returns a list of all currently supported cultures @@ -173,11 +278,7 @@ namespace Umbraco.Cms.Core.Services.Implement /// public IEnumerable GetSupportedCultures() { - var xmlSource = _xmlSource ?? (_fileSources != null - ? _fileSources.Value.GetXmlSources() - : null); - - return xmlSource != null ? xmlSource.Keys : _dictionarySource.Keys; + return _dictionarySource.Keys; } /// @@ -213,27 +314,25 @@ namespace Umbraco.Cms.Core.Services.Implement return "[" + key + "]"; } - var cultureSource = _dictionarySource[culture]; - string found; - if (area.IsNullOrWhiteSpace()) + string found = null; + if (string.IsNullOrWhiteSpace(area)) { - found = cultureSource - .SelectMany(x => x.Value) - .Where(keyvals => keyvals.Key.InvariantEquals(key)) - .Select(x => x.Value) - .FirstOrDefault(); + _noAreaDictionarySource[culture].TryGetValue(key, out found); } else { - found = cultureSource - .Where(areas => areas.Key.InvariantEquals(area)) - .SelectMany(a => a.Value) - .Where(keyvals => keyvals.Key.InvariantEquals(key)) - .Select(x => x.Value) - .FirstOrDefault(); + if (_dictionarySource[culture].TryGetValue(area, out var areaDictionary)) + { + areaDictionary.TryGetValue(key, out found); + } + if (found == null) + { + _noAreaDictionarySource[culture].TryGetValue(key, out found); + } } + if (found != null) { return ParseTokens(found, tokens); @@ -242,44 +341,6 @@ 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 + "]"; } - - private string GetFromXmlSource(IDictionary> xmlSource, CultureInfo culture, string area, string key, IDictionary tokens) - { - if (xmlSource.ContainsKey(culture) == false) - { - _logger.LogWarning("The culture specified {Culture} was not found in any configured sources for this service", culture); - return "[" + key + "]"; - } - - var found = FindTranslation(xmlSource, culture, area, key); - - if (found != null) - { - return ParseTokens(found.Value, tokens); - } - - // Fall back to English by default if we can't find the key - found = FindTranslation(xmlSource, new CultureInfo("en-US"), area, key); - if (found != null) - return ParseTokens(found.Value, tokens); - - // If it can't be found in either file, fall back to the default, showing just the key in square brackets - // NOTE: Based on how legacy works, the default text does not contain the area, just the key - return "[" + key + "]"; - } - - private XElement FindTranslation(IDictionary> xmlSource, CultureInfo culture, string area, string key) - { - var cultureSource = xmlSource[culture].Value; - - var xpath = area.IsNullOrWhiteSpace() - ? string.Format("//key [@alias = '{0}']", key) - : string.Format("//area [@alias = '{0}']/key [@alias = '{1}']", area, key); - - var found = cultureSource.XPathSelectElement(xpath); - return found; - } - /// /// Parses the tokens in the value /// @@ -303,11 +364,26 @@ namespace Umbraco.Cms.Core.Services.Implement foreach (var token in tokens) { - value = value.Replace(string.Format("{0}{1}{0}", "%", token.Key), token.Value); + value = value.Replace(string.Concat("%", token.Key, "%"), token.Value); } return value; } + public IDictionary> GetAllStoredValuesByAreaAndAlias(CultureInfo 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); + return new Dictionary>(0); + } + + return _dictionarySource[culture]; + } } } diff --git a/src/Umbraco.Persistence.SqlCe/SqlCeSyntaxProvider.cs b/src/Umbraco.Persistence.SqlCe/SqlCeSyntaxProvider.cs index 717eada77f..e81b6135da 100644 --- a/src/Umbraco.Persistence.SqlCe/SqlCeSyntaxProvider.cs +++ b/src/Umbraco.Persistence.SqlCe/SqlCeSyntaxProvider.cs @@ -299,7 +299,7 @@ where table_name=@0 and column_name=@1", tableName, columnName).FirstOrDefault() return string.Format(CreateIndex, GetIndexType(index.IndexType), " ", GetQuotedName(name), GetQuotedTableName(index.TableName), columns); } - + public override string GetSpecialDbType(SpecialDbTypes dbTypes) { if (dbTypes == SpecialDbTypes.NVARCHARMAX) // SqlCE does not have nvarchar(max) for now diff --git a/src/Umbraco.PublishedCache.NuCache/DataSource/BTree.DictionaryOfPropertyDataSerializer.cs b/src/Umbraco.PublishedCache.NuCache/DataSource/BTree.DictionaryOfPropertyDataSerializer.cs index 31b19ae1ca..2603937fc5 100644 --- a/src/Umbraco.PublishedCache.NuCache/DataSource/BTree.DictionaryOfPropertyDataSerializer.cs +++ b/src/Umbraco.PublishedCache.NuCache/DataSource/BTree.DictionaryOfPropertyDataSerializer.cs @@ -16,8 +16,8 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource // read properties count var pcount = PrimitiveSerializer.Int32.ReadFrom(stream); - var dict = new Dictionary(pcount,StringComparer.InvariantCultureIgnoreCase); + // read each property for (var i = 0; i < pcount; i++) { @@ -34,7 +34,7 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource for (var j = 0; j < vcount; j++) { var pdata = new PropertyData(); - pdatas[j] =pdata; + pdatas[j] = pdata; // everything that can be null is read/written as object // even though - culture and segment should never be null here, as 'null' represents diff --git a/src/Umbraco.PublishedCache.NuCache/DataSource/ContentSourceDto.cs b/src/Umbraco.PublishedCache.NuCache/DataSource/ContentSourceDto.cs index 78a8ea6e81..2d456e4c0f 100644 --- a/src/Umbraco.PublishedCache.NuCache/DataSource/ContentSourceDto.cs +++ b/src/Umbraco.PublishedCache.NuCache/DataSource/ContentSourceDto.cs @@ -1,4 +1,4 @@ -using System; +using System; using Umbraco.Cms.Core.Models; namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource diff --git a/src/Umbraco.PublishedCache.NuCache/DataSource/MsgPackContentNestedDataSerializerFactory.cs b/src/Umbraco.PublishedCache.NuCache/DataSource/MsgPackContentNestedDataSerializerFactory.cs index 9c3d1fe3c2..f93cd71ad2 100644 --- a/src/Umbraco.PublishedCache.NuCache/DataSource/MsgPackContentNestedDataSerializerFactory.cs +++ b/src/Umbraco.PublishedCache.NuCache/DataSource/MsgPackContentNestedDataSerializerFactory.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Services; +using Umbraco.Core.PropertyEditors; namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource { diff --git a/src/Umbraco.Tests.Benchmarks/LocalizedTextServiceGetAllStoredValuesBenchmarks.cs b/src/Umbraco.Tests.Benchmarks/LocalizedTextServiceGetAllStoredValuesBenchmarks.cs new file mode 100644 index 0000000000..f16df01ef4 --- /dev/null +++ b/src/Umbraco.Tests.Benchmarks/LocalizedTextServiceGetAllStoredValuesBenchmarks.cs @@ -0,0 +1,561 @@ +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Loggers; +using Moq; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Umbraco.Core.Services.Implement; +using System.Xml.Linq; +using Umbraco.Core.Logging; +using Umbraco.Tests.Benchmarks.Config; +using Umbraco.Core.Services; +using Umbraco.Core; +using System.Xml.XPath; +using ILogger = Umbraco.Core.Logging.ILogger; + +namespace Umbraco.Tests.Benchmarks +{ + [QuickRunWithMemoryDiagnoserConfig] + public class LocalizedTextServiceGetAllStoredValuesBenchmarks + { + private CultureInfo culture; + private OldLocalizedTextService _dictionaryService; + private OldLocalizedTextService _xmlService; + + private LocalizedTextService _optimized; + private LocalizedTextService _optimizedDict; + [GlobalSetup] + public void Setup() + { + culture = CultureInfo.GetCultureInfo("en-US"); + _dictionaryService = GetDictionaryLocalizedTextService(culture); + _xmlService = GetXmlService(culture); + _optimized = GetOptimizedService(culture); + _optimizedDict = GetOptimizedServiceDict(culture); + var result1 = _dictionaryService.Localize("language", culture); + var result2 = _xmlService.Localize("language", culture); + var result3 = _dictionaryService.GetAllStoredValues(culture); + var result4 = _xmlService.GetAllStoredValues(culture); + var result5 = _optimized.GetAllStoredValues(culture); + var result6 = _xmlService.GetAllStoredValues(culture); + var result7 = _optimized.GetAllStoredValuesByAreaAndAlias(culture); + } + + [Benchmark] + public void OriginalDictionaryGetAll() + { + for (int i = 0; i < 10000; i++) + { + var result = _dictionaryService.GetAllStoredValues(culture); + } + + } + + [Benchmark] + public void OriginalXmlGetAll() + { + for (int i = 0; i < 10000; i++) + { + var result = _xmlService.GetAllStoredValues(culture); + } + + } + + [Benchmark] + public void OriginalDictionaryLocalize() + { + for (int i = 0; i < 10000; i++) + { + var result = _dictionaryService.Localize("language", culture); + } + + } + + + [Benchmark(Baseline = true)] + public void OriginalXmlLocalize() + { + for (int i = 0; i < 10000; i++) + { + var result = _xmlService.Localize("language", culture); + } + } + [Benchmark] + public void OptimizedXmlGetAll() + { + for (int i = 0; i < 10000; i++) + { + var result = _optimized.GetAllStoredValues(culture); + } + + } + [Benchmark] + public void OptimizedDictGetAll() + { + for (int i = 0; i < 10000; i++) + { + var result = _optimizedDict.GetAllStoredValues(culture); + } + } + + [Benchmark] + public void OptimizedDictGetAllV2() + { + for (int i = 0; i < 10000; i++) + { + var result = _optimizedDict.GetAllStoredValuesByAreaAndAlias(culture); + } + } + + [Benchmark()] + public void OptimizedXmlLocalize() + { + for (int i = 0; i < 10000; i++) + { + var result = _optimized.Localize(null, "language", culture); + } + } + [Benchmark()] + public void OptimizedDictLocalize() + { + for (int i = 0; i < 10000; i++) + { + var result = _optimizedDict.Localize(null, "language", culture); + } + } + + private static LocalizedTextService GetOptimizedServiceDict(CultureInfo culture) + { + return new LocalizedTextService( + new Dictionary>> + { + { + culture, new Dictionary> + { + { + "testArea1", new Dictionary + { + {"testKey1", "testValue1"}, + {"testKey2", "testValue2"} + } + }, + { + "testArea2", new Dictionary + { + {"blah1", "blahValue1"}, + {"blah2", "blahValue2"} + } + }, + } + } + }, Mock.Of()); + } + private static LocalizedTextService GetOptimizedService(CultureInfo culture) + { + var txtService = new LocalizedTextService(new Dictionary> + { + { + culture, new Lazy(() => new XDocument( + new XElement("language", + new XElement("area", new XAttribute("alias", "testArea1"), + new XElement("key", new XAttribute("alias", "testKey1"), "testValue1"), + new XElement("key", new XAttribute("alias", "testKey2"), "testValue2")), + new XElement("area", new XAttribute("alias", "testArea2"), + new XElement("key", new XAttribute("alias", "blah1"), "blahValue1"), + new XElement("key", new XAttribute("alias", "blah2"), "blahValue2"))))) + } + }, Mock.Of()); + return txtService; + } + + private static OldLocalizedTextService GetXmlService(CultureInfo culture) + { + var txtService = new OldLocalizedTextService(new Dictionary> + { + { + culture, new Lazy(() => new XDocument( + new XElement("language", + new XElement("area", new XAttribute("alias", "testArea1"), + new XElement("key", new XAttribute("alias", "testKey1"), "testValue1"), + new XElement("key", new XAttribute("alias", "testKey2"), "testValue2")), + new XElement("area", new XAttribute("alias", "testArea2"), + new XElement("key", new XAttribute("alias", "blah1"), "blahValue1"), + new XElement("key", new XAttribute("alias", "blah2"), "blahValue2"))))) + } + }, Mock.Of()); + return txtService; + } + + private static OldLocalizedTextService GetDictionaryLocalizedTextService(CultureInfo culture) + { + return new OldLocalizedTextService( + new Dictionary>> + { + { + culture, new Dictionary> + { + { + "testArea1", new Dictionary + { + {"testKey1", "testValue1"}, + {"testKey2", "testValue2"} + } + }, + { + "testArea2", new Dictionary + { + {"blah1", "blahValue1"}, + {"blah2", "blahValue2"} + } + }, + } + } + }, Mock.Of()); + } + } + + //Original + public class OldLocalizedTextService : ILocalizedTextService + { + private readonly ILogger _logger; + private readonly Lazy _fileSources; + private readonly IDictionary>> _dictionarySource; + private readonly IDictionary> _xmlSource; + + /// + /// Initializes with a file sources instance + /// + /// + /// + public OldLocalizedTextService(Lazy fileSources, ILogger logger) + { + if (logger == null) throw new ArgumentNullException("logger"); + _logger = logger; + if (fileSources == null) throw new ArgumentNullException("fileSources"); + _fileSources = fileSources; + } + + /// + /// Initializes with an XML source + /// + /// + /// + public OldLocalizedTextService(IDictionary> source, ILogger logger) + { + if (source == null) throw new ArgumentNullException("source"); + if (logger == null) throw new ArgumentNullException("logger"); + _xmlSource = source; + _logger = logger; + } + + /// + /// Initializes with a source of a dictionary of culture -> areas -> sub dictionary of keys/values + /// + /// + /// + public OldLocalizedTextService(IDictionary>> source, ILogger logger) + { + _dictionarySource = source ?? throw new ArgumentNullException(nameof(source)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public string Localize(string key, CultureInfo culture, IDictionary tokens = null) + { + if (culture == null) throw new ArgumentNullException(nameof(culture)); + + // TODO: Hack, see notes on ConvertToSupportedCultureWithRegionCode + culture = ConvertToSupportedCultureWithRegionCode(culture); + + //This is what the legacy ui service did + if (string.IsNullOrEmpty(key)) + return string.Empty; + + var keyParts = key.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries); + var area = keyParts.Length > 1 ? keyParts[0] : null; + var alias = keyParts.Length > 1 ? keyParts[1] : keyParts[0]; + + var xmlSource = _xmlSource ?? (_fileSources != null + ? _fileSources.Value.GetXmlSources() + : null); + + if (xmlSource != null) + { + return GetFromXmlSource(xmlSource, culture, area, alias, tokens); + } + else + { + return GetFromDictionarySource(culture, area, alias, tokens); + } + } + + /// + /// Returns all key/values in storage for the given culture + /// + /// + public IDictionary GetAllStoredValues(CultureInfo culture) + { + if (culture == null) throw new ArgumentNullException("culture"); + + // TODO: Hack, see notes on ConvertToSupportedCultureWithRegionCode + culture = ConvertToSupportedCultureWithRegionCode(culture); + + var result = new Dictionary(); + + var xmlSource = _xmlSource ?? (_fileSources != null + ? _fileSources.Value.GetXmlSources() + : null); + + if (xmlSource != null) + { + if (xmlSource.ContainsKey(culture) == false) + { + _logger.Warn("The culture specified {Culture} was not found in any configured sources for this service", culture); + return result; + } + + //convert all areas + keys to a single key with a '/' + result = GetStoredTranslations(xmlSource, culture); + + //merge with the English file in case there's keys in there that don't exist in the local file + var englishCulture = new CultureInfo("en-US"); + if (culture.Equals(englishCulture) == false) + { + var englishResults = GetStoredTranslations(xmlSource, englishCulture); + foreach (var englishResult in englishResults.Where(englishResult => result.ContainsKey(englishResult.Key) == false)) + result.Add(englishResult.Key, englishResult.Value); + } + } + else + { + if (_dictionarySource.ContainsKey(culture) == false) + { + _logger.Warn("The culture specified {Culture} was not found in any configured sources for this service", culture); + return result; + } + + //convert all areas + keys to a single key with a '/' + foreach (var area in _dictionarySource[culture]) + { + foreach (var key in area.Value) + { + var dictionaryKey = string.Format("{0}/{1}", area.Key, key.Key); + //i don't think it's possible to have duplicates because we're dealing with a dictionary in the first place, but we'll double check here just in case. + if (result.ContainsKey(dictionaryKey) == false) + { + result.Add(dictionaryKey, key.Value); + } + } + } + } + + return result; + } + + private Dictionary GetStoredTranslations(IDictionary> xmlSource, CultureInfo cult) + { + var result = new Dictionary(); + var areas = xmlSource[cult].Value.XPathSelectElements("//area"); + foreach (var area in areas) + { + var keys = area.XPathSelectElements("./key"); + foreach (var key in keys) + { + var dictionaryKey = string.Format("{0}/{1}", (string)area.Attribute("alias"), + (string)key.Attribute("alias")); + //there could be duplicates if the language file isn't formatted nicely - which is probably the case for quite a few lang files + if (result.ContainsKey(dictionaryKey) == false) + result.Add(dictionaryKey, key.Value); + } + } + return result; + } + + /// + /// Returns a list of all currently supported cultures + /// + /// + public IEnumerable GetSupportedCultures() + { + var xmlSource = _xmlSource ?? (_fileSources != null + ? _fileSources.Value.GetXmlSources() + : null); + + return xmlSource != null ? xmlSource.Keys : _dictionarySource.Keys; + } + + /// + /// Tries to resolve a full 4 letter culture from a 2 letter culture name + /// + /// + /// The culture to determine if it is only a 2 letter culture, if so we'll try to convert it, otherwise it will just be returned + /// + /// + /// + /// TODO: This is just a hack due to the way we store the language files, they should be stored with 4 letters since that + /// is what they reference but they are stored with 2, further more our user's languages are stored with 2. So this attempts + /// to resolve the full culture if possible. + /// + /// This only works when this service is constructed with the LocalizedTextServiceFileSources + /// + public CultureInfo ConvertToSupportedCultureWithRegionCode(CultureInfo currentCulture) + { + if (currentCulture == null) throw new ArgumentNullException("currentCulture"); + + if (_fileSources == null) return currentCulture; + if (currentCulture.Name.Length > 2) return currentCulture; + + var attempt = _fileSources.Value.TryConvert2LetterCultureTo4Letter(currentCulture.TwoLetterISOLanguageName); + return attempt ? attempt.Result : currentCulture; + } + + private string GetFromDictionarySource(CultureInfo culture, string area, string key, IDictionary tokens) + { + if (_dictionarySource.ContainsKey(culture) == false) + { + _logger.Warn("The culture specified {Culture} was not found in any configured sources for this service", culture); + return "[" + key + "]"; + } + + var cultureSource = _dictionarySource[culture]; + + string found; + if (area.IsNullOrWhiteSpace()) + { + found = cultureSource + .SelectMany(x => x.Value) + .Where(keyvals => keyvals.Key.InvariantEquals(key)) + .Select(x => x.Value) + .FirstOrDefault(); + } + else + { + found = cultureSource + .Where(areas => areas.Key.InvariantEquals(area)) + .SelectMany(a => a.Value) + .Where(keyvals => keyvals.Key.InvariantEquals(key)) + .Select(x => x.Value) + .FirstOrDefault(); + } + + if (found != null) + { + return ParseTokens(found, tokens); + } + + //NOTE: Based on how legacy works, the default text does not contain the area, just the key + return "[" + key + "]"; + } + + private string GetFromXmlSource(IDictionary> xmlSource, CultureInfo culture, string area, string key, IDictionary tokens) + { + if (xmlSource.ContainsKey(culture) == false) + { + _logger.Warn("The culture specified {Culture} was not found in any configured sources for this service", culture); + return "[" + key + "]"; + } + + var found = FindTranslation(xmlSource, culture, area, key); + + if (found != null) + { + return ParseTokens(found.Value, tokens); + } + + // Fall back to English by default if we can't find the key + found = FindTranslation(xmlSource, new CultureInfo("en-US"), area, key); + if (found != null) + return ParseTokens(found.Value, tokens); + + // If it can't be found in either file, fall back to the default, showing just the key in square brackets + // NOTE: Based on how legacy works, the default text does not contain the area, just the key + return "[" + key + "]"; + } + + private XElement FindTranslation(IDictionary> xmlSource, CultureInfo culture, string area, string key) + { + var cultureSource = xmlSource[culture].Value; + + var xpath = area.IsNullOrWhiteSpace() + ? string.Format("//key [@alias = '{0}']", key) + : string.Format("//area [@alias = '{0}']/key [@alias = '{1}']", area, key); + + var found = cultureSource.XPathSelectElement(xpath); + return found; + } + + /// + /// Parses the tokens in the value + /// + /// + /// + /// + /// + /// This is based on how the legacy ui localized text worked, each token was just a sequential value delimited with a % symbol. + /// For example: hello %0%, you are %1% ! + /// + /// Since we're going to continue using the same language files for now, the token system needs to remain the same. With our new service + /// we support a dictionary which means in the future we can really have any sort of token system. + /// Currently though, the token key's will need to be an integer and sequential - though we aren't going to throw exceptions if that is not the case. + /// + internal static string ParseTokens(string value, IDictionary tokens) + { + if (tokens == null || tokens.Any() == false) + { + return value; + } + + foreach (var token in tokens) + { + value = value.Replace(string.Format("{0}{1}{0}", "%", token.Key), token.Value); + } + + return value; + } + + } + +// // * Summary * + +// BenchmarkDotNet=v0.11.3, OS=Windows 10.0.18362 +//Intel Core i5-8265U CPU 1.60GHz(Kaby Lake R), 1 CPU, 8 logical and 4 physical cores +// [Host] : .NET Framework 4.7.2 (CLR 4.0.30319.42000), 32bit LegacyJIT-v4.8.4250.0 +// Job-JIATTD : .NET Framework 4.7.2 (CLR 4.0.30319.42000), 32bit LegacyJIT-v4.8.4250.0 + +//IterationCount=3 IterationTime=100.0000 ms LaunchCount = 1 +//WarmupCount=3 + +// Method | Mean | Error | StdDev | Ratio | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | +//---------------------- |----------:|-----------:|----------:|------:|------------:|------------:|------------:|--------------------:| +// DictionaryGetAll | 11.199 ms | 1.8170 ms | 0.0996 ms | 0.14 | 1888.8889 | - | - | 5868.59 KB | +// XmlGetAll | 62.963 ms | 24.0615 ms | 1.3189 ms | 0.81 | 13500.0000 | - | - | 42448.71 KB | +// DictionaryLocalize | 9.757 ms | 1.6966 ms | 0.0930 ms | 0.13 | 1100.0000 | - | - | 3677.65 KB | +// XmlLocalize | 77.725 ms | 14.6069 ms | 0.8007 ms | 1.00 | 14000.0000 | - | - | 43032.8 KB | +// OptimizedXmlLocalize | 2.402 ms | 0.4256 ms | 0.0233 ms | 0.03 | 187.5000 | - | - | 626.01 KB | +// OptimizedDictLocalize | 2.345 ms | 0.2411 ms | 0.0132 ms | 0.03 | 187.5000 | - | - | 626.01 KB | + +//// * Warnings * +//MinIterationTime +// LocalizedTextServiceGetAllStoredValuesBenchmarks.DictionaryGetAll: IterationCount= 3, IterationTime= 100.0000 ms, LaunchCount= 1, WarmupCount= 3->MinIterationTime = 99.7816 ms which is very small. It's recommended to increase it. +// LocalizedTextServiceGetAllStoredValuesBenchmarks.DictionaryLocalize: IterationCount= 3, IterationTime= 100.0000 ms, LaunchCount= 1, WarmupCount= 3->MinIterationTime = 96.7415 ms which is very small. It's recommended to increase it. +// LocalizedTextServiceGetAllStoredValuesBenchmarks.XmlLocalize: IterationCount= 3, IterationTime= 100.0000 ms, LaunchCount= 1, WarmupCount= 3->MinIterationTime = 76.8151 ms which is very small. It's recommended to increase it. + +//// * Legends * +// Mean : Arithmetic mean of all measurements +// Error : Half of 99.9% confidence interval +// StdDev : Standard deviation of all measurements +// Ratio : Mean of the ratio distribution ([Current]/[Baseline]) +// Gen 0/1k Op : GC Generation 0 collects per 1k Operations +// Gen 1/1k Op : GC Generation 1 collects per 1k Operations +// Gen 2/1k Op : GC Generation 2 collects per 1k Operations +// Allocated Memory/Op : Allocated memory per single operation(managed only, inclusive, 1KB = 1024B) +// 1 ms : 1 Millisecond(0.001 sec) + +//// * Diagnostic Output - MemoryDiagnoser * + + +// // ***** BenchmarkRunner: End ***** +// Run time: 00:00:09 (9.15 sec), executed benchmarks: 6 +} diff --git a/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj b/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj index b6932ff74f..bb152a0bfd 100644 --- a/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj +++ b/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj @@ -11,6 +11,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -33,4 +65,5 @@ + diff --git a/src/Umbraco.Tests.Integration/Umbraco.Core/Mapping/UmbracoMapperTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Core/Mapping/UmbracoMapperTests.cs index 276a6f0f4c..9807640ca9 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Core/Mapping/UmbracoMapperTests.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Core/Mapping/UmbracoMapperTests.cs @@ -260,8 +260,8 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Mapping } Assert.AreEqual(contentType.CompositionPropertyGroups.Count(), invariantContent.Tabs.Count() - 1); - Assert.IsTrue(invariantContent.Tabs.Any(x => x.Label == _localizedTextService.Localize("general/properties"))); - Assert.AreEqual(2, invariantContent.Tabs.Where(x => x.Label == _localizedTextService.Localize("general/properties")).SelectMany(x => x.Properties.Where(p => p.Alias.StartsWith("_umb_") == false)).Count()); + Assert.IsTrue(invariantContent.Tabs.Any(x => x.Label == _localizedTextService.Localize("general","properties"))); + Assert.AreEqual(2, invariantContent.Tabs.Where(x => x.Label == _localizedTextService.Localize("general","properties")).SelectMany(x => x.Properties.Where(p => p.Alias.StartsWith("_umb_") == false)).Count()); } private void AssertBasics(ContentItemDisplay result, IContent content) diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/MultiValuePropertyEditorTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/MultiValuePropertyEditorTests.cs index 46a8073a46..316de73efb 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/MultiValuePropertyEditorTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/MultiValuePropertyEditorTests.cs @@ -117,7 +117,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors // editor wants ApplicationContext.Current.Services.TextService // (that should be fixed with proper injection) var textService = new Mock(); - textService.Setup(x => x.Localize(It.IsAny(), It.IsAny(), It.IsAny>())).Returns("blah"); + textService.Setup(x => x.Localize(It.IsAny(), It.IsAny(),It.IsAny(), It.IsAny>())).Returns("blah"); //// var appContext = new ApplicationContext( //// new DatabaseContext(TestObjects.GetIDatabaseFactoryMock(), logger, Mock.Of(), Mock.Of()), diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Services/PropertyValidationServiceTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Services/PropertyValidationServiceTests.cs index 16ef6d9715..dc70e1e8ce 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Services/PropertyValidationServiceTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Services/PropertyValidationServiceTests.cs @@ -26,7 +26,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Services private void MockObjects(out PropertyValidationService validationService, out IDataType dt) { var textService = new Mock(); - textService.Setup(x => x.Localize(It.IsAny(), Thread.CurrentThread.CurrentCulture, null)).Returns("Localized text"); + textService.Setup(x => x.Localize(It.IsAny(),It.IsAny(), Thread.CurrentThread.CurrentCulture, null)).Returns("Localized text"); var dataTypeService = new Mock(); IDataType dataType = Mock.Of( diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ImageCropperTest.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ImageCropperTest.cs index cce29ff54c..1873b30c99 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ImageCropperTest.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ImageCropperTest.cs @@ -54,48 +54,60 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.Common Assert.AreEqual(cropperValue, obj); } - //// [TestCase(CropperJson1, CropperJson1, true)] - //// [TestCase(CropperJson1, CropperJson2, false)] - //// public void CanConvertImageCropperPropertyEditor(string val1, string val2, bool expected) - //// { - //// try - //// { - //// var container = TestHelper.GetRegister(); - //// var composition = new Composition(container, TestHelper.GetMockedTypeLoader(), Mock.Of(), ComponentTests.MockRuntimeState(RuntimeLevel.Run), TestHelper.GetConfigs(), TestHelper.IOHelper, AppCaches.NoCache); - //// - //// composition.WithCollectionBuilder(); - //// - //// Current.Factory = composition.CreateFactory(); - //// - //// var logger = Mock.Of(); - //// var scheme = Mock.Of(); - //// var shortStringHelper = Mock.Of(); - //// - //// var mediaFileSystem = new MediaFileSystem(Mock.Of(), scheme, logger, shortStringHelper); - //// - //// var dataTypeService = new TestObjects.TestDataTypeService( - //// new DataType(new ImageCropperPropertyEditor(Mock.Of(), mediaFileSystem, Mock.Of(), Mock.Of(), Mock.Of(), TestHelper.IOHelper, TestHelper.ShortStringHelper, Mock.Of())) { Id = 1 }); - //// - //// var factory = new PublishedContentTypeFactory(Mock.Of(), new PropertyValueConverterCollection(Array.Empty()), dataTypeService); - //// - //// var converter = new ImageCropperValueConverter(); - //// var result = converter.ConvertSourceToIntermediate(null, factory.CreatePropertyType("test", 1), val1, false); // does not use type for conversion - //// - //// var resultShouldMatch = val2.DeserializeImageCropperValue(); - //// if (expected) - //// { - //// Assert.AreEqual(resultShouldMatch, result); - //// } - //// else - //// { - //// Assert.AreNotEqual(resultShouldMatch, result); - //// } - //// } - //// finally - //// { - //// Current.Reset(); - //// } - //// } + // [TestCase(CropperJson1, CropperJson1, true)] + // [TestCase(CropperJson1, CropperJson2, false)] + // public void CanConvertImageCropperPropertyEditor(string val1, string val2, bool expected) + // { + // try + // { + // var container = RegisterFactory.Create(); + // var composition = new Composition(container, new TypeLoader(), Mock.Of(), ComponentTests.MockRuntimeState(RuntimeLevel.Run)); + // + // composition.WithCollectionBuilder(); + // + // Current.Factory = composition.CreateFactory(); + // + // var logger = Mock.Of(); + // var scheme = Mock.Of(); + // var config = Mock.Of(); + // + // var mediaFileSystem = new MediaFileSystem(Mock.Of(), config, scheme, logger); + // + // var imageCropperConfiguration = new ImageCropperConfiguration() + // { + // Crops = new[] + // { + // new ImageCropperConfiguration.Crop() + // { + // Alias = "thumb", + // Width = 100, + // Height = 100 + // } + // } + // }; + // var dataTypeService = new TestObjects.TestDataTypeService( + // new DataType(new ImageCropperPropertyEditor(Mock.Of(), mediaFileSystem, Mock.Of(), Mock.Of())) { Id = 1, Configuration = imageCropperConfiguration }); + // + // var factory = new PublishedContentTypeFactory(Mock.Of(), new PropertyValueConverterCollection(Array.Empty()), dataTypeService); + // + // var converter = new ImageCropperValueConverter(); + // var result = converter.ConvertSourceToIntermediate(null, factory.CreatePropertyType("test", 1), val1, false); // does not use type for conversion + // + // var resultShouldMatch = val2.DeserializeImageCropperValue(); + // if (expected) + // { + // Assert.AreEqual(resultShouldMatch, result); + // } + // else + // { + // Assert.AreNotEqual(resultShouldMatch, result); + // } + // } + // finally + // { + // Current.Reset(); + // } + // } [Test] public void GetCropUrl_CropAliasTest() diff --git a/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs b/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs index aa06724d75..17e78647f2 100644 --- a/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs +++ b/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs @@ -142,7 +142,7 @@ namespace Umbraco.Tests.PublishedContent var globalSettings = new GlobalSettings(); var nuCacheSettings = new NuCacheSettings(); - + // at last, create the complete NuCache snapshot service! var options = new PublishedSnapshotServiceOptions { IgnoreLocalDb = true }; _snapshotService = new PublishedSnapshotService( diff --git a/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs b/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs index 66c0c4b849..ae7776dfaf 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs @@ -383,12 +383,12 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers var code = await _userManager.GeneratePasswordResetTokenAsync(identityUser); var callbackUrl = ConstructCallbackUrl(identityUser.Id, code); - var message = _textService.Localize("login/resetPasswordEmailCopyFormat", + var message = _textService.Localize("login","resetPasswordEmailCopyFormat", // Ensure the culture of the found user is used for the email! UmbracoUserExtensions.GetUserCulture(identityUser.Culture, _textService, _globalSettings), new[] { identityUser.UserName, callbackUrl }); - var subject = _textService.Localize("login/resetPasswordEmailCopySubject", + var subject = _textService.Localize("login","resetPasswordEmailCopySubject", // Ensure the culture of the found user is used for the email! UmbracoUserExtensions.GetUserCulture(identityUser.Culture, _textService, _globalSettings)); @@ -445,11 +445,11 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers return BadRequest("Invalid code"); } - var subject = _textService.Localize("login/mfaSecurityCodeSubject", + var subject = _textService.Localize("login","mfaSecurityCodeSubject", // Ensure the culture of the found user is used for the email! UmbracoUserExtensions.GetUserCulture(user.Culture, _textService, _globalSettings)); - var message = _textService.Localize("login/mfaSecurityCodeMessage", + var message = _textService.Localize("login","mfaSecurityCodeMessage", // Ensure the culture of the found user is used for the email! UmbracoUserExtensions.GetUserCulture(user.Culture, _textService, _globalSettings), new[] { code }); diff --git a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs index d97ae3e8ae..161896628b 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs @@ -335,7 +335,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers } //Add error and redirect for it to be displayed - TempData[ViewDataExtensions.TokenPasswordResetCode] = new[] { _textService.Localize("login/resetCodeExpired") }; + TempData[ViewDataExtensions.TokenPasswordResetCode] = new[] { _textService.Localize("login","resetCodeExpired") }; return RedirectToLocal(Url.Action(nameof(Default), this.GetControllerName())); } @@ -431,7 +431,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers // Sign in the user with this external login provider (which auto links, etc...) var result = await _signInManager.ExternalLoginSignInAsync(loginInfo, isPersistent: false); - + var errors = new List(); if (result == Microsoft.AspNetCore.Identity.SignInResult.Success) diff --git a/src/Umbraco.Web.BackOffice/Controllers/CodeFileController.cs b/src/Umbraco.Web.BackOffice/Controllers/CodeFileController.cs index 15a0469a4a..a88cdc5087 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/CodeFileController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/CodeFileController.cs @@ -127,7 +127,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (string.IsNullOrWhiteSpace(parentId)) throw new ArgumentException("Value cannot be null or whitespace.", "parentId"); if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value cannot be null or whitespace.", "name"); if (name.ContainsAny(Path.GetInvalidPathChars())) { - return ValidationProblem(_localizedTextService.Localize("codefile/createFolderIllegalChars")); + return ValidationProblem(_localizedTextService.Localize("codefile", "createFolderIllegalChars")); } // if the parentId is root (-1) then we just need an empty string as we are @@ -422,8 +422,8 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers } display.AddErrorNotification( - _localizedTextService.Localize("speechBubbles/partialViewErrorHeader"), - _localizedTextService.Localize("speechBubbles/partialViewErrorText")); + _localizedTextService.Localize("speechBubbles", "partialViewErrorHeader"), + _localizedTextService.Localize("speechBubbles", "partialViewErrorText")); break; case Constants.Trees.PartialViewMacros: @@ -437,8 +437,8 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers } display.AddErrorNotification( - _localizedTextService.Localize("speechBubbles/partialViewErrorHeader"), - _localizedTextService.Localize("speechBubbles/partialViewErrorText")); + _localizedTextService.Localize("speechBubbles", "partialViewErrorHeader"), + _localizedTextService.Localize("speechBubbles", "partialViewErrorText")); break; case Constants.Trees.Scripts: diff --git a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs index e82c22680d..03de07769d 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs @@ -275,7 +275,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers new ContentVariantDisplay { CreateDate = DateTime.Now, - Name = _localizedTextService.Localize("general/recycleBin") + Name = _localizedTextService.Localize("general","recycleBin") } }, ContentApps = apps @@ -417,16 +417,65 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers { var emptyContent = _contentService.Create("", parentId, contentType.Alias, _backofficeSecurityAccessor.BackOfficeSecurity.GetUserId().ResultOr(0)); var mapped = MapToDisplay(emptyContent); + + return CleanContentItemDisplay(mapped); + } + + private ContentItemDisplay CleanContentItemDisplay(ContentItemDisplay display) + { // translate the content type name if applicable - mapped.ContentTypeName = _localizedTextService.UmbracoDictionaryTranslate(CultureDictionary, mapped.ContentTypeName); + display.ContentTypeName = _localizedTextService.UmbracoDictionaryTranslate(CultureDictionary, display.ContentTypeName); // if your user type doesn't have access to the Settings section it would not get this property mapped - if (mapped.DocumentType != null) - mapped.DocumentType.Name = _localizedTextService.UmbracoDictionaryTranslate(CultureDictionary, mapped.DocumentType.Name); + if (display.DocumentType != null) + display.DocumentType.Name = _localizedTextService.UmbracoDictionaryTranslate(CultureDictionary, display.DocumentType.Name); //remove the listview app if it exists - mapped.ContentApps = mapped.ContentApps.Where(x => x.Alias != "umbListView").ToList(); + display.ContentApps = display.ContentApps.Where(x => x.Alias != "umbListView").ToList(); - return mapped; + return display; + } + + /// + /// Gets an empty for each content type in the IEnumerable, all with the same parent ID + /// + /// Will attempt to re-use the same permissions for every content as long as the path and user are the same + /// + /// + /// + private IEnumerable GetEmpties(IEnumerable contentTypes, int parentId) + { + var result = new List(); + var backOfficeSecurity = _backofficeSecurityAccessor.BackOfficeSecurity; + + var userId = backOfficeSecurity.GetUserId().ResultOr(0); + var currentUser = backOfficeSecurity.CurrentUser; + // We know that if the ID is less than 0 the parent is null. + // Since this is called with parent ID it's safe to assume that the parent is the same for all the content types. + var parent = parentId > 0 ? _contentService.GetById(parentId) : null; + // Since the parent is the same and the path used to get permissions is based on the parent we only have to do it once + var path = parent == null ? "-1" : parent.Path; + var permissions = new Dictionary + { + [path] = _userService.GetPermissionsForPath(currentUser, path) + }; + + foreach (var contentType in contentTypes) + { + var emptyContent = _contentService.Create("", parentId, contentType, userId); + + var mapped = MapToDisplay(emptyContent, context => + { + // Since the permissions depend on current user and path, we add both of these to context as well, + // that way we can compare the path and current user when mapping, if they're the same just take permissions + // and skip getting them again, in theory they should always be the same, but better safe than sorry., + context.Items["Parent"] = parent; + context.Items["CurrentUser"] = currentUser; + context.Items["Permissions"] = permissions; + }); + result.Add(CleanContentItemDisplay(mapped)); + } + + return result; } /// @@ -437,22 +486,9 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers [OutgoingEditorModelEvent] public ActionResult> GetEmptyByKeys([FromQuery] Guid[] contentTypeKeys, [FromQuery] int parentId) { - var result = new Dictionary(); - using var scope = _scopeProvider.CreateScope(autoComplete: true); var contentTypes = _contentTypeService.GetAll(contentTypeKeys).ToList(); - - foreach (var contentType in contentTypes) - { - if (contentType is null) - { - return NotFound(); - } - - result.Add(contentType.Key, GetEmptyInner(contentType, parentId)); - } - - return result; + return GetEmpties(contentTypes, parentId).ToDictionary(x => x.ContentTypeKey); } [OutgoingEditorModelEvent] @@ -616,8 +652,8 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers var notificationModel = new SimpleNotificationModel(); notificationModel.AddSuccessNotification( - _localizedTextService.Localize("blueprints/createdBlueprintHeading"), - _localizedTextService.Localize("blueprints/createdBlueprintMessage", new[] { content.Name }) + _localizedTextService.Localize("blueprints", "createdBlueprintHeading"), + _localizedTextService.Localize("blueprints", "createdBlueprintMessage", new[] { content.Name }) ); return notificationModel; @@ -628,7 +664,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers var existing = _contentService.GetBlueprintsForContentTypes(content.ContentTypeId); if (existing.Any(x => x.Name == name && x.Id != content.Id)) { - ModelState.AddModelError(modelName, _localizedTextService.Localize("blueprints/duplicateBlueprintMessage")); + ModelState.AddModelError(modelName, _localizedTextService.Localize("blueprints", "duplicateBlueprintMessage")); return false; } @@ -794,15 +830,15 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers var variantName = GetVariantName(culture, segment); AddSuccessNotification(notifications, culture, segment, - _localizedTextService.Localize("speechBubbles/editContentSendToPublish"), - _localizedTextService.Localize("speechBubbles/editVariantSendToPublishText", new[] { variantName })); + _localizedTextService.Localize("speechBubbles", "editContentSendToPublish"), + _localizedTextService.Localize("speechBubbles", "editVariantSendToPublishText", new[] { variantName })); } } else if (ModelState.IsValid) { globalNotifications.AddSuccessNotification( - _localizedTextService.Localize("speechBubbles/editContentSendToPublish"), - _localizedTextService.Localize("speechBubbles/editContentSendToPublishText")); + _localizedTextService.Localize("speechBubbles", "editContentSendToPublish"), + _localizedTextService.Localize("speechBubbles", "editContentSendToPublishText")); } } break; @@ -819,8 +855,8 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (!await ValidatePublishBranchPermissionsAsync(contentItem)) { globalNotifications.AddErrorNotification( - _localizedTextService.Localize("publish"), - _localizedTextService.Localize("publish/invalidPublishBranchPermissions")); + _localizedTextService.Localize(null,"publish"), + _localizedTextService.Localize("publish", "invalidPublishBranchPermissions")); wasCancelled = false; break; } @@ -835,8 +871,8 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (!await ValidatePublishBranchPermissionsAsync(contentItem)) { globalNotifications.AddErrorNotification( - _localizedTextService.Localize("publish"), - _localizedTextService.Localize("publish/invalidPublishBranchPermissions")); + _localizedTextService.Localize(null,"publish"), + _localizedTextService.Localize("publish", "invalidPublishBranchPermissions")); wasCancelled = false; break; } @@ -931,7 +967,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers //if there's more than 1 variant, then we need to add the culture specific error //messages based on the variants in error so that the messages show in the publish/save dialog if (variants.Count > 1) - AddVariantValidationError(variant.Culture, variant.Segment, "publish/contentPublishedFailedByMissingName"); + AddVariantValidationError(variant.Culture, variant.Segment, "publish","contentPublishedFailedByMissingName"); else return false; //It's invariant and is missing critical data, it cannot be saved } @@ -968,15 +1004,15 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers /// /// /// - /// - /// + /// + /// /// /// /// Method is used for normal Saving and Scheduled Publishing /// private void SaveAndNotify(ContentItemSave contentItem, Func saveMethod, int variantCount, Dictionary notifications, SimpleNotificationModel globalNotifications, - string invariantSavedLocalizationKey, string variantSavedLocalizationKey, string cultureForInvariantErrors, + string invariantSavedLocalizationAlias, string variantSavedLocalizationAlias, string cultureForInvariantErrors, out bool wasCancelled) { var saveResult = saveMethod(contentItem.PersistedContent); @@ -996,15 +1032,15 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers var variantName = GetVariantName(culture, segment); AddSuccessNotification(notifications, culture, segment, - _localizedTextService.Localize("speechBubbles/editContentSavedHeader"), - _localizedTextService.Localize(variantSavedLocalizationKey, new[] { variantName })); + _localizedTextService.Localize("speechBubbles", "editContentSavedHeader"), + _localizedTextService.Localize(null,variantSavedLocalizationAlias, new[] { variantName })); } } else if (ModelState.IsValid) { globalNotifications.AddSuccessNotification( - _localizedTextService.Localize("speechBubbles/editContentSavedHeader"), - _localizedTextService.Localize(invariantSavedLocalizationKey)); + _localizedTextService.Localize("speechBubbles", "editContentSavedHeader"), + _localizedTextService.Localize("speechBubbles",invariantSavedLocalizationAlias)); } } } @@ -1131,7 +1167,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers { //can't continue, a mandatory variant is not published and not scheduled for publishing // TODO: Add segment - AddVariantValidationError(culture, null, "speechBubbles/scheduleErrReleaseDate2"); + AddVariantValidationError(culture, null, "speechBubbles", "scheduleErrReleaseDate2"); isValid = false; continue; } @@ -1139,7 +1175,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers { //can't continue, a mandatory variant is not published and it's scheduled for publishing after a non-mandatory // TODO: Add segment - AddVariantValidationError(culture, null, "speechBubbles/scheduleErrReleaseDate3"); + AddVariantValidationError(culture, null, "speechBubbles", "scheduleErrReleaseDate3"); isValid = false; continue; } @@ -1153,7 +1189,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers //1) release date cannot be less than now if (variant.ReleaseDate.HasValue && variant.ReleaseDate < DateTime.Now) { - AddVariantValidationError(variant.Culture, variant.Segment, "speechBubbles/scheduleErrReleaseDate1"); + AddVariantValidationError(variant.Culture, variant.Segment, "speechBubbles", "scheduleErrReleaseDate1"); isValid = false; continue; } @@ -1161,7 +1197,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers //2) expire date cannot be less than now if (variant.ExpireDate.HasValue && variant.ExpireDate < DateTime.Now) { - AddVariantValidationError(variant.Culture, variant.Segment, "speechBubbles/scheduleErrExpireDate1"); + AddVariantValidationError(variant.Culture, variant.Segment, "speechBubbles", "scheduleErrExpireDate1"); isValid = false; continue; } @@ -1169,7 +1205,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers //3) expire date cannot be less than release date if (variant.ExpireDate.HasValue && variant.ReleaseDate.HasValue && variant.ExpireDate <= variant.ReleaseDate) { - AddVariantValidationError(variant.Culture, variant.Segment, "speechBubbles/scheduleErrExpireDate2"); + AddVariantValidationError(variant.Culture, variant.Segment, "speechBubbles", "scheduleErrExpireDate2"); isValid = false; continue; } @@ -1397,19 +1433,19 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (r.publishing && !r.isValid) { //flagged for publishing but the mandatory culture is invalid - AddVariantValidationError(r.model.Culture, r.model.Segment, "publish/contentPublishedFailedReqCultureValidationError"); + AddVariantValidationError(r.model.Culture, r.model.Segment, "publish", "contentPublishedFailedReqCultureValidationError"); canPublish = false; } else if (r.publishing && r.isValid && firstInvalidMandatoryCulture != null) { //in this case this culture also cannot be published because another mandatory culture is invalid - AddVariantValidationError(r.model.Culture, r.model.Segment, "publish/contentPublishedFailedReqCultureValidationError", firstInvalidMandatoryCulture); + AddVariantValidationError(r.model.Culture, r.model.Segment, "publish", "contentPublishedFailedReqCultureValidationError", firstInvalidMandatoryCulture); canPublish = false; } else if (!r.publishing) { //cannot continue publishing since a required culture that is not currently being published isn't published - AddVariantValidationError(r.model.Culture, r.model.Segment, "speechBubbles/contentReqCulturePublishError"); + AddVariantValidationError(r.model.Culture, r.model.Segment, "speechBubbles", "contentReqCulturePublishError"); canPublish = false; } } @@ -1434,7 +1470,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers var valid = persistentContent.PublishCulture(CultureImpact.Explicit(variant.Culture, defaultCulture.InvariantEquals(variant.Culture))); if (!valid) { - AddVariantValidationError(variant.Culture, variant.Segment, "speechBubbles/contentCultureValidationError"); + AddVariantValidationError(variant.Culture, variant.Segment, "speechBubbles", "contentCultureValidationError"); return false; } } @@ -1451,12 +1487,12 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers /// /// The culture used in the localization message, null by default which means will be used. /// - private void AddVariantValidationError(string culture, string segment, string localizationKey, string cultureToken = null) + private void AddVariantValidationError(string culture, string segment, string localizationArea,string localizationAlias, string cultureToken = null) { var cultureToUse = cultureToken ?? culture; var variantName = GetVariantName(cultureToUse, segment); - var errMsg = _localizedTextService.Localize(localizationKey, new[] { variantName }); + var errMsg = _localizedTextService.Localize(localizationArea, localizationAlias, new[] { variantName }); ModelState.AddVariantValidationError(culture, segment, errMsg); } @@ -1585,7 +1621,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers { _contentService.EmptyRecycleBin(_backofficeSecurityAccessor.BackOfficeSecurity.GetUserId().ResultOr(Constants.Security.SuperUserId)); - return Ok(_localizedTextService.Localize("defaultdialogs/recycleBinIsEmpty")); + return Ok(_localizedTextService.Localize("defaultdialogs", "recycleBinIsEmpty")); } /// @@ -1726,8 +1762,8 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers else { content.AddSuccessNotification( - _localizedTextService.Localize("content/unpublish"), - _localizedTextService.Localize("speechBubbles/contentUnpublished")); + _localizedTextService.Localize("content", "unpublish"), + _localizedTextService.Localize("speechBubbles", "contentUnpublished")); return content; } } @@ -1752,8 +1788,8 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (results.Any(x => x.Value.Result == PublishResultType.SuccessUnpublishMandatoryCulture)) { content.AddSuccessNotification( - _localizedTextService.Localize("content/unpublish"), - _localizedTextService.Localize("speechBubbles/contentMandatoryCultureUnpublished")); + _localizedTextService.Localize("content", "unpublish"), + _localizedTextService.Localize("speechBubbles", "contentMandatoryCultureUnpublished")); return content; } @@ -1761,8 +1797,8 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers foreach (var r in results) { content.AddSuccessNotification( - _localizedTextService.Localize("content/unpublish"), - _localizedTextService.Localize("speechBubbles/contentCultureUnpublished", new[] { _allLangs.Value[r.Key].CultureName })); + _localizedTextService.Localize("conten", "unpublish"), + _localizedTextService.Localize("speechBubbles", "contentCultureUnpublished", new[] { _allLangs.Value[r.Key].CultureName })); } return content; @@ -1793,7 +1829,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers } catch (UriFormatException) { - return ValidationProblem(_localizedTextService.Localize("assignDomain/invalidDomain")); + return ValidationProblem(_localizedTextService.Localize("assignDomain", "invalidDomain")); } } @@ -1943,7 +1979,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers foreach (var (culture, segment) in variantErrors) { - AddVariantValidationError(culture, segment, "speechBubbles/contentCultureValidationError"); + AddVariantValidationError(culture, segment, "speechBubbles", "contentCultureValidationError"); } } } @@ -2064,7 +2100,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (toMove.ContentType.AllowedAsRoot == false) { return ValidationProblem( - _localizedTextService.Localize("moveOrCopy/notAllowedAtRoot")); + _localizedTextService.Localize("moveOrCopy", "notAllowedAtRoot")); } } else @@ -2081,14 +2117,14 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers .Any(x => x.Value == toMove.ContentType.Id) == false) { return ValidationProblem( - _localizedTextService.Localize("moveOrCopy/notAllowedByContentType")); + _localizedTextService.Localize("moveOrCopy", "notAllowedByContentType")); } // Check on paths if ($",{parent.Path},".IndexOf($",{toMove.Id},", StringComparison.Ordinal) > -1) { return ValidationProblem( - _localizedTextService.Localize("moveOrCopy/notAllowedByPath")); + _localizedTextService.Localize("moveOrCopy", "notAllowedByPath")); } } @@ -2157,16 +2193,16 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers { //either invariant single publish, or bulk publish where all statuses are already published display.AddSuccessNotification( - _localizedTextService.Localize("speechBubbles/editContentPublishedHeader"), - _localizedTextService.Localize("speechBubbles/editContentPublishedText")); + _localizedTextService.Localize("speechBubbles", "editContentPublishedHeader"), + _localizedTextService.Localize("speechBubbles", "editContentPublishedText")); } else { foreach (var c in successfulCultures) { display.AddSuccessNotification( - _localizedTextService.Localize("speechBubbles/editContentPublishedHeader"), - _localizedTextService.Localize("speechBubbles/editVariantPublishedText", new[] { _allLangs.Value[c].CultureName })); + _localizedTextService.Localize("speechBubbles", "editContentPublishedHeader"), + _localizedTextService.Localize("speechBubbles", "editVariantPublishedText", new[] { _allLangs.Value[c].CultureName })); } } } @@ -2182,20 +2218,20 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (successfulCultures == null) { display.AddSuccessNotification( - _localizedTextService.Localize("speechBubbles/editContentPublishedHeader"), + _localizedTextService.Localize("speechBubbles", "editContentPublishedHeader"), totalStatusCount > 1 - ? _localizedTextService.Localize("speechBubbles/editMultiContentPublishedText", new[] { itemCount.ToInvariantString() }) - : _localizedTextService.Localize("speechBubbles/editContentPublishedText")); + ? _localizedTextService.Localize("speechBubbles", "editMultiContentPublishedText", new[] { itemCount.ToInvariantString() }) + : _localizedTextService.Localize("speechBubbles", "editContentPublishedText")); } else { foreach (var c in successfulCultures) { display.AddSuccessNotification( - _localizedTextService.Localize("speechBubbles/editContentPublishedHeader"), + _localizedTextService.Localize("speechBubbles", "editContentPublishedHeader"), totalStatusCount > 1 - ? _localizedTextService.Localize("speechBubbles/editMultiVariantPublishedText", new[] { itemCount.ToInvariantString(), _allLangs.Value[c].CultureName }) - : _localizedTextService.Localize("speechBubbles/editVariantPublishedText", new[] { _allLangs.Value[c].CultureName })); + ? _localizedTextService.Localize("speechBubbles", "editMultiVariantPublishedText", new[] { itemCount.ToInvariantString(), _allLangs.Value[c].CultureName }) + : _localizedTextService.Localize("speechBubbles", "editVariantPublishedText", new[] { _allLangs.Value[c].CultureName })); } } } @@ -2205,8 +2241,8 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers //TODO: This doesn't take into account variations with the successfulCultures param var names = string.Join(", ", status.Select(x => $"'{x.Content.Name}'")); display.AddWarningNotification( - _localizedTextService.Localize("publish"), - _localizedTextService.Localize("publish/contentPublishedFailedByParent", + _localizedTextService.Localize(null,"publish"), + _localizedTextService.Localize("publish", "contentPublishedFailedByParent", new[] { names }).Trim()); } break; @@ -2214,7 +2250,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers { //TODO: This doesn't take into account variations with the successfulCultures param var names = string.Join(", ", status.Select(x => $"'{x.Content.Name}'")); - AddCancelMessage(display, message: "publish/contentPublishedFailedByEvent", messageParams: new[] { names }); + AddCancelMessage(display, "publish","contentPublishedFailedByEvent", messageParams: new[] { names }); } break; case PublishResultType.FailedPublishAwaitingRelease: @@ -2222,8 +2258,8 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers //TODO: This doesn't take into account variations with the successfulCultures param var names = string.Join(", ", status.Select(x => $"'{x.Content.Name}'")); display.AddWarningNotification( - _localizedTextService.Localize("publish"), - _localizedTextService.Localize("publish/contentPublishedFailedAwaitingRelease", + _localizedTextService.Localize(null,"publish"), + _localizedTextService.Localize("publish", "contentPublishedFailedAwaitingRelease", new[] { names }).Trim()); } break; @@ -2232,8 +2268,8 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers //TODO: This doesn't take into account variations with the successfulCultures param var names = string.Join(", ", status.Select(x => $"'{x.Content.Name}'")); display.AddWarningNotification( - _localizedTextService.Localize("publish"), - _localizedTextService.Localize("publish/contentPublishedFailedExpired", + _localizedTextService.Localize(null,"publish"), + _localizedTextService.Localize("publish", "contentPublishedFailedExpired", new[] { names }).Trim()); } break; @@ -2242,8 +2278,8 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers //TODO: This doesn't take into account variations with the successfulCultures param var names = string.Join(", ", status.Select(x => $"'{x.Content.Name}'")); display.AddWarningNotification( - _localizedTextService.Localize("publish"), - _localizedTextService.Localize("publish/contentPublishedFailedIsTrashed", + _localizedTextService.Localize(null,"publish"), + _localizedTextService.Localize("publish", "contentPublishedFailedIsTrashed", new[] { names }).Trim()); } break; @@ -2253,8 +2289,8 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers { var names = string.Join(", ", status.Select(x => $"'{x.Content.Name}'")); display.AddWarningNotification( - _localizedTextService.Localize("publish"), - _localizedTextService.Localize("publish/contentPublishedFailedInvalid", + _localizedTextService.Localize(null,"publish"), + _localizedTextService.Localize("publish", "contentPublishedFailedInvalid", new[] { names }).Trim()); } else @@ -2263,8 +2299,8 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers { var names = string.Join(", ", status.Select(x => $"'{(x.Content.ContentType.VariesByCulture() ? x.Content.GetCultureName(c) : x.Content.Name)}'")); display.AddWarningNotification( - _localizedTextService.Localize("publish"), - _localizedTextService.Localize("publish/contentPublishedFailedInvalid", + _localizedTextService.Localize(null,"publish"), + _localizedTextService.Localize("publish", "contentPublishedFailedInvalid", new[] { names }).Trim()); } } @@ -2272,7 +2308,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers break; case PublishResultType.FailedPublishMandatoryCultureMissing: display.AddWarningNotification( - _localizedTextService.Localize("publish"), + _localizedTextService.Localize(null,"publish"), "publish/contentPublishedFailedByCulture"); break; default: @@ -2286,12 +2322,22 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers /// /// /// - private ContentItemDisplay MapToDisplay(IContent content) - { - var display = _umbracoMapper.Map(content, context => + private ContentItemDisplay MapToDisplay(IContent content) => + MapToDisplay(content, context => { context.Items["CurrentUser"] = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser; }); + + /// + /// Used to map an instance to a and ensuring AllowPreview is set correctly. + /// Also allows you to pass in an action for the mapper context where you can pass additional information on to the mapper. + /// + /// + /// + /// + private ContentItemDisplay MapToDisplay(IContent content, Action contextOptions) + { + var display = _umbracoMapper.Map(content, contextOptions); display.AllowPreview = display.AllowPreview && content.Trashed == false && content.ContentType.IsElement == false; return display; } @@ -2404,11 +2450,11 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers case OperationResultType.FailedExceptionThrown: case OperationResultType.NoOperation: default: - return ValidationProblem(_localizedTextService.Localize("speechBubbles/operationFailedHeader")); + return ValidationProblem(_localizedTextService.Localize("speechBubbles", "operationFailedHeader")); case OperationResultType.FailedCancelledByEvent: return ValidationProblem( - _localizedTextService.Localize("speechBubbles/operationCancelledHeader"), - _localizedTextService.Localize("speechBubbles/operationCancelledText")); + _localizedTextService.Localize("speechBubbles", "operationCancelledHeader"), + _localizedTextService.Localize("speechBubbles", "operationCancelledText")); } } } diff --git a/src/Umbraco.Web.BackOffice/Controllers/ContentControllerBase.cs b/src/Umbraco.Web.BackOffice/Controllers/ContentControllerBase.cs index 22ca9d073d..f389641777 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ContentControllerBase.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ContentControllerBase.cs @@ -190,15 +190,13 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers /// Adds a cancelled message to the display /// /// - /// - /// - /// - /// - /// + /// + /// /// - protected void AddCancelMessage(INotificationModel display, string header = "speechBubbles/operationCancelledHeader", string message = "speechBubbles/operationCancelledText", bool localizeHeader = true, - bool localizeMessage = true, - string[] headerParams = null, + protected void AddCancelMessage( + INotificationModel display, + string messageArea = "speechBubbles", + string messageAlias ="operationCancelledText", string[] messageParams = null) { // if there's already a default event message, don't add our default one @@ -209,8 +207,29 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers } display.AddWarningNotification( - localizeHeader ? LocalizedTextService.Localize(header, headerParams) : header, - localizeMessage ? LocalizedTextService.Localize(message, messageParams) : message); + LocalizedTextService.Localize("speechBubbles", "operationCancelledHeader"), + LocalizedTextService.Localize(messageArea, messageAlias, messageParams)); + } + + /// + /// Adds a cancelled message to the display + /// + /// + /// + /// + /// + /// + /// + protected void AddCancelMessage(INotificationModel display, string message) + { + // if there's already a default event message, don't add our default one + IEventMessagesFactory messages = EventMessages; + if (messages != null && messages.GetOrDefault().GetAll().Any(x => x.IsDefaultEventMessage)) + { + return; + } + + display.AddWarningNotification(LocalizedTextService.Localize("speechBubbles", "operationCancelledHeader"), message); } } } diff --git a/src/Umbraco.Web.BackOffice/Controllers/ContentTypeController.cs b/src/Umbraco.Web.BackOffice/Controllers/ContentTypeController.cs index 95e9b5ecfe..d14e9741fc 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ContentTypeController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ContentTypeController.cs @@ -364,7 +364,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers display.AddSuccessNotification( - _localizedTextService.Localize("speechBubbles/contentTypeSavedHeader"), + _localizedTextService.Localize("speechBubbles","contentTypeSavedHeader"), string.Empty); return display; @@ -612,8 +612,8 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers else { model.Notifications.Add(new BackOfficeNotification( - _localizedTextService.Localize("speechBubbles/operationFailedHeader"), - _localizedTextService.Localize("media/disallowedFileType"), + _localizedTextService.Localize("speechBubbles","operationFailedHeader"), + _localizedTextService.Localize("media","disallowedFileType"), NotificationStyle.Warning)); } } diff --git a/src/Umbraco.Web.BackOffice/Controllers/ContentTypeControllerBase.cs b/src/Umbraco.Web.BackOffice/Controllers/ContentTypeControllerBase.cs index ab7788e139..0b4c311a8c 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ContentTypeControllerBase.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ContentTypeControllerBase.cs @@ -272,7 +272,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers var exists = allAliases.InvariantContains(contentTypeSave.Alias); if (exists && (ctId == 0 || !ct.Alias.InvariantEquals(contentTypeSave.Alias))) { - ModelState.AddModelError("Alias", LocalizedTextService.Localize("editcontenttype/aliasAlreadyExists")); + ModelState.AddModelError("Alias", LocalizedTextService.Localize("editcontenttype", "aliasAlreadyExists")); } // execute the external validators @@ -417,7 +417,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers case MoveOperationStatusType.FailedCancelledByEvent: return ValidationProblem(); case MoveOperationStatusType.FailedNotAllowedByPath: - return ValidationProblem(LocalizedTextService.Localize("moveOrCopy/notAllowedByPath")); + return ValidationProblem(LocalizedTextService.Localize("moveOrCopy", "notAllowedByPath")); default: throw new ArgumentOutOfRangeException(); } @@ -453,8 +453,8 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers return NotFound(); case MoveOperationStatusType.FailedCancelledByEvent: return ValidationProblem(); - case MoveOperationStatusType.FailedNotAllowedByPath: - return ValidationProblem(LocalizedTextService.Localize("moveOrCopy/notAllowedByPath")); + case MoveOperationStatusType.FailedNotAllowedByPath: + return ValidationProblem(LocalizedTextService.Localize("moveOrCopy", "notAllowedByPath")); default: throw new ArgumentOutOfRangeException(); } diff --git a/src/Umbraco.Web.BackOffice/Controllers/CurrentUserController.cs b/src/Umbraco.Web.BackOffice/Controllers/CurrentUserController.cs index d6ebe946bf..1435eb6c52 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/CurrentUserController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/CurrentUserController.cs @@ -233,7 +233,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers { // even if we weren't resetting this, it is the correct value (null), otherwise if we were resetting then it will contain the new pword var result = new ModelWithNotifications(passwordChangeResult.Result.ResetPassword); - result.AddSuccessNotification(_localizedTextService.Localize("user/password"), _localizedTextService.Localize("user/passwordChanged")); + result.AddSuccessNotification(_localizedTextService.Localize("user","password"), _localizedTextService.Localize("user","passwordChanged")); return result; } diff --git a/src/Umbraco.Web.BackOffice/Controllers/DashboardController.cs b/src/Umbraco.Web.BackOffice/Controllers/DashboardController.cs index 32ad18e909..770f73b5a6 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/DashboardController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/DashboardController.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Newtonsoft.Json.Linq; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Configuration; @@ -20,6 +21,7 @@ using Umbraco.Cms.Web.Common.Attributes; using Umbraco.Cms.Web.Common.Authorization; using Umbraco.Cms.Web.Common.Controllers; using Umbraco.Cms.Web.Common.Filters; +using Umbraco.Core.Dashboards; using Umbraco.Extensions; using Constants = Umbraco.Cms.Core.Constants; @@ -39,7 +41,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers private readonly IDashboardService _dashboardService; private readonly IUmbracoVersion _umbracoVersion; private readonly IShortStringHelper _shortStringHelper; - + private readonly IOptions _dashboardSettings; /// /// Initializes a new instance of the with all its dependencies. /// @@ -49,7 +51,8 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers ILogger logger, IDashboardService dashboardService, IUmbracoVersion umbracoVersion, - IShortStringHelper shortStringHelper) + IShortStringHelper shortStringHelper, + IOptions dashboardSettings) { _backOfficeSecurityAccessor = backOfficeSecurityAccessor; @@ -58,6 +61,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers _dashboardService = dashboardService; _umbracoVersion = umbracoVersion; _shortStringHelper = shortStringHelper; + _dashboardSettings = dashboardSettings; } //we have just one instance of HttpClient shared for the entire application @@ -65,7 +69,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers //we have baseurl as a param to make previewing easier, so we can test with a dev domain from client side [ValidateAngularAntiForgeryToken] - public async Task GetRemoteDashboardContent(string section, string baseUrl = "https://dashboard.umbraco.org/") + public async Task GetRemoteDashboardContent(string section, string baseUrl = "https://dashboard.umbraco.com/") { var user = _backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser; var allowedSections = string.Join(",", user.AllowedSections); @@ -73,7 +77,14 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers var version = _umbracoVersion.SemanticVersion.ToSemanticStringWithoutBuild(); var isAdmin = user.IsAdmin(); - var url = string.Format(baseUrl + "{0}?section={0}&allowed={1}&lang={2}&version={3}&admin={4}", section, allowedSections, language, version, isAdmin); + var url = string.Format("{0}{1}?section={2}&allowed={3}&lang={4}&version={5}&admin={6}", + baseUrl, + _dashboardSettings.Value.ContentDashboardPath, + section, + allowedSections, + language, + version, + isAdmin); var key = "umbraco-dynamic-dashboard-" + language + allowedSections.Replace(",", "-") + section; var content = _appCaches.RuntimeCache.GetCacheItem(key); diff --git a/src/Umbraco.Web.BackOffice/Controllers/DataTypeController.cs b/src/Umbraco.Web.BackOffice/Controllers/DataTypeController.cs index c29b17f3a3..d68c3f06f5 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/DataTypeController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/DataTypeController.cs @@ -305,7 +305,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers // map back to display model, and return var display = _umbracoMapper.Map(dataType.PersistedDataType); - display.AddSuccessNotification(_localizedTextService.Localize("speechBubbles/dataTypeSaved"), ""); + display.AddSuccessNotification(_localizedTextService.Localize("speechBubbles", "dataTypeSaved"), ""); return display; } @@ -336,7 +336,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers return ValidationProblem(); case MoveOperationStatusType.FailedNotAllowedByPath: var notificationModel = new SimpleNotificationModel(); - notificationModel.AddErrorNotification(_localizedTextService.Localize("moveOrCopy/notAllowedByPath"), ""); + notificationModel.AddErrorNotification(_localizedTextService.Localize("moveOrCopy", "notAllowedByPath"), ""); return ValidationProblem(notificationModel); default: throw new ArgumentOutOfRangeException(); diff --git a/src/Umbraco.Web.BackOffice/Controllers/DictionaryController.cs b/src/Umbraco.Web.BackOffice/Controllers/DictionaryController.cs index b00fa20141..8ee4ee1182 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/DictionaryController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/DictionaryController.cs @@ -104,7 +104,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (_localizationService.DictionaryItemExists(key)) { var message = _localizedTextService.Localize( - "dictionaryItem/changeKeyError", + "dictionaryItem","changeKeyError", _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.GetUserCulture(_localizedTextService, _globalSettings), new Dictionary { { "0", key } }); return ValidationProblem(message); @@ -218,7 +218,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers { var message = _localizedTextService.Localize( - "dictionaryItem/changeKeyError", + "dictionaryItem","changeKeyError", userCulture, new Dictionary { { "0", dictionary.Name } }); ModelState.AddModelError("Name", message); @@ -241,7 +241,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers var model = _umbracoMapper.Map(dictionaryItem); model.Notifications.Add(new BackOfficeNotification( - _localizedTextService.Localize("speechBubbles/dictionaryItemSaved", userCulture), string.Empty, + _localizedTextService.Localize("speechBubbles","dictionaryItemSaved", userCulture), string.Empty, NotificationStyle.Success)); return model; diff --git a/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs b/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs index a2f1fe36c3..51a7c082e9 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs @@ -157,7 +157,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers Id = Constants.System.RecycleBinMedia, Alias = "recycleBin", ParentId = -1, - Name = _localizedTextService.Localize("general/recycleBin"), + Name = _localizedTextService.Localize("general", "recycleBin"), ContentTypeAlias = "recycleBin", CreateDate = DateTime.Now, IsContainer = true, @@ -496,7 +496,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (sourceParentID == destinationParentID) { - return ValidationProblem(new SimpleNotificationModel(new BackOfficeNotification("",_localizedTextService.Localize("media/moveToSameFolderFailed"),NotificationStyle.Error))); + return ValidationProblem(new SimpleNotificationModel(new BackOfficeNotification("",_localizedTextService.Localize("media", "moveToSameFolderFailed"),NotificationStyle.Error))); } if (moveResult == false) { @@ -584,8 +584,8 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (saveStatus.Success) { display.AddSuccessNotification( - _localizedTextService.Localize("speechBubbles/editMediaSaved"), - _localizedTextService.Localize("speechBubbles/editMediaSavedText")); + _localizedTextService.Localize("speechBubbles", "editMediaSaved"), + _localizedTextService.Localize("speechBubbles", "editMediaSavedText")); } else { @@ -616,7 +616,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers { _mediaService.EmptyRecycleBin(_backofficeSecurityAccessor.BackOfficeSecurity.GetUserId().ResultOr(Constants.Security.SuperUserId)); - return Ok(_localizedTextService.Localize("defaultdialogs/recycleBinIsEmpty")); + return Ok(_localizedTextService.Localize("defaultdialogs", "recycleBinIsEmpty")); } /// @@ -831,15 +831,14 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers var saveResult = _mediaService.Save(f, _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id); if (saveResult == false) { - AddCancelMessage(tempFiles, - message: _localizedTextService.Localize("speechBubbles/operationCancelledText") + " -- " + mediaItemName); + AddCancelMessage(tempFiles, _localizedTextService.Localize("speechBubbles", "operationCancelledText") + " -- " + mediaItemName); } } else { tempFiles.Notifications.Add(new BackOfficeNotification( - _localizedTextService.Localize("speechBubbles/operationFailedHeader"), - _localizedTextService.Localize("media/disallowedFileType"), + _localizedTextService.Localize("speechBubbles", "operationFailedHeader"), + _localizedTextService.Localize("media", "disallowedFileType"), NotificationStyle.Warning)); } } @@ -927,8 +926,8 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers { return ValidationProblem( new SimpleNotificationModel(new BackOfficeNotification( - _localizedTextService.Localize("speechBubbles/operationFailedHeader"), - _localizedTextService.Localize("speechBubbles/invalidUserPermissionsText"), + _localizedTextService.Localize("speechBubbles", "operationFailedHeader"), + _localizedTextService.Localize("speechBubbles", "invalidUserPermissionsText"), NotificationStyle.Warning)), StatusCodes.Status403Forbidden); } @@ -963,7 +962,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (toMove.ContentType.AllowedAsRoot == false && mediaTypeService.GetAll().Any(ct => ct.AllowedAsRoot)) { var notificationModel = new SimpleNotificationModel(); - notificationModel.AddErrorNotification(_localizedTextService.Localize("moveOrCopy/notAllowedAtRoot"), ""); + notificationModel.AddErrorNotification(_localizedTextService.Localize("moveOrCopy", "notAllowedAtRoot"), ""); return ValidationProblem(notificationModel); } } @@ -981,7 +980,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers .Any(x => x.Value == toMove.ContentType.Id) == false) { var notificationModel = new SimpleNotificationModel(); - notificationModel.AddErrorNotification(_localizedTextService.Localize("moveOrCopy/notAllowedByContentType"), ""); + notificationModel.AddErrorNotification(_localizedTextService.Localize("moveOrCopy", "notAllowedByContentType"), ""); return ValidationProblem(notificationModel); } @@ -989,7 +988,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if ((string.Format(",{0},", parent.Path)).IndexOf(string.Format(",{0},", toMove.Id), StringComparison.Ordinal) > -1) { var notificationModel = new SimpleNotificationModel(); - notificationModel.AddErrorNotification(_localizedTextService.Localize("moveOrCopy/notAllowedByPath"), ""); + notificationModel.AddErrorNotification(_localizedTextService.Localize("moveOrCopy", "notAllowedByPath"), ""); return ValidationProblem(notificationModel); } } diff --git a/src/Umbraco.Web.BackOffice/Controllers/MediaTypeController.cs b/src/Umbraco.Web.BackOffice/Controllers/MediaTypeController.cs index 3ca33c3643..3233679ad9 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MediaTypeController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MediaTypeController.cs @@ -297,7 +297,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers display.AddSuccessNotification( - _localizedTextService.Localize("speechBubbles/mediaTypeSavedHeader"), + _localizedTextService.Localize("speechBubbles","mediaTypeSavedHeader"), string.Empty); return display; diff --git a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs index 7f4270c3d6..6cada09db3 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs @@ -305,8 +305,8 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers case ContentSaveAction.Save: case ContentSaveAction.SaveNew: display.AddSuccessNotification( - _localizedTextService.Localize("speechBubbles/editMemberSaved"), - _localizedTextService.Localize("speechBubbles/editMemberSaved")); + _localizedTextService.Localize("speechBubbles","editMemberSaved"), + _localizedTextService.Localize("speechBubbles","editMemberSaved")); break; } diff --git a/src/Umbraco.Web.BackOffice/Controllers/MemberGroupController.cs b/src/Umbraco.Web.BackOffice/Controllers/MemberGroupController.cs index 845e157650..81303ba55e 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MemberGroupController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MemberGroupController.cs @@ -135,7 +135,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers MemberGroupDisplay display = _umbracoMapper.Map(memberGroup); display.AddSuccessNotification( - _localizedTextService.Localize("speechBubbles/memberGroupSavedHeader"), + _localizedTextService.Localize("speechBubbles","memberGroupSavedHeader"), string.Empty); return display; diff --git a/src/Umbraco.Web.BackOffice/Controllers/MemberTypeController.cs b/src/Umbraco.Web.BackOffice/Controllers/MemberTypeController.cs index 133581208e..4af907bdfc 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MemberTypeController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MemberTypeController.cs @@ -246,7 +246,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers var display =_umbracoMapper.Map(savedCt.Value); display.AddSuccessNotification( - _localizedTextService.Localize("speechBubbles/memberTypeSavedHeader"), + _localizedTextService.Localize("speechBubbles","memberTypeSavedHeader"), string.Empty); return display; diff --git a/src/Umbraco.Web.BackOffice/Controllers/PackageInstallController.cs b/src/Umbraco.Web.BackOffice/Controllers/PackageInstallController.cs index da2205f59c..4c61632ad3 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/PackageInstallController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/PackageInstallController.cs @@ -188,7 +188,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers { //this package is already installed return ValidationProblem( - _localizedTextService.Localize("packager/packageAlreadyInstalled")); + _localizedTextService.Localize("packager", "packageAlreadyInstalled")); } model.OriginalVersion = installType == PackageInstallType.Upgrade ? alreadyInstalled.Version : null; @@ -197,8 +197,8 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers else { model.Notifications.Add(new BackOfficeNotification( - _localizedTextService.Localize("speechBubbles/operationFailedHeader"), - _localizedTextService.Localize("media/disallowedFileType"), + _localizedTextService.Localize("speechBubbles", "operationFailedHeader"), + _localizedTextService.Localize("media", "disallowedFileType"), NotificationStyle.Warning)); } @@ -242,7 +242,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (installType == PackageInstallType.AlreadyInstalled) { return ValidationProblem( - _localizedTextService.Localize("packager/packageAlreadyInstalled")); + _localizedTextService.Localize("packager", "packageAlreadyInstalled")); } model.OriginalVersion = installType == PackageInstallType.Upgrade ? alreadyInstalled.Version : null; @@ -268,7 +268,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers var packageMinVersion = packageInfo.UmbracoVersion; if (_umbracoVersion.Version < packageMinVersion) return ValidationProblem( - _localizedTextService.Localize("packager/targetVersionMismatch", new[] {packageMinVersion.ToString()})); + _localizedTextService.Localize("packager", "targetVersionMismatch", new[] {packageMinVersion.ToString()})); } var installType = _packagingService.GetPackageInstallType(packageInfo.Name, SemVersion.Parse(packageInfo.Version), out var alreadyInstalled); diff --git a/src/Umbraco.Web.BackOffice/Controllers/TemplateQueryController.cs b/src/Umbraco.Web.BackOffice/Controllers/TemplateQueryController.cs index 9183c8c9ed..7f5298066a 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/TemplateQueryController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/TemplateQueryController.cs @@ -47,28 +47,28 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers private IEnumerable Terms => new List { - new OperatorTerm(_localizedTextService.Localize("template/is"), Operator.Equals, new [] {"string"}), - new OperatorTerm(_localizedTextService.Localize("template/isNot"), Operator.NotEquals, new [] {"string"}), - new OperatorTerm(_localizedTextService.Localize("template/before"), Operator.LessThan, new [] {"datetime"}), - new OperatorTerm(_localizedTextService.Localize("template/beforeIncDate"), Operator.LessThanEqualTo, new [] {"datetime"}), - new OperatorTerm(_localizedTextService.Localize("template/after"), Operator.GreaterThan, new [] {"datetime"}), - new OperatorTerm(_localizedTextService.Localize("template/afterIncDate"), Operator.GreaterThanEqualTo, new [] {"datetime"}), - new OperatorTerm(_localizedTextService.Localize("template/equals"), Operator.Equals, new [] {"int"}), - new OperatorTerm(_localizedTextService.Localize("template/doesNotEqual"), Operator.NotEquals, new [] {"int"}), - new OperatorTerm(_localizedTextService.Localize("template/contains"), Operator.Contains, new [] {"string"}), - new OperatorTerm(_localizedTextService.Localize("template/doesNotContain"), Operator.NotContains, new [] {"string"}), - new OperatorTerm(_localizedTextService.Localize("template/greaterThan"), Operator.GreaterThan, new [] {"int"}), - new OperatorTerm(_localizedTextService.Localize("template/greaterThanEqual"), Operator.GreaterThanEqualTo, new [] {"int"}), - new OperatorTerm(_localizedTextService.Localize("template/lessThan"), Operator.LessThan, new [] {"int"}), - new OperatorTerm(_localizedTextService.Localize("template/lessThanEqual"), Operator.LessThanEqualTo, new [] {"int"}) + new OperatorTerm(_localizedTextService.Localize("template","is"), Operator.Equals, new [] {"string"}), + new OperatorTerm(_localizedTextService.Localize("template","isNot"), Operator.NotEquals, new [] {"string"}), + new OperatorTerm(_localizedTextService.Localize("template","before"), Operator.LessThan, new [] {"datetime"}), + new OperatorTerm(_localizedTextService.Localize("template","beforeIncDate"), Operator.LessThanEqualTo, new [] {"datetime"}), + new OperatorTerm(_localizedTextService.Localize("template","after"), Operator.GreaterThan, new [] {"datetime"}), + new OperatorTerm(_localizedTextService.Localize("template","afterIncDate"), Operator.GreaterThanEqualTo, new [] {"datetime"}), + new OperatorTerm(_localizedTextService.Localize("template","equals"), Operator.Equals, new [] {"int"}), + new OperatorTerm(_localizedTextService.Localize("template","doesNotEqual"), Operator.NotEquals, new [] {"int"}), + new OperatorTerm(_localizedTextService.Localize("template","contains"), Operator.Contains, new [] {"string"}), + new OperatorTerm(_localizedTextService.Localize("template","doesNotContain"), Operator.NotContains, new [] {"string"}), + new OperatorTerm(_localizedTextService.Localize("template","greaterThan"), Operator.GreaterThan, new [] {"int"}), + new OperatorTerm(_localizedTextService.Localize("template","greaterThanEqual"), Operator.GreaterThanEqualTo, new [] {"int"}), + new OperatorTerm(_localizedTextService.Localize("template","lessThan"), Operator.LessThan, new [] {"int"}), + new OperatorTerm(_localizedTextService.Localize("template","lessThanEqual"), Operator.LessThanEqualTo, new [] {"int"}) }; private IEnumerable Properties => new List { - new PropertyModel { Name = _localizedTextService.Localize("template/id"), Alias = "Id", Type = "int" }, - new PropertyModel { Name = _localizedTextService.Localize("template/name"), Alias = "Name", Type = "string" }, - new PropertyModel { Name = _localizedTextService.Localize("template/createdDate"), Alias = "CreateDate", Type = "datetime" }, - new PropertyModel { Name = _localizedTextService.Localize("template/lastUpdatedDate"), Alias = "UpdateDate", Type = "datetime" } + new PropertyModel { Name = _localizedTextService.Localize("template","id"), Alias = "Id", Type = "int" }, + new PropertyModel { Name = _localizedTextService.Localize("template","name"), Alias = "Name", Type = "string" }, + new PropertyModel { Name = _localizedTextService.Localize("template","createdDate"), Alias = "CreateDate", Type = "datetime" }, + new PropertyModel { Name = _localizedTextService.Localize("template","lastUpdatedDate"), Alias = "UpdateDate", Type = "datetime" } }; public QueryResultModel PostTemplateQuery(QueryModel model) @@ -232,10 +232,10 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers public IEnumerable GetContentTypes() { var contentTypes = _contentTypeService.GetAll() - .Select(x => new ContentTypeModel { Alias = x.Alias, Name = _localizedTextService.Localize("template/contentOfType", tokens: new string[] { x.Name }) }) + .Select(x => new ContentTypeModel { Alias = x.Alias, Name = _localizedTextService.Localize("template", "contentOfType", tokens: new string[] { x.Name }) }) .OrderBy(x => x.Name).ToList(); - contentTypes.Insert(0, new ContentTypeModel { Alias = string.Empty, Name = _localizedTextService.Localize("template/allContent") }); + contentTypes.Insert(0, new ContentTypeModel { Alias = string.Empty, Name = _localizedTextService.Localize("template", "allContent") }); return contentTypes; } diff --git a/src/Umbraco.Web.BackOffice/Controllers/UserGroupsController.cs b/src/Umbraco.Web.BackOffice/Controllers/UserGroupsController.cs index 971d7400de..ad8d11f95a 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/UserGroupsController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/UserGroupsController.cs @@ -117,7 +117,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers var display = _umbracoMapper.Map(userGroupSave.PersistedUserGroup); - display.AddSuccessNotification(_localizedTextService.Localize("speechBubbles/operationSavedHeader"), _localizedTextService.Localize("speechBubbles/editUserGroupSaved")); + display.AddSuccessNotification(_localizedTextService.Localize("speechBubbles","operationSavedHeader"), _localizedTextService.Localize("speechBubbles","editUserGroupSaved")); return display; } @@ -202,10 +202,10 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers } if (userGroups.Length > 1) { - return Ok(_localizedTextService.Localize("speechBubbles/deleteUserGroupsSuccess", new[] {userGroups.Length.ToString()})); + return Ok(_localizedTextService.Localize("speechBubbles","deleteUserGroupsSuccess", new[] {userGroups.Length.ToString()})); } - return Ok(_localizedTextService.Localize("speechBubbles/deleteUserGroupSuccess", new[] {userGroups[0].Name})); + return Ok(_localizedTextService.Localize("speechBubbles","deleteUserGroupSuccess", new[] {userGroups[0].Name})); } } } diff --git a/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs b/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs index ff2e087aa4..3d08d2dab1 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs @@ -491,7 +491,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers //send the email await SendUserInviteEmailAsync(display, _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Name, _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Email, user, userSave.Message); - display.AddSuccessNotification(_localizedTextService.Localize("speechBubbles/resendInviteHeader"), _localizedTextService.Localize("speechBubbles/resendInviteSuccess", new[] { user.Name })); + display.AddSuccessNotification(_localizedTextService.Localize("speechBubbles","resendInviteHeader"), _localizedTextService.Localize("speechBubbles","resendInviteSuccess", new[] { user.Name })); return display; } @@ -543,10 +543,10 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers var applicationUri = _hostingEnvironment.ApplicationMainUrl; var inviteUri = new Uri(applicationUri, action); - var emailSubject = _localizedTextService.Localize("user/inviteEmailCopySubject", + var emailSubject = _localizedTextService.Localize("user","inviteEmailCopySubject", //Ensure the culture of the found user is used for the email! UmbracoUserExtensions.GetUserCulture(to.Language, _localizedTextService, _globalSettings)); - var emailBody = _localizedTextService.Localize("user/inviteEmailCopyFormat", + var emailBody = _localizedTextService.Localize("user","inviteEmailCopyFormat", //Ensure the culture of the found user is used for the email! UmbracoUserExtensions.GetUserCulture(to.Language, _localizedTextService, _globalSettings), new[] { userDisplay.Name, from, message, inviteUri.ToString(), fromEmail }); @@ -645,11 +645,11 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers var userHasChangedOwnLanguage = user.Id == currentUser.Id && currentUser.Language != user.Language; - var textToLocalise = userHasChangedOwnLanguage ? "speechBubbles/operationSavedHeaderReloadUser" : "speechBubbles/operationSavedHeader"; + var textToLocalise = userHasChangedOwnLanguage ? "operationSavedHeaderReloadUser" : "operationSavedHeader"; var culture = userHasChangedOwnLanguage ? CultureInfo.GetCultureInfo(user.Language) : Thread.CurrentThread.CurrentUICulture; - display.AddSuccessNotification(_localizedTextService.Localize(textToLocalise, culture), _localizedTextService.Localize("speechBubbles/editUserSaved", culture)); + display.AddSuccessNotification(_localizedTextService.Localize("speechBubbles", textToLocalise, culture), _localizedTextService.Localize("speechBubbles","editUserSaved", culture)); return display; } @@ -697,7 +697,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (passwordChangeResult.Success) { var result = new ModelWithNotifications(passwordChangeResult.Result.ResetPassword); - result.AddSuccessNotification(_localizedTextService.Localize("general/success"), _localizedTextService.Localize("user/passwordChangedGeneric")); + result.AddSuccessNotification(_localizedTextService.Localize("general","success"), _localizedTextService.Localize("user","passwordChangedGeneric")); return result; } @@ -733,10 +733,10 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (users.Length > 1) { - return Ok(_localizedTextService.Localize("speechBubbles/disableUsersSuccess", new[] {userIds.Length.ToString()})); + return Ok(_localizedTextService.Localize("speechBubbles","disableUsersSuccess", new[] {userIds.Length.ToString()})); } - return Ok(_localizedTextService.Localize("speechBubbles/disableUserSuccess", new[] { users[0].Name })); + return Ok(_localizedTextService.Localize("speechBubbles","disableUserSuccess", new[] { users[0].Name })); } /// @@ -756,11 +756,11 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (users.Length > 1) { return Ok( - _localizedTextService.Localize("speechBubbles/enableUsersSuccess", new[] { userIds.Length.ToString() })); + _localizedTextService.Localize("speechBubbles","enableUsersSuccess", new[] { userIds.Length.ToString() })); } return Ok( - _localizedTextService.Localize("speechBubbles/enableUserSuccess", new[] { users[0].Name })); + _localizedTextService.Localize("speechBubbles","enableUserSuccess", new[] { users[0].Name })); } /// @@ -792,12 +792,12 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (userIds.Length == 1) { return Ok( - _localizedTextService.Localize("speechBubbles/unlockUserSuccess", new[] {user.Name})); + _localizedTextService.Localize("speechBubbles","unlockUserSuccess", new[] {user.Name})); } } return Ok( - _localizedTextService.Localize("speechBubbles/unlockUsersSuccess", new[] {(userIds.Length - notFound.Count).ToString()})); + _localizedTextService.Localize("speechBubbles","unlockUsersSuccess", new[] {(userIds.Length - notFound.Count).ToString()})); } [Authorize(Policy = AuthorizationPolicies.AdminUserEditsRequireAdmin)] @@ -815,7 +815,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers } _userService.Save(users); return Ok( - _localizedTextService.Localize("speechBubbles/setUserGroupOnUsersSuccess")); + _localizedTextService.Localize("speechBubbles","setUserGroupOnUsersSuccess")); } /// @@ -846,7 +846,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers _userService.Delete(user, true); return Ok( - _localizedTextService.Localize("speechBubbles/deleteUserSuccess", new[] { userName })); + _localizedTextService.Localize("speechBubbles","deleteUserSuccess", new[] { userName })); } public class PagedUserResult : PagedResult diff --git a/src/Umbraco.Web.BackOffice/Mapping/ContentMapDefinition.cs b/src/Umbraco.Web.BackOffice/Mapping/ContentMapDefinition.cs index 8396d77f1c..a30b6ba69d 100644 --- a/src/Umbraco.Web.BackOffice/Mapping/ContentMapDefinition.cs +++ b/src/Umbraco.Web.BackOffice/Mapping/ContentMapDefinition.cs @@ -113,7 +113,20 @@ namespace Umbraco.Cms.Web.BackOffice.Mapping // Umbraco.Code.MapAll -AllowPreview -Errors -PersistedContent private void Map(IContent source, ContentItemDisplay target, MapperContext context) { - target.AllowedActions = GetActions(source); + // Both GetActions and DetermineIsChildOfListView use parent, so get it once here + // Parent might already be in context, so check there before using content service + IContent parent; + if (context.Items.TryGetValue("Parent", out var parentObj) && + parentObj is IContent typedParent) + { + parent = typedParent; + } + else + { + parent = _contentService.GetParent(source); + } + + target.AllowedActions = GetActions(source, parent, context); target.AllowedTemplates = GetAllowedTemplates(source); target.ContentApps = _commonMapper.GetContentApps(source); target.ContentTypeId = source.ContentType.Id; @@ -124,7 +137,7 @@ namespace Umbraco.Cms.Web.BackOffice.Mapping target.Icon = source.ContentType.Icon; target.Id = source.Id; target.IsBlueprint = source.Blueprint; - target.IsChildOfListView = DetermineIsChildOfListView(source, context); + target.IsChildOfListView = DetermineIsChildOfListView(source, parent, context); target.IsContainer = source.ContentType.IsContainer; target.IsElement = source.ContentType.IsElement; target.Key = source.Key; @@ -183,7 +196,7 @@ namespace Umbraco.Cms.Web.BackOffice.Mapping target.VariesByCulture = source.ContentType.VariesByCulture(); } - private IEnumerable GetActions(IContent source) + private IEnumerable GetActions(IContent source, IContent parent, MapperContext context) { var backOfficeSecurity = _backOfficeSecurityAccessor.BackOfficeSecurity; @@ -196,10 +209,28 @@ namespace Umbraco.Cms.Web.BackOffice.Mapping path = source.Path; else { - var parent = _contentService.GetById(source.ParentId); path = parent == null ? "-1" : parent.Path; } + // A bit of a mess, but we need to ensure that all the required values are here AND that they're the right type. + if (context.Items.TryGetValue("CurrentUser", out var userObject) && + context.Items.TryGetValue("Permissions", out var permissionsObject) && + userObject is IUser currentUser && + permissionsObject is Dictionary permissionsDict) + { + // If we already have permissions for a given path, + // and the current user is the same as was used to generate the permissions, return the stored permissions. + if (backOfficeSecurity.CurrentUser.Id == currentUser.Id && + permissionsDict.TryGetValue(path, out var permissions)) + { + return permissions.GetAllPermissions(); + } + } + + // TODO: This is certainly not ideal usage here - perhaps the best way to deal with this in the future is + // with the IUmbracoContextAccessor. In the meantime, if used outside of a web app this will throw a null + // reference exception :( + return _userService.GetPermissionsForPath(backOfficeSecurity.CurrentUser, path).GetAllPermissions(); } @@ -266,6 +297,7 @@ namespace Umbraco.Cms.Web.BackOffice.Mapping /// Checks if the content item is a descendant of a list view /// /// + /// /// /// /// Returns true if the content item is a descendant of a list view and where the content is @@ -277,7 +309,7 @@ namespace Umbraco.Cms.Web.BackOffice.Mapping /// false because the item is technically not being rendered as part of a list view but instead as a /// real tree node. If we didn't perform this check then tree syncing wouldn't work correctly. /// - private bool DetermineIsChildOfListView(IContent source, MapperContext context) + private bool DetermineIsChildOfListView(IContent source, IContent parent, MapperContext context) { var userStartNodes = Array.Empty(); @@ -296,8 +328,6 @@ namespace Umbraco.Cms.Web.BackOffice.Mapping } } - var parent = _contentService.GetParent(source); - if (parent == null) return false; @@ -331,6 +361,12 @@ namespace Umbraco.Cms.Web.BackOffice.Mapping private IDictionary GetAllowedTemplates(IContent source) { + // Element types can't have templates, so no need to query to get the content type + if (source.ContentType.IsElement) + { + return new Dictionary(); + } + var contentType = _contentTypeService.Get(source.ContentTypeId); return contentType.AllowedTemplates diff --git a/src/Umbraco.Web.BackOffice/Trees/ApplicationTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/ApplicationTreeController.cs index 077207913b..db714bb675 100644 --- a/src/Umbraco.Web.BackOffice/Trees/ApplicationTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/ApplicationTreeController.cs @@ -82,7 +82,7 @@ namespace Umbraco.Cms.Web.BackOffice.Trees { // if there are no trees defined for this section but the section is defined then we can have a simple // full screen section without trees - var name = _localizedTextService.Localize("sections/" + application); + var name = _localizedTextService.Localize("sections", application); return TreeRootNode.CreateSingleTreeRoot(Constants.System.RootString, null, null, name, TreeNodeCollection.Empty, true); } @@ -128,7 +128,7 @@ namespace Umbraco.Cms.Web.BackOffice.Trees } } - var name = _localizedTextService.Localize("sections/" + application); + var name = _localizedTextService.Localize("sections", application); if (nodes.Count > 0) { @@ -173,7 +173,7 @@ namespace Umbraco.Cms.Web.BackOffice.Trees var name = groupName.IsNullOrWhiteSpace() ? "thirdPartyGroup" : groupName; var groupRootNode = TreeRootNode.CreateGroupNode(nodes, application); - groupRootNode.Name = _localizedTextService.Localize("treeHeaders/" + name); + groupRootNode.Name = _localizedTextService.Localize("treeHeaders", name); treeRootNodes.Add(groupRootNode); } diff --git a/src/Umbraco.Web.BackOffice/Trees/ContentTreeControllerBase.cs b/src/Umbraco.Web.BackOffice/Trees/ContentTreeControllerBase.cs index a307a67d23..e84338fc9b 100644 --- a/src/Umbraco.Web.BackOffice/Trees/ContentTreeControllerBase.cs +++ b/src/Umbraco.Web.BackOffice/Trees/ContentTreeControllerBase.cs @@ -372,7 +372,7 @@ namespace Umbraco.Cms.Web.BackOffice.Trees RecycleBinId.ToInvariantString(), id, queryStrings, - LocalizedTextService.Localize("general/recycleBin"), + LocalizedTextService.Localize("general", "recycleBin"), "icon-trash", RecycleBinSmells, queryStrings.GetRequiredValue("application") + TreeAlias.EnsureStartsWith('/') + "/recyclebin")); diff --git a/src/Umbraco.Web.BackOffice/Trees/DataTypeTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/DataTypeTreeController.cs index 98d65562e2..bddf823c43 100644 --- a/src/Umbraco.Web.BackOffice/Trees/DataTypeTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/DataTypeTreeController.cs @@ -142,7 +142,7 @@ namespace Umbraco.Cms.Web.BackOffice.Trees menu.Items.Add(LocalizedTextService, opensDialog: true); - menu.Items.Add(new MenuItem("rename", LocalizedTextService.Localize("actions/rename")) + menu.Items.Add(new MenuItem("rename", LocalizedTextService.Localize("actions", "rename")) { Icon = "icon icon-edit" }); diff --git a/src/Umbraco.Web.BackOffice/Trees/MediaTypeTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/MediaTypeTreeController.cs index 4fc108486d..2cda5802b8 100644 --- a/src/Umbraco.Web.BackOffice/Trees/MediaTypeTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/MediaTypeTreeController.cs @@ -105,7 +105,7 @@ namespace Umbraco.Cms.Web.BackOffice.Trees menu.Items.Add(LocalizedTextService, opensDialog: true); - menu.Items.Add(new MenuItem("rename", LocalizedTextService.Localize("actions/rename")) + menu.Items.Add(new MenuItem("rename", LocalizedTextService.Localize("actions", "rename")) { Icon = "icon icon-edit" }); diff --git a/src/Umbraco.Web.BackOffice/Trees/MemberTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/MemberTreeController.cs index cae9eed13f..4e42b10b26 100644 --- a/src/Umbraco.Web.BackOffice/Trees/MemberTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/MemberTreeController.cs @@ -107,7 +107,7 @@ namespace Umbraco.Cms.Web.BackOffice.Trees if (id == Constants.System.RootString) { nodes.Add( - CreateTreeNode(Constants.Conventions.MemberTypes.AllMembersListId, id, queryStrings, LocalizedTextService.Localize("member/allMembers"), Constants.Icons.MemberType, true, + CreateTreeNode(Constants.Conventions.MemberTypes.AllMembersListId, id, queryStrings, LocalizedTextService.Localize("member","allMembers"), Constants.Icons.MemberType, true, queryStrings.GetRequiredValue("application") + TreeAlias.EnsureStartsWith('/') + "/list/" + Constants.Conventions.MemberTypes.AllMembersListId)); nodes.AddRange(_memberTypeService.GetAll() diff --git a/src/Umbraco.Web.Common/Extensions/FriendlyImageCropperTemplateExtensions.cs b/src/Umbraco.Web.Common/Extensions/FriendlyImageCropperTemplateExtensions.cs index 82a1513658..e8d580968e 100644 --- a/src/Umbraco.Web.Common/Extensions/FriendlyImageCropperTemplateExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/FriendlyImageCropperTemplateExtensions.cs @@ -1,10 +1,11 @@ -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.Media; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PropertyEditors.ValueConverters; using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Web.Common.DependencyInjection; +using Umbraco.Core.Models; namespace Umbraco.Extensions { @@ -37,12 +38,23 @@ namespace Umbraco.Extensions string cropAlias) => mediaItem.GetCropUrl(cropAlias, ImageUrlGenerator, PublishedValueFallback, PublishedUrlProvider); + public static string GetCropUrl(this MediaWithCrops mediaWithCrops, string cropAlias) + => ImageCropperTemplateCoreExtensions.GetCropUrl(mediaWithCrops, cropAlias, ImageUrlGenerator, PublishedValueFallback, PublishedUrlProvider); + /// + /// Gets the crop URL by using only the specified . + /// + /// The media item. + /// The image cropper value. + /// The crop alias. + /// + /// The image crop URL. + /// public static string GetCropUrl( this IPublishedContent mediaItem, - string cropAlias, - ImageCropperValue imageCropperValue) - => mediaItem.GetCropUrl(cropAlias, ImageUrlGenerator, imageCropperValue); + ImageCropperValue imageCropperValue, + string cropAlias) + => ImageCropperTemplateCoreExtensions.GetCropUrl(mediaItem, imageCropperValue, cropAlias, ImageUrlGenerator, PublishedValueFallback, PublishedUrlProvider); /// /// Gets the underlying image processing service URL by the crop alias using the specified property containing the image cropper Json data on the IPublishedContent item. @@ -65,6 +77,9 @@ namespace Umbraco.Extensions string cropAlias) => mediaItem.GetCropUrl(propertyAlias, cropAlias, ImageUrlGenerator, PublishedValueFallback, PublishedUrlProvider); + public static string GetCropUrl(this MediaWithCrops mediaWithCrops, string propertyAlias, string cropAlias) + => ImageCropperTemplateCoreExtensions.GetCropUrl(mediaWithCrops, propertyAlias, cropAlias, ImageUrlGenerator, PublishedValueFallback, PublishedUrlProvider); + /// /// Gets the underlying image processing service URL from the IPublishedContent item. /// @@ -326,6 +341,6 @@ namespace Umbraco.Extensions this MediaWithCrops mediaWithCrops, string alias, string cacheBusterValue = null) - => mediaWithCrops.GetLocalCropUrl(alias, ImageUrlGenerator, cacheBusterValue); + => mediaWithCrops.GetLocalCropUrl(alias, cacheBusterValue); } } diff --git a/src/Umbraco.Web.Common/Extensions/ImageCropperTemplateCoreExtensions.cs b/src/Umbraco.Web.Common/Extensions/ImageCropperTemplateCoreExtensions.cs index 01c4ca1413..5e24a51f6a 100644 --- a/src/Umbraco.Web.Common/Extensions/ImageCropperTemplateCoreExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/ImageCropperTemplateCoreExtensions.cs @@ -1,11 +1,13 @@ -using System; +using System; using System.Globalization; using Newtonsoft.Json.Linq; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Media; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PropertyEditors.ValueConverters; using Umbraco.Cms.Core.Routing; +using Umbraco.Core.Models; namespace Umbraco.Extensions { @@ -36,9 +38,35 @@ namespace Umbraco.Extensions return mediaItem.GetCropUrl(imageUrlGenerator, publishedValueFallback, publishedUrlProvider, cropAlias: cropAlias, useCropDimensions: true); } - public static string GetCropUrl(this IPublishedContent mediaItem, string cropAlias, IImageUrlGenerator imageUrlGenerator, ImageCropperValue imageCropperValue) + public static string GetCropUrl( + this MediaWithCrops mediaWithCrops, + string cropAlias, + IImageUrlGenerator imageUrlGenerator, + IPublishedValueFallback publishedValueFallback, + IPublishedUrlProvider publishedUrlProvider) { - return mediaItem.Url().GetCropUrl(imageUrlGenerator, imageCropperValue, cropAlias: cropAlias, useCropDimensions: true); + return mediaWithCrops.GetCropUrl(imageUrlGenerator, publishedValueFallback, publishedUrlProvider, cropAlias: cropAlias, useCropDimensions: true); + } + + /// + /// Gets the crop URL by using only the specified . + /// + /// The media item. + /// The image cropper value. + /// The crop alias. + /// The image URL generator. + /// + /// The image crop URL. + /// + public static string GetCropUrl( + this IPublishedContent mediaItem, + ImageCropperValue imageCropperValue, + string cropAlias, + IImageUrlGenerator imageUrlGenerator, + IPublishedValueFallback publishedValueFallback, + IPublishedUrlProvider publishedUrlProvider) + { + return mediaItem.GetCropUrl(imageUrlGenerator, publishedValueFallback, publishedUrlProvider, imageCropperValue, true, cropAlias: cropAlias, useCropDimensions: true); } /// @@ -70,6 +98,16 @@ namespace Umbraco.Extensions return mediaItem.GetCropUrl( imageUrlGenerator, publishedValueFallback, publishedUrlProvider, propertyAlias: propertyAlias, cropAlias: cropAlias, useCropDimensions: true); } + public static string GetCropUrl(this MediaWithCrops mediaWithCrops, + IPublishedValueFallback publishedValueFallback, + IPublishedUrlProvider publishedUrlProvider, + string propertyAlias, + string cropAlias, + IImageUrlGenerator imageUrlGenerator) + { + return mediaWithCrops.GetCropUrl(imageUrlGenerator, publishedValueFallback, publishedUrlProvider, propertyAlias: propertyAlias, cropAlias: cropAlias, useCropDimensions: true); + } + /// /// Gets the underlying image processing service URL from the IPublishedContent item. /// @@ -145,7 +183,55 @@ namespace Umbraco.Extensions ImageCropRatioMode? ratioMode = null, bool upScale = true) { - if (mediaItem == null) throw new ArgumentNullException("mediaItem"); + return mediaItem.GetCropUrl(imageUrlGenerator, publishedValueFallback, publishedUrlProvider, null, false, width, height, propertyAlias, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBuster, furtherOptions, ratioMode, upScale); + } + + public static string GetCropUrl( + this MediaWithCrops mediaWithCrops, + IImageUrlGenerator imageUrlGenerator, + IPublishedValueFallback publishedValueFallback, + IPublishedUrlProvider publishedUrlProvider, + int? width = null, + int? height = null, + string propertyAlias = Constants.Conventions.Media.File, + string cropAlias = null, + int? quality = null, + ImageCropMode? imageCropMode = null, + ImageCropAnchor? imageCropAnchor = null, + bool preferFocalPoint = false, + bool useCropDimensions = false, + bool cacheBuster = true, + string furtherOptions = null, + ImageCropRatioMode? ratioMode = null, + bool upScale = true) + { + if (mediaWithCrops == null) throw new ArgumentNullException(nameof(mediaWithCrops)); + + return mediaWithCrops.Content.GetCropUrl(imageUrlGenerator, publishedValueFallback, publishedUrlProvider, mediaWithCrops.LocalCrops, false, width, height, propertyAlias, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBuster, furtherOptions, ratioMode, upScale); + } + + private static string GetCropUrl( + this IPublishedContent mediaItem, + IImageUrlGenerator imageUrlGenerator, + IPublishedValueFallback publishedValueFallback, + IPublishedUrlProvider publishedUrlProvider, + ImageCropperValue localCrops, + bool localCropsOnly, + int? width = null, + int? height = null, + string propertyAlias = Constants.Conventions.Media.File, + string cropAlias = null, + int? quality = null, + ImageCropMode? imageCropMode = null, + ImageCropAnchor? imageCropAnchor = null, + bool preferFocalPoint = false, + bool useCropDimensions = false, + bool cacheBuster = true, + string furtherOptions = null, + ImageCropRatioMode? ratioMode = null, + bool upScale = true) + { + if (mediaItem == null) throw new ArgumentNullException(nameof(mediaItem)); var cacheBusterValue = cacheBuster ? mediaItem.UpdateDate.ToFileTimeUtc().ToString(CultureInfo.InvariantCulture) : null; @@ -154,31 +240,38 @@ namespace Umbraco.Extensions var mediaItemUrl = mediaItem.MediaUrl(publishedUrlProvider, propertyAlias: propertyAlias); - //get the default obj from the value converter - var cropperValue = mediaItem.Value(publishedValueFallback, propertyAlias); - - //is it strongly typed? - var stronglyTyped = cropperValue as ImageCropperValue; - if (stronglyTyped != null) + // Only get crops from media when required and used + if (localCropsOnly == false && (imageCropMode == ImageCropMode.Crop || imageCropMode == null)) { - return GetCropUrl( - mediaItemUrl, imageUrlGenerator, stronglyTyped, width, height, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, - cacheBusterValue, furtherOptions, ratioMode, upScale); + // Get the default cropper value from the value converter + var cropperValue = mediaItem.Value(publishedValueFallback, propertyAlias); + + var mediaCrops = cropperValue as ImageCropperValue; + + if (mediaCrops == null && cropperValue is JObject jobj) + { + mediaCrops = jobj.ToObject(); + } + + if (mediaCrops == null && cropperValue is string imageCropperValue && + string.IsNullOrEmpty(imageCropperValue) == false && imageCropperValue.DetectIsJson()) + { + mediaCrops = imageCropperValue.DeserializeImageCropperValue(); + } + + // Merge crops + if (localCrops == null) + { + localCrops = mediaCrops; + } + else if (mediaCrops != null) + { + localCrops = localCrops.Merge(mediaCrops); + } } - //this shouldn't be the case but we'll check - var jobj = cropperValue as JObject; - if (jobj != null) - { - stronglyTyped = jobj.ToObject(); - return GetCropUrl( - mediaItemUrl, imageUrlGenerator, stronglyTyped, width, height, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, - cacheBusterValue, furtherOptions, ratioMode, upScale); - } - - //it's a single string return GetCropUrl( - mediaItemUrl, imageUrlGenerator, width, height, mediaItemUrl, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, + mediaItemUrl, imageUrlGenerator, localCrops, width, height, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBusterValue, furtherOptions, ratioMode, upScale); } @@ -259,6 +352,7 @@ namespace Umbraco.Extensions { cropDataSet = imageCropperValue.DeserializeImageCropperValue(); } + return GetCropUrl( imageUrl, imageUrlGenerator, cropDataSet, width, height, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBusterValue, furtherOptions, ratioMode, upScale); @@ -402,11 +496,5 @@ namespace Umbraco.Extensions return imageUrlGenerator.GetImageUrl(options); } - - public static string GetLocalCropUrl(this MediaWithCrops mediaWithCrops, string alias, IImageUrlGenerator imageUrlGenerator, string cacheBusterValue) - { - return mediaWithCrops.LocalCrops.Src + mediaWithCrops.LocalCrops.GetCropUrl(alias, imageUrlGenerator, cacheBusterValue: cacheBusterValue); - - } } } diff --git a/src/Umbraco.Web.Common/Macros/MacroRenderer.cs b/src/Umbraco.Web.Common/Macros/MacroRenderer.cs index 9910065afd..b05900ca75 100644 --- a/src/Umbraco.Web.Common/Macros/MacroRenderer.cs +++ b/src/Umbraco.Web.Common/Macros/MacroRenderer.cs @@ -362,7 +362,7 @@ namespace Umbraco.Cms.Web.Common.Macros $"Executing PartialView: MacroSource=\"{model.MacroSource}\".", "Executed PartialView.", () => _partialViewMacroEngine.Execute(model, content), - () => _textService.Localize("errors/macroErrorLoadingPartialView", new[] { model.MacroSource })); + () => _textService.Localize("errors", "macroErrorLoadingPartialView", new[] { model.MacroSource })); } diff --git a/src/Umbraco.Web.UI.Client/src/common/services/datatypehelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/datatypehelper.service.js index f4317b51b7..7aff3faaaf 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/datatypehelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/datatypehelper.service.js @@ -14,7 +14,7 @@ function dataTypeHelper() { for (var i = 0; i < preVals.length; i++) { preValues.push({ hideLabel: preVals[i].hideLabel, - alias: preVals[i].key, + alias: preVals[i].key != undefined ? preVals[i].key : preVals[i].alias, description: preVals[i].description, label: preVals[i].label, view: preVals[i].view, diff --git a/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js b/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js index e3d972b47d..b83367ef6e 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js @@ -11,7 +11,7 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s //These are absolutely required in order for the macros to render inline //we put these as extended elements because they get merged on top of the normal allowed elements by tiny mce - var extendedValidElements = "@[id|class|style],-div[id|dir|class|align|style],ins[datetime|cite],-ul[class|style],-li[class|style],-h1[id|dir|class|align|style],-h2[id|dir|class|align|style],-h3[id|dir|class|align|style],-h4[id|dir|class|align|style],-h5[id|dir|class|align|style],-h6[id|style|dir|class|align],span[id|class|style]"; + var extendedValidElements = "@[id|class|style],-div[id|dir|class|align|style],ins[datetime|cite],-ul[class|style],-li[class|style],-h1[id|dir|class|align|style],-h2[id|dir|class|align|style],-h3[id|dir|class|align|style],-h4[id|dir|class|align|style],-h5[id|dir|class|align|style],-h6[id|style|dir|class|align],span[id|class|style|lang]"; var fallbackStyles = [{ title: "Page header", block: "h2" }, { title: "Section header", block: "h3" }, { title: "Paragraph header", block: "h4" }, { title: "Normal", block: "p" }, { title: "Quote", block: "blockquote" }, { title: "Code", block: "code" }]; // these languages are available for localization var availableLanguages = [ diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-nested-content.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-nested-content.less index 9dd40a4386..f6c252cc4d 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-nested-content.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-nested-content.less @@ -15,17 +15,6 @@ pointer-events: none; } -.umb-nested-content--mandatory { - /* - yeah so this is a pain, but we must be super specific in targeting the mandatory property labels, - otherwise all properties within a reqired, nested, nested content property will all appear mandatory - */ - .umb-property > ng-form > .control-group > .umb-el-wrap > .control-header label:after { - content: '*'; - color: @red; - } -} - .umb-nested-content-overlay { position: absolute; top: 0; diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/rollback/rollback.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/rollback/rollback.controller.js index 0d49c7dd9c..4b0dfcb8b4 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/rollback/rollback.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/rollback/rollback.controller.js @@ -112,7 +112,6 @@ * This will load in a new version */ function createDiff(currentVersion, previousVersion) { - vm.diff = {}; vm.diff.properties = []; @@ -120,41 +119,55 @@ vm.diff.name = JsDiff.diffWords(currentVersion.name, previousVersion.name); // extract all properties from the tabs and create new object for the diff - currentVersion.tabs.forEach((tab, tabIndex) => { - tab.properties.forEach((property, propertyIndex) => { - var oldProperty = previousVersion.tabs[tabIndex].properties[propertyIndex]; + currentVersion.tabs.forEach(function (tab) { + tab.properties.forEach(function (property) { + let oldTabIndex = -1; + let oldTabPropertyIndex = -1; + const previousVersionTabs = previousVersion.tabs; - // copy existing properties, so it doesn't manipulate existing properties on page - oldProperty = Utilities.copy(oldProperty); - property = Utilities.copy(property); - - // we have to make properties storing values as object into strings (Grid, nested content, etc.) - if(property.value instanceof Object) { - property.value = JSON.stringify(property.value, null, 1); - property.isObject = true; + // find the property by alias, but only search until we find it + for (var oti = 0, length = previousVersionTabs.length; oti < length; oti++) { + const opi = previousVersionTabs[oti].properties.findIndex(p => p.alias === property.alias); + if (opi !== -1) { + oldTabIndex = oti; + oldTabPropertyIndex = opi; + break; + } } - if(oldProperty.value instanceof Object) { - oldProperty.value = JSON.stringify(oldProperty.value, null, 1); - oldProperty.isObject = true; + if (oldTabIndex !== -1 && oldTabPropertyIndex !== -1) { + let oldProperty = previousVersion.tabs[oldTabIndex].properties[oldTabPropertyIndex]; + + // copy existing properties, so it doesn't manipulate existing properties on page + oldProperty = Utilities.copy(oldProperty); + property = Utilities.copy(property); + + // we have to make properties storing values as object into strings (Grid, nested content, etc.) + if (property.value instanceof Object) { + property.value = JSON.stringify(property.value, null, 1); + property.isObject = true; + } + + if (oldProperty.value instanceof Object) { + oldProperty.value = JSON.stringify(oldProperty.value, null, 1); + oldProperty.isObject = true; + } + + // diff requires a string + property.value = property.value ? property.value + '' : ''; + oldProperty.value = oldProperty.value ? oldProperty.value + '' : ''; + + const diffProperty = { + 'alias': property.alias, + 'label': property.label, + 'diff': property.isObject ? JsDiff.diffJson(property.value, oldProperty.value) : JsDiff.diffWords(property.value, oldProperty.value), + 'isObject': property.isObject || oldProperty.isObject + }; + + vm.diff.properties.push(diffProperty); } - - // diff requires a string - property.value = property.value ? property.value + "" : ""; - oldProperty.value = oldProperty.value ? oldProperty.value + "" : ""; - - var diffProperty = { - "alias": property.alias, - "label": property.label, - "diff": (property.isObject) ? JsDiff.diffJson(property.value, oldProperty.value) : JsDiff.diffWords(property.value, oldProperty.value), - "isObject": (property.isObject || oldProperty.isObject) ? true : false - }; - - vm.diff.properties.push(diffProperty); - }); }); - } function rollback() { diff --git a/src/Umbraco.Web.UI.Client/src/views/components/property/umb-property.html b/src/Umbraco.Web.UI.Client/src/views/components/property/umb-property.html index 5b8e6d8f04..51a6f65c9c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/property/umb-property.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/property/umb-property.html @@ -16,7 +16,7 @@ {{vm.property.label}} - + * diff --git a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/obsoletemediapickernotice.html b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/obsoletemediapickernotice.html deleted file mode 100644 index 617ffd80d6..0000000000 --- a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/obsoletemediapickernotice.html +++ /dev/null @@ -1 +0,0 @@ -

Important: switching from "Media Picker (legacy)" to "Media Picker" is not supported and doing so will mean all data (references to previously selected media items) will no longer be available on existing content items.

diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js index 68d375360e..446fb8c076 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js @@ -190,7 +190,7 @@ }; vm.openNodeTypePicker = function ($event) { - + if (vm.nodes.length >= vm.maxItems) { return; } @@ -537,15 +537,20 @@ if (tab) { scaffold.variants[0].tabs.push(tab); - tab.properties.forEach(function (property) { + tab.properties.forEach( + function (property) { if (_.find(notSupported, function (x) { return x === property.editor; })) { property.notSupported = true; // TODO: Not supported message to be replaced with 'content_nestedContentEditorNotSupported' dictionary key. Currently not possible due to async/timing quirk. property.notSupportedMessage = "Property " + property.label + " uses editor " + property.editor + " which is not supported by Nested Content."; } - }); + } + ); } + // Ensure Culture Data for Complex Validation. + ensureCultureData(scaffold); + // Store the scaffold object vm.scaffolds.push(scaffold); } @@ -558,6 +563,29 @@ }); }); + /** + * Ensure that the containing content variant language and current property culture is transferred along + * to the scaffolded content object representing this block. + * This is required for validation along with ensuring that the umb-property inheritance is constantly maintained. + * @param {any} content + */ + function ensureCultureData(content) { + + if (!content || !vm.umbVariantContent || !vm.umbProperty) return; + + if (vm.umbVariantContent.editor.content.language) { + // set the scaffolded content's language to the language of the current editor + content.language = vm.umbVariantContent.editor.content.language; + } + // currently we only ever deal with invariant content for blocks so there's only one + content.variants[0].tabs.forEach(tab => { + tab.properties.forEach(prop => { + // set the scaffolded property to the culture of the containing property + prop.culture = vm.umbProperty.property.culture; + }); + }); + } + var initIfAllScaffoldsHaveLoaded = function () { // Initialize when all scaffolds have loaded if (model.config.contentTypes.length === scaffoldsLoaded) { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.editor.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.editor.html index 125e920fe6..e14bd03291 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.editor.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.editor.html @@ -4,7 +4,7 @@ diff --git a/src/Umbraco.Web.UI.NetCore/umbraco/config/lang/it.xml b/src/Umbraco.Web.UI.NetCore/umbraco/config/lang/it.xml index 63b7a2fff8..d7b878ef3d 100644 --- a/src/Umbraco.Web.UI.NetCore/umbraco/config/lang/it.xml +++ b/src/Umbraco.Web.UI.NetCore/umbraco/config/lang/it.xml @@ -1,437 +1,437 @@ - - The Umbraco community - https://our.umbraco.com/documentation/Extending-Umbraco/Language-Files - - - Gestisci hostnames - Audit Trail - Sfoglia - Copia - Crea - Crea pacchetto - Cancella - Disabilita - Svuota il cestino - Esporta il tipo di documento - Importa il tipo di documento - Importa il pacchetto - Modifica in Area di Lavoro - Uscita - Sposta - Notifiche - Accesso pubblico - Pubblica - Aggiorna nodi - Ripubblica intero sito - Permessi - Annulla ultima modifica - Invia per la pubblicazione - Invia per la traduzione - Ordina - Invia la pubblicazione - Traduci - Annulla pubblicazione - Aggiorna - Rimuovi - Ripristina - Crea Content Template - Crea gruppo - - - Aggiungi nuovo dominio - Dominio - - - - - Hostname non valido - Modifica il dominio corrente - - - Visualizzazione per - Contenuto pubblicato - Contenuto salvato - - - Grassetto - Cancella rientro paragrafo - Inserisci dal file - Inserisci intestazione grafica - Modifica Html - Inserisci rientro paragrafo - Corsivo - Centra - Allinea testo a sinistra - Allinea testo a destra - Inserisci Link - Inserisci local link (ancora) - Elenco puntato - Elenco numerato - Inserisci macro - Inserisci immagine - Modifica relazioni - Salva - Salva e pubblica - Salva e invia per approvazione - Anteprima - - Scegli lo stile - Mostra gli stili - Inserisci tabella - Altre azioni - Pubblica con i discendenti - Pianifica - Seleziona - Annulla selezione - - - Informazioni su questa pagina - Link alternativo - - Links alternativi - Clicca per modificare questo elemento - Creato da - Creato il - Tipo di documento - Modifica - Attivo fino al - - - Ultima pubblicazione - Link ai media - Tipo di media - Gruppo di membri - Ruolo - Tipologia Membro - - Titolo della Pagina - - - Pubblicato - Stato della pubblicazione - Pubblicato il - Rimuovi data - Ordinamento dei nodi aggiornato - - Statistiche - Titolo (opzionale) - Tipo - Non pubblicare - Ultima modifica - Rimuovi il file - Link al documento - Elementi - Pubblicato - Seleziona da data e l'ora in cui pubblicare/depubblicare il contenuto. - Imposta data - Depubblicato il - - - - Crea un elemento sotto - Scegli il tipo ed il titolo - Cartella - - - - - - hai aperto una nuova finestra - Riavvia - Visita - Benvenuto - - - Rimani - Scarta le modifiche - Hai delle modifiche non salvate - Sei sicuro di voler lasciare questa pagina? - hai delle modifiche non salvate - - - Fatto - Elimianto %0% elemento - Elimianto %0% elementi - Eliminato %0% su %1% elemento - Eliminato %0% su %1% elementi - Pubblicato %0% elemento - Pubblicato %0% elementi - Pubblicato %0% su %1% elemento - Pubblicato %0% su %1% elementi - %0% elemento non pubblicato - %0% elementi non pubblicati - Elementi non pubblicati - %0% su %1% - Elementi non pubblicati - %0% su %1% - Spostato %0% elemento - Spsotato %0% elementi - Spostato %0% su %1% elemento - Spostato %0% su %1% elementi - Copiato %0% elemento - Copiato %0% elementi - Copiato %0% su %1% elemento - Copiato %0% su %1% elementi - - - Titolo del Link - Link - Nome - Gestione alias Hostnames - Chiudi questa finestra - - - - - Taglia - Modifica elemento Dictionary - Modifica il linguaggio - Inserisci il link locale - Inserisci carattere - - - Inserisci link - Inserisci macro - Inserisci tabella - Ultima modifica - Link - - - - - Incolla - Modifica il Permesso per - - - - regexlib.com ha attualmente qualche problema, di cui non abbiamo il controllo. Siamo spiacevoli dell'inconveniente.]]> - - Elimina Macro - Campo obbligatorio - - - - Numero di colonne - Numero di righe - - Seleziona elemento - Visualizza gli elementi in cache - Seleziona contenuto - - - - - - - - - - - - Rendering controllo - Bottoni - Abilita impostazioni avanzate per - Abilita menu contestuale - Dimensione massima delle immagini inserite - Fogli di stile collegati - Visualizza etichetta - Larghezza e altezza - - - - - - - - - - - - - - - - - - - - - - - - - - - Questa proprietà non è valida - - - Info - Azione - Aggiungi - Alias - - Bordo - o - Annulla - - Scegli - Chiudi - Chiudi la finestra - Commento - Conferma - Blocca le proporzioni - Continua - Copia - Crea - Base di dati - Data - Default - Elimina - Eliminato - Elimina... - Design - Dimensioni - - Scarica - Modifica - Modificato - Elementi - Email - Errore - Trova - Cartella - Altezza - Guida - Icona - Importa - - Inserisci - Installa - Giustificato - Lingua - Layout - Caricamento - Bloccato - Login - Log off - Logout - Macro - Sposta - Nome - Nuovo - Successivo - No - di - Ok - Apri - o - Password - Percorso - - Precedente - - - Cestino - Rimangono - Rinomina - Rinnova - Riprova - Permessi - Cerca - Server - Mostra - Mostra la pagina inviata - Dimensione - Ordina - Conferma - Tipo - Digita per cercare... - Su - Aggiorna - Aggiornamento - Carica - URL - Utente - - Valore - Vedi - Benvenuto... - Larghezza - Si - Riordina - Ho finito di ordinare - Richiesto - Contenuti - Azioni - Cerca solo in questa cartella - Pianifica pubblicazione - selezionato - Annulla - Cambia password - Cronologia - Generale - Rimuovi - Gruppi - - - Colore di sfondo - Grassetto - Colore del testo - Carattere - Testo - - - Pagina - - - - - - - installa per installare il database Umbraco %0% ]]> - Avanti per proseguire.]]> - - Database non trovato! Perfavore, controlla che le informazioni della stringa di connessione nel file "web.config" siano corrette.

+ + The Umbraco community + https://our.umbraco.com/documentation/Extending-Umbraco/Language-Files + + + Gestisci hostnames + Audit Trail + Sfoglia + Copia + Crea + Crea pacchetto + Cancella + Disabilita + Svuota il cestino + Esporta il tipo di documento + Importa il tipo di documento + Importa il pacchetto + Modifica in Area di Lavoro + Uscita + Sposta + Notifiche + Accesso pubblico + Pubblica + Aggiorna nodi + Ripubblica intero sito + Permessi + Annulla ultima modifica + Invia per la pubblicazione + Invia per la traduzione + Ordina + Invia la pubblicazione + Traduci + Annulla pubblicazione + Aggiorna + Rimuovi + Ripristina + Crea Content Template + Crea gruppo + + + Aggiungi nuovo dominio + Dominio + + + + + Hostname non valido + Modifica il dominio corrente + + + Visualizzazione per + Contenuto pubblicato + Contenuto salvato + + + Grassetto + Cancella rientro paragrafo + Inserisci dal file + Inserisci intestazione grafica + Modifica Html + Inserisci rientro paragrafo + Corsivo + Centra + Allinea testo a sinistra + Allinea testo a destra + Inserisci Link + Inserisci local link (ancora) + Elenco puntato + Elenco numerato + Inserisci macro + Inserisci immagine + Modifica relazioni + Salva + Salva e pubblica + Salva e invia per approvazione + Anteprima + + Scegli lo stile + Mostra gli stili + Inserisci tabella + Altre azioni + Pubblica con i discendenti + Pianifica + Seleziona + Annulla selezione + + + Informazioni su questa pagina + Link alternativo + + Links alternativi + Clicca per modificare questo elemento + Creato da + Creato il + Tipo di documento + Modifica + Attivo fino al + + + Ultima pubblicazione + Link ai media + Tipo di media + Gruppo di membri + Ruolo + Tipologia Membro + + Titolo della Pagina + + + Pubblicato + Stato della pubblicazione + Pubblicato il + Rimuovi data + Ordinamento dei nodi aggiornato + + Statistiche + Titolo (opzionale) + Tipo + Non pubblicare + Ultima modifica + Rimuovi il file + Link al documento + Elementi + Pubblicato + Seleziona da data e l'ora in cui pubblicare/depubblicare il contenuto. + Imposta data + Depubblicato il + + + + Crea un elemento sotto + Scegli il tipo ed il titolo + Cartella + + + + + + hai aperto una nuova finestra + Riavvia + Visita + Benvenuto + + + Rimani + Scarta le modifiche + Hai delle modifiche non salvate + Sei sicuro di voler lasciare questa pagina? - hai delle modifiche non salvate + + + Fatto + Elimianto %0% elemento + Elimianto %0% elementi + Eliminato %0% su %1% elemento + Eliminato %0% su %1% elementi + Pubblicato %0% elemento + Pubblicato %0% elementi + Pubblicato %0% su %1% elemento + Pubblicato %0% su %1% elementi + %0% elemento non pubblicato + %0% elementi non pubblicati + Elementi non pubblicati - %0% su %1% + Elementi non pubblicati - %0% su %1% + Spostato %0% elemento + Spsotato %0% elementi + Spostato %0% su %1% elemento + Spostato %0% su %1% elementi + Copiato %0% elemento + Copiato %0% elementi + Copiato %0% su %1% elemento + Copiato %0% su %1% elementi + + + Titolo del Link + Link + Nome + Gestione alias Hostnames + Chiudi questa finestra + + + + + Taglia + Modifica elemento Dictionary + Modifica il linguaggio + Inserisci il link locale + Inserisci carattere + + + Inserisci link + Inserisci macro + Inserisci tabella + Ultima modifica + Link + + + + + Incolla + Modifica il Permesso per + + + + regexlib.com ha attualmente qualche problema, di cui non abbiamo il controllo. Siamo spiacevoli dell'inconveniente.]]> + + Elimina Macro + Campo obbligatorio + + + + Numero di colonne + Numero di righe + + Seleziona elemento + Visualizza gli elementi in cache + Seleziona contenuto + + + + + + + + + + + + Rendering controllo + Bottoni + Abilita impostazioni avanzate per + Abilita menu contestuale + Dimensione massima delle immagini inserite + Fogli di stile collegati + Visualizza etichetta + Larghezza e altezza + + + + + + + + + + + + + + + + + + + + + + + + + + + Questa proprietà non è valida + + + Info + Azione + Aggiungi + Alias + + Bordo + o + Annulla + + Scegli + Chiudi + Chiudi la finestra + Commento + Conferma + Blocca le proporzioni + Continua + Copia + Crea + Base di dati + Data + Default + Elimina + Eliminato + Elimina... + Design + Dimensioni + + Scarica + Modifica + Modificato + Elementi + Email + Errore + Trova + Cartella + Altezza + Guida + Icona + Importa + + Inserisci + Installa + Giustificato + Lingua + Layout + Caricamento + Bloccato + Login + Log off + Logout + Macro + Sposta + Nome + Nuovo + Successivo + No + di + Ok + Apri + o + Password + Percorso + + Precedente + + + Cestino + Rimangono + Rinomina + Rinnova + Riprova + Permessi + Cerca + Server + Mostra + Mostra la pagina inviata + Dimensione + Ordina + Conferma + Tipo + Digita per cercare... + Su + Aggiorna + Aggiornamento + Carica + URL + Utente + + Valore + Vedi + Benvenuto... + Larghezza + Si + Riordina + Ho finito di ordinare + Richiesto + Contenuti + Azioni + Cerca solo in questa cartella + Pianifica pubblicazione + selezionato + Annulla + Cambia password + Cronologia + Generale + Rimuovi + Gruppi + + + Colore di sfondo + Grassetto + Colore del testo + Carattere + Testo + + + Pagina + + + + + + + installa per installare il database Umbraco %0% ]]> + Avanti per proseguire.]]> + + Database non trovato! Perfavore, controlla che le informazioni della stringa di connessione nel file "web.config" siano corrette.

Per procedere, edita il file "web.config" (utilizzando Visual Studio o l'editor di testo che preferisci), scorri in basso, aggiungi la stringa di connessione per il database chiamato "umbracoDbDSN" e salva il file.

Clicca il tasto riprova quando hai finito.
Maggiori dettagli per la modifica del file web.config qui.

]]> -
- - Premi il tasto aggiorna per aggiornare il database ad Umbraco %0%

Non preoccuparti, il contenuto non verrà perso e tutto continuerà a funzionare dopo l'aggiornamento!

]]>
- Premi il tasto Avanti per continuare.]]> - Avanti per continuare la configurazione.]]> - La password predefinita per l'utente di default deve essere cambiata!]]> - L'utente di default è stato disabilitato o non ha accesso ad Umbraco!

Non è necessario eseguire altre operazioni. Clicca il tasto Avanti per continuare.]]> - La password è stata modificata con successo

Non è necessario eseguire altre operazioni. Clicca il tasto Avanti per continuare.]]> - - - - - - - - Le impostazioni dei permessi sono perfette!

Puoi eseguire Umbraco senza problemi, ma potresti non poter installare i pacchetti che sono consigliati per sfruttare tutti i vantaggi offerti da Umbraco.]]>
- - - video tutorial su come impostare i permessi delle cartelle per Umbraco o leggi la versione testuale.]]> - Le impostazioni dei permessi potrebbero avere dei problemi!

Puoi eseguire Umbraco, ma potresti non essere in grado di creare cartelle o installare pacchetti che sono raccomandati per sfruttare tutti i vantaggi di Umbraco.]]>
- Le impostazioni dei permessi non sono corrette per Umbraco!

Per eseguire Umbraco, devi aggiornare le impostazioni dei permessi.]]>
- La configurazione dei permessi è perfetta!

Sei pronto per avviare Umbraco e installare i pacchetti!]]>
- - - - - - Guarda come) Puoi anche installare eventuali Runway in un secondo momento. Vai nella sezione Developer e scegli Pacchetti.]]> - - Runway è installato - - + + Premi il tasto aggiorna per aggiornare il database ad Umbraco %0%

Non preoccuparti, il contenuto non verrà perso e tutto continuerà a funzionare dopo l'aggiornamento!

]]>
+ Premi il tasto Avanti per continuare.]]> + Avanti per continuare la configurazione.]]> + La password predefinita per l'utente di default deve essere cambiata!]]> + L'utente di default è stato disabilitato o non ha accesso ad Umbraco!

Non è necessario eseguire altre operazioni. Clicca il tasto Avanti per continuare.]]> + La password è stata modificata con successo

Non è necessario eseguire altre operazioni. Clicca il tasto Avanti per continuare.]]> + + + + + + + + Le impostazioni dei permessi sono perfette!

Puoi eseguire Umbraco senza problemi, ma potresti non poter installare i pacchetti che sono consigliati per sfruttare tutti i vantaggi offerti da Umbraco.]]>
+ + + video tutorial su come impostare i permessi delle cartelle per Umbraco o leggi la versione testuale.]]> + Le impostazioni dei permessi potrebbero avere dei problemi!

Puoi eseguire Umbraco, ma potresti non essere in grado di creare cartelle o installare pacchetti che sono raccomandati per sfruttare tutti i vantaggi di Umbraco.]]>
+ Le impostazioni dei permessi non sono corrette per Umbraco!

Per eseguire Umbraco, devi aggiornare le impostazioni dei permessi.]]>
+ La configurazione dei permessi è perfetta!

Sei pronto per avviare Umbraco e installare i pacchetti!]]>
+ + + + + + Guarda come) Puoi anche installare eventuali Runway in un secondo momento. Vai nella sezione Developer e scegli Pacchetti.]]> + + Runway è installato + + Questa è la lista dei nostri moduli raccomandati, seleziona quali vorresti installare, o vedi l'intera lista di moduli ]]> - - Raccommandato solo per utenti esperti - Vorrei iniziare da un sito semplice - - + Raccommandato solo per utenti esperti + Vorrei iniziare da un sito semplice + + "Runway" è un semplice sito web contenente alcuni tipi di documento e alcuni templates di base. L'installer configurerà Runway per te automaticamente, ma tu potrai facilmente modificarlo, estenderlo o eliminarlo. Non è necessario installarlo e potrai usare Umbraco anche senza di esso, ma @@ -443,84 +443,84 @@ Moduli opzionali: Top Navigation, Sitemap, Contatti, Gallery. ]]> - - Cosa è Runway - Passo 1/5 Accettazione licenza - Passo 2/5: Configurazione database - Passo 3/5: Controllo permessi dei file - Passo 4/5: Controllo impostazioni sicurezza - Passo 5/5: Umbraco è pronto per iniziare - Grazie per aver scelto Umbraco - - Naviga per il tuo nuovo sito + + Cosa è Runway + Passo 1/5 Accettazione licenza + Passo 2/5: Configurazione database + Passo 3/5: Controllo permessi dei file + Passo 4/5: Controllo impostazioni sicurezza + Passo 5/5: Umbraco è pronto per iniziare + Grazie per aver scelto Umbraco + + Naviga per il tuo nuovo sito Hai installato Runway, quindi perché non dare uno sguardo al vostro nuovo sito web.]]> - - - Ulteriori informazioni e assistenza + + + Ulteriori informazioni e assistenza Fatti aiutare dalla nostra community, consulta la documentazione o guarda alcuni video gratuiti su come costruire un semplice sito web, come usare i pacchetti e una guida rapida alla terminologia Umbraco]]> - - - /web.config e aggiornare la chiave AppSetting UmbracoConfigurationStatus impostando il valore '%0%'.]]> - iniziare immediatamente cliccando sul bottone "Avvia Umbraco".
Se sei nuovo a Umbraco, si possono trovare un sacco di risorse sulle nostre pagine Getting Started.]]>
- - Avvia Umbraco + + + /web.config e aggiornare la chiave AppSetting UmbracoConfigurationStatus impostando il valore '%0%'.]]> + iniziare immediatamente cliccando sul bottone "Avvia Umbraco".
Se sei nuovo a Umbraco, si possono trovare un sacco di risorse sulle nostre pagine Getting Started.]]>
+ + Avvia Umbraco Per gestire il tuo sito web, è sufficiente aprire il backoffice di Umbraco e iniziare ad aggiungere i contenuti, aggiornando i modelli e i fogli di stile o aggiungere nuove funzionalità]]> - - Connessione al database non riuscita. - Umbraco Versione 3 - Umbraco Versione 4 - Guarda - - Umbraco %0% per una nuova installazione o per l'aggiornamento dalla versione 3.0. + + Connessione al database non riuscita. + Umbraco Versione 3 + Umbraco Versione 4 + Guarda + + Umbraco %0% per una nuova installazione o per l'aggiornamento dalla versione 3.0.

Clicca "avanti" per avviare la procedura.]]> -
- - - Codice cultura - Nome cultura - - - - Riconnetti adesso per salvare il tuo lavoro - - - © 2001 - %0%
umbraco.com

]]>
- Buona domenica - Buon lunedì - Buon martedì - Buon mercoledì - Buon giovedì - Buon venerdì - Buon sabato - Mostra password - Nascondi password - Password dimenticata? - Una email verrà inviata all'indirizzo specificato con un link per il reset della password - Ritorna alla finestra di login - - - Dashboard - Sezioni - Contenuto - - - Scegli la pagina sopra... - - Seleziona dove il documento %0% deve essere copiato - - Seleziona dove il documento %0% deve essere spostato - - - - - - - - - - - + + + Codice cultura + Nome cultura + + + + Riconnetti adesso per salvare il tuo lavoro + + + © 2001 - %0%
umbraco.com

]]>
+ Buona domenica + Buon lunedì + Buon martedì + Buon mercoledì + Buon giovedì + Buon venerdì + Buon sabato + Mostra password + Nascondi password + Password dimenticata? + Una email verrà inviata all'indirizzo specificato con un link per il reset della password + Ritorna alla finestra di login + + + Dashboard + Sezioni + Contenuto + + + Scegli la pagina sopra... + + Seleziona dove il documento %0% deve essere copiato + + Seleziona dove il documento %0% deve essere spostato + + + + + + + + + + + - - - Salve %0%

+
+ + Salve %0%

Questa è un'email automatica per informare che l'azione '%1%' è stata eseguita sulla pagina '%2%' @@ -562,265 +562,265 @@ Per gestire il tuo sito web, è sufficiente aprire il backoffice di Umbraco e in

Buona giornata!

Grazie da Umbraco

]]> -
- [%0%] Notifica per %1% eseguita su %2% - Notifiche - - - - + [%0%] Notifica per %1% eseguita su %2% + Notifiche + + + + e selezionando il pacchetto. I pacchetti Umbraco generalmente hanno l'estensione ".umb" o ".zip". ]]> - - Autore - Documentazione - Meta dati pacchetto - Nome del pacchetto - Il pacchetto non contiene tutti gli elementi - -
+
+ Autore + Documentazione + Meta dati pacchetto + Nome del pacchetto + Il pacchetto non contiene tutti gli elementi + +
E' possibile rimuovere questo pacchetto dal sistema cliccando "rimuovi pacchetto" in basso.]]> -
- Opzioni pacchetto - Pacchetto leggimi - Pacchetto repository - Conferma eliminazione - - - Disinstalla pacchetto - - + + Opzioni pacchetto + Pacchetto leggimi + Pacchetto repository + Conferma eliminazione + + + Disinstalla pacchetto + + Avviso: tutti i documenti, i media, etc a seconda degli elementi che rimuoverai, smetteranno di funzionare, e potrebbero portare a un'instabilità del sistema, perciò disinstalla con cautela. In caso di dubbio contattare l'autore del pacchetto.]]> - - Versione del pacchetto - - - - - - - - - - usando i gruppi di membri di Umbraco.]]> - Devi creare un gruppo di membri prima di utilizzare l'autenticazione basata sui ruoli - - - - - - - - - - - - - - - - - - - - - - - - ok per pubblicare %0% e rendere questo contenuto accessibile al pubblico.

Puoi pubblicare questa pagina e tutte le sue sottopagine selezionando pubblica tutti i figli qui sotto.]]>
- - - - - - - - - - - - - - - - Il testo in rosso non verrà mostrato nella versione selezionata, quello in verde verrà aggiunto]]> - - - - - - - - - - - Concierge - Contenuto - Courier - Sviluppo - Configurazione guidata Umbraco - Media - Membri - Newsletters - Impostazioni - Statistiche - Traduzione - Utenti - - - Tipo di contenuto master abilitato - Questo tipo di contenuto usa - - - - - Tipo - Foglio di stile - Tab - Titolo tab - Tabs - - - Ordinamento - Data creazione - - - - - - - - - Tipo di dati: %1%]]> - - Tipo di documento salvato - Tab creata - Tab eliminata - Tab con id: %0% eliminata - Contenuto non pubblicare - - - - Tipo di dato salvato - - - - - - - - - - - - - - - Tipo utente salvato - - - - - - Partial view salvata - Partial view salvata senza errori! - Partial view non salvata - Errore durante il salvataggio del file. - - - - - - - - - - - Anteprima - Stili - - - - - - - - - Master template - - Template - Data creazione - - - Immagine - Macro - Seleziona il tipo di contenuto - Seleziona un layout - Aggiungi una riga - Aggiungi contenuto - Elimina contenuto - Impostazioni applicati - Questo contenuto non è consentito qui - Questo contenuto è consentito qui - Clicca per incorporare - Clicca per inserire l'immagine - Didascalia dell'immagine... - Scrivi qui... - I Grid Layout - I layout sono l'area globale di lavoro per il grid editor, di solito ti serve solo uno o due layout differenti - Aggiungi un Grid Layout - Sistema il layout impostando la larghezza della colonna ed aggiungendo ulteriori sezioni - Configurazioni della riga - Le righe sono le colonne predefinite disposte orizzontalmente - Aggiungi configurazione della riga - Sistema la riga impostando la larghezza della colonna ed aggiungendo ulteriori colonne - Colonne - Totale combinazioni delle colonne nel grid layout - Impostazioni - Configura le impostazioni che possono essere cambiate dai editori - Stili - Configura i stili che possono essere cambiati dai editori - Permetti tutti i editor - Permetti tutte le configurazioni della riga - - - - - - Scegli il campo - Converte le interruzioni di linea - - Campi Personalizzati - - - - - - - Minuscolo - Nessuno - - - Ricorsivo - - - Campi Standard - Maiuscolo - - - - - - - - Dettagli - Scarica xml DTD - Campi - Includi le sottopagine - - + Versione del pacchetto + + + + + + + + + + usando i gruppi di membri di Umbraco.]]> + Devi creare un gruppo di membri prima di utilizzare l'autenticazione basata sui ruoli + + + + + + + + + + + + + + + + + + + + + + + + ok per pubblicare %0% e rendere questo contenuto accessibile al pubblico.

Puoi pubblicare questa pagina e tutte le sue sottopagine selezionando pubblica tutti i figli qui sotto.]]>
+ + + + + + + + + + + + + + + + Il testo in rosso non verrà mostrato nella versione selezionata, quello in verde verrà aggiunto]]> + + + + + + + + + + + Concierge + Contenuto + Courier + Sviluppo + Configurazione guidata Umbraco + Media + Membri + Newsletters + Impostazioni + Statistiche + Traduzione + Utenti + + + Tipo di contenuto master abilitato + Questo tipo di contenuto usa + + + + + Tipo + Foglio di stile + Tab + Titolo tab + Tabs + + + Ordinamento + Data creazione + + + + + + + + + Tipo di dati: %1%]]> + + Tipo di documento salvato + Tab creata + Tab eliminata + Tab con id: %0% eliminata + Contenuto non pubblicare + + + + Tipo di dato salvato + + + + + + + + + + + + + + + Tipo utente salvato + + + + + + Partial view salvata + Partial view salvata senza errori! + Partial view non salvata + Errore durante il salvataggio del file. + + + + + + + + + + + Anteprima + Stili + + + + + + + + + Master template + + Template + Data creazione + + + Immagine + Macro + Seleziona il tipo di contenuto + Seleziona un layout + Aggiungi una riga + Aggiungi contenuto + Elimina contenuto + Impostazioni applicati + Questo contenuto non è consentito qui + Questo contenuto è consentito qui + Clicca per incorporare + Clicca per inserire l'immagine + Didascalia dell'immagine... + Scrivi qui... + I Grid Layout + I layout sono l'area globale di lavoro per il grid editor, di solito ti serve solo uno o due layout differenti + Aggiungi un Grid Layout + Sistema il layout impostando la larghezza della colonna ed aggiungendo ulteriori sezioni + Configurazioni della riga + Le righe sono le colonne predefinite disposte orizzontalmente + Aggiungi configurazione della riga + Sistema la riga impostando la larghezza della colonna ed aggiungendo ulteriori colonne + Colonne + Totale combinazioni delle colonne nel grid layout + Impostazioni + Configura le impostazioni che possono essere cambiate dai editori + Stili + Configura i stili che possono essere cambiati dai editori + Permetti tutti i editor + Permetti tutte le configurazioni della riga + + + + + + Scegli il campo + Converte le interruzioni di linea + + Campi Personalizzati + + + + + + + Minuscolo + Nessuno + + + Ricorsivo + + + Campi Standard + Maiuscolo + + + + + + + + Dettagli + Scarica xml DTD + Campi + Includi le sottopagine + + - - - - - - - - - - - Traduttore - - - - Cache Browser - Cestino - Pacchetti creati - Tipi di dato - Dizionario - Pacchetti installati - Installare skin - Installare starter kit - Lingue - Installa un pacchetto locale - Macros - Tipi di media - Membri - Gruppi di Membri - Ruoli - Tipologia Membri - Tipi di documento - Pacchetti - Pacchetti - Installa dal repository - Installa Runway - Moduli Runway - Files di scripting - Scripts - Fogli di stile - Templates - Permessi Utente - Tipi di Utente - Utenti - Contenuti - - - - - - - - - Amministratore - Campo Categoria - Cambia la tua password - - Conferma la nuova password - Contenuto del canale - Campo Descrizione - Disabilita l'utente - Tipo di Documento - Editor - Campo Eccezione - Lingua - Login - - Sezioni - Modifica la tua password - - Password - - - Password attuale - - - - - - - - - - - Username - - - - Autore - Il tuo profilo - La tua storia recente - Crea utente - Crea nuovi utenti e dai loro accesso ad Umbraco. Quando un nuovo utente viene creato viene generata una password che potrai condividere con l'utente. - Aggiungi gruppi per assegnare accessi e permessi - Torna agli utenti - Gestione utenti - - - Devi aggiungere almeno - elementi - - - Digita per cercare... - Inserisci la tua email - Inserisci la tua password - Inserisci un nome... - Inserisci una email... - - - o clicca qui per scegliere i file - Trascina i tuoi file all'interno di quest'area - - - Contenuti - Info - Elementi - +
+ + + + + + + + + + Traduttore + + + + Cache Browser + Cestino + Pacchetti creati + Tipi di dato + Dizionario + Pacchetti installati + Installare skin + Installare starter kit + Lingue + Installa un pacchetto locale + Macros + Tipi di media + Membri + Gruppi di Membri + Ruoli + Tipologia Membri + Tipi di documento + Pacchetti + Pacchetti + Installa dal repository + Installa Runway + Moduli Runway + Files di scripting + Scripts + Fogli di stile + Templates + Permessi Utente + Tipi di Utente + Utenti + Contenuti + + + + + + + + + Amministratore + Campo Categoria + Cambia la tua password + + Conferma la nuova password + Contenuto del canale + Campo Descrizione + Disabilita l'utente + Tipo di Documento + Editor + Campo Eccezione + Lingua + Login + + Sezioni + Modifica la tua password + + Password + + + Password attuale + + + + + + + + + + + Username + + + + Autore + Il tuo profilo + La tua storia recente + Crea utente + Crea nuovi utenti e dai loro accesso ad Umbraco. Quando un nuovo utente viene creato viene generata una password che potrai condividere con l'utente. + Aggiungi gruppi per assegnare accessi e permessi + Torna agli utenti + Gestione utenti + + + Devi aggiungere almeno + elementi + + + Digita per cercare... + Inserisci la tua email + Inserisci la tua password + Inserisci un nome... + Inserisci una email... + + + o clicca qui per scegliere i file + Trascina i tuoi file all'interno di quest'area + + + Contenuti + Info + Elementi +
diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index a6a9e7188c..a05b7a4250 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -214,6 +214,7 @@ $(MSBuildExtensionsPath)\Microsoft\VisualStudio\v15.0\Web\Microsoft.Web.Publishing.Tasks.dll $(MSBuildExtensionsPath)\Microsoft\VisualStudio\v16.0\Web\Microsoft.Web.Publishing.Tasks.dll $(MSBuildExtensionsPath)\Microsoft\VisualStudio\v17.0\Web\Microsoft.Web.Publishing.Tasks.dll + $(MSBuildExtensionsPath)\Microsoft\VisualStudio\v17.0\Web\Microsoft.Web.Publishing.Tasks.dll @@ -236,8 +237,9 @@ http://localhost:8121 8130 8140 + 8150 / - http://localhost:8140 + http://localhost:8150 8131 / http://localhost:8131 diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/ContentCacheDataSerializationResult.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/ContentCacheDataSerializationResult.cs new file mode 100644 index 0000000000..cde39eaa3c --- /dev/null +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/ContentCacheDataSerializationResult.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; + +namespace Umbraco.Web.PublishedCache.NuCache.DataSource +{ + /// + /// The serialization result from for which the serialized value + /// will be either a string or a byte[] + /// + public struct ContentCacheDataSerializationResult : IEquatable + { + public ContentCacheDataSerializationResult(string stringData, byte[] byteData) + { + StringData = stringData; + ByteData = byteData; + } + + public string StringData { get; } + public byte[] ByteData { get; } + + public override bool Equals(object obj) + { + return obj is ContentCacheDataSerializationResult result && Equals(result); + } + + public bool Equals(ContentCacheDataSerializationResult other) + { + return StringData == other.StringData && + EqualityComparer.Default.Equals(ByteData, other.ByteData); + } + + public override int GetHashCode() + { + var hashCode = 1910544615; + hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(StringData); + hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(ByteData); + return hashCode; + } + + public static bool operator ==(ContentCacheDataSerializationResult left, ContentCacheDataSerializationResult right) + { + return left.Equals(right); + } + + public static bool operator !=(ContentCacheDataSerializationResult left, ContentCacheDataSerializationResult right) + { + return !(left == right); + } + } + +} diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/ContentCacheDataSerializerEntityType.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/ContentCacheDataSerializerEntityType.cs new file mode 100644 index 0000000000..e5b15f8dce --- /dev/null +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/ContentCacheDataSerializerEntityType.cs @@ -0,0 +1,13 @@ +using System; + +namespace Umbraco.Web.PublishedCache.NuCache.DataSource +{ + [Flags] + public enum ContentCacheDataSerializerEntityType + { + Document = 1, + Media = 2, + Member = 4 + } + +} diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/IContentCacheDataSerializer.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/IContentCacheDataSerializer.cs new file mode 100644 index 0000000000..d1a83d8452 --- /dev/null +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/IContentCacheDataSerializer.cs @@ -0,0 +1,25 @@ +using Umbraco.Core.Models; + +namespace Umbraco.Web.PublishedCache.NuCache.DataSource +{ + + /// + /// Serializes/Deserializes document to the SQL Database as a string + /// + /// + /// Resolved from the . This cannot be resolved from DI. + /// + public interface IContentCacheDataSerializer + { + /// + /// Deserialize the data into a + /// + ContentCacheDataModel Deserialize(IReadOnlyContentBase content, string stringData, byte[] byteData); + + /// + /// Serializes the + /// + ContentCacheDataSerializationResult Serialize(IReadOnlyContentBase content, ContentCacheDataModel model); + } + +} diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/IContentCacheDataSerializerFactory.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/IContentCacheDataSerializerFactory.cs new file mode 100644 index 0000000000..14dfd7dc5b --- /dev/null +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/IContentCacheDataSerializerFactory.cs @@ -0,0 +1,16 @@ +namespace Umbraco.Web.PublishedCache.NuCache.DataSource +{ + public interface IContentCacheDataSerializerFactory + { + /// + /// Gets or creates a new instance of + /// + /// + /// + /// This method may return the same instance, however this depends on the state of the application and if any underlying data has changed. + /// This method may also be used to initialize anything before a serialization/deserialization session occurs. + /// + IContentCacheDataSerializer Create(ContentCacheDataSerializerEntityType types); + } + +} diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/IDictionaryOfPropertyDataSerializer.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/IDictionaryOfPropertyDataSerializer.cs new file mode 100644 index 0000000000..a086e3e2f3 --- /dev/null +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/IDictionaryOfPropertyDataSerializer.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using System.IO; + +namespace Umbraco.Web.PublishedCache.NuCache.DataSource +{ + internal interface IDictionaryOfPropertyDataSerializer + { + IDictionary ReadFrom(Stream stream); + void WriteTo(IDictionary value, Stream stream); + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/JsonContentNestedDataSerializer.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/JsonContentNestedDataSerializer.cs new file mode 100644 index 0000000000..21cd0bf763 --- /dev/null +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/JsonContentNestedDataSerializer.cs @@ -0,0 +1,91 @@ +using Newtonsoft.Json; +using System; +using System.Buffers; +using System.Collections.Generic; +using System.IO; +using Umbraco.Core.Models; +using Umbraco.Core.Serialization; + +namespace Umbraco.Web.PublishedCache.NuCache.DataSource +{ + + public class JsonContentNestedDataSerializer : IContentCacheDataSerializer + { + // by default JsonConvert will deserialize our numeric values as Int64 + // which is bad, because they were Int32 in the database - take care + private readonly JsonSerializerSettings _jsonSerializerSettings = new JsonSerializerSettings + { + Converters = new List { new ForceInt32Converter() }, + + // Explicitly specify date handling so that it's consistent and follows the same date handling as MessagePack + DateParseHandling = DateParseHandling.DateTime, + DateFormatHandling = DateFormatHandling.IsoDateFormat, + DateTimeZoneHandling = DateTimeZoneHandling.Utc, + DateFormatString = "o" + }; + private readonly JsonNameTable _propertyNameTable = new DefaultJsonNameTable(); + public ContentCacheDataModel Deserialize(IReadOnlyContentBase content, string stringData, byte[] byteData) + { + if (stringData == null && byteData != null) + throw new NotSupportedException($"{typeof(JsonContentNestedDataSerializer)} does not support byte[] serialization"); + + JsonSerializer serializer = JsonSerializer.Create(_jsonSerializerSettings); + using (JsonTextReader reader = new JsonTextReader(new StringReader(stringData))) + { + // reader will get buffer from array pool + reader.ArrayPool = JsonArrayPool.Instance; + reader.PropertyNameTable = _propertyNameTable; + return serializer.Deserialize(reader); + } + } + + public ContentCacheDataSerializationResult Serialize(IReadOnlyContentBase content, ContentCacheDataModel model) + { + // note that numeric values (which are Int32) are serialized without their + // type (eg "value":1234) and JsonConvert by default deserializes them as Int64 + + var json = JsonConvert.SerializeObject(model); + return new ContentCacheDataSerializationResult(json, null); + } + } + public class JsonArrayPool : IArrayPool + { + public static readonly JsonArrayPool Instance = new JsonArrayPool(); + + public char[] Rent(int minimumLength) + { + // get char array from System.Buffers shared pool + return ArrayPool.Shared.Rent(minimumLength); + } + + public void Return(char[] array) + { + // return char array to System.Buffers shared pool + ArrayPool.Shared.Return(array); + } + } + public class AutomaticJsonNameTable : DefaultJsonNameTable + { + int nAutoAdded = 0; + int maxToAutoAdd; + + public AutomaticJsonNameTable(int maxToAdd) + { + this.maxToAutoAdd = maxToAdd; + } + + public override string Get(char[] key, int start, int length) + { + var s = base.Get(key, start, length); + + if (s == null && nAutoAdded < maxToAutoAdd) + { + s = new string(key, start, length); + Add(s); + nAutoAdded++; + } + + return s; + } + } +} diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/JsonContentNestedDataSerializerFactory.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/JsonContentNestedDataSerializerFactory.cs new file mode 100644 index 0000000000..e857eb8bf5 --- /dev/null +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/JsonContentNestedDataSerializerFactory.cs @@ -0,0 +1,10 @@ +using System; + +namespace Umbraco.Web.PublishedCache.NuCache.DataSource +{ + internal class JsonContentNestedDataSerializerFactory : IContentCacheDataSerializerFactory + { + private Lazy _serializer = new Lazy(); + public IContentCacheDataSerializer Create(ContentCacheDataSerializerEntityType types) => _serializer.Value; + } +} diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/LazyCompressedString.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/LazyCompressedString.cs new file mode 100644 index 0000000000..2be2568f7e --- /dev/null +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/LazyCompressedString.cs @@ -0,0 +1,109 @@ +using K4os.Compression.LZ4; +using System; +using System.Diagnostics; +using System.Text; +using Umbraco.Core.Exceptions; + +namespace Umbraco.Web.PublishedCache.NuCache.DataSource +{ + /// + /// Lazily decompresses a LZ4 Pickler compressed UTF8 string + /// + [DebuggerDisplay("{Display}")] + internal struct LazyCompressedString + { + private byte[] _bytes; + private string _str; + private readonly object _locker; + + /// + /// Constructor + /// + /// LZ4 Pickle compressed UTF8 String + public LazyCompressedString(byte[] bytes) + { + _locker = new object(); + _bytes = bytes; + _str = null; + } + + public byte[] GetBytes() + { + if (_bytes == null) + { + throw new InvalidOperationException("The bytes have already been expanded"); + } + + return _bytes; + } + + /// + /// Returns the decompressed string from the bytes. This methods can only be called once. + /// + /// + /// Throws if this is called more than once + public string DecompressString() + { + if (_str != null) + { + return _str; + } + + lock (_locker) + { + if (_str != null) + { + // double check + return _str; + } + + if (_bytes == null) + { + throw new InvalidOperationException("Bytes have already been cleared"); + } + + _str = Encoding.UTF8.GetString(LZ4Pickler.Unpickle(_bytes)); + _bytes = null; + } + return _str; + } + + /// + /// Used to display debugging output since ToString() can only be called once + /// + private string Display + { + get + { + if (_str != null) + { + return $"Decompressed: {_str}"; + } + + lock (_locker) + { + if (_str != null) + { + // double check + return $"Decompressed: {_str}"; + } + + if (_bytes == null) + { + // This shouldn't happen + throw new PanicException("Bytes have already been cleared"); + } + else + { + return $"Compressed Bytes: {_bytes.Length}"; + } + } + } + } + + public override string ToString() => DecompressString(); + + public static implicit operator string(LazyCompressedString l) => l.ToString(); + } + +} diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs new file mode 100644 index 0000000000..6ae872ef69 --- /dev/null +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializer.cs @@ -0,0 +1,126 @@ +using K4os.Compression.LZ4; +using MessagePack; +using MessagePack.Resolvers; +using System; +using System.Linq; +using System.Text; +using Umbraco.Core.Models; +using Umbraco.Core.PropertyEditors; + +namespace Umbraco.Web.PublishedCache.NuCache.DataSource +{ + + /// + /// Serializes/Deserializes document to the SQL Database as bytes using MessagePack + /// + public class MsgPackContentNestedDataSerializer : IContentCacheDataSerializer + { + private readonly MessagePackSerializerOptions _options; + private readonly IPropertyCacheCompression _propertyOptions; + + public MsgPackContentNestedDataSerializer(IPropertyCacheCompression propertyOptions) + { + _propertyOptions = propertyOptions ?? throw new ArgumentNullException(nameof(propertyOptions)); + + var defaultOptions = ContractlessStandardResolver.Options; + var resolver = CompositeResolver.Create( + + // TODO: We want to be able to intern the strings for aliases when deserializing like we do for Newtonsoft but I'm unsure exactly how + // to do that but it would seem to be with a custom message pack resolver but I haven't quite figured out based on the docs how + // to do that since that is part of the int key -> string mapping operation, might have to see the source code to figure that one out. + // There are docs here on how to build one of these: https://github.com/neuecc/MessagePack-CSharp/blob/master/README.md#low-level-api-imessagepackformattert + // and there are a couple examples if you search on google for them but this will need to be a separate project. + // NOTE: resolver custom types first + // new ContentNestedDataResolver(), + + // finally use standard resolver + defaultOptions.Resolver + ); + + _options = defaultOptions + .WithResolver(resolver) + .WithCompression(MessagePackCompression.Lz4BlockArray); + } + + public string ToJson(byte[] bin) + { + var json = MessagePackSerializer.ConvertToJson(bin, _options); + return json; + } + + public ContentCacheDataModel Deserialize(IReadOnlyContentBase content, string stringData, byte[] byteData) + { + if (byteData != null) + { + var cacheModel = MessagePackSerializer.Deserialize(byteData, _options); + Expand(content, cacheModel); + return cacheModel; + } + else if (stringData != null) + { + // NOTE: We don't really support strings but it's possible if manually used (i.e. tests) + var bin = Convert.FromBase64String(stringData); + var cacheModel = MessagePackSerializer.Deserialize(bin, _options); + Expand(content, cacheModel); + return cacheModel; + } + else + { + return null; + } + } + + public ContentCacheDataSerializationResult Serialize(IReadOnlyContentBase content, ContentCacheDataModel model) + { + Compress(content, model); + var bytes = MessagePackSerializer.Serialize(model, _options); + return new ContentCacheDataSerializationResult(null, bytes); + } + + /// + /// Used during serialization to compress properties + /// + /// + /// + /// This will essentially 'double compress' property data. The MsgPack data as a whole will already be compressed + /// but this will go a step further and double compress property data so that it is stored in the nucache file + /// as compressed bytes and therefore will exist in memory as compressed bytes. That is, until the bytes are + /// read/decompressed as a string to be displayed on the front-end. This allows for potentially a significant + /// memory savings but could also affect performance of first rendering pages while decompression occurs. + /// + private void Compress(IReadOnlyContentBase content, ContentCacheDataModel model) + { + foreach(var propertyAliasToData in model.PropertyData) + { + if (_propertyOptions.IsCompressed(content, propertyAliasToData.Key)) + { + foreach(var property in propertyAliasToData.Value.Where(x => x.Value != null && x.Value is string)) + { + property.Value = LZ4Pickler.Pickle(Encoding.UTF8.GetBytes((string)property.Value), LZ4Level.L00_FAST); + } + } + } + } + + /// + /// Used during deserialization to map the property data as lazy or expand the value + /// + /// + private void Expand(IReadOnlyContentBase content, ContentCacheDataModel nestedData) + { + foreach (var propertyAliasToData in nestedData.PropertyData) + { + if (_propertyOptions.IsCompressed(content, propertyAliasToData.Key)) + { + foreach (var property in propertyAliasToData.Value.Where(x => x.Value != null)) + { + if (property.Value is byte[] byteArrayValue) + { + property.Value = new LazyCompressedString(byteArrayValue); + } + } + } + } + } + } +} diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializerFactory.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializerFactory.cs new file mode 100644 index 0000000000..fcc3fa2bb8 --- /dev/null +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/MsgPackContentNestedDataSerializerFactory.cs @@ -0,0 +1,69 @@ +using System.Collections.Concurrent; +using System.Collections.Generic; +using Umbraco.Core.Models; +using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Services; + +namespace Umbraco.Web.PublishedCache.NuCache.DataSource +{ + internal class MsgPackContentNestedDataSerializerFactory : IContentCacheDataSerializerFactory + { + private readonly IContentTypeService _contentTypeService; + private readonly IMediaTypeService _mediaTypeService; + private readonly IMemberTypeService _memberTypeService; + private readonly PropertyEditorCollection _propertyEditors; + private readonly IPropertyCacheCompressionOptions _compressionOptions; + private readonly ConcurrentDictionary<(int, string), bool> _isCompressedCache = new ConcurrentDictionary<(int, string), bool>(); + + public MsgPackContentNestedDataSerializerFactory( + IContentTypeService contentTypeService, + IMediaTypeService mediaTypeService, + IMemberTypeService memberTypeService, + PropertyEditorCollection propertyEditors, + IPropertyCacheCompressionOptions compressionOptions) + { + _contentTypeService = contentTypeService; + _mediaTypeService = mediaTypeService; + _memberTypeService = memberTypeService; + _propertyEditors = propertyEditors; + _compressionOptions = compressionOptions; + } + + public IContentCacheDataSerializer Create(ContentCacheDataSerializerEntityType types) + { + // Depending on which entity types are being requested, we need to look up those content types + // to initialize the compression options. + // We need to initialize these options now so that any data lookups required are completed and are not done while the content cache + // is performing DB queries which will result in errors since we'll be trying to query with open readers. + // NOTE: The calls to GetAll() below should be cached if the data has not been changed. + + var contentTypes = new Dictionary(); + if ((types & ContentCacheDataSerializerEntityType.Document) == ContentCacheDataSerializerEntityType.Document) + { + foreach(var ct in _contentTypeService.GetAll()) + { + contentTypes[ct.Id] = ct; + } + } + if ((types & ContentCacheDataSerializerEntityType.Media) == ContentCacheDataSerializerEntityType.Media) + { + foreach (var ct in _mediaTypeService.GetAll()) + { + contentTypes[ct.Id] = ct; + } + } + if ((types & ContentCacheDataSerializerEntityType.Member) == ContentCacheDataSerializerEntityType.Member) + { + foreach (var ct in _memberTypeService.GetAll()) + { + contentTypes[ct.Id] = ct; + } + } + + var compression = new PropertyCacheCompression(_compressionOptions, contentTypes, _propertyEditors, _isCompressedCache); + var serializer = new MsgPackContentNestedDataSerializer(compression); + + return serializer; + } + } +} diff --git a/src/Umbraco.Web/PublishedCache/NuCache/NuCacheSerializerComponent.cs b/src/Umbraco.Web/PublishedCache/NuCache/NuCacheSerializerComponent.cs new file mode 100644 index 0000000000..a1d3ed2b12 --- /dev/null +++ b/src/Umbraco.Web/PublishedCache/NuCache/NuCacheSerializerComponent.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.Configuration; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Umbraco.Core.Composing; +using Umbraco.Core.Logging; +using Umbraco.Core.Services; + +namespace Umbraco.Web.PublishedCache.NuCache +{ + /// + /// Rebuilds the database cache if required when the serializer changes + /// + public class NuCacheSerializerComponent : IComponent + { + internal const string Nucache_Serializer_Key = "Umbraco.Web.PublishedCache.NuCache.Serializer"; + private const string JSON_SERIALIZER_VALUE = "JSON"; + private readonly Lazy _service; + private readonly IKeyValueService _keyValueService; + private readonly IProfilingLogger _profilingLogger; + + public NuCacheSerializerComponent(Lazy service, IKeyValueService keyValueService, IProfilingLogger profilingLogger) + { + // We are using lazy here as a work around because the service does quite a lot of initialization in the ctor which + // we want to avoid where possible. Since we only need the service if we are rebuilding, we don't want to eagerly + // initialize anything unless we need to. + _service = service; + _keyValueService = keyValueService; + _profilingLogger = profilingLogger; + } + + public void Initialize() + { + RebuildDatabaseCacheIfSerializerChanged(); + } + + private void RebuildDatabaseCacheIfSerializerChanged() + { + var serializer = ConfigurationManager.AppSettings[Nucache_Serializer_Key]; + var currentSerializer = _keyValueService.GetValue(Nucache_Serializer_Key); + + if (currentSerializer == null) + { + currentSerializer = JSON_SERIALIZER_VALUE; + } + if (serializer == null) + { + serializer = JSON_SERIALIZER_VALUE; + } + + if (serializer != currentSerializer) + { + _profilingLogger.Warn($"Database NuCache was serialized using {currentSerializer}. Currently configured NuCache serializer {serializer}. Rebuilding Nucache"); + + using (_profilingLogger.TraceDuration($"Rebuilding NuCache database with {currentSerializer} serializer")) + { + _service.Value.Rebuild(); + _keyValueService.SetValue(Nucache_Serializer_Key, serializer); + } + } + } + + public void Terminate() + { } + } +} diff --git a/src/Umbraco.Web/PublishedCache/NuCache/NuCacheSerializerComposer.cs b/src/Umbraco.Web/PublishedCache/NuCache/NuCacheSerializerComposer.cs new file mode 100644 index 0000000000..59a206bc47 --- /dev/null +++ b/src/Umbraco.Web/PublishedCache/NuCache/NuCacheSerializerComposer.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Umbraco.Core; +using Umbraco.Core.Composing; + +namespace Umbraco.Web.PublishedCache.NuCache +{ + + [ComposeAfter(typeof(NuCacheComposer))] + [RuntimeLevel(MinLevel = RuntimeLevel.Run)] + public class NuCacheSerializerComposer : ICoreComposer + { + public void Compose(Composition composition) + { + composition.Components().Append(); + } + } +} diff --git a/src/Umbraco.Web/Search/IUmbracoTreeSearcherFields2.cs b/src/Umbraco.Web/Search/IUmbracoTreeSearcherFields2.cs new file mode 100644 index 0000000000..da0cd26644 --- /dev/null +++ b/src/Umbraco.Web/Search/IUmbracoTreeSearcherFields2.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; + +namespace Umbraco.Web.Search +{ + // TODO: Merge this interface to IUmbracoTreeSearcherFields for v9. + // We should probably make these method make a little more sense when they are combined so have + // a single method for getting fields to search and fields to load for each category. + public interface IUmbracoTreeSearcherFields2 : IUmbracoTreeSearcherFields + { + /// + /// Set of fields for all node types to be loaded + /// + ISet GetBackOfficeFieldsToLoad(); + + /// + /// Additional set list of fields for Members to be loaded + /// + ISet GetBackOfficeMembersFieldsToLoad(); + + /// + /// Additional set of fields for Media to be loaded + /// + ISet GetBackOfficeMediaFieldsToLoad(); + + /// + /// Additional set of fields for Documents to be loaded + /// + ISet GetBackOfficeDocumentFieldsToLoad(); + } +} diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 9c6dca46fb..1b278634d1 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -78,6 +78,18 @@ + + 1.1.11 + + + + + + + + 2.2.85 + + @@ -145,6 +157,217 @@ + + Properties\SolutionInfo.cs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -158,6 +381,242 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -175,6 +634,21 @@ + + + + + + + + + + + + + + + @@ -242,3 +716,20 @@ + + + $(PlatformTargetAsMSBuildArchitecture) + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Umbraco.Web/UrlHelperRenderExtensions.cs b/src/Umbraco.Web/UrlHelperRenderExtensions.cs index 63a59315a4..d1eb46a2b8 100644 --- a/src/Umbraco.Web/UrlHelperRenderExtensions.cs +++ b/src/Umbraco.Web/UrlHelperRenderExtensions.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Web; using System.Web.Mvc; using Umbraco.Cms.Core; +using Umbraco.Core.Models; using Umbraco.Extensions; using Umbraco.Web.Composing;