diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs index ae907051ca..b4c0e51c22 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs @@ -66,8 +66,22 @@ namespace Umbraco.Core.Persistence.Repositories.Implement //no matter what we always must have node id ordered at the end sql = ordering.Direction == Direction.Ascending ? sql.OrderBy("NodeId") : sql.OrderByDescending("NodeId"); - var page = Database.Page(pageIndex + 1, pageSize, sql); - var dtos = page.Items; + // for content we must query for ContentEntityDto entities to produce the correct culture variant entity names + var pageIndexToFetch = pageIndex + 1; + IEnumerable dtos; + if(isContent) + { + var page = Database.Page(pageIndexToFetch, pageSize, sql); + dtos = page.Items; + totalRecords = page.TotalItems; + } + else + { + var page = Database.Page(pageIndexToFetch, pageSize, sql); + dtos = page.Items; + totalRecords = page.TotalItems; + } + var entities = dtos.Select(x => BuildEntity(isContent, isMedia, x)).ToArray(); if (isContent) @@ -75,9 +89,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // TODO: see https://github.com/umbraco/Umbraco-CMS/pull/3460#issuecomment-434903930 we need to not load any property data at all for media if (isMedia) - BuildProperties(entities, dtos); + BuildProperties(entities, dtos.ToList()); - totalRecords = page.TotalItems; return entities; } diff --git a/src/Umbraco.Web.UI.Client/src/common/interceptors/_module.js b/src/Umbraco.Web.UI.Client/src/common/interceptors/_module.js index 05263cebf2..69a4fe35c9 100644 --- a/src/Umbraco.Web.UI.Client/src/common/interceptors/_module.js +++ b/src/Umbraco.Web.UI.Client/src/common/interceptors/_module.js @@ -7,4 +7,6 @@ angular.module('umbraco.interceptors', []) $httpProvider.interceptors.push('securityInterceptor'); $httpProvider.interceptors.push('debugRequestInterceptor'); $httpProvider.interceptors.push('doNotPostDollarVariablesOnPostRequestInterceptor'); + $httpProvider.interceptors.push('cultureRequestInterceptor'); + }]); diff --git a/src/Umbraco.Web.UI.Client/src/common/interceptors/culturerequest.interceptor.js b/src/Umbraco.Web.UI.Client/src/common/interceptors/culturerequest.interceptor.js new file mode 100644 index 0000000000..6496d7a1f9 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/interceptors/culturerequest.interceptor.js @@ -0,0 +1,30 @@ +(function() { + 'use strict'; + + /** + * Used to set the current client culture on all requests API requests + * @param {any} $q + * @param {any} $routeParams + */ + function cultureRequestInterceptor($q, $routeParams) { + return { + //dealing with requests: + 'request': function (config) { + if (!Umbraco.Sys.ServerVariables.umbracoSettings.umbracoPath) { + // no settings available, we're probably on the login screen + return config; + } + if (!config.url.match(RegExp(Umbraco.Sys.ServerVariables.umbracoSettings.umbracoPath + "\/backoffice\/", "i"))) { + // it's not an API request, no handling + return config; + } + // it's an API request, add the current client culture as a header value + config.headers["X-UMB-CULTURE"] = $routeParams.cculture ? $routeParams.cculture : $routeParams.mculture; + return config; + } + }; + } + + angular.module('umbraco.interceptors').factory('cultureRequestInterceptor', cultureRequestInterceptor); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js index 2bebc55beb..866cfb54ab 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js @@ -148,9 +148,7 @@ function contentPickerController($scope, entityResource, editorState, iconHelper }, treeAlias: $scope.model.config.startNode.type, section: $scope.model.config.startNode.type, - idType: "udi", - //only show the lang selector for content - showLanguageSelector: $scope.model.config.startNode.type === "content" + idType: "udi" }; //since most of the pre-value config's are used in the dialog options (i.e. maxNumber, minNumber, etc...) we'll merge the diff --git a/src/Umbraco.Web/Editors/EntityController.cs b/src/Umbraco.Web/Editors/EntityController.cs index 9e91bb1d99..2705fc531c 100644 --- a/src/Umbraco.Web/Editors/EntityController.cs +++ b/src/Umbraco.Web/Editors/EntityController.cs @@ -504,10 +504,14 @@ namespace Umbraco.Web.Editors return new PagedResult(0, 0, 0); } + var culture = ClientCulture(); var pagedResult = new PagedResult(totalRecords, pageNumber, pageSize) { Items = entities.Select(entity => Mapper.Map(entity, options => - options.AfterMap((src, dest) => { dest.AdditionalData["hasChildren"] = src.HasChildren; }) + { + options.SetCulture(culture); + options.AfterMap((src, dest) => { dest.AdditionalData["hasChildren"] = src.HasChildren; }); + } ) ) }; @@ -586,7 +590,7 @@ namespace Umbraco.Web.Editors var pagedResult = new PagedResult(totalRecords, pageNumber, pageSize) { - Items = entities.Select(Mapper.Map) + Items = entities.Select(MapEntities()) }; return pagedResult; @@ -632,7 +636,7 @@ namespace Umbraco.Web.Editors return Services.EntityService.GetChildren(id, objectType.Value) .WhereNotNull() - .Select(Mapper.Map); + .Select(MapEntities()); } //now we need to convert the unknown ones switch (entityType) @@ -693,7 +697,7 @@ namespace Umbraco.Web.Editors : Services.EntityService.GetAll(objectType.Value, ids) .WhereNotNull() .OrderBy(x => x.Level) - .Select(x => Mapper.Map(x, opts => { opts.SetCulture(culture);})); + .Select(MapEntities(culture)); } //now we need to convert the unknown ones switch (entityType) @@ -719,7 +723,7 @@ namespace Umbraco.Web.Editors { var entities = Services.EntityService.GetAll(objectType.Value, keys) .WhereNotNull() - .Select(Mapper.Map); + .Select(MapEntities()); // entities are in "some" order, put them back in order var xref = entities.ToDictionary(x => x.Key); @@ -751,7 +755,7 @@ namespace Umbraco.Web.Editors { var entities = Services.EntityService.GetAll(objectType.Value, ids) .WhereNotNull() - .Select(Mapper.Map); + .Select(MapEntities()); // entities are in "some" order, put them back in order var xref = entities.ToDictionary(x => x.Id); @@ -815,7 +819,7 @@ namespace Umbraco.Web.Editors { throw new HttpResponseException(HttpStatusCode.NotFound); } - return Mapper.Map(found); + return MapEntity(found); } //now we need to convert the unknown ones switch (entityType) @@ -886,7 +890,7 @@ namespace Umbraco.Web.Editors if (objectType.HasValue) { // TODO: Should we order this by something ? - var entities = Services.EntityService.GetAll(objectType.Value).WhereNotNull().Select(Mapper.Map); + var entities = Services.EntityService.GetAll(objectType.Value).WhereNotNull().Select(MapEntities()); return ExecutePostFilter(entities, postFilter); } //now we need to convert the unknown ones @@ -895,13 +899,13 @@ namespace Umbraco.Web.Editors case UmbracoEntityTypes.Template: var templates = Services.FileService.GetTemplates(); var filteredTemplates = ExecutePostFilter(templates, postFilter); - return filteredTemplates.Select(Mapper.Map); + return filteredTemplates.Select(MapEntities()); case UmbracoEntityTypes.Macro: //Get all macros from the macro service var macros = Services.MacroService.GetAll().WhereNotNull().OrderBy(x => x.Name); var filteredMacros = ExecutePostFilter(macros, postFilter); - return filteredMacros.Select(Mapper.Map); + return filteredMacros.Select(MapEntities()); case UmbracoEntityTypes.PropertyType: @@ -936,14 +940,14 @@ namespace Umbraco.Web.Editors if (!postFilter.IsNullOrWhiteSpace()) throw new NotSupportedException("Filtering on stylesheets is not currently supported"); - return Services.FileService.GetStylesheets().Select(Mapper.Map); + return Services.FileService.GetStylesheets().Select(MapEntities()); case UmbracoEntityTypes.Language: if (!postFilter.IsNullOrWhiteSpace() ) throw new NotSupportedException("Filtering on languages is not currently supported"); - return Services.LocalizationService.GetAllLanguages().Select(Mapper.Map); + return Services.LocalizationService.GetAllLanguages().Select(MapEntities()); case UmbracoEntityTypes.DictionaryItem: if (!postFilter.IsNullOrWhiteSpace()) @@ -1039,8 +1043,19 @@ namespace Umbraco.Web.Editors return queryCondition; } + private Func MapEntities(string culture = null) + { + culture = culture ?? ClientCulture(); + return x => MapEntity(x, culture); + } + private EntityBasic MapEntity(object entity, string culture = null) + { + culture = culture ?? ClientCulture(); + return Mapper.Map(entity, opts => { opts.SetCulture(culture); }); + } + private string ClientCulture() => Request.ClientCulture(); #region Methods to get all dictionary items private IEnumerable GetAllDictionaryItems() diff --git a/src/Umbraco.Web/Models/Mapping/ContentMapperProfile.cs b/src/Umbraco.Web/Models/Mapping/ContentMapperProfile.cs index 9555cf2ef9..5318552eef 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentMapperProfile.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentMapperProfile.cs @@ -139,7 +139,7 @@ namespace Umbraco.Web.Models.Mapping // if we don't have a name for a culture, it means the culture is not available, and // hey we should probably not be mapping it, but it's too late, return a fallback name - return source.CultureInfos.TryGetValue(culture, out var name) && !name.Name.IsNullOrWhiteSpace() ? name.Name : $"(({source.Name}))"; + return source.CultureInfos.TryGetValue(culture, out var name) && !name.Name.IsNullOrWhiteSpace() ? name.Name : $"({source.Name})"; } } diff --git a/src/Umbraco.Web/Models/Mapping/EntityMapperProfile.cs b/src/Umbraco.Web/Models/Mapping/EntityMapperProfile.cs index ab3929166f..5b711d7251 100644 --- a/src/Umbraco.Web/Models/Mapping/EntityMapperProfile.cs +++ b/src/Umbraco.Web/Models/Mapping/EntityMapperProfile.cs @@ -209,7 +209,7 @@ namespace Umbraco.Web.Models.Mapping // if we don't have a name for a culture, it means the culture is not available, and // hey we should probably not be mapping it, but it's too late, return a fallback name - return doc.CultureNames.TryGetValue(culture, out var name) && !name.IsNullOrWhiteSpace() ? name : $"(({source.Name}))"; + return doc.CultureNames.TryGetValue(culture, out var name) && !name.IsNullOrWhiteSpace() ? name : $"({source.Name})"; } } } diff --git a/src/Umbraco.Web/WebApi/Filters/HttpQueryStringModelBinder.cs b/src/Umbraco.Web/WebApi/Filters/HttpQueryStringModelBinder.cs index 6ffbb239f8..4d58e2b512 100644 --- a/src/Umbraco.Web/WebApi/Filters/HttpQueryStringModelBinder.cs +++ b/src/Umbraco.Web/WebApi/Filters/HttpQueryStringModelBinder.cs @@ -1,7 +1,9 @@ using System.Collections.Generic; +using System.Linq; using System.Net.Http.Formatting; using System.Web.Http.Controllers; using System.Web.Http.ModelBinding; +using Umbraco.Core; namespace Umbraco.Web.WebApi.Filters { @@ -21,7 +23,13 @@ namespace Umbraco.Web.WebApi.Filters { if (actionContext.Request.Properties["MS_QueryNameValuePairs"] is IEnumerable> queryStrings) { - var formData = new FormDataCollection(queryStrings); + var queryStringKeys = queryStrings.Select(kvp => kvp.Key).ToArray(); + var additionalParameters = new Dictionary(); + if(queryStringKeys.InvariantContains("culture") == false) { + additionalParameters["culture"] = actionContext.Request.ClientCulture(); + } + + var formData = new FormDataCollection(queryStrings.Union(additionalParameters)); bindingContext.Model = formData; return true; } @@ -29,4 +37,4 @@ namespace Umbraco.Web.WebApi.Filters return false; } } -} \ No newline at end of file +} diff --git a/src/Umbraco.Web/WebApi/HttpRequestMessageExtensions.cs b/src/Umbraco.Web/WebApi/HttpRequestMessageExtensions.cs index 96c9c0e9a0..744b60cd52 100644 --- a/src/Umbraco.Web/WebApi/HttpRequestMessageExtensions.cs +++ b/src/Umbraco.Web/WebApi/HttpRequestMessageExtensions.cs @@ -1,4 +1,6 @@ using System; +using System.Globalization; +using System.Linq; using System.Net; using System.Net.Http; using System.Web; @@ -158,6 +160,11 @@ namespace Umbraco.Web.WebApi msg.Headers.Add("X-Status-Reason", "Validation failed"); return msg; } + + public static string ClientCulture(this HttpRequestMessage request) + { + return request.Headers.Contains("X-UMB-CULTURE") ? request.Headers.GetValues("X-UMB-CULTURE").First() : null; + } } }