using System.Xml; using System.Globalization; 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.Hosting; 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 Umbraco.Cms.Infrastructure.Packaging; using System.Xml.Linq; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Web.Common.DependencyInjection; 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 IBackOfficeSecurityAccessor _backofficeSecurityAccessor; private readonly GlobalSettings _globalSettings; private readonly ILocalizationService _localizationService; private readonly ILocalizedTextService _localizedTextService; private readonly ILogger _logger; private readonly IUmbracoMapper _umbracoMapper; private readonly IEntityXmlSerializer _serializer; private readonly IHostingEnvironment _hostingEnvironment; private readonly PackageDataInstallation _packageDataInstallation; [ActivatorUtilitiesConstructor] public DictionaryController( ILogger logger, ILocalizationService localizationService, IBackOfficeSecurityAccessor backofficeSecurityAccessor, IOptionsSnapshot globalSettings, ILocalizedTextService localizedTextService, IUmbracoMapper umbracoMapper, IEntityXmlSerializer serializer, IHostingEnvironment hostingEnvironment, PackageDataInstallation packageDataInstallation) { _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)); _serializer = serializer ?? throw new ArgumentNullException(nameof(serializer)); _hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment)); _packageDataInstallation = packageDataInstallation ?? throw new ArgumentNullException(nameof(packageDataInstallation)); } [Obsolete("Please use ctor that also takes an IEntityXmlSerializer, IHostingEnvironment & PackageDataInstallation instead, scheduled for removal in v12")] public DictionaryController( ILogger logger, ILocalizationService localizationService, IBackOfficeSecurityAccessor backofficeSecurityAccessor, IOptionsSnapshot globalSettings, ILocalizedTextService localizedTextService, IUmbracoMapper umbracoMapper) : this( logger, localizationService, backofficeSecurityAccessor, globalSettings, localizedTextService, umbracoMapper, StaticServiceProvider.Instance.GetRequiredService(), StaticServiceProvider.Instance.GetRequiredService(), StaticServiceProvider.Instance.GetRequiredService()) { } /// /// Deletes a data type with a given ID /// /// /// /// /// [HttpDelete] [HttpPost] public IActionResult DeleteById(int id) { IDictionaryItem? foundDictionary = _localizationService.GetDictionaryItemById(id); if (foundDictionary == null) { return NotFound(); } IEnumerable foundDictionaryDescendants = _localizationService.GetDictionaryItemDescendants(foundDictionary.Key); foreach (IDictionaryItem 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; } IDictionaryItem 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) { IDictionaryItem? 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) { IDictionaryItem? 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(); } IDictionaryItem? 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) { IDictionaryItem? dictionaryItem = _localizationService.GetDictionaryItemById(move.Id); if (dictionaryItem == null) { return ValidationProblem(_localizedTextService.Localize("dictionary", "itemDoesNotExists")); } IDictionaryItem? 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); DictionaryDisplay? 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) { IDictionaryItem? 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 currentUser = _backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser; CultureInfo? userCulture = currentUser?.GetUserCulture(_localizedTextService, _globalSettings); if (dictionary.NameIsDirty) { // if the name (key) has changed, we need to check if the new key does not exist IDictionaryItem? 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!; } var allowedLanguageIds = currentUser?.CalculateAllowedLanguageIds(_localizationService); var allowedLanguageIdHashSet =allowedLanguageIds is null ? new HashSet() : new HashSet(allowedLanguageIds); foreach (DictionaryTranslationSave translation in dictionary.Translations) { if (allowedLanguageIdHashSet.Contains(translation.LanguageId)) { _localizationService.AddOrUpdateDictionaryValue(dictionaryItem, _localizationService.GetLanguageById(translation.LanguageId), translation.Translation); } } try { _localizationService.Save(dictionaryItem); DictionaryDisplay? 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() { IDictionaryItem[] 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) { IDictionaryItem[] children = items.Where(t => t.ParentId == parentId).ToArray(); if (children.Any() == false) { return; } foreach (IDictionaryItem child in children.OrderBy(ItemSort())) { DictionaryOverviewDisplay? 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 (IDictionaryItem childItem in _localizationService.GetDictionaryItemChildren(dictionaryItem.Key) ?.OrderBy(ItemSort()) ?? Enumerable.Empty()) { DictionaryOverviewDisplay? item = _umbracoMapper.Map(childItem); if (item is not null) { item.Level = level; list.Add(item); } GetChildItemsForList(childItem, level + 1, list); } } public IActionResult ExportDictionary(int id, bool includeChildren = false) { IDictionaryItem? dictionaryItem = _localizationService.GetDictionaryItemById(id); if (dictionaryItem == null) { throw new NullReferenceException("No dictionary item found with id " + id); } XElement xml = _serializer.Serialize(dictionaryItem, includeChildren); var fileName = $"{dictionaryItem.ItemKey}.udt"; // Set custom header so umbRequestHelper.downloadFile can save the correct filename HttpContext.Response.Headers.Add("x-filename", fileName); return File(Encoding.UTF8.GetBytes(xml.ToDataString()), MediaTypeNames.Application.Octet, fileName); } public IActionResult ImportDictionary(string file, int parentId) { if (string.IsNullOrWhiteSpace(file)) { return NotFound(); } var filePath = Path.Combine(_hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Data), file); if (!System.IO.File.Exists(filePath)) { return NotFound(); } var xd = new XmlDocument { XmlResolver = null }; xd.Load(filePath); var userId = _backofficeSecurityAccessor.BackOfficeSecurity?.GetUserId().Result ?? 0; var element = XElement.Parse(xd.InnerXml); IDictionaryItem? parentDictionaryItem = _localizationService.GetDictionaryItemById(parentId); IEnumerable dictionaryItems = _packageDataInstallation .ImportDictionaryItem(element, userId, parentDictionaryItem?.Key); // Try to clean up the temporary file. try { System.IO.File.Delete(filePath); } catch (Exception ex) { _logger.LogError(ex, "Error cleaning up temporary udt file in {File}", filePath); } var model = _umbracoMapper.Map(dictionaryItems.FirstOrDefault()); return Content(model!.Path, MediaTypeNames.Text.Plain, Encoding.UTF8); } public ActionResult Upload(IFormFile file) { if (file == null) { return ValidationProblem( _localizedTextService.Localize("media", "failedFileUpload"), _localizedTextService.Localize("speechBubbles", "fileErrorNotFound")); } var fileName = file.FileName.Trim(Constants.CharArrays.DoubleQuote); var ext = fileName.Substring(fileName.LastIndexOf('.') + 1).ToLower(); var root = _hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.TempFileUploads); var tempPath = Path.Combine(root, fileName); if (!Path.GetFullPath(tempPath).StartsWith(Path.GetFullPath(root))) { return ValidationProblem( _localizedTextService.Localize("media", "failedFileUpload"), _localizedTextService.Localize("media", "invalidFileName")); } if (!ext.InvariantEquals("udt")) { return ValidationProblem( _localizedTextService.Localize("media", "failedFileUpload"), _localizedTextService.Localize("media", "disallowedFileType")); } using (FileStream stream = System.IO.File.Create(tempPath)) { file.CopyToAsync(stream).GetAwaiter().GetResult(); } var xd = new XmlDocument { XmlResolver = null }; xd.Load(tempPath); if (xd.DocumentElement == null) { return ValidationProblem( _localizedTextService.Localize("media", "failedFileUpload"), _localizedTextService.Localize("speechBubbles", "fileErrorNotFound")); } var model = new DictionaryImportModel() { TempFileName = tempPath, DictionaryItems = new List(), }; var level = 1; var currentParent = string.Empty; foreach (XmlNode dictionaryItem in xd.GetElementsByTagName("DictionaryItem")) { var name = dictionaryItem.Attributes?.GetNamedItem("Name")?.Value ?? string.Empty; var parentKey = dictionaryItem?.ParentNode?.Attributes?.GetNamedItem("Key")?.Value ?? string.Empty; if (parentKey != currentParent || level == 1) { level += 1; currentParent = parentKey; } model.DictionaryItems.Add(new DictionaryPreviewImportModel() { Level = level, Name = name }); } if (!model.DictionaryItems.Any()) { return ValidationProblem( _localizedTextService.Localize("media", "failedFileUpload"), _localizedTextService.Localize("dictionary", "noItemsInFile")); } return model; } private static Func ItemSort() => item => item.ItemKey; }