using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Dictionary; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Models.Editors; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Web.Common.Filters; using Umbraco.Extensions; namespace Umbraco.Cms.Web.BackOffice.Controllers; /// /// An abstract base controller used for media/content/members to try to reduce code replication. /// [JsonDateTimeFormat] public abstract class ContentControllerBase : BackOfficeNotificationsController { private readonly ILogger _logger; private readonly IJsonSerializer _serializer; /// /// Initializes a new instance of the class. /// protected ContentControllerBase( ICultureDictionary cultureDictionary, ILoggerFactory loggerFactory, IShortStringHelper shortStringHelper, IEventMessagesFactory eventMessages, ILocalizedTextService localizedTextService, IJsonSerializer serializer) { CultureDictionary = cultureDictionary; LoggerFactory = loggerFactory; _logger = loggerFactory.CreateLogger(); ShortStringHelper = shortStringHelper; EventMessages = eventMessages; LocalizedTextService = localizedTextService; _serializer = serializer; } /// /// Gets the /// protected ICultureDictionary CultureDictionary { get; } /// /// Gets the /// protected ILoggerFactory LoggerFactory { get; } /// /// Gets the /// protected IShortStringHelper ShortStringHelper { get; } /// /// Gets the /// protected IEventMessagesFactory EventMessages { get; } /// /// Gets the /// protected ILocalizedTextService LocalizedTextService { get; } /// /// Handles if the content for the specified ID isn't found /// /// The content ID to find /// The error response protected NotFoundObjectResult HandleContentNotFound(object id) { ModelState.AddModelError("id", $"content with id: {id} was not found"); NotFoundObjectResult errorResponse = NotFound(ModelState); return errorResponse; } /// /// Maps the dto property values to the persisted model /// internal void MapPropertyValuesForPersistence( TSaved contentItem, ContentPropertyCollectionDto? dto, Func getPropertyValue, Action savePropertyValue, string? culture) where TPersisted : IContentBase where TSaved : IContentSave { if (dto is null) { return; } // map the property values foreach (ContentPropertyDto propertyDto in dto.Properties) { // get the property editor if (propertyDto.PropertyEditor == null) { _logger.LogWarning("No property editor found for property {PropertyAlias}", propertyDto.Alias); continue; } // get the value editor // nothing to save/map if it is readonly IDataValueEditor valueEditor = propertyDto.PropertyEditor.GetValueEditor(); if (valueEditor.IsReadOnly) { continue; } // get the property IProperty property = contentItem.PersistedContent.Properties[propertyDto.Alias]!; // prepare files, if any matching property and culture ContentPropertyFile[] files = contentItem.UploadedFiles .Where(x => x.PropertyAlias == propertyDto.Alias && x.Culture == propertyDto.Culture && x.Segment == propertyDto.Segment) .ToArray(); foreach (ContentPropertyFile file in files) { file.FileName = file.FileName?.ToSafeFileName(ShortStringHelper); } // create the property data for the property editor var data = new ContentPropertyData(propertyDto.Value, propertyDto.DataType?.Configuration) { ContentKey = contentItem.PersistedContent!.Key, PropertyTypeKey = property.PropertyType.Key, Files = files }; // let the editor convert the value that was received, deal with files, etc var value = valueEditor.FromEditor(data, getPropertyValue(contentItem, property)); // set the value - tags are special TagsPropertyEditorAttribute? tagAttribute = propertyDto.PropertyEditor.GetTagAttribute(); // when TagsPropertyEditorAttribute is removed this whole if can also be removed // since the call to sovePropertyValue is all that's needed now if (tagAttribute is not null && valueEditor is not IDataValueTags) { TagConfiguration? tagConfiguration = ConfigurationEditor.ConfigurationAs(propertyDto.DataType?.Configuration); if (tagConfiguration is not null && tagConfiguration.Delimiter == default) { tagConfiguration.Delimiter = tagAttribute.Delimiter; } var tagCulture = property?.PropertyType.VariesByCulture() ?? false ? culture : null; property?.SetTagsValue(_serializer, value, tagConfiguration, tagCulture); } else { savePropertyValue(contentItem, property, value); } } } /// /// A helper method to attempt to get the instance from the request storage if it can be found there, /// otherwise gets it from the callback specified /// /// /// /// /// /// This is useful for when filters have already looked up a persisted entity and we don't want to have /// to look it up again. /// protected TPersisted? GetObjectFromRequest(Func getFromService) => // checks if the request contains the key and the item is not null, if that is the case, return it from the request, otherwise return // it from the callback HttpContext.Items.ContainsKey(typeof(TPersisted).ToString()) && HttpContext.Items[typeof(TPersisted).ToString()] != null ? (TPersisted?)HttpContext.Items[typeof(TPersisted).ToString()] : getFromService(); /// /// Returns true if the action passed in means we need to create something new /// /// The content action /// Returns true if this is a creating action internal static bool IsCreatingAction(ContentSaveAction action) => action.ToString().EndsWith("New"); /// /// Adds a cancelled message to the display /// /// /// /// /// protected void AddCancelMessage( INotificationModel? display, string messageArea = "speechBubbles", string messageAlias = "operationCancelledText", string[]? messageParams = null) { // if there's already a default event message, don't add our default one IEventMessagesFactory messages = EventMessages; if (messages != null && (messages.GetOrDefault()?.GetAll().Any(x => x.IsDefaultEventMessage) ?? false)) { return; } display?.AddWarningNotification( LocalizedTextService.Localize("speechBubbles", "operationCancelledHeader"), LocalizedTextService.Localize(messageArea, messageAlias, messageParams)); } /// /// Adds a cancelled message to the display /// /// /// /// /// /// /// protected void AddCancelMessage(INotificationModel display, string message) { // if there's already a default event message, don't add our default one IEventMessagesFactory messages = EventMessages; if (messages?.GetOrDefault()?.GetAll().Any(x => x.IsDefaultEventMessage) == true) { return; } display.AddWarningNotification(LocalizedTextService.Localize("speechBubbles", "operationCancelledHeader"), message); } }