using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using Microsoft.AspNetCore.Mvc; using Umbraco.Core; using Umbraco.Core.Configuration; using Umbraco.Core.Mapping; using Umbraco.Core.Models; using Umbraco.Core.Services; using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.Common.Attributes; using Umbraco.Web.Common.Exceptions; using Umbraco.Web.Editors; using Language = Umbraco.Web.Models.ContentEditing.Language; namespace Umbraco.Web.BackOffice.Controllers { /// /// Backoffice controller supporting the dashboard for language administration. /// [PluginController("UmbracoApi")] public class LanguageController : UmbracoAuthorizedJsonController { private readonly ILocalizationService _localizationService; private readonly UmbracoMapper _umbracoMapper; private readonly IGlobalSettings _globalSettings; public LanguageController(ILocalizationService localizationService, UmbracoMapper umbracoMapper, IGlobalSettings globalSettings) { _localizationService = localizationService ?? throw new ArgumentNullException(nameof(localizationService)); _umbracoMapper = umbracoMapper ?? throw new ArgumentNullException(nameof(umbracoMapper)); _globalSettings = globalSettings ?? throw new ArgumentNullException(nameof(globalSettings)); } /// /// Returns all cultures available for creating languages. /// /// [HttpGet] public IDictionary GetAllCultures() { // get cultures - new-ing instances to get proper display name, // in the current culture, and not the cached one // (see notes in Language class about culture info names) return CultureInfo.GetCultures(CultureTypes.AllCultures) .Where(x => !x.Name.IsNullOrWhiteSpace()) .Select(x => new CultureInfo(x.Name)) // important! .OrderBy(x => x.DisplayName) .ToDictionary(x => x.Name, x => x.DisplayName); } /// /// Returns all currently configured languages. /// /// [HttpGet] public IEnumerable GetAllLanguages() { var allLanguages = _localizationService.GetAllLanguages(); return _umbracoMapper.Map, IEnumerable>(allLanguages); } [HttpGet] public ActionResult GetLanguage(int id) { var lang = _localizationService.GetLanguageById(id); if (lang == null) return NotFound(); return _umbracoMapper.Map(lang); } /// /// Deletes a language with a given ID /// [TypeFilter(typeof(UmbracoTreeAuthorizeAttribute), Arguments = new object[]{new string[]{ Constants.Trees.Languages} })] [HttpDelete] [HttpPost] public IActionResult DeleteLanguage(int id) { var language = _localizationService.GetLanguageById(id); if (language == null) { return NotFound(); } // the service would not let us do it, but test here nevertheless if (language.IsDefault) { var message = $"Language '{language.IsoCode}' is currently set to 'default' and can not be deleted."; throw HttpResponseException.CreateNotificationValidationErrorResponse(message); } // service is happy deleting a language that's fallback for another language, // will just remove it - so no need to check here _localizationService.Delete(language); return Ok(); } /// /// Creates or saves a language /// [TypeFilter(typeof(UmbracoTreeAuthorizeAttribute), Arguments = new object[]{new string[]{ Constants.Trees.Languages} })] [HttpPost] public Language SaveLanguage(Language language) { if (!ModelState.IsValid) throw HttpResponseException.CreateValidationErrorResponse(ModelState); // this is prone to race conditions but the service will not let us proceed anyways var existingByCulture = _localizationService.GetLanguageByIsoCode(language.IsoCode); // the localization service might return the generic language even when queried for specific ones (e.g. "da" when queried for "da-DK") // - we need to handle that explicitly if (existingByCulture?.IsoCode != language.IsoCode) { existingByCulture = null; } if (existingByCulture != null && language.Id != existingByCulture.Id) { //someone is trying to create a language that already exist ModelState.AddModelError("IsoCode", "The language " + language.IsoCode + " already exists"); throw HttpResponseException.CreateValidationErrorResponse(ModelState); } var existingById = language.Id != default ? _localizationService.GetLanguageById(language.Id) : null; if (existingById == null) { //Creating a new lang... CultureInfo culture; try { culture = CultureInfo.GetCultureInfo(language.IsoCode); } catch (CultureNotFoundException) { ModelState.AddModelError("IsoCode", "No Culture found with name " + language.IsoCode); throw HttpResponseException.CreateValidationErrorResponse(ModelState); } // create it (creating a new language cannot create a fallback cycle) var newLang = new Core.Models.Language(_globalSettings, culture.Name) { CultureName = culture.DisplayName, IsDefault = language.IsDefault, IsMandatory = language.IsMandatory, FallbackLanguageId = language.FallbackLanguageId }; _localizationService.Save(newLang); return _umbracoMapper.Map(newLang); } existingById.IsMandatory = language.IsMandatory; // note that the service will prevent the default language from being "un-defaulted" // but does not hurt to test here - though the UI should prevent it too if (existingById.IsDefault && !language.IsDefault) { ModelState.AddModelError("IsDefault", "Cannot un-default the default language."); throw HttpResponseException.CreateValidationErrorResponse(ModelState); } existingById.IsDefault = language.IsDefault; existingById.FallbackLanguageId = language.FallbackLanguageId; existingById.IsoCode = language.IsoCode; // modifying an existing language can create a fallback, verify // note that the service will check again, dealing with race conditions if (existingById.FallbackLanguageId.HasValue) { var languages = _localizationService.GetAllLanguages().ToDictionary(x => x.Id, x => x); if (!languages.ContainsKey(existingById.FallbackLanguageId.Value)) { ModelState.AddModelError("FallbackLanguage", "The selected fall back language does not exist."); throw HttpResponseException.CreateValidationErrorResponse(ModelState); } if (CreatesCycle(existingById, languages)) { ModelState.AddModelError("FallbackLanguage", $"The selected fall back language {languages[existingById.FallbackLanguageId.Value].IsoCode} would create a circular path."); throw HttpResponseException.CreateValidationErrorResponse(ModelState); } } _localizationService.Save(existingById); return _umbracoMapper.Map(existingById); } // see LocalizationService private bool CreatesCycle(ILanguage language, IDictionary languages) { // a new language is not referenced yet, so cannot be part of a cycle if (!language.HasIdentity) return false; var id = language.FallbackLanguageId; while (true) // assuming languages does not already contains a cycle, this must end { if (!id.HasValue) return false; // no fallback means no cycle if (id.Value == language.Id) return true; // back to language = cycle! id = languages[id.Value].FallbackLanguageId; // else keep chaining } } } }