diff --git a/.github/ISSUE_TEMPLATE/01_bug_report.yml b/.github/ISSUE_TEMPLATE/01_bug_report.yml
index 04d1a0e04c..800ac53e68 100644
--- a/.github/ISSUE_TEMPLATE/01_bug_report.yml
+++ b/.github/ISSUE_TEMPLATE/01_bug_report.yml
@@ -6,7 +6,7 @@ body:
- type: input
id: "version"
attributes:
- label: "Which Umbraco version are you using?"
+ label: "Which *exact* Umbraco version are you using? For example: 8.13.1 - don't just write v8"
description: "Use the help icon in the Umbraco backoffice to find the version you're using"
validations:
required: true
diff --git a/src/Umbraco.Core/Configuration/ContentDashboardSettings.cs b/src/Umbraco.Core/Configuration/ContentDashboardSettings.cs
new file mode 100644
index 0000000000..6840d974aa
--- /dev/null
+++ b/src/Umbraco.Core/Configuration/ContentDashboardSettings.cs
@@ -0,0 +1,23 @@
+
+namespace Umbraco.Core.Dashboards
+{
+ public class ContentDashboardSettings
+ {
+ private const string DefaultContentDashboardPath = "cms";
+
+ ///
+ /// Gets a value indicating whether the content dashboard should be available to all users.
+ ///
+ ///
+ /// true if the dashboard is visible for all user groups; otherwise, false
+ /// and the default access rules for that dashboard will be in use.
+ ///
+ public bool AllowContentDashboardAccessToAllUsers { get; set; } = true;
+
+ ///
+ /// Gets the path to use when constructing the URL for retrieving data for the content dashboard.
+ ///
+ /// The URL path.
+ public string ContentDashboardPath { get; set; } = DefaultContentDashboardPath;
+ }
+}
diff --git a/src/Umbraco.Core/Constants-SqlTemplates.cs b/src/Umbraco.Core/Constants-SqlTemplates.cs
index c6bee5ca4f..a2fe501ab3 100644
--- a/src/Umbraco.Core/Constants-SqlTemplates.cs
+++ b/src/Umbraco.Core/Constants-SqlTemplates.cs
@@ -1,4 +1,4 @@
-namespace Umbraco.Cms.Core
+namespace Umbraco.Cms.Core
{
public static partial class Constants
{
@@ -30,7 +30,7 @@ namespace Umbraco.Cms.Core
public const string WhereNodeId = "Umbraco.Web.PublishedCache.NuCache.DataSource.WhereNodeId";
public const string WhereNodeIdX = "Umbraco.Web.PublishedCache.NuCache.DataSource.WhereNodeIdX";
public const string SourcesSelectUmbracoNodeJoin = "Umbraco.Web.PublishedCache.NuCache.DataSource.SourcesSelectUmbracoNodeJoin";
- public const string ContentSourcesSelect = "Umbraco.Web.PublishedCache.NuCache.DataSource.ContentSourcesSelect";
+ public const string ContentSourcesSelect = "Umbraco.Web.PublishedCache.NuCache.DataSource.ContentSourcesSelect";
public const string ContentSourcesCount = "Umbraco.Web.PublishedCache.NuCache.DataSource.ContentSourcesCount";
public const string MediaSourcesSelect = "Umbraco.Web.PublishedCache.NuCache.DataSource.MediaSourcesSelect";
public const string MediaSourcesCount = "Umbraco.Web.PublishedCache.NuCache.DataSource.MediaSourcesCount";
diff --git a/src/Umbraco.Core/Events/UserNotificationsHandler.cs b/src/Umbraco.Core/Events/UserNotificationsHandler.cs
index f5172d3e28..45be01ce65 100644
--- a/src/Umbraco.Core/Events/UserNotificationsHandler.cs
+++ b/src/Umbraco.Core/Events/UserNotificationsHandler.cs
@@ -188,12 +188,12 @@ namespace Umbraco.Cms.Core.Events
siteUri,
((IUser user, NotificationEmailSubjectParams subject) x)
=> _textService.Localize(
- "notifications/mailSubject",
+ "notifications", "mailSubject",
x.user.GetUserCulture(_textService, _globalSettings),
new[] { x.subject.SiteUrl, x.subject.Action, x.subject.ItemName }),
((IUser user, NotificationEmailBodyParams body, bool isHtml) x)
=> _textService.Localize(
- x.isHtml ? "notifications/mailBodyHtml" : "notifications/mailBody",
+ "notifications", x.isHtml ? "mailBodyHtml" : "mailBody",
x.user.GetUserCulture(_textService, _globalSettings),
new[]
{
@@ -205,7 +205,7 @@ namespace Umbraco.Cms.Core.Events
x.body.ItemId,
//format the summary depending on if it's variant or not
contentVariantGroup.Key == ContentVariation.Culture
- ? (x.isHtml ? _textService.Localize("notifications/mailBodyVariantHtmlSummary", new[]{ x.body.Summary }) : _textService.Localize("notifications/mailBodyVariantSummary", new []{ x.body.Summary }))
+ ? (x.isHtml ? _textService.Localize("notifications", "mailBodyVariantHtmlSummary", new[]{ x.body.Summary }) : _textService.Localize("notifications","mailBodyVariantSummary", new []{ x.body.Summary }))
: x.body.Summary,
x.body.ItemUrl
}));
diff --git a/src/Umbraco.Core/Extensions/PublishedPropertyExtension.cs b/src/Umbraco.Core/Extensions/PublishedPropertyExtension.cs
index c559fc9a74..a36942862a 100644
--- a/src/Umbraco.Core/Extensions/PublishedPropertyExtension.cs
+++ b/src/Umbraco.Core/Extensions/PublishedPropertyExtension.cs
@@ -31,10 +31,17 @@ namespace Umbraco.Extensions
{
// we have a value
// try to cast or convert it
- var value = property.GetValue(culture, segment);
- if (value is T valueAsT) return valueAsT;
+ var value = property.GetValue(culture, segment);
+ if (value is T valueAsT)
+ {
+ return valueAsT;
+ }
+
var valueConverted = value.TryConvertTo();
- if (valueConverted) return valueConverted.Result;
+ if (valueConverted)
+ {
+ return valueConverted.Result;
+ }
// cannot cast nor convert the value, nothing we can return but 'default'
// note: we don't want to fallback in that case - would make little sense
@@ -43,14 +50,23 @@ namespace Umbraco.Extensions
// we don't have a value, try fallback
if (publishedValueFallback.TryGetValue(property, culture, segment, fallback, defaultValue, out var fallbackValue))
+ {
return fallbackValue;
+ }
// we don't have a value - neither direct nor fallback
// give a chance to the converter to return something (eg empty enumerable)
var noValue = property.GetValue(culture, segment);
- if (noValue is T noValueAsT) return noValueAsT;
+ if (noValue is T noValueAsT)
+ {
+ return noValueAsT;
+ }
+
var noValueConverted = noValue.TryConvertTo();
- if (noValueConverted) return noValueConverted.Result;
+ if (noValueConverted)
+ {
+ return noValueConverted.Result;
+ }
// cannot cast noValue nor convert it, nothing we can return but 'default'
return default;
diff --git a/src/Umbraco.Core/HealthChecks/Checks/AbstractSettingsCheck.cs b/src/Umbraco.Core/HealthChecks/Checks/AbstractSettingsCheck.cs
index 99b2ef2f97..7123255b0d 100644
--- a/src/Umbraco.Core/HealthChecks/Checks/AbstractSettingsCheck.cs
+++ b/src/Umbraco.Core/HealthChecks/Checks/AbstractSettingsCheck.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Umbraco.
+// Copyright (c) Umbraco.
// See LICENSE for more details.
using System;
@@ -54,7 +54,7 @@ namespace Umbraco.Cms.Core.HealthChecks.Checks
///
/// Gets the message for when the check has succeeded.
///
- public virtual string CheckSuccessMessage => LocalizedTextService.Localize("healthcheck/checkSuccessMessage", new[] { CurrentValue, Values.First(v => v.IsRecommended).Value, ItemPath });
+ public virtual string CheckSuccessMessage => LocalizedTextService.Localize("healthcheck", "checkSuccessMessage", new[] { CurrentValue, Values.First(v => v.IsRecommended).Value, ItemPath });
///
/// Gets the message for when the check has failed.
@@ -62,10 +62,10 @@ namespace Umbraco.Cms.Core.HealthChecks.Checks
public virtual string CheckErrorMessage =>
ValueComparisonType == ValueComparisonType.ShouldEqual
? LocalizedTextService.Localize(
- "healthcheck/checkErrorMessageDifferentExpectedValue",
+ "healthcheck", "checkErrorMessageDifferentExpectedValue",
new[] { CurrentValue, Values.First(v => v.IsRecommended).Value, ItemPath })
: LocalizedTextService.Localize(
- "healthcheck/checkErrorMessageUnexpectedValue",
+ "healthcheck", "checkErrorMessageUnexpectedValue",
new[] { CurrentValue, Values.First(v => v.IsRecommended).Value, ItemPath });
///
diff --git a/src/Umbraco.Core/HealthChecks/Checks/Configuration/MacroErrorsCheck.cs b/src/Umbraco.Core/HealthChecks/Checks/Configuration/MacroErrorsCheck.cs
index 7359a60341..2ded5a0659 100644
--- a/src/Umbraco.Core/HealthChecks/Checks/Configuration/MacroErrorsCheck.cs
+++ b/src/Umbraco.Core/HealthChecks/Checks/Configuration/MacroErrorsCheck.cs
@@ -78,7 +78,7 @@ namespace Umbraco.Cms.Core.HealthChecks.Checks.Configuration
///
public override string CheckSuccessMessage =>
_textService.Localize(
- "healthcheck/macroErrorModeCheckSuccessMessage",
+ "healthcheck","macroErrorModeCheckSuccessMessage",
new[] { CurrentValue, Values.First(v => v.IsRecommended).Value });
///
@@ -86,7 +86,7 @@ namespace Umbraco.Cms.Core.HealthChecks.Checks.Configuration
///
public override string CheckErrorMessage =>
_textService.Localize(
- "healthcheck/macroErrorModeCheckErrorMessage",
+ "healthcheck","macroErrorModeCheckErrorMessage",
new[] { CurrentValue, Values.First(v => v.IsRecommended).Value });
}
}
diff --git a/src/Umbraco.Core/HealthChecks/Checks/Configuration/NotificationEmailCheck.cs b/src/Umbraco.Core/HealthChecks/Checks/Configuration/NotificationEmailCheck.cs
index b139cad710..8d76f1cf4e 100644
--- a/src/Umbraco.Core/HealthChecks/Checks/Configuration/NotificationEmailCheck.cs
+++ b/src/Umbraco.Core/HealthChecks/Checks/Configuration/NotificationEmailCheck.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Umbraco.
+// Copyright (c) Umbraco.
// See LICENSE for more details.
using System.Collections.Generic;
@@ -50,11 +50,11 @@ namespace Umbraco.Cms.Core.HealthChecks.Checks.Configuration
///
public override string CheckSuccessMessage =>
- LocalizedTextService.Localize("healthcheck/notificationEmailsCheckSuccessMessage",
+ LocalizedTextService.Localize("healthcheck","notificationEmailsCheckSuccessMessage",
new[] { CurrentValue ?? "<null>" });
///
- public override string CheckErrorMessage => LocalizedTextService.Localize("healthcheck/notificationEmailsCheckErrorMessage", new[] { DefaultFromEmail });
+ public override string CheckErrorMessage => LocalizedTextService.Localize("healthcheck","notificationEmailsCheckErrorMessage", new[] { DefaultFromEmail });
///
public override string ReadMoreLink => Constants.HealthChecks.DocumentationLinks.Configuration.NotificationEmailCheck;
diff --git a/src/Umbraco.Core/HealthChecks/Checks/LiveEnvironment/CompilationDebugCheck.cs b/src/Umbraco.Core/HealthChecks/Checks/LiveEnvironment/CompilationDebugCheck.cs
index ff37807f27..d28c3ca8f5 100644
--- a/src/Umbraco.Core/HealthChecks/Checks/LiveEnvironment/CompilationDebugCheck.cs
+++ b/src/Umbraco.Core/HealthChecks/Checks/LiveEnvironment/CompilationDebugCheck.cs
@@ -51,9 +51,9 @@ namespace Umbraco.Cms.Core.HealthChecks.Checks.LiveEnvironment
public override string CurrentValue => _hostingSettings.CurrentValue.Debug.ToString();
///
- public override string CheckSuccessMessage => LocalizedTextService.Localize("healthcheck/compilationDebugCheckSuccessMessage");
+ public override string CheckSuccessMessage => LocalizedTextService.Localize("healthcheck","compilationDebugCheckSuccessMessage");
///
- public override string CheckErrorMessage => LocalizedTextService.Localize("healthcheck/compilationDebugCheckErrorMessage");
+ public override string CheckErrorMessage => LocalizedTextService.Localize("healthcheck","compilationDebugCheckErrorMessage");
}
}
diff --git a/src/Umbraco.Core/HealthChecks/Checks/Security/BaseHttpHeaderCheck.cs b/src/Umbraco.Core/HealthChecks/Checks/Security/BaseHttpHeaderCheck.cs
index f9dccbc585..eeb291c41f 100644
--- a/src/Umbraco.Core/HealthChecks/Checks/Security/BaseHttpHeaderCheck.cs
+++ b/src/Umbraco.Core/HealthChecks/Checks/Security/BaseHttpHeaderCheck.cs
@@ -95,12 +95,12 @@ namespace Umbraco.Cms.Core.HealthChecks.Checks.Security
}
message = success
- ? LocalizedTextService.Localize($"healthcheck/{_localizedTextPrefix}CheckHeaderFound")
- : LocalizedTextService.Localize($"healthcheck/{_localizedTextPrefix}CheckHeaderNotFound");
+ ? LocalizedTextService.Localize($"healthcheck", $"{_localizedTextPrefix}CheckHeaderFound")
+ : LocalizedTextService.Localize($"healthcheck", $"{_localizedTextPrefix}CheckHeaderNotFound");
}
catch (Exception ex)
{
- message = LocalizedTextService.Localize("healthcheck/healthCheckInvalidUrl", new[] { url.ToString(), ex.Message });
+ message = LocalizedTextService.Localize("healthcheck","healthCheckInvalidUrl", new[] { url.ToString(), ex.Message });
}
return
diff --git a/src/Umbraco.Core/HealthChecks/Checks/Security/ExcessiveHeadersCheck.cs b/src/Umbraco.Core/HealthChecks/Checks/Security/ExcessiveHeadersCheck.cs
index 825ff09a69..377fbff718 100644
--- a/src/Umbraco.Core/HealthChecks/Checks/Security/ExcessiveHeadersCheck.cs
+++ b/src/Umbraco.Core/HealthChecks/Checks/Security/ExcessiveHeadersCheck.cs
@@ -68,12 +68,12 @@ namespace Umbraco.Cms.Core.HealthChecks.Checks.Security
.ToArray();
success = headersFound.Any() == false;
message = success
- ? _textService.Localize("healthcheck/excessiveHeadersNotFound")
- : _textService.Localize("healthcheck/excessiveHeadersFound", new[] { string.Join(", ", headersFound) });
+ ? _textService.Localize("healthcheck","excessiveHeadersNotFound")
+ : _textService.Localize("healthcheck","excessiveHeadersFound", new[] { string.Join(", ", headersFound) });
}
catch (Exception ex)
{
- message = _textService.Localize("healthcheck/healthCheckInvalidUrl", new[] { url.ToString(), ex.Message });
+ message = _textService.Localize("healthcheck","healthCheckInvalidUrl", new[] { url.ToString(), ex.Message });
}
return
diff --git a/src/Umbraco.Core/HealthChecks/Checks/Security/HttpsCheck.cs b/src/Umbraco.Core/HealthChecks/Checks/Security/HttpsCheck.cs
index 01638366d1..2e3ef9f5c8 100644
--- a/src/Umbraco.Core/HealthChecks/Checks/Security/HttpsCheck.cs
+++ b/src/Umbraco.Core/HealthChecks/Checks/Security/HttpsCheck.cs
@@ -102,23 +102,23 @@ namespace Umbraco.Cms.Core.HealthChecks.Checks.Security
if (s_certificateDaysToExpiry <= 0)
{
result = StatusResultType.Error;
- message = _textService.Localize("healthcheck/httpsCheckExpiredCertificate");
+ message = _textService.Localize("healthcheck","httpsCheckExpiredCertificate");
}
else if (s_certificateDaysToExpiry < numberOfDaysForExpiryWarning)
{
result = StatusResultType.Warning;
- message = _textService.Localize("healthcheck/httpsCheckExpiringCertificate", new[] { s_certificateDaysToExpiry.ToString() });
+ message = _textService.Localize("healthcheck","httpsCheckExpiringCertificate", new[] { s_certificateDaysToExpiry.ToString() });
}
else
{
result = StatusResultType.Success;
- message = _textService.Localize("healthcheck/httpsCheckValidCertificate");
+ message = _textService.Localize("healthcheck","httpsCheckValidCertificate");
}
}
else
{
result = StatusResultType.Error;
- message = _textService.Localize("healthcheck/healthCheckInvalidUrl", new[] { url, response.ReasonPhrase });
+ message = _textService.Localize("healthcheck","healthCheckInvalidUrl", new[] { url, response.ReasonPhrase });
}
}
catch (Exception ex)
@@ -127,12 +127,12 @@ namespace Umbraco.Cms.Core.HealthChecks.Checks.Security
if (exception != null)
{
message = exception.Status == WebExceptionStatus.TrustFailure
- ? _textService.Localize("healthcheck/httpsCheckInvalidCertificate", new[] { exception.Message })
- : _textService.Localize("healthcheck/healthCheckInvalidUrl", new[] { url, exception.Message });
+ ? _textService.Localize("healthcheck","httpsCheckInvalidCertificate", new[] { exception.Message })
+ : _textService.Localize("healthcheck","healthCheckInvalidUrl", new[] { url, exception.Message });
}
else
{
- message = _textService.Localize("healthcheck/healthCheckInvalidUrl", new[] { url, ex.Message });
+ message = _textService.Localize("healthcheck","healthCheckInvalidUrl", new[] { url, ex.Message });
}
result = StatusResultType.Error;
@@ -152,7 +152,7 @@ namespace Umbraco.Cms.Core.HealthChecks.Checks.Security
Uri uri = _hostingEnvironment.ApplicationMainUrl;
var success = uri.Scheme == "https";
- return Task.FromResult(new HealthCheckStatus(_textService.Localize("healthcheck/httpsCheckIsCurrentSchemeHttps", new[] { success ? string.Empty : "not" }))
+ return Task.FromResult(new HealthCheckStatus(_textService.Localize("healthcheck","httpsCheckIsCurrentSchemeHttps", new[] { success ? string.Empty : "not" }))
{
ResultType = success ? StatusResultType.Success : StatusResultType.Error,
ReadMoreLink = success ? null : Constants.HealthChecks.DocumentationLinks.Security.HttpsCheck.CheckIfCurrentSchemeIsHttps
@@ -168,13 +168,13 @@ namespace Umbraco.Cms.Core.HealthChecks.Checks.Security
StatusResultType resultType;
if (uri.Scheme != "https")
{
- resultMessage = _textService.Localize("healthcheck/httpsCheckConfigurationRectifyNotPossible");
+ resultMessage = _textService.Localize("healthcheck","httpsCheckConfigurationRectifyNotPossible");
resultType = StatusResultType.Info;
}
else
{
resultMessage = _textService.Localize(
- "healthcheck/httpsCheckConfigurationCheckResult",
+ "healthcheck","httpsCheckConfigurationCheckResult",
new[] { httpsSettingEnabled.ToString(), httpsSettingEnabled ? string.Empty : "not" });
resultType = httpsSettingEnabled ? StatusResultType.Success : StatusResultType.Error;
}
diff --git a/src/Umbraco.Core/HealthChecks/Checks/Services/SmtpCheck.cs b/src/Umbraco.Core/HealthChecks/Checks/Services/SmtpCheck.cs
index 618b44b9b3..bc9f7fcaba 100644
--- a/src/Umbraco.Core/HealthChecks/Checks/Services/SmtpCheck.cs
+++ b/src/Umbraco.Core/HealthChecks/Checks/Services/SmtpCheck.cs
@@ -56,21 +56,21 @@ namespace Umbraco.Cms.Core.HealthChecks.Checks.Services
string message;
if (smtpSettings == null)
{
- message = _textService.Localize("healthcheck/smtpMailSettingsNotFound");
+ message = _textService.Localize("healthcheck", "smtpMailSettingsNotFound");
}
else
{
if (string.IsNullOrEmpty(smtpSettings.Host))
{
- message = _textService.Localize("healthcheck/smtpMailSettingsHostNotConfigured");
+ message = _textService.Localize("healthcheck", "smtpMailSettingsHostNotConfigured");
}
else
{
success = CanMakeSmtpConnection(smtpSettings.Host, smtpSettings.Port);
message = success
- ? _textService.Localize("healthcheck/smtpMailSettingsConnectionSuccess")
+ ? _textService.Localize("healthcheck", "smtpMailSettingsConnectionSuccess")
: _textService.Localize(
- "healthcheck/smtpMailSettingsConnectionFail",
+ "healthcheck", "smtpMailSettingsConnectionFail",
new[] { smtpSettings.Host, smtpSettings.Port.ToString() });
}
}
diff --git a/src/Umbraco.Core/HealthChecks/NotificationMethods/EmailNotificationMethod.cs b/src/Umbraco.Core/HealthChecks/NotificationMethods/EmailNotificationMethod.cs
index 6e679ddbb1..0811feeb7d 100644
--- a/src/Umbraco.Core/HealthChecks/NotificationMethods/EmailNotificationMethod.cs
+++ b/src/Umbraco.Core/HealthChecks/NotificationMethods/EmailNotificationMethod.cs
@@ -59,7 +59,7 @@ namespace Umbraco.Cms.Core.HealthChecks.NotificationMethods
return;
}
- var message = _textService.Localize("healthcheck/scheduledHealthCheckEmailBody", new[]
+ var message = _textService.Localize("healthcheck","scheduledHealthCheckEmailBody", new[]
{
DateTime.Now.ToShortDateString(),
DateTime.Now.ToShortTimeString(),
@@ -70,7 +70,7 @@ namespace Umbraco.Cms.Core.HealthChecks.NotificationMethods
// you can identify the site that these results are for.
var host = _hostingEnvironment.ApplicationMainUrl?.ToString();
- var subject = _textService.Localize("healthcheck/scheduledHealthCheckEmailSubject", new[] { host });
+ var subject = _textService.Localize("healthcheck","scheduledHealthCheckEmailSubject", new[] { host });
var mailMessage = CreateMailMessage(subject, message);
diff --git a/src/Umbraco.Core/Models/Mapping/CommonMapper.cs b/src/Umbraco.Core/Models/Mapping/CommonMapper.cs
index b6a220e04c..3cfcc89085 100644
--- a/src/Umbraco.Core/Models/Mapping/CommonMapper.cs
+++ b/src/Umbraco.Core/Models/Mapping/CommonMapper.cs
@@ -54,7 +54,7 @@ namespace Umbraco.Cms.Core.Models.Mapping
// localize content app names
foreach (var app in apps)
{
- var localizedAppName = _localizedTextService.Localize($"apps/{app.Alias}");
+ var localizedAppName = _localizedTextService.Localize("apps", app.Alias);
if (localizedAppName.Equals($"[{app.Alias}]", StringComparison.OrdinalIgnoreCase) == false)
{
app.Name = localizedAppName;
diff --git a/src/Umbraco.Core/Models/Mapping/ContentVariantMapper.cs b/src/Umbraco.Core/Models/Mapping/ContentVariantMapper.cs
index ddc7add7ed..8aaa515dcd 100644
--- a/src/Umbraco.Core/Models/Mapping/ContentVariantMapper.cs
+++ b/src/Umbraco.Core/Models/Mapping/ContentVariantMapper.cs
@@ -150,7 +150,7 @@ namespace Umbraco.Cms.Core.Models.Mapping
if(!isCultureVariant && !isSegmentVariant)
{
- return _localizedTextService.Localize("general/default");
+ return _localizedTextService.Localize("general", "default");
}
var parts = new List();
diff --git a/src/Umbraco.Core/Models/Mapping/MemberTabsAndPropertiesMapper.cs b/src/Umbraco.Core/Models/Mapping/MemberTabsAndPropertiesMapper.cs
index 0d3a5b7536..8de419bd0e 100644
--- a/src/Umbraco.Core/Models/Mapping/MemberTabsAndPropertiesMapper.cs
+++ b/src/Umbraco.Core/Models/Mapping/MemberTabsAndPropertiesMapper.cs
@@ -72,7 +72,7 @@ namespace Umbraco.Cms.Core.Models.Mapping
if (isLockedOutProperty?.Value != null && isLockedOutProperty.Value.ToString() != "1")
{
isLockedOutProperty.View = "readonlyvalue";
- isLockedOutProperty.Value = _localizedTextService.Localize("general/no");
+ isLockedOutProperty.Value = _localizedTextService.Localize("general", "no");
}
if (_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser != null
@@ -108,14 +108,14 @@ namespace Umbraco.Cms.Core.Models.Mapping
new ContentPropertyDisplay
{
Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}id",
- Label = _localizedTextService.Localize("general/id"),
+ Label = _localizedTextService.Localize("general","id"),
Value = new List {member.Id.ToString(), member.Key.ToString()},
View = "idwithguid"
},
new ContentPropertyDisplay
{
Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}doctype",
- Label = _localizedTextService.Localize("content/membertype"),
+ Label = _localizedTextService.Localize("content","membertype"),
Value = _localizedTextService.UmbracoDictionaryTranslate(CultureDictionary, member.ContentType.Name),
View = _propertyEditorCollection[Constants.PropertyEditors.Aliases.Label].GetValueEditor().View
},
@@ -123,7 +123,7 @@ namespace Umbraco.Cms.Core.Models.Mapping
new ContentPropertyDisplay
{
Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}email",
- Label = _localizedTextService.Localize("general/email"),
+ Label = _localizedTextService.Localize("general","email"),
Value = member.Email,
View = "email",
Validation = {Mandatory = true}
@@ -131,8 +131,7 @@ namespace Umbraco.Cms.Core.Models.Mapping
new ContentPropertyDisplay
{
Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}password",
- Label = _localizedTextService.Localize("password"),
-
+ Label = _localizedTextService.Localize(null,"password"),
Value = new Dictionary
{
// TODO: why ignoreCase, what are we doing here?!
@@ -146,7 +145,7 @@ namespace Umbraco.Cms.Core.Models.Mapping
new ContentPropertyDisplay
{
Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}membergroup",
- Label = _localizedTextService.Localize("content/membergroup"),
+ Label = _localizedTextService.Localize("content","membergroup"),
Value = GetMemberGroupValue(member.Username),
View = "membergroups",
Config = new Dictionary {{"IsRequired", true}}
@@ -222,7 +221,7 @@ namespace Umbraco.Cms.Core.Models.Mapping
var prop = new ContentPropertyDisplay
{
Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}login",
- Label = localizedText.Localize("login"),
+ Label = localizedText.Localize(null,"login"),
Value = member.Username
};
diff --git a/src/Umbraco.Core/Models/Mapping/SectionMapDefinition.cs b/src/Umbraco.Core/Models/Mapping/SectionMapDefinition.cs
index af1bafed4c..b7bdbccd26 100644
--- a/src/Umbraco.Core/Models/Mapping/SectionMapDefinition.cs
+++ b/src/Umbraco.Core/Models/Mapping/SectionMapDefinition.cs
@@ -35,7 +35,7 @@ namespace Umbraco.Cms.Core.Models.Mapping
private void Map(ISection source, Section target, MapperContext context)
{
target.Alias = source.Alias;
- target.Name = _textService.Localize("sections/" + source.Alias);
+ target.Name = _textService.Localize("sections", source.Alias);
}
// Umbraco.Code.MapAll
diff --git a/src/Umbraco.Core/Models/Mapping/TabsAndPropertiesMapper.cs b/src/Umbraco.Core/Models/Mapping/TabsAndPropertiesMapper.cs
index 3716767b3d..583f921e3f 100644
--- a/src/Umbraco.Core/Models/Mapping/TabsAndPropertiesMapper.cs
+++ b/src/Umbraco.Core/Models/Mapping/TabsAndPropertiesMapper.cs
@@ -59,7 +59,7 @@ namespace Umbraco.Cms.Core.Models.Mapping
tabs.Add(new Tab
{
Id = 0,
- Label = LocalizedTextService.Localize("general/properties"),
+ Label = LocalizedTextService.Localize("general", "properties"),
Alias = "Generic properties",
Properties = genericproperties
});
diff --git a/src/Umbraco.Core/Models/Mapping/UserMapDefinition.cs b/src/Umbraco.Core/Models/Mapping/UserMapDefinition.cs
index 4a19fbe530..f66e3a6b23 100644
--- a/src/Umbraco.Core/Models/Mapping/UserMapDefinition.cs
+++ b/src/Umbraco.Core/Models/Mapping/UserMapDefinition.cs
@@ -284,8 +284,8 @@ namespace Umbraco.Cms.Core.Models.Mapping
{
target.AvailableCultures = _textService.GetSupportedCultures().ToDictionary(x => x.Name, x => x.DisplayName);
target.Avatars = source.GetUserAvatarUrls(_appCaches.RuntimeCache, _mediaFileManager, _imageUrlGenerator);
- target.CalculatedStartContentIds = GetStartNodes(source.CalculateContentStartNodeIds(_entityService, _appCaches), UmbracoObjectTypes.Document, "content/contentRoot", context);
- target.CalculatedStartMediaIds = GetStartNodes(source.CalculateMediaStartNodeIds(_entityService, _appCaches), UmbracoObjectTypes.Media, "media/mediaRoot", context);
+ target.CalculatedStartContentIds = GetStartNodes(source.CalculateContentStartNodeIds(_entityService,_appCaches), UmbracoObjectTypes.Document, "content","contentRoot", context);
+ target.CalculatedStartMediaIds = GetStartNodes(source.CalculateMediaStartNodeIds(_entityService, _appCaches), UmbracoObjectTypes.Media, "media","mediaRoot", context);
target.CreateDate = source.CreateDate;
target.Culture = source.GetUserCulture(_textService, _globalSettings).ToString();
target.Email = source.Email;
@@ -300,8 +300,8 @@ namespace Umbraco.Cms.Core.Models.Mapping
target.Navigation = CreateUserEditorNavigation();
target.ParentId = -1;
target.Path = "-1," + source.Id;
- target.StartContentIds = GetStartNodes(source.StartContentIds.ToArray(), UmbracoObjectTypes.Document, "content/contentRoot", context);
- target.StartMediaIds = GetStartNodes(source.StartMediaIds.ToArray(), UmbracoObjectTypes.Media, "media/mediaRoot", context);
+ target.StartContentIds = GetStartNodes(source.StartContentIds.ToArray(), UmbracoObjectTypes.Document, "content","contentRoot", context);
+ target.StartMediaIds = GetStartNodes(source.StartMediaIds.ToArray(), UmbracoObjectTypes.Media, "media","mediaRoot", context);
target.UpdateDate = source.UpdateDate;
target.UserGroups = context.MapEnumerable(source.Groups);
target.Username = source.Username;
@@ -358,12 +358,12 @@ namespace Umbraco.Cms.Core.Models.Mapping
if (sourceStartMediaId > 0)
target.MediaStartNode = context.Map(_entityService.Get(sourceStartMediaId.Value, UmbracoObjectTypes.Media));
else if (sourceStartMediaId == -1)
- target.MediaStartNode = CreateRootNode(_textService.Localize("media/mediaRoot"));
+ target.MediaStartNode = CreateRootNode(_textService.Localize("media", "mediaRoot"));
if (sourceStartContentId > 0)
target.ContentStartNode = context.Map(_entityService.Get(sourceStartContentId.Value, UmbracoObjectTypes.Document));
else if (sourceStartContentId == -1)
- target.ContentStartNode = CreateRootNode(_textService.Localize("content/contentRoot"));
+ target.ContentStartNode = CreateRootNode(_textService.Localize("content", "contentRoot"));
if (target.Icon.IsNullOrWhiteSpace())
target.Icon = Constants.Icons.UserGroup;
@@ -375,10 +375,10 @@ namespace Umbraco.Cms.Core.Models.Mapping
=> new Permission
{
Category = action.Category.IsNullOrWhiteSpace()
- ? _textService.Localize($"actionCategories/{Constants.Conventions.PermissionCategories.OtherCategory}")
- : _textService.Localize($"actionCategories/{action.Category}"),
- Name = _textService.Localize($"actions/{action.Alias}"),
- Description = _textService.Localize($"actionDescriptions/{action.Alias}"),
+ ? _textService.Localize("actionCategories",Constants.Conventions.PermissionCategories.OtherCategory)
+ : _textService.Localize("actionCategories", action.Category),
+ Name = _textService.Localize("actions", action.Alias),
+ Description = _textService.Localize("actionDescriptions", action.Alias),
Icon = action.Icon,
Checked = source.Permissions != null && source.Permissions.Contains(action.Letter.ToString(CultureInfo.InvariantCulture)),
PermissionCode = action.Letter.ToString(CultureInfo.InvariantCulture)
@@ -394,14 +394,14 @@ namespace Umbraco.Cms.Core.Models.Mapping
private static string MapContentTypeIcon(IEntitySlim entity)
=> entity is IContentEntitySlim contentEntity ? contentEntity.ContentTypeIcon : null;
- private IEnumerable GetStartNodes(int[] startNodeIds, UmbracoObjectTypes objectType, string localizedKey, MapperContext context)
+ private IEnumerable GetStartNodes(int[] startNodeIds, UmbracoObjectTypes objectType, string localizedArea,string localizedAlias, MapperContext context)
{
if (startNodeIds.Length <= 0)
return Enumerable.Empty();
var startNodes = new List();
if (startNodeIds.Contains(-1))
- startNodes.Add(CreateRootNode(_textService.Localize(localizedKey)));
+ startNodes.Add(CreateRootNode(_textService.Localize(localizedArea, localizedAlias)));
var mediaItems = _entityService.GetAll(objectType, startNodeIds);
startNodes.AddRange(context.MapEnumerable(mediaItems));
@@ -417,7 +417,7 @@ namespace Umbraco.Cms.Core.Models.Mapping
Active = true,
Alias = "details",
Icon = "icon-umb-users",
- Name = _textService.Localize("general/user"),
+ Name = _textService.Localize("general","user"),
View = "views/users/views/user/details.html"
}
};
diff --git a/src/Umbraco.Core/Models/Trees/MenuItem.cs b/src/Umbraco.Core/Models/Trees/MenuItem.cs
index 4c275709aa..8407530548 100644
--- a/src/Umbraco.Core/Models/Trees/MenuItem.cs
+++ b/src/Umbraco.Core/Models/Trees/MenuItem.cs
@@ -32,12 +32,9 @@ namespace Umbraco.Cms.Core.Models.Trees
public MenuItem(string alias, ILocalizedTextService textService)
: this()
{
- var values = textService.GetAllStoredValues(Thread.CurrentThread.CurrentUICulture);
- values.TryGetValue($"visuallyHiddenTexts/{alias}_description", out var textDescription);
-
Alias = alias;
- Name = textService.Localize($"actions/{Alias}");
- TextDescription = textDescription;
+ Name = textService.Localize("actions", Alias);
+ TextDescription = textService.Localize("visuallyHiddenTexts", alias + "_description", Thread.CurrentThread.CurrentUICulture);
}
///
diff --git a/src/Umbraco.Core/PropertyEditors/IPropertyCacheCompression.cs b/src/Umbraco.Core/PropertyEditors/IPropertyCacheCompression.cs
index 817bc5aeae..f0973f3157 100644
--- a/src/Umbraco.Core/PropertyEditors/IPropertyCacheCompression.cs
+++ b/src/Umbraco.Core/PropertyEditors/IPropertyCacheCompression.cs
@@ -6,7 +6,7 @@ namespace Umbraco.Cms.Core.PropertyEditors
/// Determines if a property type's value should be compressed in memory
///
///
- ///
+ ///
///
public interface IPropertyCacheCompression
{
diff --git a/src/Umbraco.Core/PropertyEditors/MediaPickerConfiguration.cs b/src/Umbraco.Core/PropertyEditors/MediaPickerConfiguration.cs
index 967347e83d..06faedea0d 100644
--- a/src/Umbraco.Core/PropertyEditors/MediaPickerConfiguration.cs
+++ b/src/Umbraco.Core/PropertyEditors/MediaPickerConfiguration.cs
@@ -5,9 +5,6 @@
///
public class MediaPickerConfiguration : IIgnoreUserStartNodesConfig
{
- [ConfigurationField("notice", "You can NOT change the property editor", "obsoletemediapickernotice")]
- public bool Notice { get; set; }
-
[ConfigurationField("multiPicker", "Pick multiple items", "boolean")]
public bool Multiple { get; set; }
diff --git a/src/Umbraco.Core/PropertyEditors/PropertyCacheCompression.cs b/src/Umbraco.Core/PropertyEditors/PropertyCacheCompression.cs
index 3664be6101..5216e3158f 100644
--- a/src/Umbraco.Core/PropertyEditors/PropertyCacheCompression.cs
+++ b/src/Umbraco.Core/PropertyEditors/PropertyCacheCompression.cs
@@ -2,8 +2,10 @@ using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using Umbraco.Cms.Core.Models;
+using Umbraco.Cms.Core.PropertyEditors;
-namespace Umbraco.Cms.Core.PropertyEditors
+
+namespace Umbraco.Core.PropertyEditors
{
///
diff --git a/src/Umbraco.Core/Routing/UrlProviderExtensions.cs b/src/Umbraco.Core/Routing/UrlProviderExtensions.cs
index 6dfdc89583..71ec01d0b5 100644
--- a/src/Umbraco.Core/Routing/UrlProviderExtensions.cs
+++ b/src/Umbraco.Core/Routing/UrlProviderExtensions.cs
@@ -48,7 +48,7 @@ namespace Umbraco.Extensions
if (content.Published == false)
{
- result.Add(UrlInfo.Message(textService.Localize("content/itemNotPublished")));
+ result.Add(UrlInfo.Message(textService.Localize("content", "itemNotPublished")));
return result;
}
@@ -145,7 +145,7 @@ namespace Umbraco.Extensions
// deal with exceptions
case "#ex":
- result.Add(UrlInfo.Message(textService.Localize("content/getUrlException"), culture));
+ result.Add(UrlInfo.Message(textService.Localize("content", "getUrlException"), culture));
break;
// got a URL, deal with collisions, add URL
@@ -182,17 +182,17 @@ namespace Umbraco.Extensions
if (parent == null)
{
// oops, internal error
- return UrlInfo.Message(textService.Localize("content/parentNotPublishedAnomaly"), culture);
+ return UrlInfo.Message(textService.Localize("content", "parentNotPublishedAnomaly"), culture);
}
else if (!parent.Published)
{
// totally not published
- return UrlInfo.Message(textService.Localize("content/parentNotPublished", new[] { parent.Name }), culture);
+ return UrlInfo.Message(textService.Localize("content", "parentNotPublished", new[] { parent.Name }), culture);
}
else
{
// culture not published
- return UrlInfo.Message(textService.Localize("content/parentCultureNotPublished", new[] {parent.Name}), culture);
+ return UrlInfo.Message(textService.Localize("content", "parentCultureNotPublished", new[] {parent.Name}), culture);
}
}
@@ -221,7 +221,7 @@ namespace Umbraco.Extensions
logger.LogDebug(logMsg, url, uri, culture);
}
- var urlInfo = UrlInfo.Message(textService.Localize("content/routeErrorCannotRoute"), culture);
+ var urlInfo = UrlInfo.Message(textService.Localize("content", "routeErrorCannotRoute"), culture);
return Attempt.Succeed(urlInfo);
}
@@ -243,7 +243,7 @@ namespace Umbraco.Extensions
l.Reverse();
var s = "/" + string.Join("/", l) + " (id=" + pcr.PublishedContent.Id + ")";
- var urlInfo = UrlInfo.Message(textService.Localize("content/routeError", new[] { s }), culture);
+ var urlInfo = UrlInfo.Message(textService.Localize("content", "routeError", new[] { s }), culture);
return Attempt.Succeed(urlInfo);
}
diff --git a/src/Umbraco.Core/Services/IContentService.cs b/src/Umbraco.Core/Services/IContentService.cs
index c7b930bc6e..cef8a8c6d6 100644
--- a/src/Umbraco.Core/Services/IContentService.cs
+++ b/src/Umbraco.Core/Services/IContentService.cs
@@ -490,6 +490,11 @@ namespace Umbraco.Cms.Core.Services
///
IContent Create(string name, int parentId, string documentTypeAlias, int userId = Constants.Security.SuperUserId);
+ ///
+ /// Creates a document
+ ///
+ IContent Create(string name, int parentId, IContentType contentType, int userId = Constants.Security.SuperUserId);
+
///
/// Creates a document.
///
diff --git a/src/Umbraco.Core/Services/ILocalizedTextService.cs b/src/Umbraco.Core/Services/ILocalizedTextService.cs
index fd23dd6f78..51c1c97c9b 100644
--- a/src/Umbraco.Core/Services/ILocalizedTextService.cs
+++ b/src/Umbraco.Core/Services/ILocalizedTextService.cs
@@ -3,6 +3,10 @@ using System.Globalization;
namespace Umbraco.Cms.Core.Services
{
+ // TODO: This needs to be merged into one interface in v9, but better yet
+ // the Localize method should just the based on area + alias and we should remove
+ // the one with the 'key' (the concatenated area/alias) to ensure that we never use that again.
+
///
/// The entry point to localize any key in the text storage source for a given culture
///
@@ -15,11 +19,19 @@ namespace Umbraco.Cms.Core.Services
///
/// Localize a key with variables
///
- ///
+ ///
+ ///
///
/// This can be null
///
- string Localize(string key, CultureInfo culture, IDictionary tokens = null);
+ string Localize(string area, string alias, CultureInfo culture, IDictionary tokens = null);
+
+
+ ///
+ /// Returns all key/values in storage for the given culture
+ ///
+ ///
+ IDictionary> GetAllStoredValuesByAreaAndAlias(CultureInfo culture);
///
/// Returns all key/values in storage for the given culture
diff --git a/src/Umbraco.Core/Services/LocalizedTextServiceExtensions.cs b/src/Umbraco.Core/Services/LocalizedTextServiceExtensions.cs
index 598cebc2c0..dc20774142 100644
--- a/src/Umbraco.Core/Services/LocalizedTextServiceExtensions.cs
+++ b/src/Umbraco.Core/Services/LocalizedTextServiceExtensions.cs
@@ -1,6 +1,7 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
+using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
@@ -15,88 +16,81 @@ namespace Umbraco.Extensions
///
public static class LocalizedTextServiceExtensions
{
- public static string Localize(this ILocalizedTextService manager, string area, T key)
- where T: System.Enum
- {
- var fullKey = string.Join("/", area, key);
- return manager.Localize(fullKey, Thread.CurrentThread.CurrentUICulture);
- }
+ public static string Localize(this ILocalizedTextService manager, string area, T key)
+ where T: System.Enum =>
+ manager.Localize(area, key.ToString(), Thread.CurrentThread.CurrentUICulture);
- public static string Localize(this ILocalizedTextService manager, string area, string key)
- {
- var fullKey = string.Join("/", area, key);
- return manager.Localize(fullKey, Thread.CurrentThread.CurrentUICulture);
- }
+ public static string Localize(this ILocalizedTextService manager, string area, string alias)
+ => manager.Localize(area, alias, Thread.CurrentThread.CurrentUICulture);
///
/// Localize using the current thread culture
///
///
- ///
+ ///
+ ///
///
///
- public static string Localize(this ILocalizedTextService manager, string key, string[] tokens)
- {
- return manager.Localize(key, Thread.CurrentThread.CurrentUICulture, tokens);
- }
+ public static string Localize(this ILocalizedTextService manager, string area, string alias, string[] tokens)
+ => manager.Localize(area, alias, Thread.CurrentThread.CurrentUICulture, ConvertToDictionaryVars(tokens));
///
/// Localize using the current thread culture
///
///
- ///
+ ///
+ ///
///
///
- public static string Localize(this ILocalizedTextService manager, string key, IDictionary tokens = null)
- {
- return manager.Localize(key, Thread.CurrentThread.CurrentUICulture, tokens);
- }
+ public static string Localize(this ILocalizedTextService manager, string area, string alias, IDictionary tokens = null)
+ => manager.Localize(area, alias, Thread.CurrentThread.CurrentUICulture, tokens);
///
/// Localize a key without any variables
///
///
- ///
+ ///
+ ///
///
///
///
- public static string Localize(this ILocalizedTextService manager, string key, CultureInfo culture, string[] tokens)
- {
- return manager.Localize(key, culture, ConvertToDictionaryVars(tokens));
- }
+ public static string Localize(this ILocalizedTextService manager, string area, string alias, CultureInfo culture, string[] tokens)
+ => manager.Localize(area, alias, Thread.CurrentThread.CurrentUICulture, tokens);
- ///
- /// Convert an array of strings to a dictionary of indices -> values
- ///
- ///
- ///
- internal static IDictionary ConvertToDictionaryVars(string[] variables)
- {
- if (variables == null) return null;
- if (variables.Any() == false) return null;
+ ///
+ /// Convert an array of strings to a dictionary of indices -> values
+ ///
+ ///
+ ///
+ internal static IDictionary ConvertToDictionaryVars(string[] variables)
+ {
+ if (variables == null) return null;
+ if (variables.Any() == false) return null;
- return variables.Select((s, i) => new { index = i.ToString(CultureInfo.InvariantCulture), value = s })
- .ToDictionary(keyvals => keyvals.index, keyvals => keyvals.value);
- }
+ return variables.Select((s, i) => new { index = i.ToString(CultureInfo.InvariantCulture), value = s })
+ .ToDictionary(keyvals => keyvals.index, keyvals => keyvals.value);
+ }
- public static string UmbracoDictionaryTranslate(this ILocalizedTextService manager, ICultureDictionary cultureDictionary, string text)
- {
- if (text == null)
- return null;
+ public static string UmbracoDictionaryTranslate(this ILocalizedTextService manager, ICultureDictionary cultureDictionary, string text)
+ {
+ if (text == null)
+ return null;
- if (text.StartsWith("#") == false)
- return text;
+ if (text.StartsWith("#") == false)
+ return text;
- text = text.Substring(1);
- var value = cultureDictionary[text];
- if (value.IsNullOrWhiteSpace() == false)
- {
- return value;
- }
+ text = text.Substring(1);
+ var value = cultureDictionary[text];
+ if (value.IsNullOrWhiteSpace() == false)
+ {
+ return value;
+ }
- value = manager.Localize(text.Replace('_', '/'));
- return value.StartsWith("[") ? text : value;
- }
+ var areaAndKey = text.Split('_');
+
+ value = manager.Localize(areaAndKey[0], areaAndKey[1]);
+ return value.StartsWith("[") ? text : value;
+ }
}
}
diff --git a/src/Umbraco.Core/Trees/MenuItemList.cs b/src/Umbraco.Core/Trees/MenuItemList.cs
index d2468f724b..7588fe778a 100644
--- a/src/Umbraco.Core/Trees/MenuItemList.cs
+++ b/src/Umbraco.Core/Trees/MenuItemList.cs
@@ -59,7 +59,7 @@ namespace Umbraco.Cms.Core.Trees
var values = textService.GetAllStoredValues(Thread.CurrentThread.CurrentUICulture);
values.TryGetValue($"visuallyHiddenTexts/{item.Alias}Description", out var textDescription);
- var menuItem = new MenuItem(item, textService.Localize($"actions/{item.Alias}"))
+ var menuItem = new MenuItem(item, textService.Localize($"actions", item.Alias))
{
SeparatorBefore = hasSeparator,
OpensDialog = opensDialog,
diff --git a/src/Umbraco.Core/Trees/Tree.cs b/src/Umbraco.Core/Trees/Tree.cs
index a0286090ec..a3b3d42b14 100644
--- a/src/Umbraco.Core/Trees/Tree.cs
+++ b/src/Umbraco.Core/Trees/Tree.cs
@@ -54,7 +54,7 @@ namespace Umbraco.Cms.Core.Trees
var label = $"[{tree.TreeAlias}]";
// try to look up a the localized tree header matching the tree alias
- var localizedLabel = textService.Localize("treeHeaders/" + tree.TreeAlias);
+ var localizedLabel = textService.Localize("treeHeader", tree.TreeAlias);
// if the localizedLabel returns [alias] then return the title if it's defined
if (localizedLabel != null && localizedLabel.Equals(label, StringComparison.InvariantCultureIgnoreCase))
diff --git a/src/Umbraco.Examine.Lucene/BackOfficeExamineSearcher.cs b/src/Umbraco.Examine.Lucene/BackOfficeExamineSearcher.cs
index 0a99c4d6ef..27fdf1f2f7 100644
--- a/src/Umbraco.Examine.Lucene/BackOfficeExamineSearcher.cs
+++ b/src/Umbraco.Examine.Lucene/BackOfficeExamineSearcher.cs
@@ -8,6 +8,7 @@ using System.Text;
using System.Text.RegularExpressions;
using Examine;
using Examine.Search;
+using Examine.Search;
using Lucene.Net.QueryParsers.Classic;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Cache;
@@ -61,6 +62,15 @@ namespace Umbraco.Cms.Infrastructure.Examine
var indexName = Constants.UmbracoIndexes.InternalIndexName;
var fields = _treeSearcherFields.GetBackOfficeFields().ToList();
+ ISet fieldsToLoad = new HashSet(_treeSearcherFields.GetBackOfficeFieldsToLoad());
+
+ // TODO: WE should try to allow passing in a lucene raw query, however we will still need to do some manual string
+ // manipulation for things like start paths, member types, etc...
+ //if (Examine.ExamineExtensions.TryParseLuceneQuery(query))
+ //{
+
+ //}
+
//special GUID check since if a user searches on one specifically we need to escape it
if (Guid.TryParse(query, out var g))
{
@@ -75,6 +85,11 @@ namespace Umbraco.Cms.Infrastructure.Examine
indexName = Constants.UmbracoIndexes.MembersIndexName;
type = "member";
fields.AddRange(_treeSearcherFields.GetBackOfficeMembersFields());
+ foreach(var field in _treeSearcherFields.GetBackOfficeMembersFieldsToLoad())
+ {
+ fieldsToLoad.Add(field);
+ }
+
if (searchFrom != null && searchFrom != Constants.Conventions.MemberTypes.AllMembersListId && searchFrom.Trim() != "-1")
{
sb.Append("+__NodeTypeAlias:");
@@ -85,6 +100,11 @@ namespace Umbraco.Cms.Infrastructure.Examine
case UmbracoEntityTypes.Media:
type = "media";
fields.AddRange(_treeSearcherFields.GetBackOfficeMediaFields());
+ foreach (var field in _treeSearcherFields.GetBackOfficeMediaFieldsToLoad())
+ {
+ fieldsToLoad.Add(field);
+ }
+
var allMediaStartNodes = currentUser != null
? currentUser.CalculateMediaStartNodeIds(_entityService, _appCaches)
: Array.Empty();
@@ -93,6 +113,10 @@ namespace Umbraco.Cms.Infrastructure.Examine
case UmbracoEntityTypes.Document:
type = "content";
fields.AddRange(_treeSearcherFields.GetBackOfficeDocumentFields());
+ foreach (var field in _treeSearcherFields.GetBackOfficeDocumentFieldsToLoad())
+ {
+ fieldsToLoad.Add(field);
+ }
var allContentStartNodes = currentUser != null
? currentUser.CalculateContentStartNodeIds(_entityService, _appCaches)
: Array.Empty();
@@ -113,7 +137,9 @@ namespace Umbraco.Cms.Infrastructure.Examine
var result = index.Searcher
.CreateQuery()
- .NativeQuery(sb.ToString())
+ .NativeQuery(sb.ToString())
+ .SelectFields(fieldsToLoad)
+ //only return the number of items specified to read up to the amount of records to fill from 0 -> the number of items on the page requested
.Execute(QueryOptions.SkipTake(Convert.ToInt32(pageSize * pageIndex), pageSize));
totalFound = result.TotalItemCount;
diff --git a/src/Umbraco.Examine.Lucene/UmbracoContentIndex.cs b/src/Umbraco.Examine.Lucene/UmbracoContentIndex.cs
index b3852254af..56d987830a 100644
--- a/src/Umbraco.Examine.Lucene/UmbracoContentIndex.cs
+++ b/src/Umbraco.Examine.Lucene/UmbracoContentIndex.cs
@@ -19,7 +19,7 @@ namespace Umbraco.Cms.Infrastructure.Examine
public class UmbracoContentIndex : UmbracoExamineIndex, IUmbracoContentIndex, IDisposable
{
private readonly ILogger _logger;
-
+ private readonly ISet _idOnlyFieldSet = new HashSet { "id" };
public UmbracoContentIndex(
ILoggerFactory loggerFactory,
string name,
@@ -133,7 +133,8 @@ namespace Umbraco.Cms.Infrastructure.Examine
var rawQuery = $"{UmbracoExamineFieldNames.IndexPathFieldName}:{descendantPath}";
var c = Searcher.CreateQuery();
var filtered = c.NativeQuery(rawQuery);
- var results = filtered.Execute();
+ var selectedFields = filtered.SelectFields(_idOnlyFieldSet);
+ var results = selectedFields.Execute();
_logger.
LogDebug("DeleteFromIndex with query: {Query} (found {TotalItems} results)", rawQuery, results.TotalItemCount);
diff --git a/src/Umbraco.Infrastructure/Events/RelateOnTrashNotificationHandler.cs b/src/Umbraco.Infrastructure/Events/RelateOnTrashNotificationHandler.cs
index 67c1dbda06..df0f074ea3 100644
--- a/src/Umbraco.Infrastructure/Events/RelateOnTrashNotificationHandler.cs
+++ b/src/Umbraco.Infrastructure/Events/RelateOnTrashNotificationHandler.cs
@@ -88,7 +88,7 @@ namespace Umbraco.Cms.Core.Events
item.Entity.WriterId,
item.Entity.Id,
ObjectTypes.GetName(UmbracoObjectTypes.Document),
- string.Format(_textService.Localize("recycleBin/contentTrashed"), item.Entity.Id, originalParentId)
+ string.Format(_textService.Localize("recycleBin","contentTrashed"), item.Entity.Id, originalParentId)
);
}
}
@@ -143,7 +143,7 @@ namespace Umbraco.Cms.Core.Events
item.Entity.CreatorId,
item.Entity.Id,
ObjectTypes.GetName(UmbracoObjectTypes.Media),
- string.Format(_textService.Localize("recycleBin/mediaTrashed"), item.Entity.Id, originalParentId)
+ string.Format(_textService.Localize("recycleBin","mediaTrashed"), item.Entity.Id, originalParentId)
);
}
}
diff --git a/src/Umbraco.Infrastructure/Examine/GenericIndexDiagnostics.cs b/src/Umbraco.Infrastructure/Examine/GenericIndexDiagnostics.cs
index 2ff01c51dc..36d2e60acb 100644
--- a/src/Umbraco.Infrastructure/Examine/GenericIndexDiagnostics.cs
+++ b/src/Umbraco.Infrastructure/Examine/GenericIndexDiagnostics.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Examine;
+using Examine.Search;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Composing;
using Umbraco.Extensions;
@@ -18,6 +19,7 @@ namespace Umbraco.Cms.Infrastructure.Examine
private readonly IIndex _index;
private static readonly string[] s_ignoreProperties = { "Description" };
+ private readonly ISet _idOnlyFieldSet = new HashSet { "id" };
public GenericIndexDiagnostics(IIndex index) => _index = index;
public int DocumentCount => -1; //unknown
@@ -31,7 +33,7 @@ namespace Umbraco.Cms.Infrastructure.Examine
try
{
- var result = _index.Searcher.Search("test");
+ var result = _index.Searcher.CreateQuery().ManagedQuery("test").SelectFields(_idOnlyFieldSet).Execute(new QueryOptions(0, 1));
return Attempt.Succeed(); //if we can search we'll assume it's healthy
}
catch (Exception e)
diff --git a/src/Umbraco.Infrastructure/Examine/IUmbracoTreeSearcherFields.cs b/src/Umbraco.Infrastructure/Examine/IUmbracoTreeSearcherFields.cs
index 6d0d3f8efb..fe135a82b7 100644
--- a/src/Umbraco.Infrastructure/Examine/IUmbracoTreeSearcherFields.cs
+++ b/src/Umbraco.Infrastructure/Examine/IUmbracoTreeSearcherFields.cs
@@ -8,20 +8,28 @@ namespace Umbraco.Cms.Infrastructure.Examine
public interface IUmbracoTreeSearcherFields
{
///
- /// Propagate list of searchable fields for all node types
+ /// The default index fields that are searched on in the back office search for umbraco content entities.
///
IEnumerable GetBackOfficeFields();
+
///
- /// Propagate list of searchable fields for Members
+ /// The additional index fields that are searched on in the back office for member entities.
///
IEnumerable GetBackOfficeMembersFields();
+
///
- /// Propagate list of searchable fields for Media
+ /// The additional index fields that are searched on in the back office for media entities.
///
IEnumerable GetBackOfficeMediaFields();
+
///
- /// Propagate list of searchable fields for Documents
+ /// The additional index fields that are searched on in the back office for document entities.
///
IEnumerable GetBackOfficeDocumentFields();
+
+ ISet GetBackOfficeFieldsToLoad();
+ ISet GetBackOfficeMembersFieldsToLoad();
+ ISet GetBackOfficeDocumentFieldsToLoad();
+ ISet GetBackOfficeMediaFieldsToLoad();
}
}
diff --git a/src/Umbraco.Infrastructure/Examine/UmbracoExamineFieldNames.cs b/src/Umbraco.Infrastructure/Examine/UmbracoExamineFieldNames.cs
index 90f1ddf634..72e914c584 100644
--- a/src/Umbraco.Infrastructure/Examine/UmbracoExamineFieldNames.cs
+++ b/src/Umbraco.Infrastructure/Examine/UmbracoExamineFieldNames.cs
@@ -21,5 +21,8 @@ namespace Umbraco.Cms.Infrastructure.Examine
public const string VariesByCultureFieldName = ExamineFieldNames.SpecialFieldPrefix + "VariesByCulture";
public const string NodeNameFieldName = "nodeName";
+ public const string ItemIdFieldName ="__NodeId";
+ public const string CategoryFieldName = "__IndexType";
+ public const string ItemTypeFieldName = "__NodeTypeAlias";
}
}
diff --git a/src/Umbraco.Infrastructure/Extensions/MediaPicker3ConfigurationExtensions.cs b/src/Umbraco.Infrastructure/Extensions/MediaPicker3ConfigurationExtensions.cs
new file mode 100644
index 0000000000..fe4aa541a0
--- /dev/null
+++ b/src/Umbraco.Infrastructure/Extensions/MediaPicker3ConfigurationExtensions.cs
@@ -0,0 +1,43 @@
+using System.Collections.Generic;
+using System.Linq;
+using Umbraco.Cms.Core.PropertyEditors;
+using Umbraco.Cms.Core.PropertyEditors.ValueConverters;
+
+namespace Umbraco.Extensions
+{
+ public static class MediaPicker3ConfigurationExtensions
+ {
+ ///
+ /// Applies the configuration to ensure only valid crops are kept and have the correct width/height.
+ ///
+ /// The configuration.
+ public static void ApplyConfiguration(this ImageCropperValue imageCropperValue, MediaPicker3Configuration configuration)
+ {
+ var crops = new List();
+
+ var configuredCrops = configuration?.Crops;
+ if (configuredCrops != null)
+ {
+ foreach (var configuredCrop in configuredCrops)
+ {
+ var crop = imageCropperValue.Crops?.FirstOrDefault(x => x.Alias == configuredCrop.Alias);
+
+ crops.Add(new ImageCropperValue.ImageCropperCrop
+ {
+ Alias = configuredCrop.Alias,
+ Width = configuredCrop.Width,
+ Height = configuredCrop.Height,
+ Coordinates = crop?.Coordinates
+ });
+ }
+ }
+
+ imageCropperValue.Crops = crops;
+
+ if (configuration?.EnableLocalFocalPoint == false)
+ {
+ imageCropperValue.FocalPoint = null;
+ }
+ }
+ }
+}
diff --git a/src/Umbraco.Infrastructure/IPublishedContentQuery.cs b/src/Umbraco.Infrastructure/IPublishedContentQuery.cs
index e487873ab2..24bd7ba44c 100644
--- a/src/Umbraco.Infrastructure/IPublishedContentQuery.cs
+++ b/src/Umbraco.Infrastructure/IPublishedContentQuery.cs
@@ -35,6 +35,29 @@ namespace Umbraco.Cms.Core
IEnumerable Media(IEnumerable ids);
IEnumerable MediaAtRoot();
+ ///
+ /// Searches content.
+ ///
+ /// The term to search.
+ /// The amount of results to skip.
+ /// The amount of results to take/return.
+ /// The total amount of records.
+ /// The culture (defaults to a culture insensitive search).
+ /// The name of the index to search (defaults to ).
+ /// The fields to load in the results of the search (defaults to all fields loaded).
+ ///
+ /// The search results.
+ ///
+ ///
+ ///
+ /// When the is not specified or is *, all cultures are searched.
+ /// To search for only invariant documents and fields use null.
+ /// When searching on a specific culture, all culture specific fields are searched for the provided culture and all invariant fields for all documents.
+ ///
+ /// While enumerating results, the ambient culture is changed to be the searched culture.
+ ///
+ IEnumerable Search(string term, int skip, int take, out long totalRecords, string culture = "*", string indexName = Constants.UmbracoIndexes.ExternalIndexName, ISet loadedFields = null);
+
///
/// Searches content.
///
diff --git a/src/Umbraco.Infrastructure/Models/Mapping/EntityMapDefinition.cs b/src/Umbraco.Infrastructure/Models/Mapping/EntityMapDefinition.cs
index 9d511eb877..b34257ec8c 100644
--- a/src/Umbraco.Infrastructure/Models/Mapping/EntityMapDefinition.cs
+++ b/src/Umbraco.Infrastructure/Models/Mapping/EntityMapDefinition.cs
@@ -177,7 +177,7 @@ namespace Umbraco.Cms.Core.Models.Mapping
target.Name = source.Values.ContainsKey(UmbracoExamineFieldNames.NodeNameFieldName) ? source.Values[UmbracoExamineFieldNames.NodeNameFieldName] : "[no name]";
- var culture = context.GetCulture();
+ var culture = context.GetCulture()?.ToLowerInvariant();
if(culture.IsNullOrWhiteSpace() == false)
{
target.Name = source.Values.ContainsKey($"nodeName_{culture}") ? source.Values[$"nodeName_{culture}"] : target.Name;
diff --git a/src/Umbraco.Infrastructure/Models/MediaWithCrops.cs b/src/Umbraco.Infrastructure/Models/MediaWithCrops.cs
index cc80a34310..14cc8e31f8 100644
--- a/src/Umbraco.Infrastructure/Models/MediaWithCrops.cs
+++ b/src/Umbraco.Infrastructure/Models/MediaWithCrops.cs
@@ -1,17 +1,79 @@
-
-
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PropertyEditors.ValueConverters;
-namespace Umbraco.Cms.Core.Models
+namespace Umbraco.Core.Models
{
///
- /// Model used in Razor Views for rendering
+ /// Represents a media item with local crops.
///
- public class MediaWithCrops
+ ///
+ public class MediaWithCrops : PublishedContentWrapped
{
- public IPublishedContent MediaItem { get; set; }
- public ImageCropperValue LocalCrops { get; set; }
+ ///
+ /// Gets the content/media item.
+ ///
+ ///
+ /// The content/media item.
+ ///
+ public IPublishedContent Content => Unwrap();
+
+ ///
+ /// Gets the local crops.
+ ///
+ ///
+ /// The local crops.
+ ///
+ public ImageCropperValue LocalCrops { get; }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The content.
+ /// The published value fallback.
+ /// The local crops.
+ public MediaWithCrops(IPublishedContent content, IPublishedValueFallback publishedValueFallback, ImageCropperValue localCrops)
+ : base(content, publishedValueFallback)
+ {
+ LocalCrops = localCrops;
+ }
+ }
+
+ ///
+ /// Represents a media item with local crops.
+ ///
+ /// The type of the media item.
+ ///
+ public class MediaWithCrops : MediaWithCrops
+ where T : IPublishedContent
+ {
+ ///
+ /// Gets the media item.
+ ///
+ ///
+ /// The media item.
+ ///
+ public new T Content { get; }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The content.
+ /// The published value fallback.
+ /// The local crops.
+ public MediaWithCrops(T content,IPublishedValueFallback publishedValueFallback, ImageCropperValue localCrops)
+ : base(content, publishedValueFallback, localCrops)
+ {
+ Content = content;
+ }
+
+ ///
+ /// Performs an implicit conversion from to .
+ ///
+ /// The media with crops.
+ ///
+ /// The result of the conversion.
+ ///
+ public static implicit operator T(MediaWithCrops mediaWithCrops) => mediaWithCrops.Content;
}
}
diff --git a/src/Umbraco.Infrastructure/Persistence/UmbracoDatabase.cs b/src/Umbraco.Infrastructure/Persistence/UmbracoDatabase.cs
index d95b5feaf7..090e1d4a50 100644
--- a/src/Umbraco.Infrastructure/Persistence/UmbracoDatabase.cs
+++ b/src/Umbraco.Infrastructure/Persistence/UmbracoDatabase.cs
@@ -14,6 +14,7 @@ using Umbraco.Extensions;
namespace Umbraco.Cms.Infrastructure.Persistence
{
+
///
/// Extends NPoco Database for Umbraco.
///
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyEditor.cs
index 6828b3938f..49cc6b2902 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyEditor.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyEditor.cs
@@ -265,7 +265,7 @@ namespace Umbraco.Cms.Core.PropertyEditors
|| (blockEditorData != null && validationLimit.Min.HasValue && blockEditorData.Layout.Count() < validationLimit.Min))
{
yield return new ValidationResult(
- _textService.Localize("validation/entriesShort", new[]
+ _textService.Localize("validation", "entriesShort", new[]
{
validationLimit.Min.ToString(),
(validationLimit.Min - blockEditorData.Layout.Count()).ToString()
@@ -276,7 +276,7 @@ namespace Umbraco.Cms.Core.PropertyEditors
if (blockEditorData != null && validationLimit.Max.HasValue && blockEditorData.Layout.Count() > validationLimit.Max)
{
yield return new ValidationResult(
- _textService.Localize("validation/entriesExceed", new[]
+ _textService.Localize("validation", "entriesExceed", new[]
{
validationLimit.Max.ToString(),
(blockEditorData.Layout.Count() - validationLimit.Max).ToString()
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/DropDownFlexibleConfigurationEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/DropDownFlexibleConfigurationEditor.cs
index cc755f7ed1..5538546ece 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/DropDownFlexibleConfigurationEditor.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/DropDownFlexibleConfigurationEditor.cs
@@ -17,7 +17,7 @@ namespace Umbraco.Cms.Core.PropertyEditors
var items = Fields.First(x => x.Key == "items");
// customize the items field
- items.Name = textService.Localize("editdatatype/addPrevalue");
+ items.Name = textService.Localize("editdatatype", "addPrevalue");
items.Validators.Add(new ValueListUniqueValueValidator());
}
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ImageCropperConfiguration.cs b/src/Umbraco.Infrastructure/PropertyEditors/ImageCropperConfiguration.cs
index 1ffa38d94d..987b275ee1 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/ImageCropperConfiguration.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/ImageCropperConfiguration.cs
@@ -1,7 +1,10 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
+using System.Collections.Generic;
+using System.Linq;
using System.Runtime.Serialization;
+using Umbraco.Cms.Core.PropertyEditors.ValueConverters;
namespace Umbraco.Cms.Core.PropertyEditors
{
@@ -26,4 +29,35 @@ namespace Umbraco.Cms.Core.PropertyEditors
public int Height { get; set; }
}
}
+
+ internal static class ImageCropperConfigurationExtensions
+ {
+ ///
+ /// Applies the configuration to ensure only valid crops are kept and have the correct width/height.
+ ///
+ /// The configuration.
+ public static void ApplyConfiguration(this ImageCropperValue imageCropperValue, ImageCropperConfiguration configuration)
+ {
+ var crops = new List();
+
+ var configuredCrops = configuration?.Crops;
+ if (configuredCrops != null)
+ {
+ foreach (var configuredCrop in configuredCrops)
+ {
+ var crop = imageCropperValue.Crops?.FirstOrDefault(x => x.Alias == configuredCrop.Alias);
+
+ crops.Add(new ImageCropperValue.ImageCropperCrop
+ {
+ Alias = configuredCrop.Alias,
+ Width = configuredCrop.Width,
+ Height = configuredCrop.Height,
+ Coordinates = crop?.Coordinates
+ });
+ }
+ }
+
+ imageCropperValue.Crops = crops;
+ }
+ }
}
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/MediaPicker3PropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/MediaPicker3PropertyEditor.cs
index 0b80116c2e..45aa507a54 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/MediaPicker3PropertyEditor.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/MediaPicker3PropertyEditor.cs
@@ -8,6 +8,11 @@ using Umbraco.Cms.Core.PropertyEditors.ValueConverters;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Strings;
+using Newtonsoft.Json;
+using System;
+using System.Linq;
+using System.Runtime.Serialization;
+using Umbraco.Extensions;
namespace Umbraco.Cms.Core.PropertyEditors
{
@@ -26,6 +31,9 @@ namespace Umbraco.Cms.Core.PropertyEditors
{
private readonly IIOHelper _ioHelper;
+ ///
+ /// Initializes a new instance of the class.
+ ///
public MediaPicker3PropertyEditor(
IDataValueEditorFactory dataValueEditorFactory,
IIOHelper ioHelper,
@@ -35,9 +43,11 @@ namespace Umbraco.Cms.Core.PropertyEditors
_ioHelper = ioHelper;
}
+
///
protected override IConfigurationEditor CreateConfigurationEditor() => new MediaPicker3ConfigurationEditor(_ioHelper);
+ ///
protected override IDataValueEditor CreateValueEditor() => DataValueEditorFactory.Create(Attribute);
internal class MediaPicker3PropertyValueEditor : DataValueEditor, IDataValueReference
@@ -55,6 +65,13 @@ namespace Umbraco.Cms.Core.PropertyEditors
_jsonSerializer = jsonSerializer;
}
+ public override object ToEditor(IProperty property, string culture = null, string segment = null)
+ {
+ var value = property.GetValue(culture, segment);
+
+ return Deserialize(_jsonSerializer, value);
+ }
+
///
/// Note: no FromEditor() and ToEditor() methods
/// We do not want to transform the way the data is stored in the DB and would like to keep a raw JSON string
@@ -62,19 +79,70 @@ namespace Umbraco.Cms.Core.PropertyEditors
public IEnumerable GetReferences(object value)
{
- var rawJson = value == null ? string.Empty : value is string str ? str : value.ToString();
-
- if (string.IsNullOrWhiteSpace(rawJson))
- yield break;
-
- var mediaWithCropsDtos = _jsonSerializer.Deserialize(rawJson);
-
- foreach (var mediaWithCropsDto in mediaWithCropsDtos)
+ foreach (var dto in Deserialize(_jsonSerializer, value))
{
- yield return new UmbracoEntityReference(GuidUdi.Create(Constants.UdiEntityType.Media, mediaWithCropsDto.MediaKey));
+ yield return new UmbracoEntityReference(Udi.Create(Constants.UdiEntityType.Media, dto.MediaKey));
}
}
+ internal static IEnumerable Deserialize(IJsonSerializer jsonSerializer,object value)
+ {
+ var rawJson = value is string str ? str : value?.ToString();
+ if (string.IsNullOrWhiteSpace(rawJson))
+ {
+ yield break;
+ }
+
+ if (!rawJson.DetectIsJson())
+ {
+ // Old comma seperated UDI format
+ foreach (var udiStr in rawJson.Split(Constants.CharArrays.Comma))
+ {
+ if (UdiParser.TryParse(udiStr, out GuidUdi udi))
+ {
+ yield return new MediaWithCropsDto
+ {
+ Key = Guid.NewGuid(),
+ MediaKey = udi.Guid,
+ Crops = Enumerable.Empty(),
+ FocalPoint = new ImageCropperValue.ImageCropperFocalPoint
+ {
+ Left = 0.5m,
+ Top = 0.5m
+ }
+ };
+ }
+ }
+ }
+ else
+ {
+ // New JSON format
+ foreach (var dto in jsonSerializer.Deserialize>(rawJson))
+ {
+ yield return dto;
+ }
+ }
+ }
+
+
+ ///
+ /// Model/DTO that represents the JSON that the MediaPicker3 stores.
+ ///
+ [DataContract]
+ internal class MediaWithCropsDto
+ {
+ [DataMember(Name = "key")]
+ public Guid Key { get; set; }
+
+ [DataMember(Name = "mediaKey")]
+ public Guid MediaKey { get; set; }
+
+ [DataMember(Name = "crops")]
+ public IEnumerable Crops { get; set; }
+
+ [DataMember(Name = "focalPoint")]
+ public ImageCropperValue.ImageCropperFocalPoint FocalPoint { get; set; }
+ }
}
}
}
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyEditor.cs
index 452c27c096..80ed34e6e1 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyEditor.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyEditor.cs
@@ -158,6 +158,7 @@ namespace Umbraco.Cms.Core.PropertyEditors
public override object ToEditor(IProperty property, string culture = null, string segment = null)
{
var val = property.GetValue(culture, segment);
+ var valEditors = new Dictionary();
var rows = _nestedContentValues.GetPropertyValues(val);
@@ -186,8 +187,15 @@ namespace Umbraco.Cms.Core.PropertyEditors
continue;
}
- var tempConfig = _dataTypeService.GetDataType(prop.Value.PropertyType.DataTypeId).Configuration;
- var valEditor = propEditor.GetValueEditor(tempConfig);
+ var dataTypeId = prop.Value.PropertyType.DataTypeId;
+ if (!valEditors.TryGetValue(dataTypeId, out var valEditor))
+ {
+ var tempConfig = _dataTypeService.GetDataType(dataTypeId).Configuration;
+ valEditor = propEditor.GetValueEditor(tempConfig);
+
+ valEditors.Add(dataTypeId, valEditor);
+ }
+
var convValue = valEditor.ToEditor(tempProp);
// update the raw value since this is what will get serialized out
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/UploadFileTypeValidator.cs b/src/Umbraco.Infrastructure/PropertyEditors/UploadFileTypeValidator.cs
index afa4f48249..61597bc47b 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/UploadFileTypeValidator.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/UploadFileTypeValidator.cs
@@ -50,7 +50,7 @@ namespace Umbraco.Cms.Core.PropertyEditors
{
//we only store a single value for this editor so the 'member' or 'field'
// we'll associate this error with will simply be called 'value'
- yield return new ValidationResult(_localizedTextService.Localize("errors/dissallowedMediaType"), new[] { "value" });
+ yield return new ValidationResult(_localizedTextService.Localize("errors", "dissallowedMediaType"), new[] { "value" });
}
}
}
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs
index f1995c6732..98879fb0c3 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs
@@ -116,6 +116,7 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters
settingsData = null;
}
+ // TODO: This should be optimized/cached, as calling Activator.CreateInstance is slow
var layoutType = typeof(BlockListItem<,>).MakeGenericType(contentData.GetType(), settingsData?.GetType() ?? typeof(IPublishedElement));
var layoutRef = (BlockListItem)Activator.CreateInstance(layoutType, contentGuidUdi, contentData, settingGuidUdi, settingsData);
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/ImageCropperValue.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/ImageCropperValue.cs
index c61289bcc9..32101d6cf7 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/ImageCropperValue.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/ImageCropperValue.cs
@@ -130,7 +130,7 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters
/// Determines whether the value has a specified crop.
///
public bool HasCrop(string alias)
- => Crops.Any(x => x.Alias == alias);
+ => Crops != null && Crops.Any(x => x.Alias == alias);
///
/// Determines whether the value has a source image.
@@ -138,46 +138,35 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters
public bool HasImage()
=> !string.IsNullOrWhiteSpace(Src);
- ///
- /// Applies a configuration.
- ///
- /// Ensures that all crops defined in the configuration exists in the value.
- public void ApplyConfiguration(ImageCropperConfiguration configuration)
+ public ImageCropperValue Merge(ImageCropperValue imageCropperValue)
{
- // merge the crop values - the alias + width + height comes from
- // configuration, but each crop can store its own coordinates
-
- var configuredCrops = configuration?.Crops;
- if (configuredCrops == null) return;
-
- //Use Crops if it's not null, otherwise create a new list
var crops = Crops?.ToList() ?? new List();
- foreach (var configuredCrop in configuredCrops)
+ var incomingCrops = imageCropperValue?.Crops;
+ if (incomingCrops != null)
{
- var crop = crops.FirstOrDefault(x => x.Alias == configuredCrop.Alias);
- if (crop != null)
+ foreach (var incomingCrop in incomingCrops)
{
- // found, apply the height & width
- crop.Width = configuredCrop.Width;
- crop.Height = configuredCrop.Height;
- }
- else
- {
- // not found, add
- crops.Add(new ImageCropperCrop
+ var crop = crops.FirstOrDefault(x => x.Alias == incomingCrop.Alias);
+ if (crop == null)
{
- Alias = configuredCrop.Alias,
- Width = configuredCrop.Width,
- Height = configuredCrop.Height
- });
+ // Add incoming crop
+ crops.Add(incomingCrop);
+ }
+ else if (crop.Coordinates == null)
+ {
+ // Use incoming crop coordinates
+ crop.Coordinates = incomingCrop.Coordinates;
+ }
}
}
- // assume we don't have to remove the crops in value, that
- // are not part of configuration anymore?
-
- Crops = crops;
+ return new ImageCropperValue()
+ {
+ Src = !string.IsNullOrWhiteSpace(Src) ? Src : imageCropperValue?.Src,
+ Crops = crops,
+ FocalPoint = FocalPoint ?? imageCropperValue?.FocalPoint
+ };
}
#region IEquatable
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/MediaPickerWithCropsValueConverter.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/MediaPickerWithCropsValueConverter.cs
index 1e661866b1..0aa6fca5a9 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/MediaPickerWithCropsValueConverter.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/MediaPickerWithCropsValueConverter.cs
@@ -1,13 +1,11 @@
-
-using System;
-using System.Collections;
+using System;
using System.Collections.Generic;
-using System.Runtime.Serialization;
-using Newtonsoft.Json;
-using Umbraco.Cms.Core.Models;
+using System.Linq;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Routing;
+using Umbraco.Cms.Core.Serialization;
+using Umbraco.Core.Models;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters
@@ -15,110 +13,85 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters
[DefaultPropertyValueConverter]
public class MediaPickerWithCropsValueConverter : PropertyValueConverterBase
{
-
private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor;
private readonly IPublishedUrlProvider _publishedUrlProvider;
+ private readonly IJsonSerializer _jsonSerializer;
public MediaPickerWithCropsValueConverter(
IPublishedSnapshotAccessor publishedSnapshotAccessor,
- IPublishedUrlProvider publishedUrlProvider)
+ IPublishedUrlProvider publishedUrlProvider,
+ IJsonSerializer jsonSerializer)
{
_publishedSnapshotAccessor = publishedSnapshotAccessor ?? throw new ArgumentNullException(nameof(publishedSnapshotAccessor));
_publishedUrlProvider = publishedUrlProvider;
+ _jsonSerializer = jsonSerializer;
}
+ public override bool IsConverter(IPublishedPropertyType propertyType) => propertyType.EditorAlias.Equals(Core.Constants.PropertyEditors.Aliases.MediaPicker3);
+
+ public override bool? IsValue(object value, PropertyValueLevel level)
+ {
+ var isValue = base.IsValue(value, level);
+ if (isValue != false && level == PropertyValueLevel.Source)
+ {
+ // Empty JSON array is not a value
+ isValue = value?.ToString() != "[]";
+ }
+
+ return isValue;
+ }
+
+ public override Type GetPropertyValueType(IPublishedPropertyType propertyType)
+ => IsMultipleDataType(propertyType.DataType)
+ ? typeof(IEnumerable)
+ : typeof(MediaWithCrops);
+
public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Snapshot;
- ///
- /// Enusre this property value convertor is for the New Media Picker with Crops aka MediaPicker 3
- ///
- public override bool IsConverter(IPublishedPropertyType propertyType) => propertyType.EditorAlias.Equals(Core.Constants.PropertyEditors.Aliases.MediaPicker3);
-
- ///
- /// Check if the raw JSON value is not an empty array
- ///
- public override bool? IsValue(object value, PropertyValueLevel level) => value?.ToString() != "[]";
-
- ///
- /// What C# model type does the raw JSON return for Models & Views
- ///
- public override Type GetPropertyValueType(IPublishedPropertyType propertyType)
- {
- // Check do we want to return IPublishedContent collection still or a NEW model ?
- var isMultiple = IsMultipleDataType(propertyType.DataType);
- return isMultiple
- ? typeof(IEnumerable)
- : typeof(MediaWithCrops);
- }
-
- public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview) => source?.ToString();
-
public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview)
{
- var mediaItems = new List();
var isMultiple = IsMultipleDataType(propertyType.DataType);
- if (inter == null)
+ if (string.IsNullOrEmpty(inter?.ToString()))
{
- return isMultiple ? mediaItems: null;
+ // Short-circuit on empty value
+ return isMultiple ? Enumerable.Empty() : null;
}
- var dtos = JsonConvert.DeserializeObject>(inter.ToString());
+ var mediaItems = new List();
+ var dtos = MediaPicker3PropertyEditor.MediaPicker3PropertyValueEditor.Deserialize(_jsonSerializer, inter);
+ var configuration = propertyType.DataType.ConfigurationAs();
- foreach(var media in dtos)
+ foreach (var dto in dtos)
{
- var item = _publishedSnapshotAccessor.PublishedSnapshot.Media.GetById(media.MediaKey);
- if (item != null)
+ var mediaItem = _publishedSnapshotAccessor.PublishedSnapshot.Media.GetById(preview, dto.MediaKey);
+ if (mediaItem != null)
{
- mediaItems.Add(new MediaWithCrops
+ var localCrops = new ImageCropperValue
{
- MediaItem = item,
- LocalCrops = new ImageCropperValue
- {
- Crops = media.Crops,
- FocalPoint = media.FocalPoint,
- Src = item.Url(_publishedUrlProvider)
- }
- });
+ Crops = dto.Crops,
+ FocalPoint = dto.FocalPoint,
+ Src = mediaItem.Url(_publishedUrlProvider)
+ };
+
+ localCrops.ApplyConfiguration(configuration);
+
+ // TODO: This should be optimized/cached, as calling Activator.CreateInstance is slow
+ var mediaWithCropsType = typeof(MediaWithCrops<>).MakeGenericType(mediaItem.GetType());
+ var mediaWithCrops = (MediaWithCrops)Activator.CreateInstance(mediaWithCropsType, mediaItem, localCrops);
+
+ mediaItems.Add(mediaWithCrops);
+
+ if (!isMultiple)
+ {
+ // Short-circuit on single item
+ break;
+ }
}
}
- return isMultiple ? mediaItems : FirstOrDefault(mediaItems);
+ return isMultiple ? mediaItems : mediaItems.FirstOrDefault();
}
- ///
- /// Is the media picker configured to pick multiple media items
- ///
- ///
- ///
- private bool IsMultipleDataType(PublishedDataType dataType)
- {
- var config = dataType.ConfigurationAs();
- return config.Multiple;
- }
-
- private object FirstOrDefault(IList mediaItems)
- {
- return mediaItems.Count == 0 ? null : mediaItems[0];
- }
-
-
- ///
- /// Model/DTO that represents the JSON that the MediaPicker3 stores
- ///
- [DataContract]
- internal class MediaWithCropsDto
- {
- [DataMember(Name = "key")]
- public Guid Key { get; set; }
-
- [DataMember(Name = "mediaKey")]
- public Guid MediaKey { get; set; }
-
- [DataMember(Name = "crops")]
- public IEnumerable Crops { get; set; }
-
- [DataMember(Name = "focalPoint")]
- public ImageCropperValue.ImageCropperFocalPoint FocalPoint { get; set; }
- }
+ private bool IsMultipleDataType(PublishedDataType dataType) => dataType.ConfigurationAs().Multiple;
}
}
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueListConfigurationEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueListConfigurationEditor.cs
index 1acd039b93..6b81329557 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/ValueListConfigurationEditor.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueListConfigurationEditor.cs
@@ -23,7 +23,7 @@ namespace Umbraco.Cms.Core.PropertyEditors
var items = Fields.First(x => x.Key == "items");
// customize the items field
- items.Name = textService.Localize("editdatatype/addPrevalue");
+ items.Name = textService.Localize("editdatatype", "addPrevalue");
items.Validators.Add(new ValueListUniqueValueValidator());
}
diff --git a/src/Umbraco.Infrastructure/PublishedContentQuery.cs b/src/Umbraco.Infrastructure/PublishedContentQuery.cs
index 47b98d8dc0..14298cbb4e 100644
--- a/src/Umbraco.Infrastructure/PublishedContentQuery.cs
+++ b/src/Umbraco.Infrastructure/PublishedContentQuery.cs
@@ -2,7 +2,6 @@ using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
-using System.Threading.Tasks;
using System.Xml.XPath;
using Examine;
using Examine.Search;
@@ -237,6 +236,10 @@ namespace Umbraco.Cms.Infrastructure
///
public IEnumerable Search(string term, int skip, int take, out long totalRecords,
string culture = "*", string indexName = Constants.UmbracoIndexes.ExternalIndexName)
+ => Search(term, skip, take, out totalRecords, culture, indexName, null);
+
+ ///
+ public IEnumerable Search(string term, int skip, int take, out long totalRecords, string culture = "*", string indexName = Constants.UmbracoIndexes.ExternalIndexName, ISet loadedFields = null)
{
if (skip < 0)
{
@@ -284,6 +287,10 @@ namespace Umbraco.Cms.Infrastructure
.ToArray(); // Get all index fields suffixed with the culture name supplied
queryExecutor = query.ManagedQuery(term, fields);
}
+ if (loadedFields != null && queryExecutor is IBooleanOperation booleanOperation)
+ {
+ queryExecutor = booleanOperation.SelectFields(loadedFields);
+ }
var results = skip == 0 && take == 0
? queryExecutor.Execute()
diff --git a/src/Umbraco.Infrastructure/Search/UmbracoTreeSearcherFields.cs b/src/Umbraco.Infrastructure/Search/UmbracoTreeSearcherFields.cs
index aa11c1ad54..26b638a436 100644
--- a/src/Umbraco.Infrastructure/Search/UmbracoTreeSearcherFields.cs
+++ b/src/Umbraco.Infrastructure/Search/UmbracoTreeSearcherFields.cs
@@ -1,31 +1,61 @@
+
using System.Collections.Generic;
using System.Linq;
+using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Infrastructure.Examine;
namespace Umbraco.Cms.Infrastructure.Search
{
public class UmbracoTreeSearcherFields : IUmbracoTreeSearcherFields
{
- private IReadOnlyList _backOfficeFields = new List {"id", "__NodeId", "__Key"};
- public IEnumerable GetBackOfficeFields()
- {
- return _backOfficeFields;
- }
-
-
- private IReadOnlyList _backOfficeMembersFields = new List {"email", "loginName"};
- public IEnumerable GetBackOfficeMembersFields()
- {
- return _backOfficeMembersFields;
- }
+ private IReadOnlyList _backOfficeFields = new List {"id", UmbracoExamineFieldNames.ItemIdFieldName, UmbracoExamineFieldNames.NodeKeyFieldName};
+ private readonly ISet _backOfficeFieldsToLoad = new HashSet { "id", UmbracoExamineFieldNames.ItemIdFieldName, UmbracoExamineFieldNames.NodeKeyFieldName, "nodeName", UmbracoExamineFieldNames.IconFieldName, UmbracoExamineFieldNames.CategoryFieldName, "parentID", UmbracoExamineFieldNames.ItemTypeFieldName };
private IReadOnlyList _backOfficeMediaFields = new List { UmbracoExamineFieldNames.UmbracoFileFieldName };
- public IEnumerable GetBackOfficeMediaFields()
+ private readonly ISet _backOfficeMediaFieldsToLoad = new HashSet { UmbracoExamineFieldNames.UmbracoFileFieldName };
+ private IReadOnlyList _backOfficeMembersFields = new List { "email", "loginName" };
+ private readonly ISet _backOfficeMembersFieldsToLoad = new HashSet { "email", "loginName" };
+ private readonly ISet _backOfficeDocumentFieldsToLoad = new HashSet { UmbracoExamineFieldNames.VariesByCultureFieldName };
+ private readonly ILocalizationService _localizationService;
+
+ public UmbracoTreeSearcherFields(ILocalizationService localizationService)
{
- return _backOfficeMediaFields;
+ _localizationService = localizationService;
}
- public IEnumerable GetBackOfficeDocumentFields()
+
+ ///
+ public IEnumerable GetBackOfficeFields() => _backOfficeFields;
+
+ ///
+ public IEnumerable GetBackOfficeMembersFields() => _backOfficeMembersFields;
+
+ ///
+ public IEnumerable GetBackOfficeMediaFields() => _backOfficeMediaFields;
+
+ ///
+ public IEnumerable GetBackOfficeDocumentFields() => Enumerable.Empty();
+
+ ///
+ public ISet GetBackOfficeFieldsToLoad() => _backOfficeFieldsToLoad;
+
+ ///
+ public ISet GetBackOfficeMembersFieldsToLoad() => _backOfficeMembersFieldsToLoad;
+
+ ///
+ public ISet GetBackOfficeMediaFieldsToLoad() => _backOfficeMediaFieldsToLoad;
+
+ ///
+ public ISet GetBackOfficeDocumentFieldsToLoad()
{
- return Enumerable.Empty();
+ var fields = _backOfficeDocumentFieldsToLoad;
+
+ // We need to load all nodeName_* fields but we won't know those up front so need to get
+ // all langs (this is cached)
+ foreach(var field in _localizationService.GetAllLanguages().Select(x => "nodeName_" + x.IsoCode.ToLowerInvariant()))
+ {
+ fields.Add(field);
+ }
+
+ return fields;
}
}
}
diff --git a/src/Umbraco.Infrastructure/Security/BackOfficeErrorDescriber.cs b/src/Umbraco.Infrastructure/Security/BackOfficeErrorDescriber.cs
index c0021bc967..5f09a7a524 100644
--- a/src/Umbraco.Infrastructure/Security/BackOfficeErrorDescriber.cs
+++ b/src/Umbraco.Infrastructure/Security/BackOfficeErrorDescriber.cs
@@ -17,43 +17,43 @@ namespace Umbraco.Cms.Core.Security
public override IdentityError DuplicateRoleName(string role) => new IdentityError
{
Code = nameof(DuplicateRoleName),
- Description = _textService.Localize("validation/duplicateUserGroupName", new[] { role })
+ Description = _textService.Localize("validation", "duplicateUserGroupName", new[] { role })
};
public override IdentityError InvalidRoleName(string role) => new IdentityError
{
Code = nameof(InvalidRoleName),
- Description = _textService.Localize("validation/invalidUserGroupName")
+ Description = _textService.Localize("validation", "invalidUserGroupName")
};
public override IdentityError LoginAlreadyAssociated() => new IdentityError
{
Code = nameof(LoginAlreadyAssociated),
- Description = _textService.Localize("user/duplicateLogin")
+ Description = _textService.Localize("user", "duplicateLogin")
};
public override IdentityError UserAlreadyHasPassword() => new IdentityError
{
Code = nameof(UserAlreadyHasPassword),
- Description = _textService.Localize("user/userHasPassword")
+ Description = _textService.Localize("user", "userHasPassword")
};
public override IdentityError UserAlreadyInRole(string role) => new IdentityError
{
Code = nameof(UserAlreadyInRole),
- Description = _textService.Localize("user/userHasGroup", new[] { role })
+ Description = _textService.Localize("user", "userHasGroup", new[] { role })
};
public override IdentityError UserLockoutNotEnabled() => new IdentityError
{
Code = nameof(UserLockoutNotEnabled),
- Description = _textService.Localize("user/userLockoutNotEnabled")
+ Description = _textService.Localize("user", "userLockoutNotEnabled")
};
public override IdentityError UserNotInRole(string role) => new IdentityError
{
Code = nameof(UserNotInRole),
- Description = _textService.Localize("user/userNotInGroup", new[] { role })
+ Description = _textService.Localize("user", "userNotInGroup", new[] { role })
};
}
@@ -67,43 +67,43 @@ namespace Umbraco.Cms.Core.Security
public override IdentityError DuplicateRoleName(string role) => new IdentityError
{
Code = nameof(DuplicateRoleName),
- Description = _textService.Localize("validation/duplicateMemberGroupName", new[] { role })
+ Description = _textService.Localize("validation", "duplicateMemberGroupName", new[] { role })
};
public override IdentityError InvalidRoleName(string role) => new IdentityError
{
Code = nameof(InvalidRoleName),
- Description = _textService.Localize("validation/invalidMemberGroupName")
+ Description = _textService.Localize("validation", "invalidMemberGroupName")
};
public override IdentityError LoginAlreadyAssociated() => new IdentityError
{
Code = nameof(LoginAlreadyAssociated),
- Description = _textService.Localize("member/duplicateMemberLogin")
+ Description = _textService.Localize("member", "duplicateMemberLogin")
};
public override IdentityError UserAlreadyHasPassword() => new IdentityError
{
Code = nameof(UserAlreadyHasPassword),
- Description = _textService.Localize("member/memberHasPassword")
+ Description = _textService.Localize("member", "memberHasPassword")
};
public override IdentityError UserAlreadyInRole(string role) => new IdentityError
{
Code = nameof(UserAlreadyInRole),
- Description = _textService.Localize("member/memberHasGroup", new[] { role })
+ Description = _textService.Localize("member", "memberHasGroup", new[] { role })
};
public override IdentityError UserLockoutNotEnabled() => new IdentityError
{
Code = nameof(UserLockoutNotEnabled),
- Description = _textService.Localize("member/memberLockoutNotEnabled")
+ Description = _textService.Localize("member", "memberLockoutNotEnabled")
};
public override IdentityError UserNotInRole(string role) => new IdentityError
{
Code = nameof(UserNotInRole),
- Description = _textService.Localize("member/memberNotInGroup", new[] { role })
+ Description = _textService.Localize("member", "memberNotInGroup", new[] { role })
};
}
}
diff --git a/src/Umbraco.Infrastructure/Security/UmbracoErrorDescriberBase.cs b/src/Umbraco.Infrastructure/Security/UmbracoErrorDescriberBase.cs
index 0214811274..b5ff881249 100644
--- a/src/Umbraco.Infrastructure/Security/UmbracoErrorDescriberBase.cs
+++ b/src/Umbraco.Infrastructure/Security/UmbracoErrorDescriberBase.cs
@@ -13,91 +13,91 @@ namespace Umbraco.Cms.Core.Security
public override IdentityError ConcurrencyFailure() => new IdentityError
{
Code = nameof(ConcurrencyFailure),
- Description = _textService.Localize("errors/concurrencyError")
+ Description = _textService.Localize("errors", "concurrencyError")
};
public override IdentityError DefaultError() => new IdentityError
{
Code = nameof(DefaultError),
- Description = _textService.Localize("errors/defaultError")
+ Description = _textService.Localize("errors", "defaultError")
};
public override IdentityError DuplicateEmail(string email) => new IdentityError
{
Code = nameof(DuplicateEmail),
- Description = _textService.Localize("validation/duplicateEmail", new[] { email })
+ Description = _textService.Localize("validation", "duplicateEmail", new[] { email })
};
public override IdentityError DuplicateUserName(string userName) => new IdentityError
{
Code = nameof(DuplicateUserName),
- Description = _textService.Localize("validation/duplicateUsername", new[] { userName })
+ Description = _textService.Localize("validation", "duplicateUsername", new[] { userName })
};
public override IdentityError InvalidEmail(string email) => new IdentityError
{
Code = nameof(InvalidEmail),
- Description = _textService.Localize("validation/invalidEmail")
+ Description = _textService.Localize("validation", "invalidEmail")
};
public override IdentityError InvalidToken() => new IdentityError
{
Code = nameof(InvalidToken),
- Description = _textService.Localize("validation/invalidToken")
+ Description = _textService.Localize("validation", "invalidToken")
};
public override IdentityError InvalidUserName(string userName) => new IdentityError
{
Code = nameof(InvalidUserName),
- Description = _textService.Localize("validation/invalidUsername")
+ Description = _textService.Localize("validation", "invalidUsername")
};
public override IdentityError PasswordMismatch() => new IdentityError
{
Code = nameof(PasswordMismatch),
- Description = _textService.Localize("user/passwordMismatch")
+ Description = _textService.Localize("user", "passwordMismatch")
};
public override IdentityError PasswordRequiresDigit() => new IdentityError
{
Code = nameof(PasswordRequiresDigit),
- Description = _textService.Localize("user/passwordRequiresDigit")
+ Description = _textService.Localize("user", "passwordRequiresDigit")
};
public override IdentityError PasswordRequiresLower() => new IdentityError
{
Code = nameof(PasswordRequiresLower),
- Description = _textService.Localize("user/passwordRequiresLower")
+ Description = _textService.Localize("user", "passwordRequiresLower")
};
public override IdentityError PasswordRequiresNonAlphanumeric() => new IdentityError
{
Code = nameof(PasswordRequiresNonAlphanumeric),
- Description = _textService.Localize("user/passwordRequiresNonAlphanumeric")
+ Description = _textService.Localize("user", "passwordRequiresNonAlphanumeric")
};
public override IdentityError PasswordRequiresUniqueChars(int uniqueChars) => new IdentityError
{
Code = nameof(PasswordRequiresUniqueChars),
- Description = _textService.Localize("user/passwordRequiresUniqueChars", new[] { uniqueChars.ToString() })
+ Description = _textService.Localize("user", "passwordRequiresUniqueChars", new[] { uniqueChars.ToString() })
};
public override IdentityError PasswordRequiresUpper() => new IdentityError
{
Code = nameof(PasswordRequiresUpper),
- Description = _textService.Localize("user/passwordRequiresUpper")
+ Description = _textService.Localize("user", "passwordRequiresUpper")
};
public override IdentityError PasswordTooShort(int length) => new IdentityError
{
Code = nameof(PasswordTooShort),
- Description = _textService.Localize("user/passwordTooShort", new[] { length.ToString() })
+ Description = _textService.Localize("user", "passwordTooShort", new[] { length.ToString() })
};
public override IdentityError RecoveryCodeRedemptionFailed() => new IdentityError
{
Code = nameof(RecoveryCodeRedemptionFailed),
- Description = _textService.Localize("login/resetCodeExpired")
+ Description = _textService.Localize("login", "resetCodeExpired")
};
}
}
diff --git a/src/Umbraco.Infrastructure/Services/Implement/ContentService.cs b/src/Umbraco.Infrastructure/Services/Implement/ContentService.cs
index 85bc2c7f4d..3d4c6dfbbe 100644
--- a/src/Umbraco.Infrastructure/Services/Implement/ContentService.cs
+++ b/src/Umbraco.Infrastructure/Services/Implement/ContentService.cs
@@ -194,11 +194,34 @@ namespace Umbraco.Cms.Core.Services.Implement
// TODO: what about culture?
var contentType = GetContentType(contentTypeAlias);
- if (contentType == null)
- throw new ArgumentException("No content type with that alias.", nameof(contentTypeAlias));
+ return Create(name, parentId, contentType, userId);
+ }
+
+ ///
+ /// Creates an object of a specified content type.
+ ///
+ /// This method simply returns a new, non-persisted, IContent without any identity. It
+ /// is intended as a shortcut to creating new content objects that does not invoke a save
+ /// operation against the database.
+ ///
+ /// The name of the content object.
+ /// The identifier of the parent, or -1.
+ /// The content type of the content
+ /// The optional id of the user creating the content.
+ /// The content object.
+ public IContent Create(string name, int parentId, IContentType contentType,
+ int userId = Constants.Security.SuperUserId)
+ {
+ if (contentType is null)
+ {
+ throw new ArgumentException("Content type must be specified", nameof(contentType));
+ }
+
var parent = parentId > 0 ? GetById(parentId) : null;
- if (parentId > 0 && parent == null)
+ if (parentId > 0 && parent is null)
+ {
throw new ArgumentException("No content with that id.", nameof(parentId));
+ }
var content = new Content(name, parentId, contentType, userId);
diff --git a/src/Umbraco.Infrastructure/Services/Implement/LocalizedTextService.cs b/src/Umbraco.Infrastructure/Services/Implement/LocalizedTextService.cs
index b8588ba969..68fa4c37a5 100644
--- a/src/Umbraco.Infrastructure/Services/Implement/LocalizedTextService.cs
+++ b/src/Umbraco.Infrastructure/Services/Implement/LocalizedTextService.cs
@@ -5,19 +5,18 @@ using System.Linq;
using System.Xml.Linq;
using System.Xml.XPath;
using Microsoft.Extensions.Logging;
-using Umbraco.Extensions;
namespace Umbraco.Cms.Core.Services.Implement
{
- // TODO: Convert all of this over to Niels K's localization framework one day
-
public class LocalizedTextService : ILocalizedTextService
{
private readonly ILogger _logger;
private readonly Lazy _fileSources;
- private readonly IDictionary>> _dictionarySource;
- private readonly IDictionary> _xmlSource;
-
+ private IDictionary>> _dictionarySource => _dictionarySourceLazy.Value;
+ private IDictionary> _noAreaDictionarySource => _noAreaDictionarySourceLazy.Value;
+ private readonly Lazy>>> _dictionarySourceLazy;
+ private readonly Lazy>> _noAreaDictionarySourceLazy;
+ private readonly char[] _splitter = new[] { '/' };
///
/// Initializes with a file sources instance
///
@@ -25,12 +24,50 @@ namespace Umbraco.Cms.Core.Services.Implement
///
public LocalizedTextService(Lazy fileSources, ILogger logger)
{
- if (logger == null) throw new ArgumentNullException("logger");
+ if (logger == null) throw new ArgumentNullException(nameof(logger));
_logger = logger;
- if (fileSources == null) throw new ArgumentNullException("fileSources");
+ if (fileSources == null) throw new ArgumentNullException(nameof(fileSources));
+ _dictionarySourceLazy = new Lazy>>>(() => FileSourcesToAreaDictionarySources(fileSources.Value));
+ _noAreaDictionarySourceLazy = new Lazy>>(() => FileSourcesToNoAreaDictionarySources(fileSources.Value));
_fileSources = fileSources;
}
+ private IDictionary> FileSourcesToNoAreaDictionarySources(LocalizedTextServiceFileSources fileSources)
+ {
+ var xmlSources = fileSources.GetXmlSources();
+
+ return XmlSourceToNoAreaDictionary(xmlSources);
+ }
+
+ private IDictionary> XmlSourceToNoAreaDictionary(IDictionary> xmlSources)
+ {
+ var cultureNoAreaDictionary = new Dictionary>();
+ foreach (var xmlSource in xmlSources)
+ {
+ var noAreaAliasValue = GetNoAreaStoredTranslations(xmlSources, xmlSource.Key);
+ cultureNoAreaDictionary.Add(xmlSource.Key, noAreaAliasValue);
+ }
+ return cultureNoAreaDictionary;
+ }
+
+ private IDictionary>> FileSourcesToAreaDictionarySources(LocalizedTextServiceFileSources fileSources)
+ {
+ var xmlSources = fileSources.GetXmlSources();
+ return XmlSourcesToAreaDictionary(xmlSources);
+ }
+
+ private IDictionary>> XmlSourcesToAreaDictionary(IDictionary> xmlSources)
+ {
+ var cultureDictionary = new Dictionary>>();
+ foreach (var xmlSource in xmlSources)
+ {
+ var areaAliaValue = GetAreaStoredTranslations(xmlSources, xmlSource.Key);
+ cultureDictionary.Add(xmlSource.Key, areaAliaValue);
+
+ }
+ return cultureDictionary;
+ }
+
///
/// Initializes with an XML source
///
@@ -38,12 +75,15 @@ namespace Umbraco.Cms.Core.Services.Implement
///
public LocalizedTextService(IDictionary> source, ILogger logger)
{
- if (source == null) throw new ArgumentNullException("source");
- if (logger == null) throw new ArgumentNullException("logger");
- _xmlSource = source;
- _logger = logger;
+ if (source == null) throw new ArgumentNullException(nameof(source));
+ _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+
+ _dictionarySourceLazy = new Lazy>>>(() => XmlSourcesToAreaDictionary(source));
+ _noAreaDictionarySourceLazy = new Lazy>>(() => XmlSourceToNoAreaDictionary(source));
+
}
+
///
/// Initializes with a source of a dictionary of culture -> areas -> sub dictionary of keys/values
///
@@ -51,37 +91,54 @@ namespace Umbraco.Cms.Core.Services.Implement
///
public LocalizedTextService(IDictionary>> source, ILogger logger)
{
- _dictionarySource = source ?? throw new ArgumentNullException(nameof(source));
+ var dictionarySource = source ?? throw new ArgumentNullException(nameof(source));
+ _dictionarySourceLazy = new Lazy>>>(() => dictionarySource);
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ var cultureNoAreaDictionary = new Dictionary>();
+ foreach (var cultureDictionary in dictionarySource)
+ {
+ var areaAliaValue = GetAreaStoredTranslations(source, cultureDictionary.Key);
+ var aliasValue = new Dictionary();
+ foreach (var area in areaAliaValue)
+ {
+ foreach (var alias in area.Value)
+ {
+ if (!aliasValue.ContainsKey(alias.Key))
+ {
+ aliasValue.Add(alias.Key, alias.Value);
+ }
+ }
+ }
+ cultureNoAreaDictionary.Add(cultureDictionary.Key, aliasValue);
+ }
+ _noAreaDictionarySourceLazy = new Lazy>>(() => cultureNoAreaDictionary);
}
public string Localize(string key, CultureInfo culture, IDictionary tokens = null)
{
if (culture == null) throw new ArgumentNullException(nameof(culture));
- // TODO: Hack, see notes on ConvertToSupportedCultureWithRegionCode
- culture = ConvertToSupportedCultureWithRegionCode(culture);
-
//This is what the legacy ui service did
if (string.IsNullOrEmpty(key))
return string.Empty;
- var keyParts = key.Split(new[] {'/'}, StringSplitOptions.RemoveEmptyEntries);
+ var keyParts = key.Split(_splitter, StringSplitOptions.RemoveEmptyEntries);
var area = keyParts.Length > 1 ? keyParts[0] : null;
var alias = keyParts.Length > 1 ? keyParts[1] : keyParts[0];
+ return Localize(area, alias, culture, tokens);
+ }
+ public string Localize(string area, string alias, CultureInfo culture, IDictionary tokens = null)
+ {
+ if (culture == null) throw new ArgumentNullException(nameof(culture));
- var xmlSource = _xmlSource ?? (_fileSources != null
- ? _fileSources.Value.GetXmlSources()
- : null);
+ //This is what the legacy ui service did
+ if (string.IsNullOrEmpty(alias))
+ return string.Empty;
- if (xmlSource != null)
- {
- return GetFromXmlSource(xmlSource, culture, area, alias, tokens);
- }
- else
- {
- return GetFromDictionarySource(culture, area, alias, tokens);
- }
+ // TODO: Hack, see notes on ConvertToSupportedCultureWithRegionCode
+ culture = ConvertToSupportedCultureWithRegionCode(culture);
+
+ return GetFromDictionarySource(culture, area, alias, tokens);
}
///
@@ -89,76 +146,105 @@ namespace Umbraco.Cms.Core.Services.Implement
///
public IDictionary GetAllStoredValues(CultureInfo culture)
{
- if (culture == null) throw new ArgumentNullException("culture");
+ if (culture == null) throw new ArgumentNullException(nameof(culture));
// TODO: Hack, see notes on ConvertToSupportedCultureWithRegionCode
culture = ConvertToSupportedCultureWithRegionCode(culture);
- var result = new Dictionary();
-
- var xmlSource = _xmlSource ?? (_fileSources != null
- ? _fileSources.Value.GetXmlSources()
- : null);
-
- if (xmlSource != null)
+ if (_dictionarySource.ContainsKey(culture) == false)
{
- if (xmlSource.ContainsKey(culture) == false)
+ _logger.LogWarning("The culture specified {Culture} was not found in any configured sources for this service", culture);
+ return new Dictionary(0);
+ }
+ IDictionary result = new Dictionary();
+ //convert all areas + keys to a single key with a '/'
+ foreach (var area in _dictionarySource[culture])
+ {
+ foreach (var key in area.Value)
{
- _logger.LogWarning("The culture specified {Culture} was not found in any configured sources for this service", culture);
- return result;
- }
-
- // convert all areas + keys to a single key with a '/'
- result = GetStoredTranslations(xmlSource, culture);
-
- // merge with the English file in case there's keys in there that don't exist in the local file
- var englishCulture = CultureInfo.GetCultureInfo("en-US");
- if (culture.Equals(englishCulture) == false)
- {
- var englishResults = GetStoredTranslations(xmlSource, englishCulture);
- foreach (var englishResult in englishResults.Where(englishResult => result.ContainsKey(englishResult.Key) == false))
+ var dictionaryKey = string.Format("{0}/{1}", area.Key, key.Key);
+ //i don't think it's possible to have duplicates because we're dealing with a dictionary in the first place, but we'll double check here just in case.
+ if (result.ContainsKey(dictionaryKey) == false)
{
- result.Add(englishResult.Key, englishResult.Value);
+ result.Add(dictionaryKey, key.Value);
}
}
}
- else
- {
- if (_dictionarySource.ContainsKey(culture) == false)
- {
- _logger.LogWarning("The culture specified {Culture} was not found in any configured sources for this service", culture);
- return result;
- }
-
- // convert all areas + keys to a single key with a '/'
- foreach (var area in _dictionarySource[culture])
- {
- foreach (var key in area.Value)
- {
- var dictionaryKey = string.Format("{0}/{1}", area.Key, key.Key);
- // i don't think it's possible to have duplicates because we're dealing with a dictionary in the first place, but we'll double check here just in case.
- if (result.ContainsKey(dictionaryKey) == false)
- {
- result.Add(dictionaryKey, key.Value);
- }
- }
- }
- }
-
return result;
}
- private Dictionary GetStoredTranslations(IDictionary> xmlSource, CultureInfo cult)
+ private Dictionary> GetAreaStoredTranslations(IDictionary> xmlSource, CultureInfo cult)
{
- var result = new Dictionary();
+ var overallResult = new Dictionary>(StringComparer.InvariantCulture);
var areas = xmlSource[cult].Value.XPathSelectElements("//area");
foreach (var area in areas)
{
+ var result = new Dictionary(StringComparer.InvariantCulture);
var keys = area.XPathSelectElements("./key");
foreach (var key in keys)
{
- var dictionaryKey = string.Format("{0}/{1}", (string)area.Attribute("alias"),
- (string)key.Attribute("alias"));
+ var dictionaryKey =
+ (string)key.Attribute("alias");
+ //there could be duplicates if the language file isn't formatted nicely - which is probably the case for quite a few lang files
+ if (result.ContainsKey(dictionaryKey) == false)
+ result.Add(dictionaryKey, key.Value);
+ }
+ overallResult.Add(area.Attribute("alias").Value, result);
+ }
+
+ //Merge English Dictionary
+ var englishCulture = new CultureInfo("en-US");
+ if (!cult.Equals(englishCulture))
+ {
+ var enUS = xmlSource[englishCulture].Value.XPathSelectElements("//area");
+ foreach (var area in enUS)
+ {
+ IDictionary result = new Dictionary(StringComparer.InvariantCulture);
+ if (overallResult.ContainsKey(area.Attribute("alias").Value))
+ {
+ result = overallResult[area.Attribute("alias").Value];
+ }
+ var keys = area.XPathSelectElements("./key");
+ foreach (var key in keys)
+ {
+ var dictionaryKey =
+ (string)key.Attribute("alias");
+ //there could be duplicates if the language file isn't formatted nicely - which is probably the case for quite a few lang files
+ if (result.ContainsKey(dictionaryKey) == false)
+ result.Add(dictionaryKey, key.Value);
+ }
+ if (!overallResult.ContainsKey(area.Attribute("alias").Value))
+ {
+ overallResult.Add(area.Attribute("alias").Value, result);
+ }
+ }
+ }
+ return overallResult;
+ }
+ private Dictionary GetNoAreaStoredTranslations(IDictionary> xmlSource, CultureInfo cult)
+ {
+ var result = new Dictionary(StringComparer.InvariantCulture);
+ var keys = xmlSource[cult].Value.XPathSelectElements("//key");
+
+ foreach (var key in keys)
+ {
+ var dictionaryKey =
+ (string)key.Attribute("alias");
+ //there could be duplicates if the language file isn't formatted nicely - which is probably the case for quite a few lang files
+ if (result.ContainsKey(dictionaryKey) == false)
+ result.Add(dictionaryKey, key.Value);
+ }
+
+ //Merge English Dictionary
+ var englishCulture = new CultureInfo("en-US");
+ if (!cult.Equals(englishCulture))
+ {
+ var keysEn = xmlSource[englishCulture].Value.XPathSelectElements("//key");
+
+ foreach (var key in keys)
+ {
+ var dictionaryKey =
+ (string)key.Attribute("alias");
//there could be duplicates if the language file isn't formatted nicely - which is probably the case for quite a few lang files
if (result.ContainsKey(dictionaryKey) == false)
result.Add(dictionaryKey, key.Value);
@@ -166,6 +252,25 @@ namespace Umbraco.Cms.Core.Services.Implement
}
return result;
}
+ private Dictionary> GetAreaStoredTranslations(IDictionary>> dictionarySource, CultureInfo cult)
+ {
+ var overallResult = new Dictionary>(StringComparer.InvariantCulture);
+ var areaDict = dictionarySource[cult];
+
+ foreach (var area in areaDict)
+ {
+ var result = new Dictionary(StringComparer.InvariantCulture);
+ var keys = area.Value.Keys;
+ foreach (var key in keys)
+ {
+ //there could be duplicates if the language file isn't formatted nicely - which is probably the case for quite a few lang files
+ if (result.ContainsKey(key) == false)
+ result.Add(key, area.Value[key]);
+ }
+ overallResult.Add(area.Key, result);
+ }
+ return overallResult;
+ }
///
/// Returns a list of all currently supported cultures
@@ -173,11 +278,7 @@ namespace Umbraco.Cms.Core.Services.Implement
///
public IEnumerable GetSupportedCultures()
{
- var xmlSource = _xmlSource ?? (_fileSources != null
- ? _fileSources.Value.GetXmlSources()
- : null);
-
- return xmlSource != null ? xmlSource.Keys : _dictionarySource.Keys;
+ return _dictionarySource.Keys;
}
///
@@ -213,27 +314,25 @@ namespace Umbraco.Cms.Core.Services.Implement
return "[" + key + "]";
}
- var cultureSource = _dictionarySource[culture];
- string found;
- if (area.IsNullOrWhiteSpace())
+ string found = null;
+ if (string.IsNullOrWhiteSpace(area))
{
- found = cultureSource
- .SelectMany(x => x.Value)
- .Where(keyvals => keyvals.Key.InvariantEquals(key))
- .Select(x => x.Value)
- .FirstOrDefault();
+ _noAreaDictionarySource[culture].TryGetValue(key, out found);
}
else
{
- found = cultureSource
- .Where(areas => areas.Key.InvariantEquals(area))
- .SelectMany(a => a.Value)
- .Where(keyvals => keyvals.Key.InvariantEquals(key))
- .Select(x => x.Value)
- .FirstOrDefault();
+ if (_dictionarySource[culture].TryGetValue(area, out var areaDictionary))
+ {
+ areaDictionary.TryGetValue(key, out found);
+ }
+ if (found == null)
+ {
+ _noAreaDictionarySource[culture].TryGetValue(key, out found);
+ }
}
+
if (found != null)
{
return ParseTokens(found, tokens);
@@ -242,44 +341,6 @@ namespace Umbraco.Cms.Core.Services.Implement
//NOTE: Based on how legacy works, the default text does not contain the area, just the key
return "[" + key + "]";
}
-
- private string GetFromXmlSource(IDictionary> xmlSource, CultureInfo culture, string area, string key, IDictionary tokens)
- {
- if (xmlSource.ContainsKey(culture) == false)
- {
- _logger.LogWarning("The culture specified {Culture} was not found in any configured sources for this service", culture);
- return "[" + key + "]";
- }
-
- var found = FindTranslation(xmlSource, culture, area, key);
-
- if (found != null)
- {
- return ParseTokens(found.Value, tokens);
- }
-
- // Fall back to English by default if we can't find the key
- found = FindTranslation(xmlSource, new CultureInfo("en-US"), area, key);
- if (found != null)
- return ParseTokens(found.Value, tokens);
-
- // If it can't be found in either file, fall back to the default, showing just the key in square brackets
- // NOTE: Based on how legacy works, the default text does not contain the area, just the key
- return "[" + key + "]";
- }
-
- private XElement FindTranslation(IDictionary> xmlSource, CultureInfo culture, string area, string key)
- {
- var cultureSource = xmlSource[culture].Value;
-
- var xpath = area.IsNullOrWhiteSpace()
- ? string.Format("//key [@alias = '{0}']", key)
- : string.Format("//area [@alias = '{0}']/key [@alias = '{1}']", area, key);
-
- var found = cultureSource.XPathSelectElement(xpath);
- return found;
- }
-
///
/// Parses the tokens in the value
///
@@ -303,11 +364,26 @@ namespace Umbraco.Cms.Core.Services.Implement
foreach (var token in tokens)
{
- value = value.Replace(string.Format("{0}{1}{0}", "%", token.Key), token.Value);
+ value = value.Replace(string.Concat("%", token.Key, "%"), token.Value);
}
return value;
}
+ public IDictionary> GetAllStoredValuesByAreaAndAlias(CultureInfo culture)
+ {
+ if (culture == null) throw new ArgumentNullException("culture");
+
+ // TODO: Hack, see notes on ConvertToSupportedCultureWithRegionCode
+ culture = ConvertToSupportedCultureWithRegionCode(culture);
+
+ if (_dictionarySource.ContainsKey(culture) == false)
+ {
+ _logger.LogWarning("The culture specified {Culture} was not found in any configured sources for this service", culture);
+ return new Dictionary>(0);
+ }
+
+ return _dictionarySource[culture];
+ }
}
}
diff --git a/src/Umbraco.Persistence.SqlCe/SqlCeSyntaxProvider.cs b/src/Umbraco.Persistence.SqlCe/SqlCeSyntaxProvider.cs
index 717eada77f..e81b6135da 100644
--- a/src/Umbraco.Persistence.SqlCe/SqlCeSyntaxProvider.cs
+++ b/src/Umbraco.Persistence.SqlCe/SqlCeSyntaxProvider.cs
@@ -299,7 +299,7 @@ where table_name=@0 and column_name=@1", tableName, columnName).FirstOrDefault()
return string.Format(CreateIndex, GetIndexType(index.IndexType), " ", GetQuotedName(name),
GetQuotedTableName(index.TableName), columns);
}
-
+
public override string GetSpecialDbType(SpecialDbTypes dbTypes)
{
if (dbTypes == SpecialDbTypes.NVARCHARMAX) // SqlCE does not have nvarchar(max) for now
diff --git a/src/Umbraco.PublishedCache.NuCache/DataSource/BTree.DictionaryOfPropertyDataSerializer.cs b/src/Umbraco.PublishedCache.NuCache/DataSource/BTree.DictionaryOfPropertyDataSerializer.cs
index 31b19ae1ca..2603937fc5 100644
--- a/src/Umbraco.PublishedCache.NuCache/DataSource/BTree.DictionaryOfPropertyDataSerializer.cs
+++ b/src/Umbraco.PublishedCache.NuCache/DataSource/BTree.DictionaryOfPropertyDataSerializer.cs
@@ -16,8 +16,8 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource
// read properties count
var pcount = PrimitiveSerializer.Int32.ReadFrom(stream);
-
var dict = new Dictionary(pcount,StringComparer.InvariantCultureIgnoreCase);
+
// read each property
for (var i = 0; i < pcount; i++)
{
@@ -34,7 +34,7 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource
for (var j = 0; j < vcount; j++)
{
var pdata = new PropertyData();
- pdatas[j] =pdata;
+ pdatas[j] = pdata;
// everything that can be null is read/written as object
// even though - culture and segment should never be null here, as 'null' represents
diff --git a/src/Umbraco.PublishedCache.NuCache/DataSource/ContentSourceDto.cs b/src/Umbraco.PublishedCache.NuCache/DataSource/ContentSourceDto.cs
index 78a8ea6e81..2d456e4c0f 100644
--- a/src/Umbraco.PublishedCache.NuCache/DataSource/ContentSourceDto.cs
+++ b/src/Umbraco.PublishedCache.NuCache/DataSource/ContentSourceDto.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using Umbraco.Cms.Core.Models;
namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource
diff --git a/src/Umbraco.PublishedCache.NuCache/DataSource/MsgPackContentNestedDataSerializerFactory.cs b/src/Umbraco.PublishedCache.NuCache/DataSource/MsgPackContentNestedDataSerializerFactory.cs
index 9c3d1fe3c2..f93cd71ad2 100644
--- a/src/Umbraco.PublishedCache.NuCache/DataSource/MsgPackContentNestedDataSerializerFactory.cs
+++ b/src/Umbraco.PublishedCache.NuCache/DataSource/MsgPackContentNestedDataSerializerFactory.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Core.Services;
+using Umbraco.Core.PropertyEditors;
namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource
{
diff --git a/src/Umbraco.Tests.Benchmarks/LocalizedTextServiceGetAllStoredValuesBenchmarks.cs b/src/Umbraco.Tests.Benchmarks/LocalizedTextServiceGetAllStoredValuesBenchmarks.cs
new file mode 100644
index 0000000000..f16df01ef4
--- /dev/null
+++ b/src/Umbraco.Tests.Benchmarks/LocalizedTextServiceGetAllStoredValuesBenchmarks.cs
@@ -0,0 +1,561 @@
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Loggers;
+using Moq;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Umbraco.Core.Services.Implement;
+using System.Xml.Linq;
+using Umbraco.Core.Logging;
+using Umbraco.Tests.Benchmarks.Config;
+using Umbraco.Core.Services;
+using Umbraco.Core;
+using System.Xml.XPath;
+using ILogger = Umbraco.Core.Logging.ILogger;
+
+namespace Umbraco.Tests.Benchmarks
+{
+ [QuickRunWithMemoryDiagnoserConfig]
+ public class LocalizedTextServiceGetAllStoredValuesBenchmarks
+ {
+ private CultureInfo culture;
+ private OldLocalizedTextService _dictionaryService;
+ private OldLocalizedTextService _xmlService;
+
+ private LocalizedTextService _optimized;
+ private LocalizedTextService _optimizedDict;
+ [GlobalSetup]
+ public void Setup()
+ {
+ culture = CultureInfo.GetCultureInfo("en-US");
+ _dictionaryService = GetDictionaryLocalizedTextService(culture);
+ _xmlService = GetXmlService(culture);
+ _optimized = GetOptimizedService(culture);
+ _optimizedDict = GetOptimizedServiceDict(culture);
+ var result1 = _dictionaryService.Localize("language", culture);
+ var result2 = _xmlService.Localize("language", culture);
+ var result3 = _dictionaryService.GetAllStoredValues(culture);
+ var result4 = _xmlService.GetAllStoredValues(culture);
+ var result5 = _optimized.GetAllStoredValues(culture);
+ var result6 = _xmlService.GetAllStoredValues(culture);
+ var result7 = _optimized.GetAllStoredValuesByAreaAndAlias(culture);
+ }
+
+ [Benchmark]
+ public void OriginalDictionaryGetAll()
+ {
+ for (int i = 0; i < 10000; i++)
+ {
+ var result = _dictionaryService.GetAllStoredValues(culture);
+ }
+
+ }
+
+ [Benchmark]
+ public void OriginalXmlGetAll()
+ {
+ for (int i = 0; i < 10000; i++)
+ {
+ var result = _xmlService.GetAllStoredValues(culture);
+ }
+
+ }
+
+ [Benchmark]
+ public void OriginalDictionaryLocalize()
+ {
+ for (int i = 0; i < 10000; i++)
+ {
+ var result = _dictionaryService.Localize("language", culture);
+ }
+
+ }
+
+
+ [Benchmark(Baseline = true)]
+ public void OriginalXmlLocalize()
+ {
+ for (int i = 0; i < 10000; i++)
+ {
+ var result = _xmlService.Localize("language", culture);
+ }
+ }
+ [Benchmark]
+ public void OptimizedXmlGetAll()
+ {
+ for (int i = 0; i < 10000; i++)
+ {
+ var result = _optimized.GetAllStoredValues(culture);
+ }
+
+ }
+ [Benchmark]
+ public void OptimizedDictGetAll()
+ {
+ for (int i = 0; i < 10000; i++)
+ {
+ var result = _optimizedDict.GetAllStoredValues(culture);
+ }
+ }
+
+ [Benchmark]
+ public void OptimizedDictGetAllV2()
+ {
+ for (int i = 0; i < 10000; i++)
+ {
+ var result = _optimizedDict.GetAllStoredValuesByAreaAndAlias(culture);
+ }
+ }
+
+ [Benchmark()]
+ public void OptimizedXmlLocalize()
+ {
+ for (int i = 0; i < 10000; i++)
+ {
+ var result = _optimized.Localize(null, "language", culture);
+ }
+ }
+ [Benchmark()]
+ public void OptimizedDictLocalize()
+ {
+ for (int i = 0; i < 10000; i++)
+ {
+ var result = _optimizedDict.Localize(null, "language", culture);
+ }
+ }
+
+ private static LocalizedTextService GetOptimizedServiceDict(CultureInfo culture)
+ {
+ return new LocalizedTextService(
+ new Dictionary>>
+ {
+ {
+ culture, new Dictionary>
+ {
+ {
+ "testArea1", new Dictionary
+ {
+ {"testKey1", "testValue1"},
+ {"testKey2", "testValue2"}
+ }
+ },
+ {
+ "testArea2", new Dictionary
+ {
+ {"blah1", "blahValue1"},
+ {"blah2", "blahValue2"}
+ }
+ },
+ }
+ }
+ }, Mock.Of());
+ }
+ private static LocalizedTextService GetOptimizedService(CultureInfo culture)
+ {
+ var txtService = new LocalizedTextService(new Dictionary>
+ {
+ {
+ culture, new Lazy(() => new XDocument(
+ new XElement("language",
+ new XElement("area", new XAttribute("alias", "testArea1"),
+ new XElement("key", new XAttribute("alias", "testKey1"), "testValue1"),
+ new XElement("key", new XAttribute("alias", "testKey2"), "testValue2")),
+ new XElement("area", new XAttribute("alias", "testArea2"),
+ new XElement("key", new XAttribute("alias", "blah1"), "blahValue1"),
+ new XElement("key", new XAttribute("alias", "blah2"), "blahValue2")))))
+ }
+ }, Mock.Of());
+ return txtService;
+ }
+
+ private static OldLocalizedTextService GetXmlService(CultureInfo culture)
+ {
+ var txtService = new OldLocalizedTextService(new Dictionary>
+ {
+ {
+ culture, new Lazy(() => new XDocument(
+ new XElement("language",
+ new XElement("area", new XAttribute("alias", "testArea1"),
+ new XElement("key", new XAttribute("alias", "testKey1"), "testValue1"),
+ new XElement("key", new XAttribute("alias", "testKey2"), "testValue2")),
+ new XElement("area", new XAttribute("alias", "testArea2"),
+ new XElement("key", new XAttribute("alias", "blah1"), "blahValue1"),
+ new XElement("key", new XAttribute("alias", "blah2"), "blahValue2")))))
+ }
+ }, Mock.Of());
+ return txtService;
+ }
+
+ private static OldLocalizedTextService GetDictionaryLocalizedTextService(CultureInfo culture)
+ {
+ return new OldLocalizedTextService(
+ new Dictionary>>
+ {
+ {
+ culture, new Dictionary>
+ {
+ {
+ "testArea1", new Dictionary
+ {
+ {"testKey1", "testValue1"},
+ {"testKey2", "testValue2"}
+ }
+ },
+ {
+ "testArea2", new Dictionary
+ {
+ {"blah1", "blahValue1"},
+ {"blah2", "blahValue2"}
+ }
+ },
+ }
+ }
+ }, Mock.Of());
+ }
+ }
+
+ //Original
+ public class OldLocalizedTextService : ILocalizedTextService
+ {
+ private readonly ILogger _logger;
+ private readonly Lazy _fileSources;
+ private readonly IDictionary>> _dictionarySource;
+ private readonly IDictionary> _xmlSource;
+
+ ///
+ /// Initializes with a file sources instance
+ ///
+ ///
+ ///
+ public OldLocalizedTextService(Lazy fileSources, ILogger logger)
+ {
+ if (logger == null) throw new ArgumentNullException("logger");
+ _logger = logger;
+ if (fileSources == null) throw new ArgumentNullException("fileSources");
+ _fileSources = fileSources;
+ }
+
+ ///
+ /// Initializes with an XML source
+ ///
+ ///
+ ///
+ public OldLocalizedTextService(IDictionary> source, ILogger logger)
+ {
+ if (source == null) throw new ArgumentNullException("source");
+ if (logger == null) throw new ArgumentNullException("logger");
+ _xmlSource = source;
+ _logger = logger;
+ }
+
+ ///
+ /// Initializes with a source of a dictionary of culture -> areas -> sub dictionary of keys/values
+ ///
+ ///
+ ///
+ public OldLocalizedTextService(IDictionary>> source, ILogger logger)
+ {
+ _dictionarySource = source ?? throw new ArgumentNullException(nameof(source));
+ _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ }
+
+ public string Localize(string key, CultureInfo culture, IDictionary tokens = null)
+ {
+ if (culture == null) throw new ArgumentNullException(nameof(culture));
+
+ // TODO: Hack, see notes on ConvertToSupportedCultureWithRegionCode
+ culture = ConvertToSupportedCultureWithRegionCode(culture);
+
+ //This is what the legacy ui service did
+ if (string.IsNullOrEmpty(key))
+ return string.Empty;
+
+ var keyParts = key.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
+ var area = keyParts.Length > 1 ? keyParts[0] : null;
+ var alias = keyParts.Length > 1 ? keyParts[1] : keyParts[0];
+
+ var xmlSource = _xmlSource ?? (_fileSources != null
+ ? _fileSources.Value.GetXmlSources()
+ : null);
+
+ if (xmlSource != null)
+ {
+ return GetFromXmlSource(xmlSource, culture, area, alias, tokens);
+ }
+ else
+ {
+ return GetFromDictionarySource(culture, area, alias, tokens);
+ }
+ }
+
+ ///
+ /// Returns all key/values in storage for the given culture
+ ///
+ ///
+ public IDictionary GetAllStoredValues(CultureInfo culture)
+ {
+ if (culture == null) throw new ArgumentNullException("culture");
+
+ // TODO: Hack, see notes on ConvertToSupportedCultureWithRegionCode
+ culture = ConvertToSupportedCultureWithRegionCode(culture);
+
+ var result = new Dictionary();
+
+ var xmlSource = _xmlSource ?? (_fileSources != null
+ ? _fileSources.Value.GetXmlSources()
+ : null);
+
+ if (xmlSource != null)
+ {
+ if (xmlSource.ContainsKey(culture) == false)
+ {
+ _logger.Warn("The culture specified {Culture} was not found in any configured sources for this service", culture);
+ return result;
+ }
+
+ //convert all areas + keys to a single key with a '/'
+ result = GetStoredTranslations(xmlSource, culture);
+
+ //merge with the English file in case there's keys in there that don't exist in the local file
+ var englishCulture = new CultureInfo("en-US");
+ if (culture.Equals(englishCulture) == false)
+ {
+ var englishResults = GetStoredTranslations(xmlSource, englishCulture);
+ foreach (var englishResult in englishResults.Where(englishResult => result.ContainsKey(englishResult.Key) == false))
+ result.Add(englishResult.Key, englishResult.Value);
+ }
+ }
+ else
+ {
+ if (_dictionarySource.ContainsKey(culture) == false)
+ {
+ _logger.Warn("The culture specified {Culture} was not found in any configured sources for this service", culture);
+ return result;
+ }
+
+ //convert all areas + keys to a single key with a '/'
+ foreach (var area in _dictionarySource[culture])
+ {
+ foreach (var key in area.Value)
+ {
+ var dictionaryKey = string.Format("{0}/{1}", area.Key, key.Key);
+ //i don't think it's possible to have duplicates because we're dealing with a dictionary in the first place, but we'll double check here just in case.
+ if (result.ContainsKey(dictionaryKey) == false)
+ {
+ result.Add(dictionaryKey, key.Value);
+ }
+ }
+ }
+ }
+
+ return result;
+ }
+
+ private Dictionary GetStoredTranslations(IDictionary> xmlSource, CultureInfo cult)
+ {
+ var result = new Dictionary();
+ var areas = xmlSource[cult].Value.XPathSelectElements("//area");
+ foreach (var area in areas)
+ {
+ var keys = area.XPathSelectElements("./key");
+ foreach (var key in keys)
+ {
+ var dictionaryKey = string.Format("{0}/{1}", (string)area.Attribute("alias"),
+ (string)key.Attribute("alias"));
+ //there could be duplicates if the language file isn't formatted nicely - which is probably the case for quite a few lang files
+ if (result.ContainsKey(dictionaryKey) == false)
+ result.Add(dictionaryKey, key.Value);
+ }
+ }
+ return result;
+ }
+
+ ///
+ /// Returns a list of all currently supported cultures
+ ///
+ ///
+ public IEnumerable GetSupportedCultures()
+ {
+ var xmlSource = _xmlSource ?? (_fileSources != null
+ ? _fileSources.Value.GetXmlSources()
+ : null);
+
+ return xmlSource != null ? xmlSource.Keys : _dictionarySource.Keys;
+ }
+
+ ///
+ /// Tries to resolve a full 4 letter culture from a 2 letter culture name
+ ///
+ ///
+ /// The culture to determine if it is only a 2 letter culture, if so we'll try to convert it, otherwise it will just be returned
+ ///
+ ///
+ ///
+ /// TODO: This is just a hack due to the way we store the language files, they should be stored with 4 letters since that
+ /// is what they reference but they are stored with 2, further more our user's languages are stored with 2. So this attempts
+ /// to resolve the full culture if possible.
+ ///
+ /// This only works when this service is constructed with the LocalizedTextServiceFileSources
+ ///
+ public CultureInfo ConvertToSupportedCultureWithRegionCode(CultureInfo currentCulture)
+ {
+ if (currentCulture == null) throw new ArgumentNullException("currentCulture");
+
+ if (_fileSources == null) return currentCulture;
+ if (currentCulture.Name.Length > 2) return currentCulture;
+
+ var attempt = _fileSources.Value.TryConvert2LetterCultureTo4Letter(currentCulture.TwoLetterISOLanguageName);
+ return attempt ? attempt.Result : currentCulture;
+ }
+
+ private string GetFromDictionarySource(CultureInfo culture, string area, string key, IDictionary tokens)
+ {
+ if (_dictionarySource.ContainsKey(culture) == false)
+ {
+ _logger.Warn("The culture specified {Culture} was not found in any configured sources for this service", culture);
+ return "[" + key + "]";
+ }
+
+ var cultureSource = _dictionarySource[culture];
+
+ string found;
+ if (area.IsNullOrWhiteSpace())
+ {
+ found = cultureSource
+ .SelectMany(x => x.Value)
+ .Where(keyvals => keyvals.Key.InvariantEquals(key))
+ .Select(x => x.Value)
+ .FirstOrDefault();
+ }
+ else
+ {
+ found = cultureSource
+ .Where(areas => areas.Key.InvariantEquals(area))
+ .SelectMany(a => a.Value)
+ .Where(keyvals => keyvals.Key.InvariantEquals(key))
+ .Select(x => x.Value)
+ .FirstOrDefault();
+ }
+
+ if (found != null)
+ {
+ return ParseTokens(found, tokens);
+ }
+
+ //NOTE: Based on how legacy works, the default text does not contain the area, just the key
+ return "[" + key + "]";
+ }
+
+ private string GetFromXmlSource(IDictionary> xmlSource, CultureInfo culture, string area, string key, IDictionary tokens)
+ {
+ if (xmlSource.ContainsKey(culture) == false)
+ {
+ _logger.Warn("The culture specified {Culture} was not found in any configured sources for this service", culture);
+ return "[" + key + "]";
+ }
+
+ var found = FindTranslation(xmlSource, culture, area, key);
+
+ if (found != null)
+ {
+ return ParseTokens(found.Value, tokens);
+ }
+
+ // Fall back to English by default if we can't find the key
+ found = FindTranslation(xmlSource, new CultureInfo("en-US"), area, key);
+ if (found != null)
+ return ParseTokens(found.Value, tokens);
+
+ // If it can't be found in either file, fall back to the default, showing just the key in square brackets
+ // NOTE: Based on how legacy works, the default text does not contain the area, just the key
+ return "[" + key + "]";
+ }
+
+ private XElement FindTranslation(IDictionary> xmlSource, CultureInfo culture, string area, string key)
+ {
+ var cultureSource = xmlSource[culture].Value;
+
+ var xpath = area.IsNullOrWhiteSpace()
+ ? string.Format("//key [@alias = '{0}']", key)
+ : string.Format("//area [@alias = '{0}']/key [@alias = '{1}']", area, key);
+
+ var found = cultureSource.XPathSelectElement(xpath);
+ return found;
+ }
+
+ ///
+ /// Parses the tokens in the value
+ ///
+ ///
+ ///
+ ///
+ ///
+ /// This is based on how the legacy ui localized text worked, each token was just a sequential value delimited with a % symbol.
+ /// For example: hello %0%, you are %1% !
+ ///
+ /// Since we're going to continue using the same language files for now, the token system needs to remain the same. With our new service
+ /// we support a dictionary which means in the future we can really have any sort of token system.
+ /// Currently though, the token key's will need to be an integer and sequential - though we aren't going to throw exceptions if that is not the case.
+ ///
+ internal static string ParseTokens(string value, IDictionary tokens)
+ {
+ if (tokens == null || tokens.Any() == false)
+ {
+ return value;
+ }
+
+ foreach (var token in tokens)
+ {
+ value = value.Replace(string.Format("{0}{1}{0}", "%", token.Key), token.Value);
+ }
+
+ return value;
+ }
+
+ }
+
+// // * Summary *
+
+// BenchmarkDotNet=v0.11.3, OS=Windows 10.0.18362
+//Intel Core i5-8265U CPU 1.60GHz(Kaby Lake R), 1 CPU, 8 logical and 4 physical cores
+// [Host] : .NET Framework 4.7.2 (CLR 4.0.30319.42000), 32bit LegacyJIT-v4.8.4250.0
+// Job-JIATTD : .NET Framework 4.7.2 (CLR 4.0.30319.42000), 32bit LegacyJIT-v4.8.4250.0
+
+//IterationCount=3 IterationTime=100.0000 ms LaunchCount = 1
+//WarmupCount=3
+
+// Method | Mean | Error | StdDev | Ratio | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op |
+//---------------------- |----------:|-----------:|----------:|------:|------------:|------------:|------------:|--------------------:|
+// DictionaryGetAll | 11.199 ms | 1.8170 ms | 0.0996 ms | 0.14 | 1888.8889 | - | - | 5868.59 KB |
+// XmlGetAll | 62.963 ms | 24.0615 ms | 1.3189 ms | 0.81 | 13500.0000 | - | - | 42448.71 KB |
+// DictionaryLocalize | 9.757 ms | 1.6966 ms | 0.0930 ms | 0.13 | 1100.0000 | - | - | 3677.65 KB |
+// XmlLocalize | 77.725 ms | 14.6069 ms | 0.8007 ms | 1.00 | 14000.0000 | - | - | 43032.8 KB |
+// OptimizedXmlLocalize | 2.402 ms | 0.4256 ms | 0.0233 ms | 0.03 | 187.5000 | - | - | 626.01 KB |
+// OptimizedDictLocalize | 2.345 ms | 0.2411 ms | 0.0132 ms | 0.03 | 187.5000 | - | - | 626.01 KB |
+
+//// * Warnings *
+//MinIterationTime
+// LocalizedTextServiceGetAllStoredValuesBenchmarks.DictionaryGetAll: IterationCount= 3, IterationTime= 100.0000 ms, LaunchCount= 1, WarmupCount= 3->MinIterationTime = 99.7816 ms which is very small. It's recommended to increase it.
+// LocalizedTextServiceGetAllStoredValuesBenchmarks.DictionaryLocalize: IterationCount= 3, IterationTime= 100.0000 ms, LaunchCount= 1, WarmupCount= 3->MinIterationTime = 96.7415 ms which is very small. It's recommended to increase it.
+// LocalizedTextServiceGetAllStoredValuesBenchmarks.XmlLocalize: IterationCount= 3, IterationTime= 100.0000 ms, LaunchCount= 1, WarmupCount= 3->MinIterationTime = 76.8151 ms which is very small. It's recommended to increase it.
+
+//// * Legends *
+// Mean : Arithmetic mean of all measurements
+// Error : Half of 99.9% confidence interval
+// StdDev : Standard deviation of all measurements
+// Ratio : Mean of the ratio distribution ([Current]/[Baseline])
+// Gen 0/1k Op : GC Generation 0 collects per 1k Operations
+// Gen 1/1k Op : GC Generation 1 collects per 1k Operations
+// Gen 2/1k Op : GC Generation 2 collects per 1k Operations
+// Allocated Memory/Op : Allocated memory per single operation(managed only, inclusive, 1KB = 1024B)
+// 1 ms : 1 Millisecond(0.001 sec)
+
+//// * Diagnostic Output - MemoryDiagnoser *
+
+
+// // ***** BenchmarkRunner: End *****
+// Run time: 00:00:09 (9.15 sec), executed benchmarks: 6
+}
diff --git a/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj b/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj
index b6932ff74f..bb152a0bfd 100644
--- a/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj
+++ b/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj
@@ -11,6 +11,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -33,4 +65,5 @@
+
diff --git a/src/Umbraco.Tests.Integration/Umbraco.Core/Mapping/UmbracoMapperTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Core/Mapping/UmbracoMapperTests.cs
index 276a6f0f4c..9807640ca9 100644
--- a/src/Umbraco.Tests.Integration/Umbraco.Core/Mapping/UmbracoMapperTests.cs
+++ b/src/Umbraco.Tests.Integration/Umbraco.Core/Mapping/UmbracoMapperTests.cs
@@ -260,8 +260,8 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Mapping
}
Assert.AreEqual(contentType.CompositionPropertyGroups.Count(), invariantContent.Tabs.Count() - 1);
- Assert.IsTrue(invariantContent.Tabs.Any(x => x.Label == _localizedTextService.Localize("general/properties")));
- Assert.AreEqual(2, invariantContent.Tabs.Where(x => x.Label == _localizedTextService.Localize("general/properties")).SelectMany(x => x.Properties.Where(p => p.Alias.StartsWith("_umb_") == false)).Count());
+ Assert.IsTrue(invariantContent.Tabs.Any(x => x.Label == _localizedTextService.Localize("general","properties")));
+ Assert.AreEqual(2, invariantContent.Tabs.Where(x => x.Label == _localizedTextService.Localize("general","properties")).SelectMany(x => x.Properties.Where(p => p.Alias.StartsWith("_umb_") == false)).Count());
}
private void AssertBasics(ContentItemDisplay result, IContent content)
diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/MultiValuePropertyEditorTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/MultiValuePropertyEditorTests.cs
index 46a8073a46..316de73efb 100644
--- a/src/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/MultiValuePropertyEditorTests.cs
+++ b/src/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/MultiValuePropertyEditorTests.cs
@@ -117,7 +117,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors
// editor wants ApplicationContext.Current.Services.TextService
// (that should be fixed with proper injection)
var textService = new Mock();
- textService.Setup(x => x.Localize(It.IsAny(), It.IsAny(), It.IsAny>())).Returns("blah");
+ textService.Setup(x => x.Localize(It.IsAny(), It.IsAny(),It.IsAny(), It.IsAny>())).Returns("blah");
//// var appContext = new ApplicationContext(
//// new DatabaseContext(TestObjects.GetIDatabaseFactoryMock(), logger, Mock.Of(), Mock.Of()),
diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Services/PropertyValidationServiceTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Services/PropertyValidationServiceTests.cs
index 16ef6d9715..dc70e1e8ce 100644
--- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Services/PropertyValidationServiceTests.cs
+++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Services/PropertyValidationServiceTests.cs
@@ -26,7 +26,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Services
private void MockObjects(out PropertyValidationService validationService, out IDataType dt)
{
var textService = new Mock();
- textService.Setup(x => x.Localize(It.IsAny(), Thread.CurrentThread.CurrentCulture, null)).Returns("Localized text");
+ textService.Setup(x => x.Localize(It.IsAny(),It.IsAny(), Thread.CurrentThread.CurrentCulture, null)).Returns("Localized text");
var dataTypeService = new Mock();
IDataType dataType = Mock.Of(
diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ImageCropperTest.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ImageCropperTest.cs
index cce29ff54c..1873b30c99 100644
--- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ImageCropperTest.cs
+++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ImageCropperTest.cs
@@ -54,48 +54,60 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.Common
Assert.AreEqual(cropperValue, obj);
}
- //// [TestCase(CropperJson1, CropperJson1, true)]
- //// [TestCase(CropperJson1, CropperJson2, false)]
- //// public void CanConvertImageCropperPropertyEditor(string val1, string val2, bool expected)
- //// {
- //// try
- //// {
- //// var container = TestHelper.GetRegister();
- //// var composition = new Composition(container, TestHelper.GetMockedTypeLoader(), Mock.Of(), ComponentTests.MockRuntimeState(RuntimeLevel.Run), TestHelper.GetConfigs(), TestHelper.IOHelper, AppCaches.NoCache);
- ////
- //// composition.WithCollectionBuilder();
- ////
- //// Current.Factory = composition.CreateFactory();
- ////
- //// var logger = Mock.Of();
- //// var scheme = Mock.Of();
- //// var shortStringHelper = Mock.Of();
- ////
- //// var mediaFileSystem = new MediaFileSystem(Mock.Of(), scheme, logger, shortStringHelper);
- ////
- //// var dataTypeService = new TestObjects.TestDataTypeService(
- //// new DataType(new ImageCropperPropertyEditor(Mock.Of(), mediaFileSystem, Mock.Of(), Mock.Of(), Mock.Of(), TestHelper.IOHelper, TestHelper.ShortStringHelper, Mock.Of())) { Id = 1 });
- ////
- //// var factory = new PublishedContentTypeFactory(Mock.Of(), new PropertyValueConverterCollection(Array.Empty()), dataTypeService);
- ////
- //// var converter = new ImageCropperValueConverter();
- //// var result = converter.ConvertSourceToIntermediate(null, factory.CreatePropertyType("test", 1), val1, false); // does not use type for conversion
- ////
- //// var resultShouldMatch = val2.DeserializeImageCropperValue();
- //// if (expected)
- //// {
- //// Assert.AreEqual(resultShouldMatch, result);
- //// }
- //// else
- //// {
- //// Assert.AreNotEqual(resultShouldMatch, result);
- //// }
- //// }
- //// finally
- //// {
- //// Current.Reset();
- //// }
- //// }
+ // [TestCase(CropperJson1, CropperJson1, true)]
+ // [TestCase(CropperJson1, CropperJson2, false)]
+ // public void CanConvertImageCropperPropertyEditor(string val1, string val2, bool expected)
+ // {
+ // try
+ // {
+ // var container = RegisterFactory.Create();
+ // var composition = new Composition(container, new TypeLoader(), Mock.Of(), ComponentTests.MockRuntimeState(RuntimeLevel.Run));
+ //
+ // composition.WithCollectionBuilder();
+ //
+ // Current.Factory = composition.CreateFactory();
+ //
+ // var logger = Mock.Of();
+ // var scheme = Mock.Of();
+ // var config = Mock.Of();
+ //
+ // var mediaFileSystem = new MediaFileSystem(Mock.Of(), config, scheme, logger);
+ //
+ // var imageCropperConfiguration = new ImageCropperConfiguration()
+ // {
+ // Crops = new[]
+ // {
+ // new ImageCropperConfiguration.Crop()
+ // {
+ // Alias = "thumb",
+ // Width = 100,
+ // Height = 100
+ // }
+ // }
+ // };
+ // var dataTypeService = new TestObjects.TestDataTypeService(
+ // new DataType(new ImageCropperPropertyEditor(Mock.Of(), mediaFileSystem, Mock.Of(), Mock.Of())) { Id = 1, Configuration = imageCropperConfiguration });
+ //
+ // var factory = new PublishedContentTypeFactory(Mock.Of(), new PropertyValueConverterCollection(Array.Empty()), dataTypeService);
+ //
+ // var converter = new ImageCropperValueConverter();
+ // var result = converter.ConvertSourceToIntermediate(null, factory.CreatePropertyType("test", 1), val1, false); // does not use type for conversion
+ //
+ // var resultShouldMatch = val2.DeserializeImageCropperValue();
+ // if (expected)
+ // {
+ // Assert.AreEqual(resultShouldMatch, result);
+ // }
+ // else
+ // {
+ // Assert.AreNotEqual(resultShouldMatch, result);
+ // }
+ // }
+ // finally
+ // {
+ // Current.Reset();
+ // }
+ // }
[Test]
public void GetCropUrl_CropAliasTest()
diff --git a/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs b/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs
index aa06724d75..17e78647f2 100644
--- a/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs
+++ b/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs
@@ -142,7 +142,7 @@ namespace Umbraco.Tests.PublishedContent
var globalSettings = new GlobalSettings();
var nuCacheSettings = new NuCacheSettings();
-
+
// at last, create the complete NuCache snapshot service!
var options = new PublishedSnapshotServiceOptions { IgnoreLocalDb = true };
_snapshotService = new PublishedSnapshotService(
diff --git a/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs b/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs
index 66c0c4b849..ae7776dfaf 100644
--- a/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs
+++ b/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs
@@ -383,12 +383,12 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
var code = await _userManager.GeneratePasswordResetTokenAsync(identityUser);
var callbackUrl = ConstructCallbackUrl(identityUser.Id, code);
- var message = _textService.Localize("login/resetPasswordEmailCopyFormat",
+ var message = _textService.Localize("login","resetPasswordEmailCopyFormat",
// Ensure the culture of the found user is used for the email!
UmbracoUserExtensions.GetUserCulture(identityUser.Culture, _textService, _globalSettings),
new[] { identityUser.UserName, callbackUrl });
- var subject = _textService.Localize("login/resetPasswordEmailCopySubject",
+ var subject = _textService.Localize("login","resetPasswordEmailCopySubject",
// Ensure the culture of the found user is used for the email!
UmbracoUserExtensions.GetUserCulture(identityUser.Culture, _textService, _globalSettings));
@@ -445,11 +445,11 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
return BadRequest("Invalid code");
}
- var subject = _textService.Localize("login/mfaSecurityCodeSubject",
+ var subject = _textService.Localize("login","mfaSecurityCodeSubject",
// Ensure the culture of the found user is used for the email!
UmbracoUserExtensions.GetUserCulture(user.Culture, _textService, _globalSettings));
- var message = _textService.Localize("login/mfaSecurityCodeMessage",
+ var message = _textService.Localize("login","mfaSecurityCodeMessage",
// Ensure the culture of the found user is used for the email!
UmbracoUserExtensions.GetUserCulture(user.Culture, _textService, _globalSettings),
new[] { code });
diff --git a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs
index d97ae3e8ae..161896628b 100644
--- a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs
+++ b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs
@@ -335,7 +335,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
}
//Add error and redirect for it to be displayed
- TempData[ViewDataExtensions.TokenPasswordResetCode] = new[] { _textService.Localize("login/resetCodeExpired") };
+ TempData[ViewDataExtensions.TokenPasswordResetCode] = new[] { _textService.Localize("login","resetCodeExpired") };
return RedirectToLocal(Url.Action(nameof(Default), this.GetControllerName()));
}
@@ -431,7 +431,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
// Sign in the user with this external login provider (which auto links, etc...)
var result = await _signInManager.ExternalLoginSignInAsync(loginInfo, isPersistent: false);
-
+
var errors = new List();
if (result == Microsoft.AspNetCore.Identity.SignInResult.Success)
diff --git a/src/Umbraco.Web.BackOffice/Controllers/CodeFileController.cs b/src/Umbraco.Web.BackOffice/Controllers/CodeFileController.cs
index 15a0469a4a..a88cdc5087 100644
--- a/src/Umbraco.Web.BackOffice/Controllers/CodeFileController.cs
+++ b/src/Umbraco.Web.BackOffice/Controllers/CodeFileController.cs
@@ -127,7 +127,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
if (string.IsNullOrWhiteSpace(parentId)) throw new ArgumentException("Value cannot be null or whitespace.", "parentId");
if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value cannot be null or whitespace.", "name");
if (name.ContainsAny(Path.GetInvalidPathChars())) {
- return ValidationProblem(_localizedTextService.Localize("codefile/createFolderIllegalChars"));
+ return ValidationProblem(_localizedTextService.Localize("codefile", "createFolderIllegalChars"));
}
// if the parentId is root (-1) then we just need an empty string as we are
@@ -422,8 +422,8 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
}
display.AddErrorNotification(
- _localizedTextService.Localize("speechBubbles/partialViewErrorHeader"),
- _localizedTextService.Localize("speechBubbles/partialViewErrorText"));
+ _localizedTextService.Localize("speechBubbles", "partialViewErrorHeader"),
+ _localizedTextService.Localize("speechBubbles", "partialViewErrorText"));
break;
case Constants.Trees.PartialViewMacros:
@@ -437,8 +437,8 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
}
display.AddErrorNotification(
- _localizedTextService.Localize("speechBubbles/partialViewErrorHeader"),
- _localizedTextService.Localize("speechBubbles/partialViewErrorText"));
+ _localizedTextService.Localize("speechBubbles", "partialViewErrorHeader"),
+ _localizedTextService.Localize("speechBubbles", "partialViewErrorText"));
break;
case Constants.Trees.Scripts:
diff --git a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs
index e82c22680d..03de07769d 100644
--- a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs
+++ b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs
@@ -275,7 +275,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
new ContentVariantDisplay
{
CreateDate = DateTime.Now,
- Name = _localizedTextService.Localize("general/recycleBin")
+ Name = _localizedTextService.Localize("general","recycleBin")
}
},
ContentApps = apps
@@ -417,16 +417,65 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
{
var emptyContent = _contentService.Create("", parentId, contentType.Alias, _backofficeSecurityAccessor.BackOfficeSecurity.GetUserId().ResultOr(0));
var mapped = MapToDisplay(emptyContent);
+
+ return CleanContentItemDisplay(mapped);
+ }
+
+ private ContentItemDisplay CleanContentItemDisplay(ContentItemDisplay display)
+ {
// translate the content type name if applicable
- mapped.ContentTypeName = _localizedTextService.UmbracoDictionaryTranslate(CultureDictionary, mapped.ContentTypeName);
+ display.ContentTypeName = _localizedTextService.UmbracoDictionaryTranslate(CultureDictionary, display.ContentTypeName);
// if your user type doesn't have access to the Settings section it would not get this property mapped
- if (mapped.DocumentType != null)
- mapped.DocumentType.Name = _localizedTextService.UmbracoDictionaryTranslate(CultureDictionary, mapped.DocumentType.Name);
+ if (display.DocumentType != null)
+ display.DocumentType.Name = _localizedTextService.UmbracoDictionaryTranslate(CultureDictionary, display.DocumentType.Name);
//remove the listview app if it exists
- mapped.ContentApps = mapped.ContentApps.Where(x => x.Alias != "umbListView").ToList();
+ display.ContentApps = display.ContentApps.Where(x => x.Alias != "umbListView").ToList();
- return mapped;
+ return display;
+ }
+
+ ///
+ /// Gets an empty for each content type in the IEnumerable, all with the same parent ID
+ ///
+ /// Will attempt to re-use the same permissions for every content as long as the path and user are the same
+ ///
+ ///
+ ///
+ private IEnumerable GetEmpties(IEnumerable contentTypes, int parentId)
+ {
+ var result = new List();
+ var backOfficeSecurity = _backofficeSecurityAccessor.BackOfficeSecurity;
+
+ var userId = backOfficeSecurity.GetUserId().ResultOr(0);
+ var currentUser = backOfficeSecurity.CurrentUser;
+ // We know that if the ID is less than 0 the parent is null.
+ // Since this is called with parent ID it's safe to assume that the parent is the same for all the content types.
+ var parent = parentId > 0 ? _contentService.GetById(parentId) : null;
+ // Since the parent is the same and the path used to get permissions is based on the parent we only have to do it once
+ var path = parent == null ? "-1" : parent.Path;
+ var permissions = new Dictionary
+ {
+ [path] = _userService.GetPermissionsForPath(currentUser, path)
+ };
+
+ foreach (var contentType in contentTypes)
+ {
+ var emptyContent = _contentService.Create("", parentId, contentType, userId);
+
+ var mapped = MapToDisplay(emptyContent, context =>
+ {
+ // Since the permissions depend on current user and path, we add both of these to context as well,
+ // that way we can compare the path and current user when mapping, if they're the same just take permissions
+ // and skip getting them again, in theory they should always be the same, but better safe than sorry.,
+ context.Items["Parent"] = parent;
+ context.Items["CurrentUser"] = currentUser;
+ context.Items["Permissions"] = permissions;
+ });
+ result.Add(CleanContentItemDisplay(mapped));
+ }
+
+ return result;
}
///
@@ -437,22 +486,9 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
[OutgoingEditorModelEvent]
public ActionResult> GetEmptyByKeys([FromQuery] Guid[] contentTypeKeys, [FromQuery] int parentId)
{
- var result = new Dictionary