From 9843f3a5fd6fb2f4e2d017a37e6d149984254595 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 2 May 2018 14:52:00 +1000 Subject: [PATCH] Ensures that domains assigned to non-published nodes are filtered when routing, ensures caches are cleared when content is deleted or langauges are changed, updates logic for dealing with null invariant content names, adds more validation for saving cultre variants. --- src/Umbraco.Core/Models/ContentBase.cs | 2 - src/Umbraco.Core/Models/ContentExtensions.cs | 15 +----- .../Implement/DocumentRepository.cs | 35 ++++++++++++++ .../Services/Implement/ContentService.cs | 27 ++++++----- .../services/umbdataformatter.service.js | 4 +- .../components/editor/umb-editor-header.html | 2 +- .../Cache/ContentCacheRefresher.cs | 36 +++++++++++++-- src/Umbraco.Web/Cache/DomainCacheRefresher.cs | 3 +- .../Cache/LanguageCacheRefresher.cs | 46 +++++++++++++++---- src/Umbraco.Web/Editors/ContentController.cs | 35 ++++++-------- .../NuCache/PublishedSnapshotService.cs | 1 + .../XmlPublishedCache/DomainCache.cs | 11 +++++ src/Umbraco.Web/Routing/PublishedRouter.cs | 6 ++- .../WebApi/Binders/ContentItemBinder.cs | 34 +++++++++++++- .../WebApi/Binders/MemberBinder.cs | 6 +-- .../Filters/ContentItemValidationHelper.cs | 12 +++-- 16 files changed, 200 insertions(+), 75 deletions(-) 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) {