using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Net.Http; using System.Net.Mime; using System.Text; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Web.Common.Attributes; using Umbraco.Cms.Web.Common.Authorization; using Umbraco.Extensions; using Constants = Umbraco.Cms.Core.Constants; namespace Umbraco.Cms.Web.BackOffice.Controllers { /// /// /// The API controller used for editing dictionary items /// /// /// The security for this controller is defined to allow full CRUD access to dictionary if the user has access to either: /// Dictionary /// [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] [Authorize(Policy = AuthorizationPolicies.TreeAccessDictionary)] [ParameterSwapControllerActionSelector(nameof(GetById), "id", typeof(int), typeof(Guid), typeof(Udi))] public class DictionaryController : BackOfficeNotificationsController { private readonly ILogger _logger; private readonly ILocalizationService _localizationService; private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor; private readonly GlobalSettings _globalSettings; private readonly ILocalizedTextService _localizedTextService; private readonly IUmbracoMapper _umbracoMapper; public DictionaryController( ILogger logger, ILocalizationService localizationService, IBackOfficeSecurityAccessor backofficeSecurityAccessor, IOptionsSnapshot globalSettings, ILocalizedTextService localizedTextService, IUmbracoMapper umbracoMapper ) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _localizationService = localizationService ?? throw new ArgumentNullException(nameof(localizationService)); _backofficeSecurityAccessor = backofficeSecurityAccessor ?? throw new ArgumentNullException(nameof(backofficeSecurityAccessor)); _globalSettings = globalSettings.Value ?? throw new ArgumentNullException(nameof(globalSettings)); _localizedTextService = localizedTextService ?? throw new ArgumentNullException(nameof(localizedTextService)); _umbracoMapper = umbracoMapper ?? throw new ArgumentNullException(nameof(umbracoMapper)); } /// /// Deletes a data type with a given ID /// /// /// [HttpDelete] [HttpPost] public IActionResult DeleteById(int id) { var foundDictionary = _localizationService.GetDictionaryItemById(id); if (foundDictionary == null) return NotFound(); var foundDictionaryDescendants = _localizationService.GetDictionaryItemDescendants(foundDictionary.Key); foreach (var dictionaryItem in foundDictionaryDescendants) { _localizationService.Delete(dictionaryItem, _backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.Id ?? -1); } _localizationService.Delete(foundDictionary, _backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.Id ?? -1); return Ok(); } /// /// Creates a new dictionary item /// /// /// The parent id. /// /// /// The key. /// /// /// The . /// [HttpPost] public ActionResult Create(int parentId, string key) { if (string.IsNullOrEmpty(key)) return ValidationProblem("Key can not be empty."); // TODO: translate if (_localizationService.DictionaryItemExists(key)) { var message = _localizedTextService.Localize( "dictionaryItem","changeKeyError", _backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.GetUserCulture(_localizedTextService, _globalSettings), new Dictionary { { "0", key } }); return ValidationProblem(message); } try { Guid? parentGuid = null; if (parentId > 0) parentGuid = _localizationService.GetDictionaryItemById(parentId)?.Key; var item = _localizationService.CreateDictionaryItemWithIdentity( key, parentGuid, string.Empty); return item.Id; } catch (Exception ex) { _logger.LogError(ex, "Error creating dictionary with {Name} under {ParentId}", key, parentId); return ValidationProblem("Error creating dictionary item"); } } /// /// Gets a dictionary item by id /// /// /// The id. /// /// /// The . Returns a not found response when dictionary item does not exist /// public ActionResult GetById(int id) { var dictionary = _localizationService.GetDictionaryItemById(id); if (dictionary == null) return NotFound(); return _umbracoMapper.Map(dictionary); } /// /// Gets a dictionary item by guid /// /// /// The id. /// /// /// The . Returns a not found response when dictionary item does not exist /// public ActionResult GetById(Guid id) { var dictionary = _localizationService.GetDictionaryItemById(id); if (dictionary == null) return NotFound(); return _umbracoMapper.Map(dictionary); } /// /// Gets a dictionary item by udi /// /// /// The id. /// /// /// The . Returns a not found response when dictionary item does not exist /// public ActionResult GetById(Udi id) { var guidUdi = id as GuidUdi; if (guidUdi == null) return NotFound(); var dictionary = _localizationService.GetDictionaryItemById(guidUdi.Guid); if (dictionary == null) return NotFound(); return _umbracoMapper.Map(dictionary); } /// /// Changes the structure for dictionary items /// /// /// public IActionResult? PostMove(MoveOrCopy move) { var dictionaryItem = _localizationService.GetDictionaryItemById(move.Id); if (dictionaryItem == null) return ValidationProblem(_localizedTextService.Localize("dictionary", "itemDoesNotExists")); var parent = _localizationService.GetDictionaryItemById(move.ParentId); if (parent == null) { if (move.ParentId == Constants.System.Root) dictionaryItem.ParentId = null; else return ValidationProblem(_localizedTextService.Localize("dictionary", "parentDoesNotExists")); } else { dictionaryItem.ParentId = parent.Key; if (dictionaryItem.Key == parent.ParentId) return ValidationProblem(_localizedTextService.Localize("moveOrCopy", "notAllowedByPath")); } _localizationService.Save(dictionaryItem); var model = _umbracoMapper.Map(dictionaryItem); return Content(model!.Path, MediaTypeNames.Text.Plain, Encoding.UTF8); } /// /// Saves a dictionary item /// /// /// The dictionary. /// /// /// The . /// public ActionResult PostSave(DictionarySave dictionary) { var dictionaryItem = dictionary.Id is null ? null : _localizationService.GetDictionaryItemById(int.Parse(dictionary.Id.ToString()!, CultureInfo.InvariantCulture)); if (dictionaryItem == null) return ValidationProblem("Dictionary item does not exist"); var userCulture = _backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.GetUserCulture(_localizedTextService, _globalSettings); if (dictionary.NameIsDirty) { // if the name (key) has changed, we need to check if the new key does not exist var dictionaryByKey = _localizationService.GetDictionaryItemByKey(dictionary.Name!); if (dictionaryByKey != null && dictionaryItem.Id != dictionaryByKey.Id) { var message = _localizedTextService.Localize( "dictionaryItem","changeKeyError", userCulture, new Dictionary { { "0", dictionary.Name } }); ModelState.AddModelError("Name", message); return ValidationProblem(ModelState); } dictionaryItem.ItemKey = dictionary.Name!; } foreach (var translation in dictionary.Translations) { _localizationService.AddOrUpdateDictionaryValue(dictionaryItem, _localizationService.GetLanguageById(translation.LanguageId), translation.Translation); } try { _localizationService.Save(dictionaryItem); var model = _umbracoMapper.Map(dictionaryItem); model?.Notifications.Add(new BackOfficeNotification( _localizedTextService.Localize("speechBubbles","dictionaryItemSaved", userCulture), string.Empty, NotificationStyle.Success)); return model; } catch (Exception ex) { _logger.LogError(ex, "Error saving dictionary with {Name} under {ParentId}", dictionary.Name, dictionary.ParentId); return ValidationProblem("Something went wrong saving dictionary"); } } /// /// Retrieves a list with all dictionary items /// /// /// The . /// public IEnumerable GetList() { var items = _localizationService.GetDictionaryItemDescendants(null).ToArray(); var list = new List(items.Length); // recursive method to build a tree structure from the flat structure returned above void BuildTree(int level = 0, Guid? parentId = null) { var children = items.Where(t => t.ParentId == parentId).ToArray(); if(children.Any() == false) { return; } foreach(var child in children.OrderBy(ItemSort())) { var display = _umbracoMapper.Map(child); if (display is not null) { display.Level = level; list.Add(display); } BuildTree(level + 1, child.Key); } } BuildTree(); return list; } /// /// Get child items for list. /// /// /// The dictionary item. /// /// /// The level. /// /// /// The list. /// private void GetChildItemsForList(IDictionaryItem dictionaryItem, int level, ICollection list) { foreach (var childItem in _localizationService.GetDictionaryItemChildren(dictionaryItem.Key)?.OrderBy(ItemSort()) ?? Enumerable.Empty()) { var item = _umbracoMapper.Map(childItem); if (item is not null) { item.Level = level; list.Add(item); } GetChildItemsForList(childItem, level + 1, list); } } private static Func ItemSort() => item => item.ItemKey; } }