diff --git a/src/Umbraco.Core/Models/ContentRepositoryExtensions.cs b/src/Umbraco.Core/Models/ContentRepositoryExtensions.cs
index f9917db4bee6..9fe458c29190 100644
--- a/src/Umbraco.Core/Models/ContentRepositoryExtensions.cs
+++ b/src/Umbraco.Core/Models/ContentRepositoryExtensions.cs
@@ -454,7 +454,24 @@ public static bool UnpublishCulture(this IContent content, string? culture = "*"
/// Clears all publish culture information from the content item.
///
/// The content item to clear publish information from.
- public static void ClearPublishInfos(this IContent content) => content.PublishCultureInfos = null;
+ public static void ClearPublishInfos(this IContent content)
+ {
+ if (content.PublishCultureInfos is null)
+ {
+ return;
+ }
+
+ // Pass each published culture through ClearPublishInfo([culture]) to ensure correct change tracking.
+ var cultures = content.PublishCultureInfos.Values.Select(c => c.Culture).ToArray();
+ foreach (var culture in cultures)
+ {
+ content.ClearPublishInfo(culture);
+ }
+
+ // Following #22799 the explicit calls to `ClearPublishInfo` for each culture cause the unpublish in all cultures.
+ // `PublishCultureInfos` is set to null purely to retain previous behaviour at a property level.
+ content.PublishCultureInfos = null;
+ }
///
/// Returns false if the culture is already unpublished
diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs
index 2ed186f68c1e..0bdf80702818 100644
--- a/src/Umbraco.Core/Services/ContentService.cs
+++ b/src/Umbraco.Core/Services/ContentService.cs
@@ -1670,9 +1670,7 @@ void SaveDocument(IContent c)
{
// Determine cultures publishing/unpublishing which will be based on previous calls to content.PublishCulture and ClearPublishInfo
culturesUnpublishing = content.GetCulturesUnpublishing();
- culturesPublishing = variesByCulture
- ? content.PublishCultureInfos?.Values.Where(x => x.IsDirty()).Select(x => x.Culture).ToList()
- : null;
+ culturesPublishing = GetCulturesPublishing(content);
// ensure that the document can be published, and publish handling events, business rules, etc
publishResult = StrategyCanPublish(
@@ -1743,6 +1741,12 @@ void SaveDocument(IContent c)
// won't happen in a branch
if (unpublishing)
{
+ if (culturesUnpublishing is null)
+ {
+ culturesUnpublishing = content.GetCulturesUnpublishing();
+ culturesPublishing = GetCulturesPublishing(content);
+ }
+
IContent? newest = GetById(content.Id); // ensure we have the newest version - in scope
if (content.VersionId != newest?.VersionId)
{
@@ -1805,12 +1809,13 @@ void SaveDocument(IContent c)
var langs = GetLanguageDetailsForAuditEntry(allLangs, culturesUnpublishing);
Audit(AuditType.UnpublishVariant, userId, content.Id, $"Unpublished languages: {langs}", langs);
- if (publishResult == null)
+ PublishResultType? publishResultType = publishResult?.Result ?? unpublishResult?.Result;
+ if (publishResultType == null)
{
- throw new PanicException("publishResult == null - should not happen");
+ throw new PanicException("publishResultType == null - should not happen");
}
- switch (publishResult.Result)
+ switch (publishResultType)
{
case PublishResultType.FailedPublishMandatoryCultureMissing:
// Occurs when a mandatory culture was unpublished (which means we tried publishing the document without a mandatory culture)
@@ -2428,6 +2433,11 @@ internal IEnumerable PublishBranch(
return result;
}
+ private IReadOnlyList? GetCulturesPublishing(IContent content)
+ => content.ContentType.VariesByCulture()
+ ? content.PublishCultureInfos?.Values.Where(x => x.IsDirty()).Select(x => x.Culture).ToList()
+ : null;
+
#endregion
#region Delete
diff --git a/src/Umbraco.Core/Services/PublishStatus/PublishStatusService.cs b/src/Umbraco.Core/Services/PublishStatus/PublishStatusService.cs
index 6f19c9c8310c..66287f94a5e5 100644
--- a/src/Umbraco.Core/Services/PublishStatus/PublishStatusService.cs
+++ b/src/Umbraco.Core/Services/PublishStatus/PublishStatusService.cs
@@ -144,14 +144,14 @@ public async Task AddOrUpdateStatusAsync(Guid documentKey, CancellationToken can
{
using ICoreScope scope = _coreScopeProvider.CreateCoreScope();
ISet publishedCultures = await _publishStatusRepository.GetPublishStatusAsync(documentKey, cancellationToken);
- _publishedCultures[documentKey] = publishedCultures;
+ UpdatePublishedCultures(documentKey, publishedCultures);
scope.Complete();
}
///
public Task RemoveAsync(Guid documentKey, CancellationToken cancellationToken)
{
- _publishedCultures.TryRemove(documentKey, out _);
+ RemovePublishedCultures(documentKey);
return Task.CompletedTask;
}
@@ -166,8 +166,23 @@ public async Task AddOrUpdateStatusWithDescendantsAsync(Guid rootDocumentKey, Ca
}
foreach ((Guid documentKey, ISet publishedCultures) in publishStatus)
+ {
+ UpdatePublishedCultures(documentKey, publishedCultures);
+ }
+ }
+
+ private void UpdatePublishedCultures(Guid documentKey, ISet publishedCultures)
+ {
+ if (publishedCultures.Count > 0)
{
_publishedCultures[documentKey] = publishedCultures;
}
+ else
+ {
+ RemovePublishedCultures(documentKey);
+ }
}
+
+ private void RemovePublishedCultures(Guid documentKey)
+ => _publishedCultures.TryRemove(documentKey, out _);
}
diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/PublishStatusServiceTests.Query.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/PublishStatusServiceTests.Query.cs
index e4e40f468d52..24d01a76c7fb 100644
--- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/PublishStatusServiceTests.Query.cs
+++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/PublishStatusServiceTests.Query.cs
@@ -181,4 +181,78 @@ await GetRequiredService()
Assert.IsFalse(PublishStatusQueryService.HasPublishedAncestorPath(grandchild.Key, cultureToUnpublish));
Assert.IsTrue(PublishStatusQueryService.HasPublishedAncestorPath(grandchild.Key, Constants.System.InvariantCulture));
}
+
+ [TestCase(true)]
+ [TestCase(false)]
+ public async Task Fully_Unpublished_Culture_Variant_Document_Tracks_Unpublished_State_For_All_Cultures(bool unpublishAllCulturesAtOnce)
+ {
+ await GetRequiredService()
+ .CreateAsync(new Language("da-DK", "Danish"), Constants.Security.SuperUserKey);
+
+ var contentTypeKey = Guid.NewGuid();
+ var contentType = new ContentTypeBuilder()
+ .WithKey(contentTypeKey)
+ .WithAlias("variant")
+ .WithContentVariation(ContentVariation.Culture)
+ .WithAllowAsRoot(true)
+ .Build();
+ await ContentTypeService.CreateAsync(contentType, Constants.Security.SuperUserKey);
+ contentType.AllowedContentTypes = [new ContentTypeSort(contentTypeKey, 1, "variant")];
+ await ContentTypeService.UpdateAsync(contentType, Constants.Security.SuperUserKey);
+
+ IContent root = new ContentBuilder()
+ .WithContentType(contentType)
+ .WithCultureName("en-US", "Root EN")
+ .WithCultureName("da-DK", "Root DA")
+ .Build();
+ ContentService.Save(root);
+
+ IContent child = new ContentBuilder()
+ .WithContentType(contentType)
+ .WithCultureName("en-US", "Child EN")
+ .WithCultureName("da-DK", "Child DA")
+ .WithParent(root)
+ .Build();
+ ContentService.Save(child);
+
+ IContent grandchild = new ContentBuilder()
+ .WithContentType(contentType)
+ .WithCultureName("en-US", "Grandchild EN")
+ .WithCultureName("da-DK", "Grandchild DA")
+ .WithParent(child)
+ .Build();
+ ContentService.Save(grandchild);
+
+ ContentService.PublishBranch(root, PublishBranchFilter.IncludeUnpublished, ["en-US", "da-DK"]);
+
+ // must refresh the child instance before unpublishing it, to reflect the state changes from the branch publish above
+ child = ContentService.GetById(child.Key)!;
+
+ if (unpublishAllCulturesAtOnce)
+ {
+ ContentService.Unpublish(child);
+ }
+ else
+ {
+ ContentService.Unpublish(child, "en-US");
+
+ // refresh again before unpublishing the last culture
+ child = ContentService.GetById(child.Key)!;
+ ContentService.Unpublish(child, "da-DK");
+ }
+
+ // refresh to get the latest state
+ child = ContentService.GetById(child.Key)!;
+ Assert.IsFalse(child.Published);
+ Assert.IsEmpty(child.PublishedCultures);
+ Assert.IsEmpty(child.PublishCultureInfos!);
+
+ Assert.IsFalse(PublishStatusQueryService.IsDocumentPublished(child.Key, "en-US"));
+ Assert.IsFalse(PublishStatusQueryService.IsDocumentPublished(child.Key, "da-DK"));
+ Assert.IsFalse(PublishStatusQueryService.IsDocumentPublished(child.Key, Constants.System.InvariantCulture));
+
+ Assert.IsFalse(PublishStatusQueryService.HasPublishedAncestorPath(grandchild.Key, "da-DK"));
+ Assert.IsFalse(PublishStatusQueryService.HasPublishedAncestorPath(grandchild.Key, "en-US"));
+ Assert.IsFalse(PublishStatusQueryService.HasPublishedAncestorPath(grandchild.Key, Constants.System.InvariantCulture));
+ }
}