diff --git a/src/Umbraco.Core/Models/ContentBase.cs b/src/Umbraco.Core/Models/ContentBase.cs index f0cb8c0574..ff1925c72d 100644 --- a/src/Umbraco.Core/Models/ContentBase.cs +++ b/src/Umbraco.Core/Models/ContentBase.cs @@ -55,8 +55,6 @@ namespace Umbraco.Core.Models Id = 0; // no identity VersionId = 0; // no versions - //fixme we always need to set the invariant name else an exception will throw if we try to persist - Name = name; SetName(culture, name); _contentTypeId = contentType.Id; diff --git a/src/Umbraco.Core/Models/ContentExtensions.cs b/src/Umbraco.Core/Models/ContentExtensions.cs index c0e4214881..0c232ff1e5 100644 --- a/src/Umbraco.Core/Models/ContentExtensions.cs +++ b/src/Umbraco.Core/Models/ContentExtensions.cs @@ -159,20 +159,7 @@ namespace Umbraco.Core.Models return Current.Services.MediaService.GetById(media.ParentId); } #endregion - - #region Variants - - /// - /// Returns true if the content has any property type that allows language variants - /// - public static bool HasPropertyTypeVaryingByCulture(this IContent content) - { - // fixme - what about CultureSegment? what about content.ContentType.Variations? - return content.PropertyTypes.Any(x => x.Variations.Has(ContentVariation.CultureNeutral)); - } - - #endregion - + /// /// Removes characters that are not valide XML characters from all entity properties /// of type string. See: http://stackoverflow.com/a/961504/5018 diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs index 76321abe6a..7f79788091 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs @@ -177,9 +177,11 @@ namespace Umbraco.Core.Persistence.Repositories.Implement "DELETE FROM " + Constants.DatabaseSchema.Tables.TagRelationship + " WHERE nodeId = @id", "DELETE FROM " + Constants.DatabaseSchema.Tables.Domain + " WHERE domainRootStructureID = @id", "DELETE FROM " + Constants.DatabaseSchema.Tables.Document + " WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.DocumentCultureVariation + " WHERE nodeId = @id", "DELETE FROM " + Constants.DatabaseSchema.Tables.DocumentVersion + " WHERE id IN (SELECT id FROM " + Constants.DatabaseSchema.Tables.ContentVersion + " WHERE nodeId = @id)", "DELETE FROM " + Constants.DatabaseSchema.Tables.PropertyData + " WHERE versionId IN (SELECT id FROM " + Constants.DatabaseSchema.Tables.ContentVersion + " WHERE nodeId = @id)", "DELETE FROM cmsPreviewXml WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.ContentVersionCultureVariation + " WHERE versionId IN (SELECT id FROM " + Constants.DatabaseSchema.Tables.ContentVersion + " WHERE nodeId = @id)", "DELETE FROM " + Constants.DatabaseSchema.Tables.ContentVersion + " WHERE nodeId = @id", "DELETE FROM cmsContentXml WHERE nodeId = @id", "DELETE FROM " + Constants.DatabaseSchema.Tables.Content + " WHERE nodeId = @id", @@ -229,6 +231,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected override void PersistNewItem(IContent entity) { + //fixme - stop doing this just so we have access to AddingEntity var content = (Content) entity; content.AddingEntity(); @@ -238,6 +241,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement if (entity.Template == null) entity.Template = entity.ContentType.DefaultTemplate; + entity.Name = FormatInvariantNameValue(entity); // ensure unique name on the same level entity.Name = EnsureUniqueNodeName(entity.ParentId, entity.Name); @@ -416,6 +420,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement Database.Execute(Sql().Update(u => u.Set(x => x.Published, false)).Where(x => x.Id == content.PublishedVersionId)); } + entity.Name = FormatInvariantNameValue(entity); // ensure unique name on the same level entity.Name = EnsureUniqueNodeName(entity.ParentId, entity.Name, entity.Id); @@ -1074,6 +1079,36 @@ namespace Umbraco.Core.Persistence.Repositories.Implement #region Utilities + /// + /// This ensures that the Name property exists and validates if all names are null + /// + /// + /// + private string FormatInvariantNameValue(IContent content) + { + if (content.Name.IsNullOrWhiteSpace() && content.Names.Count == 0) + throw new InvalidOperationException("Cannot save content with empty name."); + + if (content.Name.IsNullOrWhiteSpace() && content.Names.Count > 0) + { + //if we are saving a variant, we current need to have the invariant name set too + //fixme - this needs to be discussed! http://issues.umbraco.org/issue/U4-11286 + + var defaultCulture = LanguageRepository.GetDefaultIsoCode(); + if (!defaultCulture.IsNullOrWhiteSpace() && content.Names.TryGetValue(defaultCulture, out var cultureName)) + { + return cultureName; + } + else + { + //our only option is to take the first + return content.Names.First().Value; + } + } + + return content.Name; + } + protected override string EnsureUniqueNodeName(int parentId, string nodeName, int id = 0) { return EnsureUniqueNaming == false ? nodeName : base.EnsureUniqueNodeName(parentId, nodeName, id); diff --git a/src/Umbraco.Core/Services/Implement/ContentService.cs b/src/Umbraco.Core/Services/Implement/ContentService.cs index fc37000e13..919ca73273 100644 --- a/src/Umbraco.Core/Services/Implement/ContentService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentService.cs @@ -163,6 +163,8 @@ namespace Umbraco.Core.Services.Implement /// public IContent Create(string name, Guid parentId, string contentTypeAlias, int userId = 0) { + //fixme - what about culture? + var parent = GetById(parentId); return Create(name, parent, contentTypeAlias, userId); } @@ -181,6 +183,8 @@ namespace Umbraco.Core.Services.Implement /// The content object. public IContent Create(string name, int parentId, string contentTypeAlias, int userId = 0) { + //fixme - what about culture? + var contentType = GetContentType(contentTypeAlias); if (contentType == null) throw new ArgumentException("No content type with that alias.", nameof(contentTypeAlias)); @@ -212,6 +216,8 @@ namespace Umbraco.Core.Services.Implement /// The content object. public IContent Create(string name, IContent parent, string contentTypeAlias, int userId = 0) { + //fixme - what about culture? + if (parent == null) throw new ArgumentNullException(nameof(parent)); using (var scope = ScopeProvider.CreateScope()) @@ -241,6 +247,8 @@ namespace Umbraco.Core.Services.Implement /// The content object. public IContent CreateAndSave(string name, int parentId, string contentTypeAlias, int userId = 0) { + //fixme - what about culture? + using (var scope = ScopeProvider.CreateScope()) { // locking the content tree secures content types too @@ -273,6 +281,8 @@ namespace Umbraco.Core.Services.Implement /// The content object. public IContent CreateAndSave(string name, IContent parent, string contentTypeAlias, int userId = 0) { + //fixme - what about culture? + if (parent == null) throw new ArgumentNullException(nameof(parent)); using (var scope = ScopeProvider.CreateScope()) @@ -864,11 +874,6 @@ namespace Umbraco.Core.Services.Implement return OperationResult.Cancel(evtMsgs); } - if (string.IsNullOrWhiteSpace(content.Name)) - { - throw new ArgumentException("Cannot save content with empty name."); - } - var isNew = content.IsNewEntity(); scope.WriteLock(Constants.Locks.ContentTree); @@ -1217,7 +1222,7 @@ namespace Umbraco.Core.Services.Implement using (var scope = ScopeProvider.CreateScope()) { var deleteEventArgs = new DeleteEventArgs(content, evtMsgs); - if (scope.Events.DispatchCancelable(Deleting, this, deleteEventArgs)) + if (scope.Events.DispatchCancelable(Deleting, this, deleteEventArgs, nameof(Deleting))) { scope.Complete(); return OperationResult.Cancel(evtMsgs); @@ -1229,7 +1234,7 @@ namespace Umbraco.Core.Services.Implement // but... UnPublishing event makes no sense (not going to cancel?) and no need to save // just raise the event if (content.Trashed == false && content.Published) - scope.Events.Dispatch(UnPublished, this, new PublishEventArgs(content, false, false), "UnPublished"); + scope.Events.Dispatch(UnPublished, this, new PublishEventArgs(content, false, false), nameof(UnPublished)); DeleteLocked(scope, content); @@ -1265,7 +1270,7 @@ namespace Umbraco.Core.Services.Implement _documentRepository.Delete(c); var args = new DeleteEventArgs(c, false); // raise event & get flagged files - scope.Events.Dispatch(Deleted, this, args); + scope.Events.Dispatch(Deleted, this, args, nameof(Deleted)); // fixme not going to work, do it differently _mediaFileSystem.DeleteFiles(args.MediaFilesToDelete, // remove flagged files @@ -2149,7 +2154,7 @@ namespace Umbraco.Core.Services.Implement var query = Query().WhereIn(x => x.ContentTypeId, contentTypeIdsA); var contents = _documentRepository.Get(query).ToArray(); - if (scope.Events.DispatchCancelable(Deleting, this, new DeleteEventArgs(contents))) + if (scope.Events.DispatchCancelable(Deleting, this, new DeleteEventArgs(contents), nameof(Deleting))) { scope.Complete(); return; @@ -2296,7 +2301,7 @@ namespace Umbraco.Core.Services.Implement { scope.WriteLock(Constants.Locks.ContentTree); _documentBlueprintRepository.Delete(content); - scope.Events.Dispatch(DeletedBlueprint, this, new DeleteEventArgs(content), "DeletedBlueprint"); + scope.Events.Dispatch(DeletedBlueprint, this, new DeleteEventArgs(content), nameof(DeletedBlueprint)); scope.Complete(); } } @@ -2357,7 +2362,7 @@ namespace Umbraco.Core.Services.Implement _documentBlueprintRepository.Delete(blueprint); } - scope.Events.Dispatch(DeletedBlueprint, this, new DeleteEventArgs(blueprints), "DeletedBlueprint"); + scope.Events.Dispatch(DeletedBlueprint, this, new DeleteEventArgs(blueprints), nameof(DeletedBlueprint)); scope.Complete(); } } diff --git a/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js b/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js index 1888ff623d..70004900f9 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js @@ -327,8 +327,8 @@ //get the selected variant and build the additional published variants saveModel.publishVariations = []; - //if there's more than 1 variant than we need to set the language and include the variants to publish - if (displayModel.variants.length > 1) { + //if there's any variants than we need to set the language and include the variants to publish + if (displayModel.variants.length > 0) { _.each(displayModel.variants, function (d) { //set the selected variant if this is current diff --git a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-header.html b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-header.html index 41d44114a0..dfe235753c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-header.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-header.html @@ -50,7 +50,7 @@ server-validation-field="Alias"> - + {{vm.currentVariant.language.name}}   diff --git a/src/Umbraco.Web/Cache/ContentCacheRefresher.cs b/src/Umbraco.Web/Cache/ContentCacheRefresher.cs index f44781886d..58c5732650 100644 --- a/src/Umbraco.Web/Cache/ContentCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/ContentCacheRefresher.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using Umbraco.Core; using Umbraco.Core.Cache; @@ -17,12 +18,14 @@ namespace Umbraco.Web.Cache { private readonly IPublishedSnapshotService _publishedSnapshotService; private readonly IdkMap _idkMap; + private readonly IDomainService _domainService; - public ContentCacheRefresher(CacheHelper cacheHelper, IPublishedSnapshotService publishedSnapshotService, IdkMap idkMap) + public ContentCacheRefresher(CacheHelper cacheHelper, IPublishedSnapshotService publishedSnapshotService, IdkMap idkMap, IDomainService domainService) : base(cacheHelper) { _publishedSnapshotService = publishedSnapshotService; _idkMap = idkMap; + _domainService = domainService; } #region Define @@ -49,6 +52,8 @@ namespace Umbraco.Web.Cache runtimeCache.ClearCacheByKeySearch(CacheKeys.IdToKeyCacheKey); runtimeCache.ClearCacheByKeySearch(CacheKeys.KeyToIdCacheKey); + var idsRemoved = new HashSet(); + foreach (var payload in payloads) { // remove that one @@ -62,6 +67,31 @@ namespace Umbraco.Web.Cache var pathid = "," + payload.Id + ","; runtimeCache.ClearCacheObjectTypes((k, v) => v.Path.Contains(pathid)); } + + //if the item is being completely removed, we need to refresh the domains cache if any domain was assigned to the content + if (payload.ChangeTypes.HasTypesAny(TreeChangeTypes.Remove)) + { + idsRemoved.Add(payload.Id); + } + } + + if (idsRemoved.Count > 0) + { + var assignedDomains = _domainService.GetAll(true).Where(x => x.RootContentId.HasValue && idsRemoved.Contains(x.RootContentId.Value)).ToList(); + + if (assignedDomains.Count > 0) + { + //fixme - this is duplicating the logic in DomainCacheRefresher BUT we cannot inject that into this because it it not registered explicitly in the container, + // and we cannot inject the CacheRefresherCollection since that would be a circular reference, so what is the best way to call directly in to the + // DomainCacheRefresher? + + ClearAllIsolatedCacheByEntityType(); + // note: must do what's above FIRST else the repositories still have the old cached + // content and when the PublishedCachesService is notified of changes it does not see + // the new content... + // notify + _publishedSnapshotService.Notify(assignedDomains.Select(x => new DomainCacheRefresher.JsonPayload(x.Id, DomainChangeTypes.Remove)).ToArray()); + } } // note: must do what's above FIRST else the repositories still have the old cached @@ -130,10 +160,6 @@ namespace Umbraco.Web.Cache #endregion - #region Events - - #endregion - #region Indirect public static void RefreshContentTypes(CacheHelper cacheHelper) diff --git a/src/Umbraco.Web/Cache/DomainCacheRefresher.cs b/src/Umbraco.Web/Cache/DomainCacheRefresher.cs index 5e3c0220a8..5520d72cd2 100644 --- a/src/Umbraco.Web/Cache/DomainCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/DomainCacheRefresher.cs @@ -84,6 +84,7 @@ namespace Umbraco.Web.Cache public DomainChangeTypes ChangeType { get; } } - #endregion + #endregion + } } diff --git a/src/Umbraco.Web/Cache/LanguageCacheRefresher.cs b/src/Umbraco.Web/Cache/LanguageCacheRefresher.cs index 2270f6471c..d523d3686f 100644 --- a/src/Umbraco.Web/Cache/LanguageCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/LanguageCacheRefresher.cs @@ -1,21 +1,30 @@ using System; +using System.Linq; using Umbraco.Core.Cache; using Umbraco.Core.Models; - +using Umbraco.Core.Services; +using Umbraco.Core.Services.Changes; +using Umbraco.Web.PublishedCache; + namespace Umbraco.Web.Cache { public sealed class LanguageCacheRefresher : CacheRefresherBase { - public LanguageCacheRefresher(CacheHelper cacheHelper) + public LanguageCacheRefresher(CacheHelper cacheHelper, IPublishedSnapshotService publishedSnapshotService, IDomainService domainService) : base(cacheHelper) - { } + { + _publishedSnapshotService = publishedSnapshotService; + _domainService = domainService; + } #region Define protected override LanguageCacheRefresher This => this; - public static readonly Guid UniqueId = Guid.Parse("3E0F95D8-0BE5-44B8-8394-2B8750B62654"); - + public static readonly Guid UniqueId = Guid.Parse("3E0F95D8-0BE5-44B8-8394-2B8750B62654"); + private readonly IPublishedSnapshotService _publishedSnapshotService; + private readonly IDomainService _domainService; + public override Guid RefresherUniqueId => UniqueId; public override string Name => "Language Cache Refresher"; @@ -26,7 +35,8 @@ namespace Umbraco.Web.Cache public override void Refresh(int id) { - ClearAllIsolatedCacheByEntityType(); + ClearAllIsolatedCacheByEntityType(); + RefreshDomains(id); base.Refresh(id); } @@ -34,10 +44,30 @@ namespace Umbraco.Web.Cache { ClearAllIsolatedCacheByEntityType(); //if a language is removed, then all dictionary cache needs to be removed - ClearAllIsolatedCacheByEntityType(); + ClearAllIsolatedCacheByEntityType(); + RefreshDomains(id); base.Remove(id); } - #endregion + #endregion + + private void RefreshDomains(int langId) + { + var assignedDomains = _domainService.GetAll(true).Where(x => x.LanguageId.HasValue && x.LanguageId.Value == langId).ToList(); + + if (assignedDomains.Count > 0) + { + //fixme - this is duplicating the logic in DomainCacheRefresher BUT we cannot inject that into this because it it not registered explicitly in the container, + // and we cannot inject the CacheRefresherCollection since that would be a circular reference, so what is the best way to call directly in to the + // DomainCacheRefresher? + + ClearAllIsolatedCacheByEntityType(); + // note: must do what's above FIRST else the repositories still have the old cached + // content and when the PublishedCachesService is notified of changes it does not see + // the new content... + // notify + _publishedSnapshotService.Notify(assignedDomains.Select(x => new DomainCacheRefresher.JsonPayload(x.Id, DomainChangeTypes.Remove)).ToArray()); + } + } } } diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index efdc6c23ab..691938c771 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -72,7 +72,9 @@ namespace Umbraco.Web.Editors /// [FilterAllowedOutgoingContent(typeof(IEnumerable))] public IEnumerable GetByIds([FromUri]int[] ids) - { + { + //fixme what about cultures? + var foundContent = Services.ContentService.GetByIds(ids); return foundContent.Select(x => MapToDisplay(x)); } @@ -217,7 +219,8 @@ namespace Umbraco.Web.Editors return display; } - + + //fixme what about cultures? public ContentItemDisplay GetBlueprintById(int id) { var foundContent = Services.ContentService.GetBlueprintById(id); @@ -270,19 +273,6 @@ namespace Umbraco.Web.Editors return content; } - [EnsureUserPermissionForContent("id")] - public ContentItemDisplay GetWithTreeDefinition(int id) - { - var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); - if (foundContent == null) - { - HandleContentNotFound(id); - } - - var content = MapToDisplay(foundContent); - return content; - } - /// /// Gets an empty content item for the /// @@ -954,8 +944,9 @@ namespace Umbraco.Web.Editors if (contentItem.Name.IsNullOrWhiteSpace() == false) { //set the name according to the culture settings - if (!contentItem.Culture.IsNullOrWhiteSpace() && contentItem.PersistedContent.ContentType.Variations.HasFlag(ContentVariation.CultureNeutral)) - { + if (contentItem.PersistedContent.ContentType.Variations.HasFlag(ContentVariation.CultureNeutral)) + { + if (contentItem.Culture.IsNullOrWhiteSpace()) throw new InvalidOperationException($"Cannot save a content item that is {ContentVariation.CultureNeutral} with a culture specified"); contentItem.PersistedContent.SetName(contentItem.Culture, contentItem.Name); } else @@ -1185,11 +1176,11 @@ namespace Umbraco.Web.Editors /// private ContentItemDisplay MapToDisplay(IContent content, string culture = null) { - //a languageId must exist in the mapping context if this content item has any property type that can be varied by language - //otherwise the property validation will fail since it's expecting to be get/set with a language ID. If a languageId is not explicitly - //sent up, then it means that the user is editing the default variant language. - if (culture == null && content.HasPropertyTypeVaryingByCulture()) - { + //A culture must exist in the mapping context if this content type is CultureNeutral since for a culture variant to be edited, + // the Cuture property of ContentItemDisplay must exist (at least currently). + if (culture == null && content.ContentType.Variations.Has(ContentVariation.CultureNeutral)) + { + //If a culture is not explicitly sent up, then it means that the user is editing the default variant language. culture = Services.LocalizationService.GetDefaultLanguageIsoCode(); } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs index b498a1a42b..06eb14898e 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs @@ -815,6 +815,7 @@ namespace Umbraco.Web.PublishedCache.NuCache switch (payload.ChangeType) { case DomainChangeTypes.RefreshAll: + //fixme why this check? if (!(_serviceContext.DomainService is DomainService)) throw new Exception("oops"); diff --git a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/DomainCache.cs b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/DomainCache.cs index 9a82840024..0f40d78225 100644 --- a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/DomainCache.cs +++ b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/DomainCache.cs @@ -17,6 +17,11 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache DefaultCulture = systemDefaultCultureProvider.DefaultCulture; } + /// + /// Returns all in the current domain cache including any domains that may be referenced by content items that are no longer published + /// + /// + /// public IEnumerable GetAll(bool includeWildcards) { return _domainService.GetAll(includeWildcards) @@ -24,6 +29,12 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache .Select(x => new Domain(x.Id, x.DomainName, x.RootContentId.Value, CultureInfo.GetCultureInfo(x.LanguageIsoCode), x.IsWildcard)); } + /// + /// Returns all assigned for the content id specified even if the content item is not published + /// + /// + /// + /// public IEnumerable GetAssigned(int contentId, bool includeWildcards) { return _domainService.GetAssignedDomains(contentId, includeWildcards) diff --git a/src/Umbraco.Web/Routing/PublishedRouter.cs b/src/Umbraco.Web/Routing/PublishedRouter.cs index 35e5ab1af4..f85d2d6c28 100644 --- a/src/Umbraco.Web/Routing/PublishedRouter.cs +++ b/src/Umbraco.Web/Routing/PublishedRouter.cs @@ -266,7 +266,11 @@ namespace Umbraco.Web.Routing _logger.Debug(() => $"{tracePrefix}Uri=\"{request.Uri}\""); var domainsCache = request.UmbracoContext.PublishedSnapshot.Domains; - var domains = domainsCache.GetAll(includeWildcards: false); + + //get the domains but filter to ensure that any referenced content is actually published + var domains = domainsCache.GetAll(includeWildcards: false) + .Where(x => request.UmbracoContext.PublishedSnapshot.Content.GetById(x.ContentId) != null); + var defaultCulture = domainsCache.DefaultCulture; // try to find a domain matching the current request diff --git a/src/Umbraco.Web/WebApi/Binders/ContentItemBinder.cs b/src/Umbraco.Web/WebApi/Binders/ContentItemBinder.cs index 8c3b705bb6..6863a267b1 100644 --- a/src/Umbraco.Web/WebApi/Binders/ContentItemBinder.cs +++ b/src/Umbraco.Web/WebApi/Binders/ContentItemBinder.cs @@ -1,15 +1,24 @@ using System; using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Web.Http.Controllers; using AutoMapper; using Umbraco.Core; using Umbraco.Core.Models; using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Models.Mapping; - +using Umbraco.Web.WebApi.Filters; + namespace Umbraco.Web.WebApi.Binders { internal class ContentItemBinder : ContentItemBaseBinder - { + { + protected override ContentItemValidationHelper GetValidationHelper() + { + return new ContentValidationHelper(); + } + protected override IContent GetExisting(ContentItemSave model) { return Services.ContentService.GetById(Convert.ToInt32(model.Id)); @@ -36,6 +45,27 @@ namespace Umbraco.Web.WebApi.Binders { [ContextMapper.CultureKey] = culture }); + } + + internal class ContentValidationHelper : ContentItemValidationHelper + { + /// + /// Validates that the correct information is in the request for saving a culture variant + /// + /// + /// + /// + protected override bool ValidateCultureVariant(ContentItemSave postedItem, HttpActionContext actionContext) + { + var contentType = postedItem.PersistedContent.GetContentType(); + if (contentType.Variations.Has(Core.Models.ContentVariation.CultureNeutral) && postedItem.Culture.IsNullOrWhiteSpace()) + { + //we cannot save a content item that is culture variant if no culture was specified in the request! + actionContext.Response = actionContext.Request.CreateValidationErrorResponse($"No 'Culture' found in request. Cannot save a content item that is of a {Core.Models.ContentVariation.CultureNeutral} content type without a specified culture."); + return false; + } + return true; + } } } } diff --git a/src/Umbraco.Web/WebApi/Binders/MemberBinder.cs b/src/Umbraco.Web/WebApi/Binders/MemberBinder.cs index 57215ab5ee..45a1947c50 100644 --- a/src/Umbraco.Web/WebApi/Binders/MemberBinder.cs +++ b/src/Umbraco.Web/WebApi/Binders/MemberBinder.cs @@ -186,9 +186,9 @@ namespace Umbraco.Web.WebApi.Binders /// /// /// - public override bool ValidatePropertyData(ContentItemBasic postedItem, ContentItemDto dto, ModelStateDictionary modelState) + public override bool ValidatePropertyData(MemberSave postedItem, ContentItemDto dto, ModelStateDictionary modelState) { - var memberSave = (MemberSave)postedItem; + var memberSave = postedItem; if (memberSave.Username.IsNullOrWhiteSpace()) { @@ -234,7 +234,7 @@ namespace Umbraco.Web.WebApi.Binders /// /// /// - protected override bool ValidateProperties(ContentItemBasic postedItem, HttpActionContext actionContext) + protected override bool ValidateProperties(MemberSave postedItem, HttpActionContext actionContext) { var propertiesToValidate = postedItem.Properties.ToList(); var defaultProps = Constants.Conventions.Member.GetStandardPropertyTypeStubs(); diff --git a/src/Umbraco.Web/WebApi/Filters/ContentItemValidationHelper.cs b/src/Umbraco.Web/WebApi/Filters/ContentItemValidationHelper.cs index c1a8c989e5..f0e23fcadf 100644 --- a/src/Umbraco.Web/WebApi/Filters/ContentItemValidationHelper.cs +++ b/src/Umbraco.Web/WebApi/Filters/ContentItemValidationHelper.cs @@ -48,9 +48,15 @@ namespace Umbraco.Web.WebApi.Filters { //now do each validation step if (ValidateExistingContent(contentItem, actionContext) == false) return; + if (ValidateCultureVariant(contentItem, actionContext) == false) return; if (ValidateProperties(contentItem, actionContext) == false) return; if (ValidatePropertyData(contentItem, contentItem.ContentDto, actionContext.ModelState) == false) return; } + + protected virtual bool ValidateCultureVariant(TModelSave postedItem, HttpActionContext actionContext) + { + return true; + } /// /// Ensure the content exists @@ -58,7 +64,7 @@ namespace Umbraco.Web.WebApi.Filters /// /// /// - protected virtual bool ValidateExistingContent(ContentItemBasic postedItem, HttpActionContext actionContext) + protected virtual bool ValidateExistingContent(TModelSave postedItem, HttpActionContext actionContext) { if (postedItem.PersistedContent == null) { @@ -76,7 +82,7 @@ namespace Umbraco.Web.WebApi.Filters /// /// /// - protected virtual bool ValidateProperties(ContentItemBasic postedItem, HttpActionContext actionContext) + protected virtual bool ValidateProperties(TModelSave postedItem, HttpActionContext actionContext) { return ValidateProperties(postedItem.Properties.ToList(), postedItem.PersistedContent.Properties.ToList(), actionContext); } @@ -116,7 +122,7 @@ namespace Umbraco.Web.WebApi.Filters /// /// All property data validation goes into the modelstate with a prefix of "Properties" /// - public virtual bool ValidatePropertyData(ContentItemBasic postedItem, ContentItemDto dto, ModelStateDictionary modelState) + public virtual bool ValidatePropertyData(TModelSave postedItem, ContentItemDto dto, ModelStateDictionary modelState) { foreach (var p in dto.Properties) {