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-Conventions.cs b/src/Umbraco.Core/Constants-Conventions.cs index 9ebbf21369..251f5ff3bf 100644 --- a/src/Umbraco.Core/Constants-Conventions.cs +++ b/src/Umbraco.Core/Constants-Conventions.cs @@ -115,25 +115,45 @@ namespace Umbraco.Cms.Core public const string Image = "Image"; /// - /// MediaType alias for a video. + /// MediaType name for a video. /// public const string Video = "Video"; /// - /// MediaType alias for an audio. + /// MediaType name for an audio. /// public const string Audio = "Audio"; /// - /// MediaType alias for an article. + /// MediaType name for an article. /// public const string Article = "Article"; /// - /// MediaType alias for vector graphics. + /// MediaType name for vector graphics. /// public const string VectorGraphics = "VectorGraphics"; + /// + /// MediaType alias for a video. + /// + public const string VideoAlias = "umbracoMediaVideo"; + + /// + /// MediaType alias for an audio. + /// + public const string AudioAlias = "umbracoMediaAudio"; + + /// + /// MediaType alias for an article. + /// + public const string ArticleAlias = "umbracoMediaArticle"; + + /// + /// MediaType alias for vector graphics. + /// + public const string VectorGraphicsAlias = "umbracoMediaVectorGraphics"; + /// /// MediaType alias indicating allowing auto-selection. /// 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/DataEditor.cs b/src/Umbraco.Core/PropertyEditors/DataEditor.cs index e2fc0071be..c8b9f3a51f 100644 --- a/src/Umbraco.Core/PropertyEditors/DataEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/DataEditor.cs @@ -21,6 +21,7 @@ namespace Umbraco.Cms.Core.PropertyEditors public class DataEditor : IDataEditor { private IDictionary _defaultConfiguration; + private IDataValueEditor _reusableEditor; /// /// Initializes a new instance of the class. @@ -89,7 +90,8 @@ namespace Umbraco.Cms.Core.PropertyEditors /// simple enough for now. /// // TODO: point of that one? shouldn't we always configure? - public IDataValueEditor GetValueEditor() => ExplicitValueEditor ?? CreateValueEditor(); + public IDataValueEditor GetValueEditor() => ExplicitValueEditor ?? (_reusableEditor ?? (_reusableEditor = CreateValueEditor())); + /// /// 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/Migrations/Install/DatabaseDataCreator.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs index 248325a30e..83adbc6e8a 100644 --- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs @@ -151,8 +151,8 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install //New UDI pickers with newer Ids _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1046, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1046", SortOrder = 2, UniqueId = new Guid("FD1E0DA5-5606-4862-B679-5D0CF3A52A59"), Text = "Content Picker", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1047, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1047", SortOrder = 2, UniqueId = new Guid("1EA2E01F-EBD8-4CE1-8D71-6B1149E63548"), Text = "Member Picker", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1048, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1048", SortOrder = 2, UniqueId = new Guid("135D60E0-64D9-49ED-AB08-893C9BA44AE5"), Text = "(Obsolete) Media Picker", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1049, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1049", SortOrder = 2, UniqueId = new Guid("9DBBCBBB-2327-434A-B355-AF1B84E5010A"), Text = "(Obsolete) Multiple Media Picker", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1048, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1048", SortOrder = 2, UniqueId = new Guid("135D60E0-64D9-49ED-AB08-893C9BA44AE5"), Text = "Media Picke (legacy)", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1049, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1049", SortOrder = 2, UniqueId = new Guid("9DBBCBBB-2327-434A-B355-AF1B84E5010A"), Text = "Multiple Media Picker (legacy)", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1050, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1050", SortOrder = 2, UniqueId = new Guid("B4E3535A-1753-47E2-8568-602CF8CFEE6F"), Text = "Multi URL Picker", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1051, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1051", SortOrder = 2, UniqueId = Cms.Core.Constants.DataTypes.Guids.MediaPicker3Guid, Text = "Media Picker", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); @@ -184,10 +184,10 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 532, NodeId = 1031, Alias = Cms.Core.Constants.Conventions.MediaTypes.Folder, Icon = Cms.Core.Constants.Icons.MediaFolder, Thumbnail = Cms.Core.Constants.Icons.MediaFolder, IsContainer = false, AllowAtRoot = true, Variations = (byte) ContentVariation.Nothing }); _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 533, NodeId = 1032, Alias = Cms.Core.Constants.Conventions.MediaTypes.Image, Icon = Cms.Core.Constants.Icons.MediaImage, Thumbnail = Cms.Core.Constants.Icons.MediaImage, AllowAtRoot = true, Variations = (byte) ContentVariation.Nothing }); _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 534, NodeId = 1033, Alias = Cms.Core.Constants.Conventions.MediaTypes.File, Icon = Cms.Core.Constants.Icons.MediaFile, Thumbnail = Cms.Core.Constants.Icons.MediaFile, AllowAtRoot = true, Variations = (byte) ContentVariation.Nothing }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 540, NodeId = 1034, Alias = Cms.Core.Constants.Conventions.MediaTypes.Video, Icon = Cms.Core.Constants.Icons.MediaVideo, Thumbnail = Cms.Core.Constants.Icons.MediaVideo, AllowAtRoot = true, Variations = (byte) ContentVariation.Nothing }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 541, NodeId = 1035, Alias = Cms.Core.Constants.Conventions.MediaTypes.Audio, Icon = Cms.Core.Constants.Icons.MediaAudio, Thumbnail = Cms.Core.Constants.Icons.MediaAudio, AllowAtRoot = true, Variations = (byte) ContentVariation.Nothing }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 542, NodeId = 1036, Alias = Cms.Core.Constants.Conventions.MediaTypes.Article, Icon = Cms.Core.Constants.Icons.MediaArticle, Thumbnail = Cms.Core.Constants.Icons.MediaArticle, AllowAtRoot = true, Variations = (byte) ContentVariation.Nothing }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 543, NodeId = 1037, Alias = Cms.Core.Constants.Conventions.MediaTypes.VectorGraphics, Icon = Cms.Core.Constants.Icons.MediaVectorGraphics, Thumbnail = Cms.Core.Constants.Icons.MediaVectorGraphics, AllowAtRoot = true, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 540, NodeId = 1034, Alias = Cms.Core.Constants.Conventions.MediaTypes.VideoAlias, Icon = Cms.Core.Constants.Icons.MediaVideo, Thumbnail = Cms.Core.Constants.Icons.MediaVideo, AllowAtRoot = true, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 541, NodeId = 1035, Alias = Cms.Core.Constants.Conventions.MediaTypes.AudioAlias, Icon = Cms.Core.Constants.Icons.MediaAudio, Thumbnail = Cms.Core.Constants.Icons.MediaAudio, AllowAtRoot = true, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 542, NodeId = 1036, Alias = Cms.Core.Constants.Conventions.MediaTypes.ArticleAlias, Icon = Cms.Core.Constants.Icons.MediaArticle, Thumbnail = Cms.Core.Constants.Icons.MediaArticle, AllowAtRoot = true, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 543, NodeId = 1037, Alias = Cms.Core.Constants.Conventions.MediaTypes.VectorGraphicsAlias, Icon = Cms.Core.Constants.Icons.MediaVectorGraphics, Thumbnail = Cms.Core.Constants.Icons.MediaVectorGraphics, AllowAtRoot = true, Variations = (byte) ContentVariation.Nothing }); _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 531, NodeId = 1044, Alias = Cms.Core.Constants.Conventions.MemberTypes.DefaultAlias, Icon = Cms.Core.Constants.Icons.Member, Thumbnail = Cms.Core.Constants.Icons.Member, Variations = (byte) ContentVariation.Nothing }); } 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 1e195ec1b2..49cc6b2902 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyEditor.cs @@ -66,6 +66,7 @@ namespace Umbraco.Cms.Core.PropertyEditors _propertyEditors = propertyEditors; _dataTypeService = dataTypeService; _logger = logger; + _blockEditorValues = new BlockEditorValues(new BlockListEditorDataConverter(), contentTypeService, _logger); Validators.Add(new BlockEditorValidator(propertyValidationService, _blockEditorValues,contentTypeService)); Validators.Add(new MinMaxValidator(_blockEditorValues, textService)); @@ -116,6 +117,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(); BlockEditorData blockEditorData; try @@ -128,7 +130,7 @@ namespace Umbraco.Cms.Core.PropertyEditors return string.Empty; } - if (blockEditorData == null || blockEditorData.BlockValue.ContentData.Count == 0) + if (blockEditorData == null) return string.Empty; foreach (var row in blockEditorData.BlockValue.ContentData) @@ -139,10 +141,8 @@ namespace Umbraco.Cms.Core.PropertyEditors // - force it to be culture invariant as the block editor can't handle culture variant element properties prop.Value.PropertyType.Variations = ContentVariation.Nothing; var tempProp = new Property(prop.Value.PropertyType); - tempProp.SetValue(prop.Value.Value); - // convert that temp property, and store the converted value var propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias]; if (propEditor == null) { @@ -164,8 +164,14 @@ namespace Umbraco.Cms.Core.PropertyEditors continue; } - var tempConfig = dataType.Configuration; - var valEditor = propEditor.GetValueEditor(tempConfig); + if (!valEditors.TryGetValue(dataType.Id, out var valEditor)) + { + var tempConfig = dataType.Configuration; + valEditor = propEditor.GetValueEditor(tempConfig); + + valEditors.Add(dataType.Id, valEditor); + } + var convValue = valEditor.ToEditor(tempProp); // update the raw value since this is what will get serialized out @@ -259,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() @@ -270,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/MediaPickerPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/MediaPickerPropertyEditor.cs index a3cd7278a6..af6d26515a 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/MediaPickerPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/MediaPickerPropertyEditor.cs @@ -16,15 +16,18 @@ namespace Umbraco.Cms.Core.PropertyEditors /// /// Represents a media picker property editor. /// + /// + /// Named "(legacy)" as it's best to use the NEW Media Picker aka MediaPicker3 + /// [DataEditor( Constants.PropertyEditors.Aliases.MediaPicker, EditorType.PropertyValue | EditorType.MacroParameter, - "(Obsolete)Media Picker", + "Media Picker (legacy)", "mediapicker", ValueType = ValueTypes.Text, Group = Constants.PropertyEditors.Groups.Media, Icon = Constants.Icons.MediaImage, - IsDeprecated = true)] + IsDeprecated = false)] public class MediaPickerPropertyEditor : DataEditor { private readonly IIOHelper _ioHelper; 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/ModelToSqlExpressionHelperBenchmarks.cs b/src/Umbraco.Tests.Benchmarks/ModelToSqlExpressionHelperBenchmarks.cs index f54ab96255..a75e9db8bc 100644 --- a/src/Umbraco.Tests.Benchmarks/ModelToSqlExpressionHelperBenchmarks.cs +++ b/src/Umbraco.Tests.Benchmarks/ModelToSqlExpressionHelperBenchmarks.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq.Expressions; using BenchmarkDotNet.Attributes; using Microsoft.Extensions.Options; @@ -9,7 +9,6 @@ using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Mappers; using Umbraco.Cms.Infrastructure.Persistence.Querying; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; -using Umbraco.Cms.Persistence.SqlCe; namespace Umbraco.Tests.Benchmarks { @@ -19,7 +18,7 @@ namespace Umbraco.Tests.Benchmarks protected Lazy MockSqlContext() { var sqlContext = Mock.Of(); - var syntax = new SqlCeSyntaxProvider(Options.Create(new GlobalSettings())); + var syntax = new SqlServerSyntaxProvider(Options.Create(new GlobalSettings())); Mock.Get(sqlContext).Setup(x => x.SqlSyntax).Returns(syntax); return new Lazy(() => sqlContext); } @@ -36,7 +35,7 @@ namespace Umbraco.Tests.Benchmarks _mapperCollection = mapperCollection.Object; } - private readonly ISqlSyntaxProvider _syntaxProvider = new SqlCeSyntaxProvider(Options.Create(new GlobalSettings())); + private readonly ISqlSyntaxProvider _syntaxProvider = new SqlServerSyntaxProvider(Options.Create(new GlobalSettings())); private readonly CachedExpression _cachedExpression; private readonly IMapperCollection _mapperCollection; diff --git a/src/Umbraco.Tests.Benchmarks/Properties/AssemblyInfo.cs b/src/Umbraco.Tests.Benchmarks/Properties/AssemblyInfo.cs deleted file mode 100644 index 9f5a3c7453..0000000000 --- a/src/Umbraco.Tests.Benchmarks/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("Umbraco.Tests.Benchmarks")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("Umbraco.Tests.Benchmarks")] -[assembly: AssemblyCopyright("Copyright © 2018")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("3a33adc9-c6c0-4db1-a613-a9af0210df3d")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/Umbraco.Tests.Benchmarks/SqlTemplatesBenchmark.cs b/src/Umbraco.Tests.Benchmarks/SqlTemplatesBenchmark.cs index 38167c1ff9..a69b6b8b76 100644 --- a/src/Umbraco.Tests.Benchmarks/SqlTemplatesBenchmark.cs +++ b/src/Umbraco.Tests.Benchmarks/SqlTemplatesBenchmark.cs @@ -1,4 +1,4 @@ -using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Configs; using BenchmarkDotNet.Diagnosers; using Microsoft.Extensions.Options; @@ -6,7 +6,7 @@ using NPoco; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Mappers; -using Umbraco.Cms.Persistence.SqlCe; +using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; using Umbraco.Extensions; namespace Umbraco.Tests.Benchmarks @@ -36,7 +36,7 @@ namespace Umbraco.Tests.Benchmarks var mappers = new NPoco.MapperCollection( ); var factory = new FluentPocoDataFactory((type, iPocoDataFactory) => new PocoDataBuilder(type, mappers).Init()); - SqlContext = new SqlContext(new SqlCeSyntaxProvider(Options.Create(new GlobalSettings())), DatabaseType.SQLCe, factory); + SqlContext = new SqlContext(new SqlServerSyntaxProvider(Options.Create(new GlobalSettings())), DatabaseType.SQLCe, factory); SqlTemplates = new SqlTemplates(SqlContext); } diff --git a/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj b/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj index b6932ff74f..2c1286172b 100644 --- a/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj +++ b/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj @@ -1,21 +1,19 @@ net5.0 - Exe - 8 + Exe false - - 7.3 - + + + - @@ -27,10 +25,7 @@ 4.16.1 - - - all - + - + 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.Integration/Umbraco.Infrastructure/Services/ContentTypeServiceVariantsTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentTypeServiceVariantsTests.cs index b1a3233277..84bb5f36b9 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentTypeServiceVariantsTests.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentTypeServiceVariantsTests.cs @@ -8,6 +8,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using NPoco; using NUnit.Framework; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Models; @@ -17,6 +18,7 @@ using Umbraco.Cms.Core.Sync; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.PublishedCache; +using Umbraco.Cms.Infrastructure.PublishedCache.DataSource; using Umbraco.Cms.Tests.Common.Builders; using Umbraco.Cms.Tests.Common.Testing; using Umbraco.Cms.Tests.Integration.Testing; @@ -51,12 +53,14 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services protected override void CustomTestSetup(IUmbracoBuilder builder) { + InMemoryConfiguration[Constants.Configuration.ConfigNuCache + ":" + nameof(NuCacheSettings.NuCacheSerializerType)] = NuCacheSerializerType.JSON.ToString(); builder.AddNuCache(); builder.Services.AddUnique(); } private void AssertJsonStartsWith(int id, string expected) { + string json = GetJson(id).Replace('"', '\''); int pos = json.IndexOf("'cd':", StringComparison.InvariantCultureIgnoreCase); json = json.Substring(0, pos + "'cd':".Length); diff --git a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/MediaServiceTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/MediaServiceTests.cs index 725a710e53..73e30ed44c 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/MediaServiceTests.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/MediaServiceTests.cs @@ -183,27 +183,27 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services // Arrange MediaType mediaType = MediaTypeBuilder.CreateNewMediaType(); MediaTypeService.Save(mediaType); - IMedia media = MediaService.CreateMedia(string.Empty, -1, "video"); + IMedia media = MediaService.CreateMedia(string.Empty, -1, Constants.Conventions.MediaTypes.VideoAlias); // Act & Assert Assert.Throws(() => MediaService.Save(media)); } - /* - [Test] - public void Ensure_Content_Xml_Created() - { - var mediaType = MockedContentTypes.CreateVideoMediaType(); - MediaTypeService.Save(mediaType); - var media = MediaService.CreateMedia("Test", -1, "video"); - MediaService.Save(media); - - using (var scope = ScopeProvider.CreateScope()) - { - Assert.IsTrue(scope.Database.Exists(media.Id)); - } - }*/ + // [Test] + // public void Ensure_Content_Xml_Created() + // { + // var mediaType = MediaTypeBuilder.CreateVideoMediaType(); + // MediaTypeService.Save(mediaType); + // var media = MediaService.CreateMedia("Test", -1, Constants.Conventions.MediaTypes.VideoAlias); + // + // MediaService.Save(media); + // + // using (var scope = ScopeProvider.CreateScope()) + // { + // Assert.IsTrue(scope.Database.Exists(media.Id)); + // } + // } [Test] public void Can_Get_Media_By_Path() diff --git a/src/Umbraco.Tests.Integration/Umbraco.Web.Website/Routing/FrontEndRouteTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Web.Website/Routing/FrontEndRouteTests.cs index e163990fe7..7a3fa7c853 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Web.Website/Routing/FrontEndRouteTests.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Web.Website/Routing/FrontEndRouteTests.cs @@ -1,5 +1,7 @@ +using System; using System.Net; using System.Net.Http; +using System.Reflection; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using NUnit.Framework; @@ -10,7 +12,9 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Web; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Tests.Integration.TestServerTest; +using Umbraco.Cms.Web.Common.Attributes; using Umbraco.Cms.Web.Website.Controllers; +using Umbraco.Extensions; namespace Umbraco.Cms.Tests.Integration.Umbraco.Web.Website.Routing { @@ -44,6 +48,24 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Web.Website.Routing // Assert Assert.AreEqual(HttpStatusCode.NoContent, response.StatusCode); } + + [Test] + public async Task Plugin_Controller_Routes_By_Area() + { + // Create URL manually, because PrepareSurfaceController URl will prepare whatever the controller is routed as + Type controllerType = typeof(TestPluginController); + var pluginAttribute = CustomAttributeExtensions.GetCustomAttribute(controllerType, false); + var controllerName = ControllerExtensions.GetControllerName(controllerType); + string url = $"/umbraco/{pluginAttribute?.AreaName}/{controllerName}"; + PrepareUrl(url); + + HttpResponseMessage response = await Client.GetAsync(url); + + string body = await response.Content.ReadAsStringAsync(); + + Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); + + } } // Test controllers must be non-nested, else we need to jump through some hoops with custom @@ -61,4 +83,14 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Web.Website.Routing public IActionResult News() => NoContent(); } + + [PluginController("TestArea")] + public class TestPluginController : SurfaceController + { + public TestPluginController(IUmbracoContextAccessor umbracoContextAccessor, IUmbracoDatabaseFactory databaseFactory, ServiceContext services, AppCaches appCaches, IProfilingLogger profilingLogger, IPublishedUrlProvider publishedUrlProvider) : base(umbracoContextAccessor, databaseFactory, services, appCaches, profilingLogger, publishedUrlProvider) + { + } + + public IActionResult Index() => Ok(); + } } 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.BackOffice/Extensions/ModelStateExtensionsTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Extensions/ModelStateExtensionsTests.cs index 3c4b78b4fa..dd2c4cd424 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Extensions/ModelStateExtensionsTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Extensions/ModelStateExtensionsTests.cs @@ -9,6 +9,7 @@ using Moq; using NUnit.Framework; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Web.BackOffice.Extensions; +using Umbraco.Extensions; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.BackOffice.Extensions { 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/ActionResults/UmbracoNotificationSuccessResponse.cs b/src/Umbraco.Web.BackOffice/ActionResults/UmbracoNotificationSuccessResponse.cs deleted file mode 100644 index 37e4902626..0000000000 --- a/src/Umbraco.Web.BackOffice/ActionResults/UmbracoNotificationSuccessResponse.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Umbraco.Cms.Core.Models.ContentEditing; -using Umbraco.Extensions; - -namespace Umbraco.Cms.Web.BackOffice.ActionResults -{ - public class UmbracoNotificationSuccessResponse : OkObjectResult - { - public UmbracoNotificationSuccessResponse(string successMessage) : base(null) - { - var notificationModel = new SimpleNotificationModel - { - Message = successMessage - }; - notificationModel.AddSuccessNotification(successMessage, string.Empty); - - Value = notificationModel; - } - } -} diff --git a/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs b/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs index c552c0d976..ae7776dfaf 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs @@ -209,7 +209,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers else { AddModelErrors(result); - return new ValidationErrorResult(new SimpleValidationModel(ModelState.ToErrorDictionary())); + return new ValidationErrorResult(ModelState); } } @@ -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 }); @@ -474,7 +474,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers { if (ModelState.IsValid == false) { - return new ValidationErrorResult(new SimpleValidationModel(ModelState.ToErrorDictionary())); + return new ValidationErrorResult(ModelState); } var user = await _signInManager.GetTwoFactorAuthenticationUserAsync(); 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/BackOfficeNotificationsController.cs b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeNotificationsController.cs index 4cecb20aa5..27be8ec263 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeNotificationsController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeNotificationsController.cs @@ -1,4 +1,10 @@ -using Umbraco.Cms.Web.BackOffice.Filters; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Web.BackOffice.ActionResults; +using Umbraco.Cms.Web.BackOffice.Filters; +using Umbraco.Cms.Web.Common.ActionsResults; +using Umbraco.Extensions; namespace Umbraco.Cms.Web.BackOffice.Controllers { @@ -11,5 +17,55 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers [AppendCurrentEventMessages] public abstract class BackOfficeNotificationsController : UmbracoAuthorizedJsonController { + /// + /// returns a 200 OK response with a notification message + /// + /// + /// + protected OkObjectResult Ok(string message) + { + var notificationModel = new SimpleNotificationModel + { + Message = message + }; + notificationModel.AddSuccessNotification(message, string.Empty); + + return new OkObjectResult(notificationModel); + } + + /// + /// Overridden to ensure that the error message is an error notification message + /// + /// + /// + protected override ActionResult ValidationProblem(string errorMessage) + => ValidationProblem(errorMessage, string.Empty); + + /// + /// Creates a notofication validation problem with a header and message + /// + /// + /// + /// + protected ActionResult ValidationProblem(string errorHeader, string errorMessage) + { + var notificationModel = new SimpleNotificationModel + { + Message = errorMessage + }; + notificationModel.AddErrorNotification(errorHeader, errorMessage); + return new ValidationErrorResult(notificationModel); + } + + /// + /// Overridden to ensure that all queued notifications are sent to the back office + /// + /// + [NonAction] + public override ActionResult ValidationProblem() + // returning an object of INotificationModel will ensure that any pending + // notification messages are added to the response. + => new ValidationErrorResult(new SimpleNotificationModel()); + } } diff --git a/src/Umbraco.Web.BackOffice/Controllers/CodeFileController.cs b/src/Umbraco.Web.BackOffice/Controllers/CodeFileController.cs index 822f5a4911..a88cdc5087 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/CodeFileController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/CodeFileController.cs @@ -86,9 +86,13 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers view.Content = display.Content; var result = _fileService.CreatePartialView(view, display.Snippet, currentUser.Id); if (result.Success) + { return Ok(); + } else - return ValidationErrorResult.CreateNotificationValidationErrorResult(result.Exception.Message); + { + return ValidationProblem(result.Exception.Message); + } case Constants.Trees.PartialViewMacros: var viewMacro = new PartialView(PartialViewType.PartialViewMacro, display.VirtualPath); @@ -97,7 +101,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (resultMacro.Success) return Ok(); else - return ValidationErrorResult.CreateNotificationValidationErrorResult(resultMacro.Exception.Message); + return ValidationProblem(resultMacro.Exception.Message); case Constants.Trees.Scripts: var script = new Script(display.VirtualPath); @@ -123,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 ValidationErrorResult.CreateNotificationValidationErrorResult(_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 @@ -418,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: @@ -433,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 dcc5753524..03de07769d 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs @@ -22,6 +22,7 @@ using Umbraco.Cms.Core.Models.Validation; using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Routing; +using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; @@ -66,6 +67,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers private readonly IAuthorizationService _authorizationService; private readonly Lazy> _allLangs; private readonly ILogger _logger; + private readonly IScopeProvider _scopeProvider; public object Domains { get; private set; } @@ -90,6 +92,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers ActionCollection actionCollection, ISqlContext sqlContext, IJsonSerializer serializer, + IScopeProvider scopeProvider, IAuthorizationService authorizationService) : base(cultureDictionary, loggerFactory, shortStringHelper, eventMessages, localizedTextService, serializer) { @@ -110,7 +113,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers _sqlContext = sqlContext; _authorizationService = authorizationService; _logger = loggerFactory.CreateLogger(); - + _scopeProvider = scopeProvider; _allLangs = new Lazy>(() => _localizationService.GetAllLanguages().ToDictionary(x => x.IsoCode, x => x, StringComparer.InvariantCultureIgnoreCase)); } @@ -272,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 @@ -395,29 +398,97 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers [OutgoingEditorModelEvent] public ActionResult GetEmptyByKey(Guid contentTypeKey, int parentId) { - var contentType = _contentTypeService.Get(contentTypeKey); - if (contentType == null) + using (var scope = _scopeProvider.CreateScope()) { - return NotFound(); - } + var contentType = _contentTypeService.Get(contentTypeKey); + if (contentType == null) + { + return NotFound(); + } - return GetEmptyInner(contentType, parentId); + var contentItem = GetEmptyInner(contentType, parentId); + scope.Complete(); + + return contentItem; + } } private ContentItemDisplay GetEmptyInner(IContentType contentType, int parentId) { 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; + } + + /// + /// Gets a collection of empty content items for all document types. + /// + /// + /// + [OutgoingEditorModelEvent] + public ActionResult> GetEmptyByKeys([FromQuery] Guid[] contentTypeKeys, [FromQuery] int parentId) + { + using var scope = _scopeProvider.CreateScope(autoComplete: true); + var contentTypes = _contentTypeService.GetAll(contentTypeKeys).ToList(); + return GetEmpties(contentTypes, parentId).ToDictionary(x => x.ContentTypeKey); } [OutgoingEditorModelEvent] @@ -572,7 +643,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (!EnsureUniqueName(name, content, nameof(name))) { - return new ValidationErrorResult(ModelState.ToErrorDictionary()); + return ValidationProblem(ModelState); } var blueprint = _contentService.CreateContentFromBlueprint(content, name, _backofficeSecurityAccessor.BackOfficeSecurity.GetUserId().ResultOr(0)); @@ -581,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; @@ -593,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; } @@ -679,8 +750,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers // ok, so the absolute mandatory data is invalid and it's new, we cannot actually continue! // add the model state to the outgoing object and throw a validation message var forDisplay = mapToDisplay(contentItem.PersistedContent); - forDisplay.Errors = ModelState.ToErrorDictionary(); - return new ValidationErrorResult(forDisplay); + return ValidationProblem(forDisplay, ModelState); } // if there's only one variant and the model state is not valid we cannot publish so change it to save @@ -760,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; @@ -785,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; } @@ -801,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; } @@ -831,8 +901,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers //lastly, if it is not valid, add the model state to the outgoing object and throw a 400 if (!ModelState.IsValid) { - display.Errors = ModelState.ToErrorDictionary(); - return new ValidationErrorResult(display); + return ValidationProblem(display, ModelState); } if (wasCancelled) @@ -843,7 +912,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers //If the item is new and the operation was cancelled, we need to return a different // status code so the UI can handle it since it won't be able to redirect since there // is no Id to redirect to! - return new ValidationErrorResult(display); + return ValidationProblem(display); } } @@ -898,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 } @@ -935,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); @@ -963,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)); } } } @@ -1098,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; } @@ -1106,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; } @@ -1120,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; } @@ -1128,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; } @@ -1136,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; } @@ -1364,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; } } @@ -1401,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; } } @@ -1418,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); } @@ -1473,7 +1542,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers { var notificationModel = new SimpleNotificationModel(); AddMessageForPublishStatus(new[] { publishResult }, notificationModel); - return new ValidationErrorResult(notificationModel); + return ValidationProblem(notificationModel); } return Ok(); @@ -1523,9 +1592,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers var moveResult = _contentService.MoveToRecycleBin(foundContent, _backofficeSecurityAccessor.BackOfficeSecurity.GetUserId().ResultOr(0)); if (moveResult.Success == false) { - //returning an object of INotificationModel will ensure that any pending - // notification messages are added to the response. - return new ValidationErrorResult(new SimpleNotificationModel()); + return ValidationProblem(); } } else @@ -1533,9 +1600,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers var deleteResult = _contentService.Delete(foundContent, _backofficeSecurityAccessor.BackOfficeSecurity.GetUserId().ResultOr(0)); if (deleteResult.Success == false) { - //returning an object of INotificationModel will ensure that any pending - // notification messages are added to the response. - return new ValidationErrorResult(new SimpleNotificationModel()); + return ValidationProblem(); } } @@ -1556,7 +1621,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers { _contentService.EmptyRecycleBin(_backofficeSecurityAccessor.BackOfficeSecurity.GetUserId().ResultOr(Constants.Security.SuperUserId)); - return new UmbracoNotificationSuccessResponse(_localizedTextService.Localize("defaultdialogs/recycleBinIsEmpty")); + return Ok(_localizedTextService.Localize("defaultdialogs", "recycleBinIsEmpty")); } /// @@ -1593,7 +1658,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers { _logger.LogWarning("Content sorting failed, this was probably caused by an event being cancelled"); // TODO: Now you can cancel sorting, does the event messages bubble up automatically? - return new ValidationErrorResult("Content sorting failed, this was probably caused by an event being cancelled"); + return ValidationProblem("Content sorting failed, this was probably caused by an event being cancelled"); } return Ok(); @@ -1692,13 +1757,13 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (!unpublishResult.Success) { AddCancelMessage(content); - return new ValidationErrorResult(content); + return ValidationProblem(content); } else { content.AddSuccessNotification( - _localizedTextService.Localize("content/unpublish"), - _localizedTextService.Localize("speechBubbles/contentUnpublished")); + _localizedTextService.Localize("content", "unpublish"), + _localizedTextService.Localize("speechBubbles", "contentUnpublished")); return content; } } @@ -1723,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; } @@ -1732,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; @@ -1764,7 +1829,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers } catch (UriFormatException) { - return new ValidationErrorResult(_localizedTextService.Localize("assignDomain/invalidDomain")); + return ValidationProblem(_localizedTextService.Localize("assignDomain", "invalidDomain")); } } @@ -1914,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"); } } } @@ -2034,8 +2099,8 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers //cannot move if the content item is not allowed at the root if (toMove.ContentType.AllowedAsRoot == false) { - return ValidationErrorResult.CreateNotificationValidationErrorResult( - _localizedTextService.Localize("moveOrCopy/notAllowedAtRoot")); + return ValidationProblem( + _localizedTextService.Localize("moveOrCopy", "notAllowedAtRoot")); } } else @@ -2051,15 +2116,15 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (parentContentType.AllowedContentTypes.Select(x => x.Id).ToArray() .Any(x => x.Value == toMove.ContentType.Id) == false) { - return ValidationErrorResult.CreateNotificationValidationErrorResult( - _localizedTextService.Localize("moveOrCopy/notAllowedByContentType")); + return ValidationProblem( + _localizedTextService.Localize("moveOrCopy", "notAllowedByContentType")); } // Check on paths if ($",{parent.Path},".IndexOf($",{toMove.Id},", StringComparison.Ordinal) > -1) { - return ValidationErrorResult.CreateNotificationValidationErrorResult( - _localizedTextService.Localize("moveOrCopy/notAllowedByPath")); + return ValidationProblem( + _localizedTextService.Localize("moveOrCopy", "notAllowedByPath")); } } @@ -2128,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 })); } } } @@ -2153,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 })); } } } @@ -2176,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; @@ -2185,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: @@ -2193,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; @@ -2203,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; @@ -2213,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; @@ -2224,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 @@ -2234,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()); } } @@ -2243,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: @@ -2257,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; } @@ -2368,8 +2443,6 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (rollbackResult.Success) return Ok(); - var notificationModel = new SimpleNotificationModel(); - switch (rollbackResult.Result) { case OperationResultType.Failed: @@ -2377,22 +2450,12 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers case OperationResultType.FailedExceptionThrown: case OperationResultType.NoOperation: default: - notificationModel.AddErrorNotification( - _localizedTextService.Localize("speechBubbles/operationFailedHeader"), - null); // TODO: There is no specific failed to save error message AFAIK - break; + return ValidationProblem(_localizedTextService.Localize("speechBubbles", "operationFailedHeader")); case OperationResultType.FailedCancelledByEvent: - notificationModel.AddErrorNotification( - _localizedTextService.Localize("speechBubbles/operationCancelledHeader"), - _localizedTextService.Localize("speechBubbles/operationCancelledText")); - break; + return ValidationProblem( + _localizedTextService.Localize("speechBubbles", "operationCancelledHeader"), + _localizedTextService.Localize("speechBubbles", "operationCancelledText")); } - - return new ValidationErrorResult(notificationModel); } - - - - } } 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 79ea6f6329..d14e9741fc 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ContentTypeController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ContentTypeController.cs @@ -297,7 +297,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (result.Success) return Ok(result.Result); //return the id else - return ValidationErrorResult.CreateNotificationValidationErrorResult(result.Exception.Message); + return ValidationProblem(result.Exception.Message); } [Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)] @@ -308,7 +308,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (result.Success) return Ok(result.Result); //return the id else - return ValidationErrorResult.CreateNotificationValidationErrorResult(result.Exception.Message); + return ValidationProblem(result.Exception.Message); } [Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)] @@ -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 46c75e5186..0b4c311a8c 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ContentTypeControllerBase.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ContentTypeControllerBase.cs @@ -13,9 +13,7 @@ using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Web.BackOffice.Extensions; using Umbraco.Cms.Web.BackOffice.Filters; -using Umbraco.Cms.Web.Common.ActionsResults; using Umbraco.Cms.Web.Common.Attributes; using Umbraco.Extensions; using Constants = Umbraco.Cms.Core.Constants; @@ -27,7 +25,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers /// [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] [PrefixlessBodyModelValidator] - public abstract class ContentTypeControllerBase : UmbracoAuthorizedJsonController + public abstract class ContentTypeControllerBase : BackOfficeNotificationsController where TContentType : class, IContentTypeComposition { private readonly EditorValidatorCollection _editorValidatorCollection; @@ -274,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 @@ -283,7 +281,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (ModelState.IsValid == false) { var err = CreateModelStateValidationEror(ctId, contentTypeSave, ct); - return new ValidationErrorResult(err); + return ValidationProblem(err); } //filter out empty properties @@ -305,11 +303,11 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers catch (Exception ex) { var responseEx = CreateInvalidCompositionResponseException(ex, contentTypeSave, ct, ctId); - if (responseEx != null) return new ValidationErrorResult(responseEx); + if (responseEx != null) return ValidationProblem(responseEx); } var exResult = CreateCompositionValidationExceptionIfInvalid(contentTypeSave, ct); - if (exResult != null) return new ValidationErrorResult(exResult); + if (exResult != null) return ValidationProblem(exResult); saveContentType(ct); @@ -348,11 +346,11 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (responseEx is null) throw ex; - return new ValidationErrorResult(responseEx); + return ValidationProblem(responseEx); } var exResult = CreateCompositionValidationExceptionIfInvalid(contentTypeSave, newCt); - if (exResult != null) return new ValidationErrorResult(exResult); + if (exResult != null) return ValidationProblem(exResult); //set id to null to ensure its handled as a new type contentTypeSave.Id = null; @@ -417,13 +415,9 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers case MoveOperationStatusType.FailedParentNotFound: return NotFound(); case MoveOperationStatusType.FailedCancelledByEvent: - //returning an object of INotificationModel will ensure that any pending - // notification messages are added to the response. - return new ValidationErrorResult(new SimpleNotificationModel()); + return ValidationProblem(); case MoveOperationStatusType.FailedNotAllowedByPath: - var notificationModel = new SimpleNotificationModel(); - notificationModel.AddErrorNotification(LocalizedTextService.Localize("moveOrCopy/notAllowedByPath"), ""); - return new ValidationErrorResult(notificationModel); + return ValidationProblem(LocalizedTextService.Localize("moveOrCopy", "notAllowedByPath")); default: throw new ArgumentOutOfRangeException(); } @@ -458,13 +452,9 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers case MoveOperationStatusType.FailedParentNotFound: return NotFound(); case MoveOperationStatusType.FailedCancelledByEvent: - //returning an object of INotificationModel will ensure that any pending - // notification messages are added to the response. - return new ValidationErrorResult(new SimpleNotificationModel()); + return ValidationProblem(); case MoveOperationStatusType.FailedNotAllowedByPath: - var notificationModel = new SimpleNotificationModel(); - notificationModel.AddErrorNotification(LocalizedTextService.Localize("moveOrCopy/notAllowedByPath"), ""); - return new ValidationErrorResult(notificationModel); + 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 49bff529bd..1435eb6c52 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/CurrentUserController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/CurrentUserController.cs @@ -190,7 +190,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers // so that is why it is being used here. ModelState.AddModelError("value", result.Errors.ToErrorMessage()); - return new ValidationErrorResult(new SimpleValidationModel(ModelState.ToErrorDictionary())); + return ValidationProblem(ModelState); } //They've successfully set their password, we can now update their user account to be approved @@ -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; } @@ -242,7 +242,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers ModelState.AddModelError(memberName, passwordChangeResult.Result.ChangeError.ErrorMessage); } - return new ValidationErrorResult(new SimpleValidationModel(ModelState.ToErrorDictionary())); + return ValidationProblem(ModelState); } // TODO: Why is this necessary? This inherits from UmbracoAuthorizedApiController 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 9f020125bb..d68c3f06f5 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/DataTypeController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/DataTypeController.cs @@ -16,9 +16,7 @@ using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Web.BackOffice.Extensions; using Umbraco.Cms.Web.BackOffice.Filters; -using Umbraco.Cms.Web.Common.ActionsResults; using Umbraco.Cms.Web.Common.Attributes; using Umbraco.Cms.Web.Common.Authorization; using Umbraco.Extensions; @@ -267,7 +265,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (result.Success) return Ok(result.Result); //return the id else - return ValidationErrorResult.CreateNotificationValidationErrorResult(result.Exception.Message); + return ValidationProblem(result.Exception.Message); } /// @@ -302,12 +300,12 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers catch (DuplicateNameException ex) { ModelState.AddModelError("Name", ex.Message); - return new ValidationErrorResult(new SimpleValidationModel(ModelState.ToErrorDictionary())); + return ValidationProblem(ModelState); } // 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; } @@ -335,13 +333,11 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers case MoveOperationStatusType.FailedParentNotFound: return NotFound(); case MoveOperationStatusType.FailedCancelledByEvent: - //returning an object of INotificationModel will ensure that any pending - // notification messages are added to the response. - return new ValidationErrorResult(new SimpleNotificationModel()); + return ValidationProblem(); case MoveOperationStatusType.FailedNotAllowedByPath: var notificationModel = new SimpleNotificationModel(); - notificationModel.AddErrorNotification(_localizedTextService.Localize("moveOrCopy/notAllowedByPath"), ""); - return new ValidationErrorResult(notificationModel); + notificationModel.AddErrorNotification(_localizedTextService.Localize("moveOrCopy", "notAllowedByPath"), ""); + return ValidationProblem(notificationModel); default: throw new ArgumentOutOfRangeException(); } @@ -355,7 +351,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (result.Success) return Ok(result.Result); else - return ValidationErrorResult.CreateNotificationValidationErrorResult(result.Exception.Message); + return ValidationProblem(result.Exception.Message); } /// diff --git a/src/Umbraco.Web.BackOffice/Controllers/DictionaryController.cs b/src/Umbraco.Web.BackOffice/Controllers/DictionaryController.cs index 980d08c2e2..8ee4ee1182 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/DictionaryController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/DictionaryController.cs @@ -13,8 +13,6 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Web.BackOffice.Extensions; -using Umbraco.Cms.Web.Common.ActionsResults; using Umbraco.Cms.Web.Common.Attributes; using Umbraco.Cms.Web.Common.Authorization; using Umbraco.Extensions; @@ -101,15 +99,15 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers public ActionResult Create(int parentId, string key) { if (string.IsNullOrEmpty(key)) - return ValidationErrorResult.CreateNotificationValidationErrorResult("Key can not be empty."); // TODO: translate + return ValidationProblem("Key can not be empty."); // TODO: translate if (_localizationService.DictionaryItemExists(key)) { var message = _localizedTextService.Localize( - "dictionaryItem/changeKeyError", + "dictionaryItem","changeKeyError", _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.GetUserCulture(_localizedTextService, _globalSettings), new Dictionary { { "0", key } }); - return ValidationErrorResult.CreateNotificationValidationErrorResult(message); + return ValidationProblem(message); } try @@ -130,7 +128,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers catch (Exception ex) { _logger.LogError(ex, "Error creating dictionary with {Name} under {ParentId}", key, parentId); - return ValidationErrorResult.CreateNotificationValidationErrorResult("Error creating dictionary item"); + return ValidationProblem("Error creating dictionary item"); } } @@ -207,7 +205,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers _localizationService.GetDictionaryItemById(int.Parse(dictionary.Id.ToString())); if (dictionaryItem == null) - return ValidationErrorResult.CreateNotificationValidationErrorResult("Dictionary item does not exist"); + return ValidationProblem("Dictionary item does not exist"); var userCulture = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.GetUserCulture(_localizedTextService, _globalSettings); @@ -220,11 +218,11 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers { var message = _localizedTextService.Localize( - "dictionaryItem/changeKeyError", + "dictionaryItem","changeKeyError", userCulture, new Dictionary { { "0", dictionary.Name } }); ModelState.AddModelError("Name", message); - return new ValidationErrorResult(new SimpleValidationModel(ModelState.ToErrorDictionary())); + return ValidationProblem(ModelState); } dictionaryItem.ItemKey = dictionary.Name; @@ -243,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; @@ -251,7 +249,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers catch (Exception ex) { _logger.LogError(ex, "Error saving dictionary with {Name} under {ParentId}", dictionary.Name, dictionary.ParentId); - return ValidationErrorResult.CreateNotificationValidationErrorResult("Something went wrong saving dictionary"); + return ValidationProblem("Something went wrong saving dictionary"); } } diff --git a/src/Umbraco.Web.BackOffice/Controllers/LanguageController.cs b/src/Umbraco.Web.BackOffice/Controllers/LanguageController.cs index 8465e7a454..310b1142c0 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/LanguageController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/LanguageController.cs @@ -53,8 +53,8 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers return CultureInfo.GetCultures(CultureTypes.AllCultures) .Where(x => !x.Name.IsNullOrWhiteSpace()) .Select(x => new CultureInfo(x.Name)) // important! - .OrderBy(x => x.DisplayName) - .ToDictionary(x => x.Name, x => x.DisplayName); + .OrderBy(x => x.EnglishName) + .ToDictionary(x => x.Name, x => x.EnglishName); } /// @@ -97,7 +97,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (language.IsDefault) { var message = $"Language '{language.IsoCode}' is currently set to 'default' and can not be deleted."; - return ValidationErrorResult.CreateNotificationValidationErrorResult(message); + return ValidationProblem(message); } // service is happy deleting a language that's fallback for another language, @@ -116,7 +116,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers public ActionResult SaveLanguage(Language language) { if (!ModelState.IsValid) - return new ValidationErrorResult(new SimpleValidationModel(ModelState.ToErrorDictionary())); + return ValidationProblem(ModelState); // this is prone to race conditions but the service will not let us proceed anyways var existingByCulture = _localizationService.GetLanguageByIsoCode(language.IsoCode); @@ -132,7 +132,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers { //someone is trying to create a language that already exist ModelState.AddModelError("IsoCode", "The language " + language.IsoCode + " already exists"); - return new ValidationErrorResult(new SimpleValidationModel(ModelState.ToErrorDictionary())); + return ValidationProblem(ModelState); } var existingById = language.Id != default ? _localizationService.GetLanguageById(language.Id) : null; @@ -149,7 +149,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers catch (CultureNotFoundException) { ModelState.AddModelError("IsoCode", "No Culture found with name " + language.IsoCode); - return new ValidationErrorResult(new SimpleValidationModel(ModelState.ToErrorDictionary())); + return ValidationProblem(ModelState); } // create it (creating a new language cannot create a fallback cycle) @@ -172,7 +172,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (existingById.IsDefault && !language.IsDefault) { ModelState.AddModelError("IsDefault", "Cannot un-default the default language."); - return new ValidationErrorResult(new SimpleValidationModel(ModelState.ToErrorDictionary())); + return ValidationProblem(ModelState); } existingById.IsDefault = language.IsDefault; @@ -187,12 +187,12 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (!languages.ContainsKey(existingById.FallbackLanguageId.Value)) { ModelState.AddModelError("FallbackLanguage", "The selected fall back language does not exist."); - return new ValidationErrorResult(new SimpleValidationModel(ModelState.ToErrorDictionary())); + return ValidationProblem(ModelState); } if (CreatesCycle(existingById, languages)) { ModelState.AddModelError("FallbackLanguage", $"The selected fall back language {languages[existingById.FallbackLanguageId.Value].IsoCode} would create a circular path."); - return new ValidationErrorResult(new SimpleValidationModel(ModelState.ToErrorDictionary())); + return ValidationProblem(ModelState); } } diff --git a/src/Umbraco.Web.BackOffice/Controllers/LogViewerController.cs b/src/Umbraco.Web.BackOffice/Controllers/LogViewerController.cs index 20d33bd83a..9fcf407581 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/LogViewerController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/LogViewerController.cs @@ -5,7 +5,6 @@ using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Logging.Viewer; using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Web.Common.ActionsResults; using Umbraco.Cms.Web.Common.Attributes; using Umbraco.Cms.Web.Common.Authorization; using Constants = Umbraco.Cms.Core.Constants; @@ -18,7 +17,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] [Authorize(Policy = AuthorizationPolicies.SectionAccessSettings)] - public class LogViewerController : UmbracoAuthorizedJsonController + public class LogViewerController : BackOfficeNotificationsController { private readonly ILogViewer _logViewer; @@ -51,7 +50,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers //We will need to stop the request if trying to do this on a 1GB file if (CanViewLogs(logTimePeriod) == false) { - return ValidationErrorResult.CreateNotificationValidationErrorResult("Unable to view logs, due to size"); + return ValidationProblem("Unable to view logs, due to size"); } return _logViewer.GetNumberOfErrors(logTimePeriod); @@ -64,7 +63,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers //We will need to stop the request if trying to do this on a 1GB file if (CanViewLogs(logTimePeriod) == false) { - return ValidationErrorResult.CreateNotificationValidationErrorResult("Unable to view logs, due to size"); + return ValidationProblem("Unable to view logs, due to size"); } return _logViewer.GetLogLevelCounts(logTimePeriod); @@ -77,7 +76,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers //We will need to stop the request if trying to do this on a 1GB file if (CanViewLogs(logTimePeriod) == false) { - return ValidationErrorResult.CreateNotificationValidationErrorResult("Unable to view logs, due to size"); + return ValidationProblem("Unable to view logs, due to size"); } return new ActionResult>(_logViewer.GetMessageTemplates(logTimePeriod)); @@ -91,7 +90,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers //We will need to stop the request if trying to do this on a 1GB file if (CanViewLogs(logTimePeriod) == false) { - return ValidationErrorResult.CreateNotificationValidationErrorResult("Unable to view logs, due to size"); + return ValidationProblem("Unable to view logs, due to size"); } var direction = orderDirection == "Descending" ? Direction.Descending : Direction.Ascending; diff --git a/src/Umbraco.Web.BackOffice/Controllers/MacrosController.cs b/src/Umbraco.Web.BackOffice/Controllers/MacrosController.cs index a7ec619ae1..ec91d76c8e 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MacrosController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MacrosController.cs @@ -15,7 +15,6 @@ using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; -using Umbraco.Cms.Web.Common.ActionsResults; using Umbraco.Cms.Web.Common.Attributes; using Umbraco.Cms.Web.Common.Authorization; using Umbraco.Extensions; @@ -74,19 +73,19 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers { if (string.IsNullOrWhiteSpace(name)) { - return ValidationErrorResult.CreateNotificationValidationErrorResult("Name can not be empty"); + return ValidationProblem("Name can not be empty"); } var alias = name.ToSafeAlias(_shortStringHelper); if (_macroService.GetByAlias(alias) != null) { - return ValidationErrorResult.CreateNotificationValidationErrorResult("Macro with this alias already exists"); + return ValidationProblem("Macro with this alias already exists"); } if (name == null || name.Length > 255) { - return ValidationErrorResult.CreateNotificationValidationErrorResult("Name cannnot be more than 255 characters in length."); + return ValidationProblem("Name cannnot be more than 255 characters in length."); } try @@ -106,7 +105,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers { const string errorMessage = "Error creating macro"; _logger.LogError(exception, errorMessage); - return ValidationErrorResult.CreateNotificationValidationErrorResult(errorMessage); + return ValidationProblem(errorMessage); } } @@ -117,7 +116,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (macro == null) { - return ValidationErrorResult.CreateNotificationValidationErrorResult($"Macro with id {id} does not exist"); + return ValidationProblem($"Macro with id {id} does not exist"); } var macroDisplay = MapToDisplay(macro); @@ -132,7 +131,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (macro == null) { - return ValidationErrorResult.CreateNotificationValidationErrorResult($"Macro with id {id} does not exist"); + return ValidationProblem($"Macro with id {id} does not exist"); } var macroDisplay = MapToDisplay(macro); @@ -145,12 +144,12 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers { var guidUdi = id as GuidUdi; if (guidUdi == null) - return ValidationErrorResult.CreateNotificationValidationErrorResult($"Macro with id {id} does not exist"); + return ValidationProblem($"Macro with id {id} does not exist"); var macro = _macroService.GetById(guidUdi.Guid); if (macro == null) { - return ValidationErrorResult.CreateNotificationValidationErrorResult($"Macro with id {id} does not exist"); + return ValidationProblem($"Macro with id {id} does not exist"); } var macroDisplay = MapToDisplay(macro); @@ -165,7 +164,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (macro == null) { - return ValidationErrorResult.CreateNotificationValidationErrorResult($"Macro with id {id} does not exist"); + return ValidationProblem($"Macro with id {id} does not exist"); } _macroService.Delete(macro); @@ -178,19 +177,19 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers { if (macroDisplay == null) { - return ValidationErrorResult.CreateNotificationValidationErrorResult("No macro data found in request"); + return ValidationProblem("No macro data found in request"); } if (macroDisplay.Name == null || macroDisplay.Name.Length > 255) { - return ValidationErrorResult.CreateNotificationValidationErrorResult("Name cannnot be more than 255 characters in length."); + return ValidationProblem("Name cannnot be more than 255 characters in length."); } var macro = _macroService.GetById(int.Parse(macroDisplay.Id.ToString())); if (macro == null) { - return ValidationErrorResult.CreateNotificationValidationErrorResult($"Macro with id {macroDisplay.Id} does not exist"); + return ValidationProblem($"Macro with id {macroDisplay.Id} does not exist"); } if (macroDisplay.Alias != macro.Alias) @@ -199,7 +198,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (macroByAlias != null) { - return ValidationErrorResult.CreateNotificationValidationErrorResult("Macro with this alias already exists"); + return ValidationProblem("Macro with this alias already exists"); } } @@ -227,7 +226,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers { const string errorMessage = "Error creating macro"; _logger.LogError(exception, errorMessage); - return ValidationErrorResult.CreateNotificationValidationErrorResult(errorMessage); + return ValidationProblem(errorMessage); } } diff --git a/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs b/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs index b98b2e9cd7..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, @@ -452,9 +452,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers var moveResult = _mediaService.MoveToRecycleBin(foundMedia, _backofficeSecurityAccessor.BackOfficeSecurity.GetUserId().ResultOr(Constants.Security.SuperUserId)); if (moveResult == false) { - //returning an object of INotificationModel will ensure that any pending - // notification messages are added to the response. - return new ValidationErrorResult(new SimpleNotificationModel()); + return ValidationProblem(); } } else @@ -462,9 +460,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers var deleteResult = _mediaService.Delete(foundMedia, _backofficeSecurityAccessor.BackOfficeSecurity.GetUserId().ResultOr(Constants.Security.SuperUserId)); if (deleteResult == false) { - //returning an object of INotificationModel will ensure that any pending - // notification messages are added to the response. - return new ValidationErrorResult(new SimpleNotificationModel()); + return ValidationProblem(); } } @@ -500,11 +496,11 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (sourceParentID == destinationParentID) { - return new ValidationErrorResult(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) { - return new ValidationErrorResult(new SimpleNotificationModel()); + return ValidationProblem(); } else { @@ -563,9 +559,8 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers { //ok, so the absolute mandatory data is invalid and it's new, we cannot actually continue! // add the model state to the outgoing object and throw validation response - var forDisplay = _umbracoMapper.Map(contentItem.PersistedContent); - forDisplay.Errors = ModelState.ToErrorDictionary(); - return new ValidationErrorResult(forDisplay); + MediaItemDisplay forDisplay = _umbracoMapper.Map(contentItem.PersistedContent); + return ValidationProblem(forDisplay, ModelState); } } @@ -578,8 +573,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers //lastly, if it is not valid, add the model state to the outgoing object and throw a 403 if (!ModelState.IsValid) { - display.Errors = ModelState.ToErrorDictionary(); - return new ValidationErrorResult(display, StatusCodes.Status403Forbidden); + return ValidationProblem(display, ModelState, StatusCodes.Status403Forbidden); } //put the correct msgs in @@ -590,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 { @@ -602,7 +596,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers // is no Id to redirect to! if (saveStatus.Result.Result == OperationResultType.FailedCancelledByEvent && IsCreatingAction(contentItem.Action)) { - return new ValidationErrorResult(display); + return ValidationProblem(display); } } @@ -622,7 +616,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers { _mediaService.EmptyRecycleBin(_backofficeSecurityAccessor.BackOfficeSecurity.GetUserId().ResultOr(Constants.Security.SuperUserId)); - return new UmbracoNotificationSuccessResponse(_localizedTextService.Localize("defaultdialogs/recycleBinIsEmpty")); + return Ok(_localizedTextService.Localize("defaultdialogs", "recycleBinIsEmpty")); } /// @@ -661,7 +655,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (_mediaService.Sort(sortedMedia) == false) { _logger.LogWarning("Media sorting failed, this was probably caused by an event being cancelled"); - return new ValidationErrorResult("Media sorting failed, this was probably caused by an event being cancelled"); + return ValidationProblem("Media sorting failed, this was probably caused by an event being cancelled"); } return Ok(); } @@ -837,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)); } } @@ -919,7 +912,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers } else { - return new ValidationErrorResult("The request was not formatted correctly, the parentId is not an integer, Guid or UDI"); + return ValidationProblem("The request was not formatted correctly, the parentId is not an integer, Guid or UDI"); } } @@ -931,10 +924,10 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers var authorizationResult = await _authorizationService.AuthorizeAsync(User, new MediaPermissionsResource(_mediaService.GetById(intParentId)), requirement); if (!authorizationResult.Succeeded) { - return new ValidationErrorResult( + 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); } @@ -969,8 +962,8 @@ 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"), ""); - return new ValidationErrorResult(notificationModel); + notificationModel.AddErrorNotification(_localizedTextService.Localize("moveOrCopy", "notAllowedAtRoot"), ""); + return ValidationProblem(notificationModel); } } else @@ -987,16 +980,16 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers .Any(x => x.Value == toMove.ContentType.Id) == false) { var notificationModel = new SimpleNotificationModel(); - notificationModel.AddErrorNotification(_localizedTextService.Localize("moveOrCopy/notAllowedByContentType"), ""); - return new ValidationErrorResult(notificationModel); + notificationModel.AddErrorNotification(_localizedTextService.Localize("moveOrCopy", "notAllowedByContentType"), ""); + return ValidationProblem(notificationModel); } // Check on paths 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"), ""); - return new ValidationErrorResult(notificationModel); + 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 5f61d1b1c1..3233679ad9 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MediaTypeController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MediaTypeController.cs @@ -266,7 +266,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (result.Success) return Ok(result.Result); //return the id else - return ValidationErrorResult.CreateNotificationValidationErrorResult(result.Exception.Message); + return ValidationProblem(result.Exception.Message); } [Authorize(Policy = AuthorizationPolicies.TreeAccessMediaTypes)] @@ -277,7 +277,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (result.Success) return Ok(result.Result); //return the id else - return ValidationErrorResult.CreateNotificationValidationErrorResult(result.Exception.Message); + return ValidationProblem(result.Exception.Message); } [Authorize(Policy = AuthorizationPolicies.TreeAccessMediaTypes)] @@ -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 6045cec8f9..6cada09db3 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs @@ -26,11 +26,8 @@ using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.Implement; using Umbraco.Cms.Core.Strings; -using Umbraco.Cms.Web.BackOffice.Extensions; using Umbraco.Cms.Web.BackOffice.Filters; using Umbraco.Cms.Web.BackOffice.ModelBinders; -using Umbraco.Cms.Web.BackOffice.Security; -using Umbraco.Cms.Web.Common.ActionsResults; using Umbraco.Cms.Web.Common.Attributes; using Umbraco.Cms.Web.Common.Authorization; using Umbraco.Cms.Web.Common.Filters; @@ -260,8 +257,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (ModelState.IsValid == false) { MemberDisplay forDisplay = _umbracoMapper.Map(contentItem.PersistedContent); - forDisplay.Errors = ModelState.ToErrorDictionary(); - return new ValidationErrorResult(forDisplay); + return ValidationProblem(forDisplay, ModelState); } // Create a scope here which will wrap all child data operations in a single transaction. @@ -300,8 +296,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers // lastly, if it is not valid, add the model state to the outgoing object and throw a 403 if (!ModelState.IsValid) { - display.Errors = ModelState.ToErrorDictionary(); - return new ValidationErrorResult(display, StatusCodes.Status403Forbidden); + return ValidationProblem(display, ModelState, StatusCodes.Status403Forbidden); } // put the correct messages in @@ -310,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; } @@ -373,7 +368,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (created.Succeeded == false) { - return new ValidationErrorResult(created.Errors.ToErrorMessage()); + return ValidationProblem(created.Errors.ToErrorMessage()); } // now re-look up the member, which will now exist @@ -460,7 +455,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers MemberIdentityUser identityMember = await _memberManager.FindByIdAsync(contentItem.Id.ToString()); if (identityMember == null) { - return new ValidationErrorResult("Member was not found"); + return ValidationProblem("Member was not found"); } // Handle unlocking with the member manager (takes care of other nuances) @@ -469,7 +464,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers IdentityResult unlockResult = await _memberManager.SetLockoutEndDateAsync(identityMember, DateTimeOffset.Now.AddMinutes(-1)); if (unlockResult.Succeeded == false) { - return new ValidationErrorResult( + return ValidationProblem( $"Could not unlock for member {contentItem.Id} - error {unlockResult.Errors.ToErrorMessage()}"); } needsResync = true; @@ -478,7 +473,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers { // NOTE: This should not ever happen unless someone is mucking around with the request data. // An admin cannot simply lock a user, they get locked out by password attempts, but an admin can unlock them - return new ValidationErrorResult("An admin cannot lock a member"); + return ValidationProblem("An admin cannot lock a member"); } // If we're changing the password... @@ -488,13 +483,13 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers IdentityResult validatePassword = await _memberManager.ValidatePasswordAsync(contentItem.Password.NewPassword); if (validatePassword.Succeeded == false) { - return new ValidationErrorResult(validatePassword.Errors.ToErrorMessage()); + return ValidationProblem(validatePassword.Errors.ToErrorMessage()); } Attempt intId = identityMember.Id.TryConvertTo(); if (intId.Success == false) { - return new ValidationErrorResult("Member ID was not valid"); + return ValidationProblem("Member ID was not valid"); } var changingPasswordModel = new ChangingPasswordModel @@ -513,7 +508,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers { ModelState.AddModelError(memberName, passwordChangeResult.Result.ChangeError?.ErrorMessage ?? string.Empty); } - return new ValidationErrorResult(new SimpleValidationModel(ModelState.ToErrorDictionary())); + return ValidationProblem(ModelState); } needsResync = true; @@ -622,7 +617,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers IdentityResult identityResult = await _memberManager.RemoveFromRolesAsync(identityMember, rolesToRemove); if (!identityResult.Succeeded) { - return ValidationErrorResult.CreateNotificationValidationErrorResult(identityResult.Errors.ToErrorMessage()); + return ValidationProblem(identityResult.Errors.ToErrorMessage()); } hasChanges = true; } @@ -635,7 +630,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers IdentityResult identityResult = await _memberManager.AddToRolesAsync(identityMember, toAdd); if (!identityResult.Succeeded) { - return ValidationErrorResult.CreateNotificationValidationErrorResult(identityResult.Errors.ToErrorMessage()); + return ValidationProblem(identityResult.Errors.ToErrorMessage()); } hasChanges = true; } 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/PackageController.cs b/src/Umbraco.Web.BackOffice/Controllers/PackageController.cs index 99dcf161ab..9fd95755f5 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/PackageController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/PackageController.cs @@ -70,14 +70,16 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers public ActionResult PostSavePackage(PackageDefinition model) { if (ModelState.IsValid == false) - return new ValidationErrorResult(new SimpleValidationModel(ModelState.ToErrorDictionary())); + return ValidationProblem(ModelState); //save it if (!_packagingService.SaveCreatedPackage(model)) - return ValidationErrorResult.CreateNotificationValidationErrorResult( + { + return ValidationProblem( model.Id == default ? $"A package with the name {model.Name} already exists" : $"The package with id {model.Id} was not found"); + } _packagingService.ExportCreatedPackage(model); @@ -108,7 +110,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers var fullPath = _hostingEnvironment.MapPathWebRoot(package.PackagePath); if (!System.IO.File.Exists(fullPath)) - return ValidationErrorResult.CreateNotificationValidationErrorResult("No file found for path " + package.PackagePath); + return ValidationProblem("No file found for path " + package.PackagePath); var fileName = Path.GetFileName(package.PackagePath); @@ -116,7 +118,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers var cd = new System.Net.Mime.ContentDisposition { - FileName = WebUtility.UrlEncode(fileName), + FileName = WebUtility.UrlEncode(fileName), Inline = false // false = prompt the user for downloading; true = browser to try to show the file inline }; Response.Headers.Add("Content-Disposition", cd.ToString()); @@ -132,7 +134,8 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers public ActionResult GetInstalledPackageById(int id) { var pack = _packagingService.GetInstalledPackageById(id); - if (pack == null) return NotFound(); + if (pack == null) + return NotFound(); return pack; } diff --git a/src/Umbraco.Web.BackOffice/Controllers/PackageInstallController.cs b/src/Umbraco.Web.BackOffice/Controllers/PackageInstallController.cs index 1c874732c4..4c61632ad3 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/PackageInstallController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/PackageInstallController.cs @@ -187,8 +187,8 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (installType == PackageInstallType.AlreadyInstalled) { //this package is already installed - return ValidationErrorResult.CreateNotificationValidationErrorResult( - _localizedTextService.Localize("packager/packageAlreadyInstalled")); + return ValidationProblem( + _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)); } @@ -241,8 +241,8 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (installType == PackageInstallType.AlreadyInstalled) { - return ValidationErrorResult.CreateNotificationValidationErrorResult( - _localizedTextService.Localize("packager/packageAlreadyInstalled")); + return ValidationProblem( + _localizedTextService.Localize("packager", "packageAlreadyInstalled")); } model.OriginalVersion = installType == PackageInstallType.Upgrade ? alreadyInstalled.Version : null; @@ -267,8 +267,8 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers { var packageMinVersion = packageInfo.UmbracoVersion; if (_umbracoVersion.Version < packageMinVersion) - return ValidationErrorResult.CreateNotificationValidationErrorResult( - _localizedTextService.Localize("packager/targetVersionMismatch", new[] {packageMinVersion.ToString()})); + return ValidationProblem( + _localizedTextService.Localize("packager", "targetVersionMismatch", new[] {packageMinVersion.ToString()})); } var installType = _packagingService.GetPackageInstallType(packageInfo.Name, SemVersion.Parse(packageInfo.Version), out var alreadyInstalled); @@ -286,7 +286,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers //save to the installedPackages.config, this will create a new entry with a new Id if (!_packagingService.SaveInstalledPackage(packageDefinition)) - return ValidationErrorResult.CreateNotificationValidationErrorResult("Could not save the package"); + return ValidationProblem("Could not save the package"); model.Id = packageDefinition.Id; break; diff --git a/src/Umbraco.Web.BackOffice/Controllers/RelationTypeController.cs b/src/Umbraco.Web.BackOffice/Controllers/RelationTypeController.cs index 187d59b446..9d95dee395 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/RelationTypeController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/RelationTypeController.cs @@ -154,7 +154,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers catch (Exception ex) { _logger.LogError(ex, "Error creating relation type with {Name}", relationType.Name); - return ValidationErrorResult.CreateNotificationValidationErrorResult("Error creating relation type."); + return ValidationProblem("Error creating relation type."); } } @@ -169,7 +169,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (relationTypePersisted == null) { - return ValidationErrorResult.CreateNotificationValidationErrorResult("Relation type does not exist"); + return ValidationProblem("Relation type does not exist"); } _umbracoMapper.Map(relationType, relationTypePersisted); @@ -185,7 +185,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers catch (Exception ex) { _logger.LogError(ex, "Error saving relation type with {Id}", relationType.Id); - return ValidationErrorResult.CreateNotificationValidationErrorResult("Something went wrong when saving the relation type"); + return ValidationProblem("Something went wrong when saving the relation type"); } } 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/UmbracoAuthorizedApiController.cs b/src/Umbraco.Web.BackOffice/Controllers/UmbracoAuthorizedApiController.cs index 3891550f1e..a370f48ebe 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/UmbracoAuthorizedApiController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/UmbracoAuthorizedApiController.cs @@ -1,10 +1,17 @@ using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Web.BackOffice.Filters; +using Umbraco.Cms.Web.Common.ActionsResults; 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.Extensions; namespace Umbraco.Cms.Web.BackOffice.Controllers { @@ -25,6 +32,91 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers [MiddlewareFilter(typeof(UnhandledExceptionLoggerFilter))] public abstract class UmbracoAuthorizedApiController : UmbracoApiController { + /// + /// Returns a validation problem result for the and the + /// + /// + /// + /// + /// + protected virtual ActionResult ValidationProblem(IErrorModel model, ModelStateDictionary modelStateDictionary, int statusCode = StatusCodes.Status400BadRequest) + { + model.Errors = modelStateDictionary.ToErrorDictionary(); + return ValidationProblem(model, statusCode); + } + /// + /// Overridden to return Umbraco compatible errors + /// + /// + /// + [NonAction] + public override ActionResult ValidationProblem(ModelStateDictionary modelStateDictionary) + { + return new ValidationErrorResult(new SimpleValidationModel(modelStateDictionary.ToErrorDictionary())); + + //ValidationProblemDetails problemDetails = GetValidationProblemDetails(modelStateDictionary: modelStateDictionary); + //return new ValidationErrorResult(problemDetails); + } + + // creates validation problem details instance. + // borrowed from netcore: https://github.com/dotnet/aspnetcore/blob/main/src/Mvc/Mvc.Core/src/ControllerBase.cs#L1970 + protected ValidationProblemDetails GetValidationProblemDetails( + string detail = null, + string instance = null, + int? statusCode = null, + string title = null, + string type = null, + [ActionResultObjectValue] ModelStateDictionary modelStateDictionary = null) + { + modelStateDictionary ??= ModelState; + + ValidationProblemDetails validationProblem; + if (ProblemDetailsFactory == null) + { + // ProblemDetailsFactory may be null in unit testing scenarios. Improvise to make this more testable. + validationProblem = new ValidationProblemDetails(modelStateDictionary) + { + Detail = detail, + Instance = instance, + Status = statusCode, + Title = title, + Type = type, + }; + } + else + { + validationProblem = ProblemDetailsFactory?.CreateValidationProblemDetails( + HttpContext, + modelStateDictionary, + statusCode: statusCode, + title: title, + type: type, + detail: detail, + instance: instance); + } + + return validationProblem; + } + + /// + /// Returns an Umbraco compatible validation problem for the given error message + /// + /// + /// + protected virtual ActionResult ValidationProblem(string errorMessage) + { + ValidationProblemDetails problemDetails = GetValidationProblemDetails(errorMessage); + return new ValidationErrorResult(problemDetails); + } + + /// + /// Returns an Umbraco compatible validation problem for the object result + /// + /// + /// + /// + protected virtual ActionResult ValidationProblem(object value, int statusCode = StatusCodes.Status400BadRequest) + => new ValidationErrorResult(value, statusCode); } } diff --git a/src/Umbraco.Web.BackOffice/Controllers/UserGroupsController.cs b/src/Umbraco.Web.BackOffice/Controllers/UserGroupsController.cs index f5cdb94d37..ad8d11f95a 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/UserGroupsController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/UserGroupsController.cs @@ -10,7 +10,6 @@ using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; -using Umbraco.Cms.Web.BackOffice.ActionResults; using Umbraco.Cms.Web.BackOffice.Filters; using Umbraco.Cms.Web.Common.Attributes; using Umbraco.Cms.Web.Common.Authorization; @@ -22,7 +21,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] [Authorize(Policy = AuthorizationPolicies.SectionAccessUsers)] [PrefixlessBodyModelValidator] - public class UserGroupsController : UmbracoAuthorizedJsonController + public class UserGroupsController : BackOfficeNotificationsController { private readonly IUserService _userService; private readonly IContentService _contentService; @@ -118,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 +201,11 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers _userService.DeleteUserGroup(userGroup); } if (userGroups.Length > 1) - return new UmbracoNotificationSuccessResponse( - _localizedTextService.Localize("speechBubbles/deleteUserGroupsSuccess", new[] {userGroups.Length.ToString()})); - return new UmbracoNotificationSuccessResponse( - _localizedTextService.Localize("speechBubbles/deleteUserGroupSuccess", new[] {userGroups[0].Name})); + { + return Ok(_localizedTextService.Localize("speechBubbles","deleteUserGroupsSuccess", new[] {userGroups.Length.ToString()})); + } + + 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 ee09b7d67b..3d08d2dab1 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs @@ -51,7 +51,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers [Authorize(Policy = AuthorizationPolicies.SectionAccessUsers)] [PrefixlessBodyModelValidator] [IsCurrentUserModelFilter] - public class UsersController : UmbracoAuthorizedJsonController + public class UsersController : BackOfficeNotificationsController { private readonly MediaFileManager _mediaFileManager; private readonly ContentSettings _contentSettings; @@ -128,7 +128,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers { var urls = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.GetUserAvatarUrls(_appCaches.RuntimeCache, _mediaFileManager, _imageUrlGenerator); if (urls == null) - return new ValidationErrorResult("Could not access Gravatar endpoint"); + return ValidationProblem("Could not access Gravatar endpoint"); return urls; } @@ -345,7 +345,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (ModelState.IsValid == false) { - return new ValidationErrorResult(new SimpleValidationModel(ModelState.ToErrorDictionary())); + return ValidationProblem(ModelState); } if (_securitySettings.UsernameIsEmail) @@ -362,7 +362,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (ModelState.IsValid == false) { - return new ValidationErrorResult(new SimpleValidationModel(ModelState.ToErrorDictionary())); + return ValidationProblem(ModelState); } //Perform authorization here to see if the current user can actually save this user with the info being requested @@ -380,7 +380,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers var created = await _userManager.CreateAsync(identityUser); if (created.Succeeded == false) { - return ValidationErrorResult.CreateNotificationValidationErrorResult(created.Errors.ToErrorMessage()); + return ValidationProblem(created.Errors.ToErrorMessage()); } string resetPassword; @@ -389,7 +389,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers var result = await _userManager.AddPasswordAsync(identityUser, password); if (result.Succeeded == false) { - return ValidationErrorResult.CreateNotificationValidationErrorResult(created.Errors.ToErrorMessage()); + return ValidationProblem(created.Errors.ToErrorMessage()); } resetPassword = password; @@ -446,19 +446,19 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (ModelState.IsValid == false) { - return new ValidationErrorResult(new SimpleValidationModel(ModelState.ToErrorDictionary())); + return ValidationProblem(ModelState); } if (!_emailSender.CanSendRequiredEmail()) { - return new ValidationErrorResult("No Email server is configured"); + return ValidationProblem("No Email server is configured"); } //Perform authorization here to see if the current user can actually save this user with the info being requested var canSaveUser = _userEditorAuthorizationHelper.IsAuthorized(_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser, user, null, null, userSave.UserGroups); if (canSaveUser == false) { - return new ValidationErrorResult(canSaveUser.Result, StatusCodes.Status401Unauthorized); + return ValidationProblem(canSaveUser.Result, StatusCodes.Status401Unauthorized); } if (user == null) @@ -471,7 +471,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers var created = await _userManager.CreateAsync(identityUser); if (created.Succeeded == false) { - return ValidationErrorResult.CreateNotificationValidationErrorResult(created.Errors.ToErrorMessage()); + return ValidationProblem(created.Errors.ToErrorMessage()); } //now re-look the user back up @@ -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; } @@ -513,7 +513,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers ModelState.AddModelError( _securitySettings.UsernameIsEmail ? "Email" : "Username", "A user with the username already exists"); - return new ValidationErrorResult(new SimpleValidationModel(ModelState.ToErrorDictionary())); + return ValidationProblem(ModelState); } return new ActionResult(user); @@ -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 }); @@ -568,7 +568,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (ModelState.IsValid == false) { - return new ValidationErrorResult(new SimpleValidationModel(ModelState.ToErrorDictionary())); + return ValidationProblem(ModelState); } var intId = userSave.Id.TryConvertTo(); @@ -631,7 +631,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers } if (hasErrors) - return new ValidationErrorResult(new SimpleValidationModel(ModelState.ToErrorDictionary())); + return ValidationProblem(ModelState); //merge the save data onto the user var user = _umbracoMapper.Map(userSave, found); @@ -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; } @@ -664,7 +664,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (ModelState.IsValid == false) { - return new ValidationErrorResult(new SimpleValidationModel(ModelState.ToErrorDictionary())); + return ValidationProblem(ModelState); } Attempt intId = changingPasswordModel.Id.TryConvertTo(); @@ -684,12 +684,12 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers // if it's the current user, the current user cannot reset their own password without providing their old password if (currentUser.Username == found.Username && string.IsNullOrEmpty(changingPasswordModel.OldPassword)) { - return ValidationErrorResult.CreateNotificationValidationErrorResult("Password reset is not allowed without providing old password"); + return ValidationProblem("Password reset is not allowed without providing old password"); } if (!currentUser.IsAdmin() && found.IsAdmin()) { - return ValidationErrorResult.CreateNotificationValidationErrorResult("The current user cannot change the password for the specified user"); + return ValidationProblem("The current user cannot change the password for the specified user"); } Attempt passwordChangeResult = await _passwordChanger.ChangePasswordWithIdentityAsync(changingPasswordModel, _userManager); @@ -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; } @@ -706,7 +706,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers ModelState.AddModelError(memberName, passwordChangeResult.Result.ChangeError.ErrorMessage); } - return new ValidationErrorResult(new SimpleValidationModel(ModelState.ToErrorDictionary())); + return ValidationProblem(ModelState); } @@ -720,7 +720,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers var tryGetCurrentUserId = _backofficeSecurityAccessor.BackOfficeSecurity.GetUserId(); if (tryGetCurrentUserId && userIds.Contains(tryGetCurrentUserId.Result)) { - return ValidationErrorResult.CreateNotificationValidationErrorResult("The current user cannot disable itself"); + return ValidationProblem("The current user cannot disable itself"); } var users = _userService.GetUsersById(userIds).ToArray(); @@ -733,12 +733,10 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (users.Length > 1) { - return new UmbracoNotificationSuccessResponse( - _localizedTextService.Localize("speechBubbles/disableUsersSuccess", new[] {userIds.Length.ToString()})); + return Ok(_localizedTextService.Localize("speechBubbles","disableUsersSuccess", new[] {userIds.Length.ToString()})); } - return new UmbracoNotificationSuccessResponse( - _localizedTextService.Localize("speechBubbles/disableUserSuccess", new[] { users[0].Name })); + return Ok(_localizedTextService.Localize("speechBubbles","disableUserSuccess", new[] { users[0].Name })); } /// @@ -757,12 +755,12 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (users.Length > 1) { - return new UmbracoNotificationSuccessResponse( - _localizedTextService.Localize("speechBubbles/enableUsersSuccess", new[] { userIds.Length.ToString() })); + return Ok( + _localizedTextService.Localize("speechBubbles","enableUsersSuccess", new[] { userIds.Length.ToString() })); } - return new UmbracoNotificationSuccessResponse( - _localizedTextService.Localize("speechBubbles/enableUserSuccess", new[] { users[0].Name })); + return Ok( + _localizedTextService.Localize("speechBubbles","enableUserSuccess", new[] { users[0].Name })); } /// @@ -787,19 +785,19 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers var unlockResult = await _userManager.SetLockoutEndDateAsync(user, DateTimeOffset.Now.AddMinutes(-1)); if (unlockResult.Succeeded == false) { - return new ValidationErrorResult( + return ValidationProblem( $"Could not unlock for user {u} - error {unlockResult.Errors.ToErrorMessage()}"); } if (userIds.Length == 1) { - return new UmbracoNotificationSuccessResponse( - _localizedTextService.Localize("speechBubbles/unlockUserSuccess", new[] {user.Name})); + return Ok( + _localizedTextService.Localize("speechBubbles","unlockUserSuccess", new[] {user.Name})); } } - return new UmbracoNotificationSuccessResponse( - _localizedTextService.Localize("speechBubbles/unlockUsersSuccess", new[] {(userIds.Length - notFound.Count).ToString()})); + return Ok( + _localizedTextService.Localize("speechBubbles","unlockUsersSuccess", new[] {(userIds.Length - notFound.Count).ToString()})); } [Authorize(Policy = AuthorizationPolicies.AdminUserEditsRequireAdmin)] @@ -816,8 +814,8 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers } } _userService.Save(users); - return new UmbracoNotificationSuccessResponse( - _localizedTextService.Localize("speechBubbles/setUserGroupOnUsersSuccess")); + return Ok( + _localizedTextService.Localize("speechBubbles","setUserGroupOnUsersSuccess")); } /// @@ -847,8 +845,8 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers var userName = user.Name; _userService.Delete(user, true); - return new UmbracoNotificationSuccessResponse( - _localizedTextService.Localize("speechBubbles/deleteUserSuccess", new[] { userName })); + return Ok( + _localizedTextService.Localize("speechBubbles","deleteUserSuccess", new[] { userName })); } public class PagedUserResult : PagedResult diff --git a/src/Umbraco.Web.BackOffice/Extensions/ModelStateExtensions.cs b/src/Umbraco.Web.BackOffice/Extensions/ModelStateExtensions.cs index a48f46f605..a08cd20071 100644 --- a/src/Umbraco.Web.BackOffice/Extensions/ModelStateExtensions.cs +++ b/src/Umbraco.Web.BackOffice/Extensions/ModelStateExtensions.cs @@ -6,18 +6,11 @@ using Microsoft.AspNetCore.Mvc.ModelBinding; using Umbraco.Cms.Web.BackOffice.PropertyEditors.Validation; using Umbraco.Extensions; -namespace Umbraco.Cms.Web.BackOffice.Extensions +namespace Umbraco.Extensions { public static class ModelStateExtensions { - /// - /// Checks if there are any model errors on any fields containing the prefix - /// - /// - /// - /// - public static bool IsValid(this ModelStateDictionary state, string prefix) => - state.Where(v => v.Key.StartsWith(prefix + ".")).All(v => !v.Value.Errors.Any()); + /// /// Adds the to the model state with the appropriate keys for property errors @@ -171,42 +164,5 @@ namespace Umbraco.Cms.Web.BackOffice.Extensions } } - - public static IDictionary ToErrorDictionary(this ModelStateDictionary modelState) - { - var modelStateError = new Dictionary(); - foreach (KeyValuePair keyModelStatePair in modelState) - { - var key = keyModelStatePair.Key; - ModelErrorCollection errors = keyModelStatePair.Value.Errors; - if (errors != null && errors.Count > 0) - { - modelStateError.Add(key, errors.Select(error => error.ErrorMessage)); - } - } - return modelStateError; - } - - /// - /// Serializes the ModelState to JSON for JavaScript to interrogate the errors - /// - /// - /// - public static JsonResult ToJsonErrors(this ModelStateDictionary state) => - new JsonResult(new - { - success = state.IsValid.ToString().ToLower(), - failureType = "ValidationError", - validationErrors = from e in state - where e.Value.Errors.Count > 0 - select new - { - name = e.Key, - errors = e.Value.Errors.Select(x => x.ErrorMessage) - .Concat( - e.Value.Errors.Where(x => x.Exception != null).Select(x => x.Exception.Message)) - } - }); - } } 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/ActionsResults/UmbracoProblemResult.cs b/src/Umbraco.Web.Common/ActionsResults/UmbracoProblemResult.cs index e3279407fa..92f5d7aed7 100644 --- a/src/Umbraco.Web.Common/ActionsResults/UmbracoProblemResult.cs +++ b/src/Umbraco.Web.Common/ActionsResults/UmbracoProblemResult.cs @@ -1,8 +1,9 @@ -using System.Net; +using System.Net; using Microsoft.AspNetCore.Mvc; namespace Umbraco.Cms.Web.Common.ActionsResults { + // TODO: What is the purpose of this? Doesn't seem to add any benefit public class UmbracoProblemResult : ObjectResult { public UmbracoProblemResult(string message, HttpStatusCode httpStatusCode = HttpStatusCode.InternalServerError) : base(new {Message = message}) diff --git a/src/Umbraco.Web.Common/ActionsResults/ValidationErrorResult.cs b/src/Umbraco.Web.Common/ActionsResults/ValidationErrorResult.cs index 8fe0ef9326..378be18440 100644 --- a/src/Umbraco.Web.Common/ActionsResults/ValidationErrorResult.cs +++ b/src/Umbraco.Web.Common/ActionsResults/ValidationErrorResult.cs @@ -1,10 +1,19 @@ -using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Extensions; namespace Umbraco.Cms.Web.Common.ActionsResults { + // TODO: This should probably follow the same conventions as in aspnet core and use ProblemDetails + // and ProblemDetails factory. See https://github.com/dotnet/aspnetcore/blob/main/src/Mvc/Mvc.Core/src/ControllerBase.cs#L1977 + // ProblemDetails is explicitly checked for in the application model. + // In our base class UmbracoAuthorizedApiController the logic is there to create a ProblemDetails. + // However, to do this will require changing how angular deals with errors since the response will + // probably be different. Would just be better to follow the aspnet patterns. + /// /// Custom result to return a validation error message with required headers /// @@ -13,6 +22,11 @@ namespace Umbraco.Cms.Web.Common.ActionsResults /// public class ValidationErrorResult : ObjectResult { + /// + /// Typically this should not be used and just use the ValidationProblem method on the base controller class. + /// + /// + /// public static ValidationErrorResult CreateNotificationValidationErrorResult(string errorMessage) { var notificationModel = new SimpleNotificationModel @@ -23,6 +37,9 @@ namespace Umbraco.Cms.Web.Common.ActionsResults return new ValidationErrorResult(notificationModel); } + public ValidationErrorResult(ModelStateDictionary modelState) + : this(new SimpleValidationModel(modelState.ToErrorDictionary())) { } + public ValidationErrorResult(object value, int statusCode) : base(value) { StatusCode = statusCode; @@ -32,6 +49,7 @@ namespace Umbraco.Cms.Web.Common.ActionsResults { } + // TODO: Like here, shouldn't we use ProblemDetails? public ValidationErrorResult(string errorMessage, int statusCode) : base(new { Message = errorMessage }) { StatusCode = statusCode; diff --git a/src/Umbraco.Web.Common/Extensions/EndpointRouteBuilderExtensions.cs b/src/Umbraco.Web.Common/Extensions/EndpointRouteBuilderExtensions.cs index d1de1a2248..c7b8190523 100644 --- a/src/Umbraco.Web.Common/Extensions/EndpointRouteBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/EndpointRouteBuilderExtensions.cs @@ -122,5 +122,27 @@ namespace Umbraco.Extensions true, constraints); } + + public static void MapUmbracoSurfaceRoute( + this IEndpointRouteBuilder endpoints, + Type controllerType, + string rootSegment, + string areaName, + string defaultAction = "Index", + bool includeControllerNameInRoute = true, + object constraints = null) + { + // If there is an area name it's a plugin controller, and we should use the area name instead of surface + string prefixPathSegment = areaName.IsNullOrWhiteSpace() ? "Surface" : areaName; + + endpoints.MapUmbracoRoute( + controllerType, + rootSegment, + areaName, + prefixPathSegment, + defaultAction, + includeControllerNameInRoute, + constraints); + } } } 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/FriendlyPublishedElementExtensions.cs b/src/Umbraco.Web.Common/Extensions/FriendlyPublishedElementExtensions.cs index bbb896fe8b..c7da62f6db 100644 --- a/src/Umbraco.Web.Common/Extensions/FriendlyPublishedElementExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/FriendlyPublishedElementExtensions.cs @@ -1,4 +1,6 @@ -using Microsoft.Extensions.DependencyInjection; +using System; +using System.Linq.Expressions; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Web.Common.DependencyInjection; @@ -70,5 +72,11 @@ namespace Umbraco.Extensions public static bool IsVisible(this IPublishedElement content) => content.IsVisible(PublishedValueFallback); + /// + /// Gets the value of a property. + /// + public static TValue ValueFor(this TModel model, Expression> property, string culture = null, string segment = null, Fallback fallback = default, TValue defaultValue = default) + where TModel : IPublishedElement => + model.ValueFor(PublishedValueFallback, property, culture, segment, fallback); } } 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/Extensions/ModelStateExtensions.cs b/src/Umbraco.Web.Common/Extensions/ModelStateExtensions.cs new file mode 100644 index 0000000000..acc2858ece --- /dev/null +++ b/src/Umbraco.Web.Common/Extensions/ModelStateExtensions.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; + +namespace Umbraco.Extensions +{ + public static class ModelStateExtensions + { + /// + /// Checks if there are any model errors on any fields containing the prefix + /// + /// + /// + /// + public static bool IsValid(this ModelStateDictionary state, string prefix) => + state.Where(v => v.Key.StartsWith(prefix + ".")).All(v => !v.Value.Errors.Any()); + + public static IDictionary ToErrorDictionary(this ModelStateDictionary modelState) + { + var modelStateError = new Dictionary(); + foreach (KeyValuePair keyModelStatePair in modelState) + { + var key = keyModelStatePair.Key; + ModelErrorCollection errors = keyModelStatePair.Value.Errors; + if (errors != null && errors.Count > 0) + { + modelStateError.Add(key, errors.Select(error => error.ErrorMessage)); + } + } + return modelStateError; + } + + /// + /// Serializes the ModelState to JSON for JavaScript to interrogate the errors + /// + /// + /// + public static JsonResult ToJsonErrors(this ModelStateDictionary state) => + new JsonResult(new + { + success = state.IsValid.ToString().ToLower(), + failureType = "ValidationError", + validationErrors = from e in state + where e.Value.Errors.Count > 0 + select new + { + name = e.Key, + errors = e.Value.Errors.Select(x => x.ErrorMessage) + .Concat( + e.Value.Errors.Where(x => x.Exception != null).Select(x => x.Exception.Message)) + } + }); + } +} diff --git a/src/Umbraco.Web.Common/Extensions/UrlHelperExtensions.cs b/src/Umbraco.Web.Common/Extensions/UrlHelperExtensions.cs index aecc36889a..d3fd3bddba 100644 --- a/src/Umbraco.Web.Common/Extensions/UrlHelperExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/UrlHelperExtensions.cs @@ -10,6 +10,7 @@ using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PropertyEditors.ValueConverters; using Umbraco.Cms.Core.WebAssets; using Umbraco.Cms.Web.Common.Controllers; @@ -204,6 +205,56 @@ namespace Umbraco.Extensions return $"{version}.{runtimeMinifier.CacheBuster}".GenerateHash(); } + public static IHtmlContent GetCropUrl(this IUrlHelper urlHelper, IPublishedContent mediaItem, string cropAlias, bool htmlEncode = true) + { + if (mediaItem == null) + { + return HtmlString.Empty; + } + + var url = mediaItem.GetCropUrl(cropAlias: cropAlias, useCropDimensions: true); + return htmlEncode ? new HtmlString(HttpUtility.HtmlEncode(url)) : new HtmlString(url); + } + + public static IHtmlContent GetCropUrl(this IUrlHelper urlHelper, IPublishedContent mediaItem, string propertyAlias, string cropAlias, bool htmlEncode = true) + { + if (mediaItem == null) + { + return HtmlString.Empty; + } + + var url = mediaItem.GetCropUrl(propertyAlias: propertyAlias, cropAlias: cropAlias, useCropDimensions: true); + return htmlEncode ? new HtmlString(HttpUtility.HtmlEncode(url)) : new HtmlString(url); + } + + public static IHtmlContent GetCropUrl(this IUrlHelper urlHelper, + IPublishedContent mediaItem, + 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, + bool htmlEncode = true) + { + if (mediaItem == null) + { + return HtmlString.Empty; + } + + var url = mediaItem.GetCropUrl(width, height, propertyAlias, cropAlias, quality, imageCropMode, + imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBuster, furtherOptions, ratioMode, + upScale); + return htmlEncode ? new HtmlString(HttpUtility.HtmlEncode(url)) : new HtmlString(url); + } + public static IHtmlContent GetCropUrl(this IUrlHelper urlHelper, ImageCropperValue imageCropperValue, string cropAlias, 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/resources/content.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js index 440d589841..8377b8779b 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js @@ -688,6 +688,24 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { }); }, + getScaffoldByKeys: function (parentId, scaffoldKeys) { + + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "GetEmptyByKeys", + { contentTypeKeys: scaffoldKeys, parentId: parentId })), + 'Failed to retrieve data for empty content items ids' + scaffoldKeys.join(", ")) + .then(function (result) { + Object.keys(result).map(function(key) { + result[key] = umbDataFormatter.formatContentGetData(result[key]); + }); + + return $q.when(result); + }); + }, + getBlueprintScaffold: function (parentId, blueprintId) { return umbRequestHelper.resourcePromise( diff --git a/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js b/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js index 2ee7d513ca..4bcbbc89d6 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js @@ -419,18 +419,18 @@ // removing duplicates. scaffoldKeys = scaffoldKeys.filter((value, index, self) => self.indexOf(value) === index); - scaffoldKeys.forEach(contentTypeKey => { - tasks.push(contentResource.getScaffoldByKey(-20, contentTypeKey).then(scaffold => { + tasks.push(contentResource.getScaffoldByKeys(-20, scaffoldKeys).then(scaffolds => { + Object.values(scaffolds).forEach(scaffold => { // self.scaffolds might not exists anymore, this happens if this instance has been destroyed before the load is complete. if (self.scaffolds) { self.scaffolds.push(formatScaffoldData(scaffold)); } - }).catch( - () => { - // Do nothing if we get an error. - } - )); - }); + }); + }).catch( + () => { + // Do nothing if we get an error. + } + )); return $q.all(tasks); }, 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/less/components/umb-packages.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-packages.less index 0045bed140..f660da8f8b 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-packages.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-packages.less @@ -157,6 +157,16 @@ color: @red !important; } +.umb-package-link .umb-package-cloud { + margin-top: 8px; + font-size: 11px; + height: 11px; // ensures vertical space is taken up even if "works on cloud" isn't visible +} + +.umb-package-link .umb-package-cloud .icon-cloud { + color: #2eadaf !important; +} + // Version .umb-package-version { display: inline-flex; diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.controller.js index 029dedf214..0b9c59f2da 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.controller.js @@ -477,9 +477,15 @@ angular.module("umbraco") vm.loading = true; entityResource.getPagedDescendants($scope.filterOptions.excludeSubFolders ? $scope.currentFolder.id : $scope.startNodeId, "Media", vm.searchOptions) .then(function (data) { + // update image data to work with image grid if (data.items) { - data.items.forEach(mediaItem => setMediaMetaData(mediaItem)); + var allowedTypes = dialogOptions.filter ? dialogOptions.filter.split(",") : null; + + data.items.forEach(function(mediaItem) { + setMediaMetaData(mediaItem); + mediaItem.filtered = allowedTypes && allowedTypes.indexOf(mediaItem.metaData.ContentTypeAlias) < 0; + }); } // update images 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/packages/views/repo.controller.js b/src/Umbraco.Web.UI.Client/src/views/packages/views/repo.controller.js index 4a2a3f29a7..90cfc9d2f7 100644 --- a/src/Umbraco.Web.UI.Client/src/views/packages/views/repo.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/packages/views/repo.controller.js @@ -1,7 +1,7 @@ (function () { "use strict"; - function PackagesRepoController($scope, $route, $location, $timeout, ourPackageRepositoryResource, $q, packageResource, localStorageService, localizationService) { + function PackagesRepoController($scope, $timeout, ourPackageRepositoryResource, $q, packageResource, localStorageService, localizationService) { var vm = this; @@ -75,7 +75,9 @@ $q.all([ ourPackageRepositoryResource.getCategories() .then(function (cats) { - vm.categories = cats; + vm.categories = cats.filter(function (cat) { + return cat.name !== "Umbraco Pro"; + }); }), ourPackageRepositoryResource.getPopular(8) .then(function (pack) { diff --git a/src/Umbraco.Web.UI.Client/src/views/packages/views/repo.html b/src/Umbraco.Web.UI.Client/src/views/packages/views/repo.html index 42b538f9ad..d467c513ec 100644 --- a/src/Umbraco.Web.UI.Client/src/views/packages/views/repo.html +++ b/src/Umbraco.Web.UI.Client/src/views/packages/views/repo.html @@ -54,14 +54,20 @@
- + {{package.downloads}} - + {{package.likes}}
+
+
+ + Verified to work on Umbraco Cloud +
+
@@ -93,18 +99,24 @@
- - {{ package.downloads }} + + {{package.downloads}} - - {{ package.likes }} + + {{package.likes}}
+
+
+ + Verified to work on Umbraco Cloud +
+
- + @@ -269,6 +281,10 @@
{{vm.package.likes}}
+
+
Verified to work on Umbraco CLoud
+
+ 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 cc861dcb4b..0000000000 --- a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/obsoletemediapickernotice.html +++ /dev/null @@ -1 +0,0 @@ -

Important: switching from the (Obsolete) Media Picker to Media Picker will mean all data (references to previously selected media items) will be deleted and no longer available.

\ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.prevalues.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.prevalues.controller.js index 9614eabb7e..c0bc493cd3 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.prevalues.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.prevalues.controller.js @@ -107,7 +107,7 @@ angular.module("umbraco") currentLayout: Utilities.copy(template), rows: $scope.model.value.layouts, columns: $scope.model.value.columns, - view: "views/propertyEditors/grid/dialogs/layoutconfig.html", + view: "views/propertyeditors/grid/dialogs/layoutconfig.html", size: "small", submit: function (model) { if (index === -1) { @@ -148,7 +148,7 @@ angular.module("umbraco") currentRow: Utilities.copy(layout), editors: $scope.editors, columns: $scope.model.value.columns, - view: "views/propertyEditors/grid/dialogs/rowconfig.html", + view: "views/propertyeditors/grid/dialogs/rowconfig.html", size: "small", submit: function (model) { if (index === -1) { @@ -169,7 +169,7 @@ angular.module("umbraco") function deleteLayout(layout, index, event) { const dialog = { - view: "views/propertyEditors/grid/overlays/rowdeleteconfirm.html", + view: "views/propertyeditors/grid/overlays/rowdeleteconfirm.html", layout: layout, submitButtonLabelKey: "contentTypeEditor_yesDelete", submitButtonStyle: "danger", 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.Client/test/unit/common/services/block-editor-service.spec.js b/src/Umbraco.Web.UI.Client/test/unit/common/services/block-editor-service.spec.js index 3c0cad32dd..ee9dd9d901 100644 --- a/src/Umbraco.Web.UI.Client/test/unit/common/services/block-editor-service.spec.js +++ b/src/Umbraco.Web.UI.Client/test/unit/common/services/block-editor-service.spec.js @@ -22,10 +22,10 @@ $timeout = _$timeout_; contentResource = $injector.get("contentResource"); - spyOn(contentResource, "getScaffoldByKey").and.callFake( + spyOn(contentResource, "getScaffoldByKeys").and.callFake( function () { var scaffold = mocksUtils.getMockVariantContent(1234, contentKey, contentUdi); - return $q.resolve(scaffold); + return $q.resolve([scaffold]); } ); // this seems to be required because of the poor promise implementation in localizationService (see TODO in that service) diff --git a/src/Umbraco.Web.UI.NetCore/umbraco/config/lang/en.xml b/src/Umbraco.Web.UI.NetCore/umbraco/config/lang/en.xml index 7da252c9d0..47cd42063e 100644 --- a/src/Umbraco.Web.UI.NetCore/umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI.NetCore/umbraco/config/lang/en.xml @@ -1291,6 +1291,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont All done, your browser will now refresh, please wait... Please click 'Finish' to complete installation and reload the page. Uploading package... + Verified to work on Umbraco Cloud Paste with full formatting (Not recommended) diff --git a/src/Umbraco.Web.UI.NetCore/umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI.NetCore/umbraco/config/lang/en_us.xml index 2db8db4ae2..18996b2f48 100644 --- a/src/Umbraco.Web.UI.NetCore/umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI.NetCore/umbraco/config/lang/en_us.xml @@ -1304,6 +1304,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont All done, your browser will now refresh, please wait... Please click 'Finish' to complete installation and reload the page. Uploading package... + Verified to work on Umbraco Cloud Paste with full formatting (Not recommended) 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.Website/Routing/FrontEndRoutes.cs b/src/Umbraco.Web.Website/Routing/FrontEndRoutes.cs index 8f7fad9864..df25f4b66e 100644 --- a/src/Umbraco.Web.Website/Routing/FrontEndRoutes.cs +++ b/src/Umbraco.Web.Website/Routing/FrontEndRoutes.cs @@ -65,11 +65,10 @@ namespace Umbraco.Cms.Web.Website.Routing // exclude front-end api controllers PluginControllerMetadata meta = PluginController.GetMetadata(controller); - endpoints.MapUmbracoRoute( + endpoints.MapUmbracoSurfaceRoute( meta.ControllerType, _umbracoPathSegment, - meta.AreaName, - "Surface"); + meta.AreaName); } } 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..244845cd12 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,22 @@ + + Properties\SolutionInfo.cs + + + + + + + + + + + + + + @@ -158,6 +186,7 @@ + @@ -241,4 +270,5 @@ + 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;