Files
Umbraco-CMS/src/Umbraco.Core/Models/ContentRepositoryExtensions.cs
Mole 1258962429 V15: Remove Nucache (#17166)
* Remove nucache reference from Web.Common

* Get tests building-ish

* Move ReservedFieldNamesService to the right project

* Remove IPublishedSnapshotStatus

* Added functionality to the INavigationQueryService to get root keys

* Fixed issue with navigation

* Remove IPublishedSnapshot from UmbracoContext

* Begin removing usage of IPublishedSnapshot from PublishedContentExtensions

* Fix PublishedContentExtensions.cs

* Don't use snapshots in delivery media api

* Use IPublishedMediaCache in QueryMediaApiController

* Remove more usages of IPublishedSnapshotAccessor

* Comment out tests

* Remove more usages of PublishedSnapshotAccessor

* Remove PublishedSnapshot from property

* Fixed test build

* Fix errors

* Fix some tests

* Delete NuCache 🎉

* Implement DatabaseCacheRebuilder

* Remove usage of IPublishedSnapshotService

* Remove IPublishedSnapshotService

* Remove TestPublishedSnapshotAccessor and make tests build

* Don't test Snapshot cachelevel

It's no longer supported

* Fix BlockEditorConverter

Element != Element document type

* Remember to set cachemanager

* Fix RichTextParserTests

* Implement TryGetLevel on INavigationQueryService

* Fake level and obsolete it in PublishedContent

* Remove ChildrenForAllCultures

* Hack Path property on PublishedContent

* Remove usages of IPublishedSnapshot in tests

* More ConvertersTests

* Add hybrid cache to integration tests

We can actually do this now because we no longer save files on disk

* Rename IPublishedSnapshotRebuilder to ICacheRebuilder

* Comment out tests

* V15: Replacing the usages of Parent (navigation data) from IPublishedContent (#17125)

* Fix .Parent references in PublishedContentExtensions

* Add missing methods to FriendlyPublishedContentExtensions (ones that you were able to call on the content directly as they now require extra params)

* Fix references from the extension methods

* Fix dependencies in tests

* Replace IPublishedSnapshotAccessor with the content cache in tests

* Resolving more .Parent references

* Fix unit tests

* Obsolete and use extension methods

* Remove private method and use extension instead

* Moving code around

* Fix tests

* Fix more references

* Cleanup

* Fix more usages

* Resolve merge conflict

* Fix tests

* Cleanup

* Fix more tests

* Fixed unit tests

* Cleanup

* Replace last usages

---------

Co-authored-by: Bjarke Berg <mail@bergmania.dk>

* Remove usage of IPublishedSnapshotAccessor from IRequestItemProvider

* Post merge fixup

* Remo IPublishedSnapshot

* Add HasAny to IDocumentUrlService

* Fix TextBuilder

* Fix modelsbuilder tests

* Use explicit types

* Implement GetByContentType

* Support element types in PublishedContentTypeCache

* Run enlistments before publishing notifications

* Fix elements cache refreshing

* Implement GetByUdi

* Implement GetAtRoot

* Implement GetByRoute

* Reimplement GetRouteById

* Fix blocks unit tests

* Initialize domain cache on boot

* Only return routes with domains on non default lanauges

* V15: Replacing the usages of `Children` (navigation data) from `IPublishedContent` (#17159)

* Update params in PublishedContentExtensions to the general interfaces for the published cache and navigation service, so that we can use the extension methods on both documents and media

* Introduce GetParent() which uses the right services

* Fix obsolete message on .Parent

* Obsolete .Children

* Fix usages of Children for ApiMediaQueryService

* Fix usage in internal

* Fix usages in views

* Fix indentation

* Fix issue with delete language

* Update nuget pacakges

* Clear elements cache when content is deleted

instead of trying to update it

* Reset publishedModelFactory

* Fixed publishing

---------

Co-authored-by: Bjarke Berg <mail@bergmania.dk>
Co-authored-by: Elitsa Marinovska <21998037+elit0451@users.noreply.github.com>
Co-authored-by: kjac <kja@umbraco.dk>
2024-10-01 15:03:02 +02:00

469 lines
17 KiB
C#

using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.PropertyEditors;
namespace Umbraco.Extensions;
/// <summary>
/// Extension methods used to manipulate content variations by the document repository
/// </summary>
public static class ContentRepositoryExtensions
{
public static void SetCultureInfo(this IContentBase content, string? culture, string? name, DateTime date)
{
if (name == null)
{
throw new ArgumentNullException(nameof(name));
}
if (string.IsNullOrWhiteSpace(name))
{
throw new ArgumentException(
"Value can't be empty or consist only of white-space characters.",
nameof(name));
}
if (culture == null)
{
throw new ArgumentNullException(nameof(culture));
}
if (string.IsNullOrWhiteSpace(culture))
{
throw new ArgumentException(
"Value can't be empty or consist only of white-space characters.",
nameof(culture));
}
content.CultureInfos?.AddOrUpdate(culture, name, date);
}
/// <summary>
/// Updates a culture date, if the culture exists.
/// </summary>
public static void TouchCulture(this IContentBase content, string? culture)
{
if (culture.IsNullOrWhiteSpace() || content.CultureInfos is null)
{
return;
}
if (!content.CultureInfos.TryGetValue(culture!, out ContentCultureInfos infos))
{
return;
}
content.CultureInfos?.AddOrUpdate(culture!, infos.Name, DateTime.Now);
}
/// <summary>
/// Used to synchronize all culture dates to the same date if they've been modified
/// </summary>
/// <param name="content"></param>
/// <param name="date"></param>
/// <param name="publishing"></param>
/// <remarks>
/// This is so that in an operation where (for example) 2 languages are updates like french and english, it is possible
/// that
/// these dates assigned to them differ by a couple of Ticks, but we need to ensure they are persisted at the exact
/// same time.
/// </remarks>
public static void AdjustDates(this IContent content, DateTime date, bool publishing)
{
if (content.EditedCultures is not null)
{
foreach (var culture in content.EditedCultures.ToList())
{
if (content.CultureInfos is null)
{
continue;
}
if (!content.CultureInfos.TryGetValue(culture, out ContentCultureInfos editedInfos))
{
continue;
}
// if it's not dirty, it means it hasn't changed so there's nothing to adjust
if (!editedInfos.IsDirty())
{
continue;
}
content.CultureInfos?.AddOrUpdate(culture, editedInfos?.Name, date);
}
}
if (!publishing)
{
return;
}
foreach (var culture in content.PublishedCultures.ToList())
{
if (content.PublishCultureInfos is null)
{
continue;
}
if (!content.PublishCultureInfos.TryGetValue(culture, out ContentCultureInfos publishInfos))
{
continue;
}
// if it's not dirty, it means it hasn't changed so there's nothing to adjust
if (!publishInfos.IsDirty())
{
continue;
}
content.PublishCultureInfos.AddOrUpdate(culture, publishInfos.Name, date);
if (content.CultureInfos?.TryGetValue(culture, out ContentCultureInfos infos) ?? false)
{
SetCultureInfo(content, culture, infos.Name, date);
}
}
}
/// <summary>
/// Gets the cultures that have been flagged for unpublishing.
/// </summary>
/// <remarks>Gets cultures for which content.UnpublishCulture() has been invoked.</remarks>
public static IReadOnlyList<string>? GetCulturesUnpublishing(this IContent content)
{
if (!content.Published || !content.ContentType.VariesByCulture() ||
!content.IsPropertyDirty("PublishCultureInfos"))
{
return Array.Empty<string>();
}
IEnumerable<string>? culturesUnpublishing = content.CultureInfos?.Values
.Where(x => content.IsPropertyDirty(ContentBase.ChangeTrackingPrefix.UnpublishedCulture + x.Culture))
.Select(x => x.Culture);
return culturesUnpublishing?.ToList();
}
/// <summary>
/// Copies values from another document.
/// </summary>
public static void CopyFrom(this IContent content, IContent other, string? culture = "*")
{
if (other.ContentTypeId != content.ContentTypeId)
{
throw new InvalidOperationException("Cannot copy values from a different content type.");
}
culture = culture?.ToLowerInvariant().NullOrWhiteSpaceAsNull();
// the variation should be supported by the content type properties
// if the content type is invariant, only '*' and 'null' is ok
// if the content type varies, everything is ok because some properties may be invariant
if (!content.ContentType.SupportsPropertyVariation(culture, "*", true))
{
throw new NotSupportedException(
$"Culture \"{culture}\" is not supported by content type \"{content.ContentType.Alias}\" with variation \"{content.ContentType.Variations}\".");
}
// copying from the same Id and VersionPk
var copyingFromSelf = content.Id == other.Id && content.VersionId == other.VersionId;
var published = copyingFromSelf;
// note: use property.SetValue(), don't assign pvalue.EditValue, else change tracking fails
// clear all existing properties for the specified culture
foreach (IProperty property in content.Properties)
{
// each property type may or may not support the variation
if ((!property.PropertyType?.SupportsVariation(culture, "*", true) ?? false) &&
!(property.PropertyType?.Variations == ContentVariation.Nothing))
{
continue;
}
foreach (IPropertyValue pvalue in property.Values)
{
if (((property.PropertyType?.SupportsVariation(pvalue.Culture, pvalue.Segment, true) ?? false) &&
(culture == "*" || (pvalue.Culture?.InvariantEquals(culture) ?? false))) ||
property.PropertyType?.Variations == ContentVariation.Nothing)
{
property.SetValue(null, pvalue.Culture, pvalue.Segment);
}
}
}
// copy properties from 'other'
IPropertyCollection otherProperties = other.Properties;
foreach (IProperty otherProperty in otherProperties)
{
if ((!otherProperty?.PropertyType?.SupportsVariation(culture, "*", true) ?? true) &&
!(otherProperty?.PropertyType?.Variations == ContentVariation.Nothing))
{
continue;
}
var alias = otherProperty?.PropertyType.Alias;
if (otherProperty is not null && alias is not null)
{
foreach (IPropertyValue pvalue in otherProperty.Values)
{
if (((otherProperty?.PropertyType?.SupportsVariation(pvalue.Culture, pvalue.Segment, true) ?? false) &&
(culture == "*" ||(pvalue.Culture?.InvariantEquals(culture) ?? false))) ||
otherProperty?.PropertyType?.Variations == ContentVariation.Nothing)
{
var value = published ? pvalue.PublishedValue : pvalue.EditedValue;
content.SetValue(alias, value, pvalue.Culture, pvalue.Segment);
}
}
}
}
// copy names, too
if (culture == "*")
{
content.CultureInfos?.Clear();
content.CultureInfos = null;
}
if (culture == null || culture == "*")
{
content.Name = other.Name;
}
// ReSharper disable once UseDeconstruction
if (other.CultureInfos is not null)
{
foreach (ContentCultureInfos cultureInfo in other.CultureInfos)
{
if (culture == "*" || culture == cultureInfo.Culture)
{
content.SetCultureName(cultureInfo.Name, cultureInfo.Culture);
}
}
}
}
public static void SetPublishInfo(this IContent content, string? culture, string? name, DateTime date)
{
if (name == null)
{
throw new ArgumentNullException(nameof(name));
}
if (string.IsNullOrWhiteSpace(name))
{
throw new ArgumentException(
"Value can't be empty or consist only of white-space characters.",
nameof(name));
}
if (culture == null)
{
throw new ArgumentNullException(nameof(culture));
}
if (string.IsNullOrWhiteSpace(culture))
{
throw new ArgumentException(
"Value can't be empty or consist only of white-space characters.",
nameof(culture));
}
content.PublishCultureInfos?.AddOrUpdate(culture, name, date);
}
// sets the edited cultures on the content
public static void SetCultureEdited(this IContent content, IEnumerable<string?>? cultures)
{
if (cultures == null)
{
content.EditedCultures = null;
}
else
{
var editedCultures = new HashSet<string>(
cultures.Where(x => !x.IsNullOrWhiteSpace())!,
StringComparer.OrdinalIgnoreCase);
content.EditedCultures = editedCultures.Count > 0 ? editedCultures : null;
}
}
[Obsolete("Please use the overload that accepts all parameters. Will be removed in V16.")]
public static bool PublishCulture(this IContent content, CultureImpact? impact)
=> PublishCulture(content, impact, DateTime.Now, StaticServiceProvider.Instance.GetRequiredService<PropertyEditorCollection>());
/// <summary>
/// Sets the publishing values for names and properties.
/// </summary>
/// <param name="content"></param>
/// <param name="impact"></param>
/// <param name="publishTime"></param>
/// <param name="propertyEditorCollection"></param>
/// <returns>
/// A value indicating whether it was possible to publish the names and values for the specified
/// culture(s). The method may fail if required names are not set, but it does NOT validate property data
/// </returns>
public static bool PublishCulture(this IContent content, CultureImpact? impact, DateTime publishTime, PropertyEditorCollection propertyEditorCollection)
{
if (impact == null)
{
throw new ArgumentNullException(nameof(impact));
}
// the variation should be supported by the content type properties
// if the content type is invariant, only '*' and 'null' is ok
// if the content type varies, everything is ok because some properties may be invariant
if (!content.ContentType.SupportsPropertyVariation(impact.Culture, "*", true))
{
throw new NotSupportedException($"Culture \"{impact.Culture}\" is not supported by content type \"{content.ContentType.Alias}\" with variation \"{content.ContentType.Variations}\".");
}
// set names
if (impact.ImpactsAllCultures)
{
// does NOT contain the invariant culture
foreach (var culture in content.AvailableCultures)
{
var name = content.GetCultureName(culture);
if (string.IsNullOrWhiteSpace(name))
{
return false;
}
content.SetPublishInfo(culture, name, publishTime);
}
}
else if (impact.ImpactsOnlyInvariantCulture)
{
if (string.IsNullOrWhiteSpace(content.Name))
{
return false;
}
// PublishName set by repository - nothing to do here
}
else if (impact.ImpactsExplicitCulture)
{
var name = content.GetCultureName(impact.Culture);
if (string.IsNullOrWhiteSpace(name))
{
return false;
}
content.SetPublishInfo(impact.Culture, name, publishTime);
}
// set values
// property.PublishValues only publishes what is valid, variation-wise,
// but accepts any culture arg: null, all, specific
foreach (IProperty property in content.Properties)
{
// for the specified culture (null or all or specific)
PublishPropertyValues(content, property, impact.Culture, propertyEditorCollection);
// maybe the specified culture did not impact the invariant culture, so PublishValues
// above would skip it, yet it *also* impacts invariant properties
if (impact.ImpactsAlsoInvariantProperties && (property.PropertyType.VariesByCulture() is false || impact.ImpactsOnlyDefaultCulture))
{
PublishPropertyValues(content, property, null, propertyEditorCollection);
}
}
content.PublishedState = PublishedState.Publishing;
return true;
}
private static void PublishPropertyValues(IContent content, IProperty property, string? culture, PropertyEditorCollection propertyEditorCollection)
{
// if the content varies by culture, let data editor opt-in to perform partial property publishing (per culture)
if (content.ContentType.VariesByCulture()
&& propertyEditorCollection.TryGet(property.PropertyType.PropertyEditorAlias, out IDataEditor? dataEditor)
&& dataEditor.CanMergePartialPropertyValues(property.PropertyType))
{
// perform partial publishing for the current culture
property.PublishPartialValues(dataEditor, culture);
return;
}
// for the specified culture (null or all or specific)
property.PublishValues(culture);
}
/// <summary>
/// Returns false if the culture is already unpublished
/// </summary>
/// <param name="content"></param>
/// <param name="culture"></param>
/// <returns></returns>
public static bool UnpublishCulture(this IContent content, string? culture = "*")
{
culture = culture?.NullOrWhiteSpaceAsNull();
// the variation should be supported by the content type properties
if (!content.ContentType.SupportsPropertyVariation(culture, "*", true))
{
throw new NotSupportedException(
$"Culture \"{culture}\" is not supported by content type \"{content.ContentType.Alias}\" with variation \"{content.ContentType.Variations}\".");
}
var keepProcessing = true;
if (culture == "*" || (content.ContentType.VariesByCulture() is false && culture is null))
{
// all cultures
content.ClearPublishInfos();
}
else
{
// one single culture
keepProcessing = content.ClearPublishInfo(culture);
}
if (keepProcessing)
{
// property.PublishValues only publishes what is valid, variation-wise
foreach (IProperty property in content.Properties)
{
property.UnpublishValues(culture);
}
content.PublishedState = PublishedState.Publishing;
}
return keepProcessing;
}
public static void ClearPublishInfos(this IContent content) => content.PublishCultureInfos = null;
/// <summary>
/// Returns false if the culture is already unpublished
/// </summary>
/// <param name="content"></param>
/// <param name="culture"></param>
/// <returns></returns>
public static bool ClearPublishInfo(this IContent content, string? culture)
{
if (culture == null)
{
throw new ArgumentNullException(nameof(culture));
}
if (string.IsNullOrWhiteSpace(culture))
{
throw new ArgumentException(
"Value can't be empty or consist only of white-space characters.",
nameof(culture));
}
var removed = content.PublishCultureInfos?.Remove(culture);
if (removed ?? false)
{
// set the culture to be dirty - it's been modified
content.TouchCulture(culture);
}
return removed ?? false;
}
}