diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs
index 6bde9c7068..7f7f3c76d3 100644
--- a/src/Umbraco.Core/Services/ContentService.cs
+++ b/src/Umbraco.Core/Services/ContentService.cs
@@ -523,35 +523,14 @@ public class ContentService : RepositoryService, IContentService
///
public ContentScheduleCollection GetContentScheduleByContentId(int contentId)
- {
- using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
- {
- scope.ReadLock(Constants.Locks.ContentTree);
- return _documentRepository.GetContentSchedule(contentId);
- }
- }
+ => PublishOperationService.GetContentScheduleByContentId(contentId);
public ContentScheduleCollection GetContentScheduleByContentId(Guid contentId)
- {
- Attempt idAttempt = _idKeyMap.GetIdForKey(contentId, UmbracoObjectTypes.Document);
- if (idAttempt.Success is false)
- {
- return new ContentScheduleCollection();
- }
-
- return GetContentScheduleByContentId(idAttempt.Result);
- }
+ => PublishOperationService.GetContentScheduleByContentId(contentId);
///
public void PersistContentSchedule(IContent content, ContentScheduleCollection contentSchedule)
- {
- using (ICoreScope scope = ScopeProvider.CreateCoreScope())
- {
- scope.WriteLock(Constants.Locks.ContentTree);
- _documentRepository.PersistContentSchedule(content, contentSchedule);
- scope.Complete();
- }
- }
+ => PublishOperationService.PersistContentSchedule(content, contentSchedule);
///
///
@@ -642,20 +621,8 @@ public class ContentService : RepositoryService, IContentService
public IEnumerable GetAncestors(IContent content)
=> CrudService.GetAncestors(content);
- ///
- /// Gets a collection of published objects by Parent Id
- ///
- /// Id of the Parent to retrieve Children from
- /// An Enumerable list of published objects
public IEnumerable GetPublishedChildren(int id)
- {
- using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
- {
- scope.ReadLock(Constants.Locks.ContentTree);
- IQuery? query = Query().Where(x => x.ParentId == id && x.Published);
- return _documentRepository.Get(query).OrderBy(x => x.SortOrder);
- }
- }
+ => PublishOperationService.GetPublishedChildren(id);
///
public IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize, out long totalChildren, IQuery? filter = null, Ordering? ordering = null)
@@ -734,23 +701,11 @@ public class ContentService : RepositoryService, IContentService
///
public IEnumerable GetContentForExpiration(DateTime date)
- {
- using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
- {
- scope.ReadLock(Constants.Locks.ContentTree);
- return _documentRepository.GetContentForExpiration(date);
- }
- }
+ => PublishOperationService.GetContentForExpiration(date);
///
public IEnumerable GetContentForRelease(DateTime date)
- {
- using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
- {
- scope.ReadLock(Constants.Locks.ContentTree);
- return _documentRepository.GetContentForRelease(date);
- }
- }
+ => PublishOperationService.GetContentForRelease(date);
///
/// Gets a collection of an objects, which resides in the Recycle Bin
@@ -785,62 +740,14 @@ public class ContentService : RepositoryService, IContentService
///
public IDictionary> GetContentSchedulesByIds(Guid[] keys)
- {
- if (keys.Length == 0)
- {
- return ImmutableDictionary>.Empty;
- }
+ => PublishOperationService.GetContentSchedulesByIds(keys);
- List contentIds = [];
- foreach (var key in keys)
- {
- Attempt contentId = _idKeyMap.GetIdForKey(key, UmbracoObjectTypes.Document);
- if (contentId.Success is false)
- {
- continue;
- }
-
- contentIds.Add(contentId.Result);
- }
-
- using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
- {
- scope.ReadLock(Constants.Locks.ContentTree);
- return _documentRepository.GetContentSchedulesByIds(contentIds.ToArray());
- }
- }
-
- ///
- /// Checks if the passed in can be published based on the ancestors publish state.
- ///
- /// to check if ancestors are published
- /// True if the Content can be published, otherwise False
+ ///
public bool IsPathPublishable(IContent content)
- {
- // fast
- if (content.ParentId == Constants.System.Root)
- {
- return true; // root content is always publishable
- }
-
- if (content.Trashed)
- {
- return false; // trashed content is never publishable
- }
-
- // not trashed and has a parent: publishable if the parent is path-published
- IContent? parent = GetById(content.ParentId);
- return parent == null || IsPathPublished(parent);
- }
+ => PublishOperationService.IsPathPublishable(content);
public bool IsPathPublished(IContent? content)
- {
- using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
- {
- scope.ReadLock(Constants.Locks.ContentTree);
- return _documentRepository.IsPathPublished(content);
- }
- }
+ => PublishOperationService.IsPathPublished(content);
#endregion
@@ -856,1076 +763,20 @@ public class ContentService : RepositoryService, IContentService
///
public PublishResult Publish(IContent content, string[] cultures, int userId = Constants.Security.SuperUserId)
- {
- if (content == null)
- {
- throw new ArgumentNullException(nameof(content));
- }
-
- if (cultures is null)
- {
- throw new ArgumentNullException(nameof(cultures));
- }
-
- if (cultures.Any(c => c.IsNullOrWhiteSpace()) || cultures.Distinct().Count() != cultures.Length)
- {
- throw new ArgumentException("Cultures cannot be null or whitespace", nameof(cultures));
- }
-
- cultures = cultures.Select(x => x.EnsureCultureCode()!).ToArray();
-
- EventMessages evtMsgs = EventMessagesFactory.Get();
-
- // we need to guard against unsaved changes before proceeding; the content will be saved, but we're not firing any saved notifications
- if (HasUnsavedChanges(content))
- {
- return new PublishResult(PublishResultType.FailedPublishUnsavedChanges, evtMsgs, content);
- }
-
- if (content.Name != null && content.Name.Length > 255)
- {
- throw new InvalidOperationException("Name cannot be more than 255 characters in length.");
- }
-
- PublishedState publishedState = content.PublishedState;
- if (publishedState != PublishedState.Published && publishedState != PublishedState.Unpublished)
- {
- throw new InvalidOperationException(
- $"Cannot save-and-publish (un)publishing content, use the dedicated {nameof(CommitDocumentChanges)} method.");
- }
-
- // cannot accept invariant (null or empty) culture for variant content type
- // cannot accept a specific culture for invariant content type (but '*' is ok)
- if (content.ContentType.VariesByCulture())
- {
- if (cultures.Length > 1 && cultures.Contains("*"))
- {
- throw new ArgumentException("Cannot combine wildcard and specific cultures when publishing variant content types.", nameof(cultures));
- }
- }
- else
- {
- if (cultures.Length == 0)
- {
- cultures = new[] { "*" };
- }
-
- if (cultures[0] != "*" || cultures.Length > 1)
- {
- throw new ArgumentException($"Only wildcard culture is supported when publishing invariant content types.", nameof(cultures));
- }
- }
-
- using (ICoreScope scope = ScopeProvider.CreateCoreScope())
- {
- scope.WriteLock(Constants.Locks.ContentTree);
-
- var allLangs = _languageRepository.GetMany().ToList();
-
- // this will create the correct culture impact even if culture is * or null
- IEnumerable impacts =
- cultures.Select(culture => _cultureImpactFactory.Create(culture, IsDefaultCulture(allLangs, culture), content));
-
- // publish the culture(s)
- // we don't care about the response here, this response will be rechecked below but we need to set the culture info values now.
- var publishTime = DateTime.UtcNow;
- foreach (CultureImpact? impact in impacts)
- {
- content.PublishCulture(impact, publishTime, _propertyEditorCollection);
- }
-
- // Change state to publishing
- content.PublishedState = PublishedState.Publishing;
-
- PublishResult result = CommitDocumentChangesInternal(scope, content, evtMsgs, allLangs, new Dictionary(), userId);
- scope.Complete();
- return result;
- }
- }
+ => PublishOperationService.Publish(content, cultures, userId);
///
public PublishResult Unpublish(IContent content, string? culture = "*", int userId = Constants.Security.SuperUserId)
- {
- if (content == null)
- {
- throw new ArgumentNullException(nameof(content));
- }
+ => PublishOperationService.Unpublish(content, culture, userId);
- EventMessages evtMsgs = EventMessagesFactory.Get();
-
- culture = culture?.NullOrWhiteSpaceAsNull().EnsureCultureCode();
-
- PublishedState publishedState = content.PublishedState;
- if (publishedState != PublishedState.Published && publishedState != PublishedState.Unpublished)
- {
- throw new InvalidOperationException(
- $"Cannot save-and-publish (un)publishing content, use the dedicated {nameof(CommitDocumentChanges)} method.");
- }
-
- // cannot accept invariant (null or empty) culture for variant content type
- // cannot accept a specific culture for invariant content type (but '*' is ok)
- if (content.ContentType.VariesByCulture())
- {
- if (culture == null)
- {
- throw new NotSupportedException("Invariant culture is not supported by variant content types.");
- }
- }
- else
- {
- if (culture != null && culture != "*")
- {
- throw new NotSupportedException(
- $"Culture \"{culture}\" is not supported by invariant content types.");
- }
- }
-
- // if the content is not published, nothing to do
- if (!content.Published)
- {
- return new PublishResult(PublishResultType.SuccessUnpublishAlready, evtMsgs, content);
- }
-
- using (ICoreScope scope = ScopeProvider.CreateCoreScope())
- {
- scope.WriteLock(Constants.Locks.ContentTree);
-
- var allLangs = _languageRepository.GetMany().ToList();
-
- var savingNotification = new ContentSavingNotification(content, evtMsgs);
- if (scope.Notifications.PublishCancelable(savingNotification))
- {
- return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, content);
- }
-
- // all cultures = unpublish whole
- if (culture == "*" || (!content.ContentType.VariesByCulture() && culture == null))
- {
- // Unpublish the culture, this will change the document state to Publishing! ... which is expected because this will
- // essentially be re-publishing the document with the requested culture removed
- // We are however unpublishing all cultures, so we will set this to unpublishing.
- content.UnpublishCulture(culture);
- content.PublishedState = PublishedState.Unpublishing;
- PublishResult result = CommitDocumentChangesInternal(scope, content, evtMsgs, allLangs, savingNotification.State, userId);
- scope.Complete();
- return result;
- }
- else
- {
- // Unpublish the culture, this will change the document state to Publishing! ... which is expected because this will
- // essentially be re-publishing the document with the requested culture removed.
- // The call to CommitDocumentChangesInternal will perform all the checks like if this is a mandatory culture or the last culture being unpublished
- // and will then unpublish the document accordingly.
- // If the result of this is false it means there was no culture to unpublish (i.e. it was already unpublished or it did not exist)
- var removed = content.UnpublishCulture(culture);
-
- // Save and publish any changes
- PublishResult result = CommitDocumentChangesInternal(scope, content, evtMsgs, allLangs, savingNotification.State, userId);
-
- scope.Complete();
-
- // In one case the result will be PublishStatusType.FailedPublishNothingToPublish which means that no cultures
- // were specified to be published which will be the case when removed is false. In that case
- // we want to swap the result type to PublishResultType.SuccessUnpublishAlready (that was the expectation before).
- if (result.Result == PublishResultType.FailedPublishNothingToPublish && !removed)
- {
- return new PublishResult(PublishResultType.SuccessUnpublishAlready, evtMsgs, content);
- }
-
- return result;
- }
- }
- }
-
- ///
- /// Publishes/unpublishes any pending publishing changes made to the document.
- ///
- ///
- ///
- /// This MUST NOT be called from within this service, this used to be a public API and must only be used outside of
- /// this service.
- /// Internally in this service, calls must be made to CommitDocumentChangesInternal
- ///
- /// This is the underlying logic for both publishing and unpublishing any document
- ///
- /// Pending publishing/unpublishing changes on a document are made with calls to
- /// and
- /// .
- ///
- ///
- /// When publishing or unpublishing a single culture, or all cultures, use
- /// and . But if the flexibility to both publish and unpublish in a single operation is
- /// required
- /// then this method needs to be used in combination with
- /// and
- /// on the content itself - this prepares the content, but does not commit anything - and then, invoke
- /// to actually commit the changes to the database.
- ///
- /// The document is *always* saved, even when publishing fails.
- ///
- internal PublishResult CommitDocumentChanges(IContent content, int userId = Constants.Security.SuperUserId)
- {
- using (ICoreScope scope = ScopeProvider.CreateCoreScope())
- {
- EventMessages evtMsgs = EventMessagesFactory.Get();
-
- scope.WriteLock(Constants.Locks.ContentTree);
-
- var savingNotification = new ContentSavingNotification(content, evtMsgs);
- if (scope.Notifications.PublishCancelable(savingNotification))
- {
- return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, content);
- }
-
- var allLangs = _languageRepository.GetMany().ToList();
-
- PublishResult result = CommitDocumentChangesInternal(scope, content, evtMsgs, allLangs, savingNotification.State, userId);
- scope.Complete();
- return result;
- }
- }
-
- ///
- /// Handles a lot of business logic cases for how the document should be persisted
- ///
- ///
- ///
- ///
- ///
- ///
- ///
- ///
- ///
- ///
- ///
- ///
- /// Business logic cases such: as unpublishing a mandatory culture, or unpublishing the last culture, checking for
- /// pending scheduled publishing, etc... is dealt with in this method.
- /// There is quite a lot of cases to take into account along with logic that needs to deal with scheduled
- /// saving/publishing, branch saving/publishing, etc...
- ///
- ///
- private PublishResult CommitDocumentChangesInternal(
- ICoreScope scope,
- IContent content,
- EventMessages eventMessages,
- IReadOnlyCollection allLangs,
- IDictionary? notificationState,
- int userId,
- bool branchOne = false,
- bool branchRoot = false)
- {
- if (scope == null)
- {
- throw new ArgumentNullException(nameof(scope));
- }
-
- if (content == null)
- {
- throw new ArgumentNullException(nameof(content));
- }
-
- if (eventMessages == null)
- {
- throw new ArgumentNullException(nameof(eventMessages));
- }
-
- PublishResult? publishResult = null;
- PublishResult? unpublishResult = null;
-
- // nothing set = republish it all
- if (content.PublishedState != PublishedState.Publishing &&
- content.PublishedState != PublishedState.Unpublishing)
- {
- content.PublishedState = PublishedState.Publishing;
- }
-
- // State here is either Publishing or Unpublishing
- // Publishing to unpublish a culture may end up unpublishing everything so these flags can be flipped later
- var publishing = content.PublishedState == PublishedState.Publishing;
- var unpublishing = content.PublishedState == PublishedState.Unpublishing;
-
- var variesByCulture = content.ContentType.VariesByCulture();
-
- // Track cultures that are being published, changed, unpublished
- IReadOnlyList? culturesPublishing = null;
- IReadOnlyList? culturesUnpublishing = null;
- IReadOnlyList? culturesChanging = variesByCulture
- ? content.CultureInfos?.Values.Where(x => x.IsDirty()).Select(x => x.Culture).ToList()
- : null;
-
- var isNew = !content.HasIdentity;
- TreeChangeTypes changeType = isNew ? TreeChangeTypes.RefreshNode : TreeChangeTypes.RefreshBranch;
- var previouslyPublished = content.HasIdentity && content.Published;
-
- // Inline method to persist the document with the documentRepository since this logic could be called a couple times below
- void SaveDocument(IContent c)
- {
- // save, always
- if (c.HasIdentity == false)
- {
- c.CreatorId = userId;
- }
-
- c.WriterId = userId;
-
- // saving does NOT change the published version, unless PublishedState is Publishing or Unpublishing
- _documentRepository.Save(c);
- }
-
- if (publishing)
- {
- // 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;
-
- // ensure that the document can be published, and publish handling events, business rules, etc
- publishResult = StrategyCanPublish(
- scope,
- content, /*checkPath:*/
- !branchOne || branchRoot,
- culturesPublishing,
- culturesUnpublishing,
- eventMessages,
- allLangs,
- notificationState);
-
- if (publishResult.Success)
- {
- // raise Publishing notification
- if (scope.Notifications.PublishCancelable(
- new ContentPublishingNotification(content, eventMessages).WithState(notificationState)))
- {
- _logger.LogInformation("Document {ContentName} (id={ContentId}) cannot be published: {Reason}", content.Name, content.Id, "publishing was cancelled");
- return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, eventMessages, content);
- }
-
- // note: StrategyPublish flips the PublishedState to Publishing!
- publishResult = StrategyPublish(content, culturesPublishing, culturesUnpublishing, eventMessages);
-
- // Check if a culture has been unpublished and if there are no cultures left, and then unpublish document as a whole
- if (publishResult.Result == PublishResultType.SuccessUnpublishCulture &&
- content.PublishCultureInfos?.Count == 0)
- {
- // This is a special case! We are unpublishing the last culture and to persist that we need to re-publish without any cultures
- // so the state needs to remain Publishing to do that. However, we then also need to unpublish the document and to do that
- // the state needs to be Unpublishing and it cannot be both. This state is used within the documentRepository to know how to
- // persist certain things. So before proceeding below, we need to save the Publishing state to publish no cultures, then we can
- // mark the document for Unpublishing.
- SaveDocument(content);
-
- // Set the flag to unpublish and continue
- unpublishing = content.Published; // if not published yet, nothing to do
- }
- }
- else
- {
- // in a branch, just give up
- if (branchOne && !branchRoot)
- {
- return publishResult;
- }
-
- // Check for mandatory culture missing, and then unpublish document as a whole
- if (publishResult.Result == PublishResultType.FailedPublishMandatoryCultureMissing)
- {
- publishing = false;
- unpublishing = content.Published; // if not published yet, nothing to do
-
- // we may end up in a state where we won't publish nor unpublish
- // keep going, though, as we want to save anyways
- }
-
- // reset published state from temp values (publishing, unpublishing) to original value
- // (published, unpublished) in order to save the document, unchanged - yes, this is odd,
- // but: (a) it means we don't reproduce the PublishState logic here and (b) setting the
- // PublishState to anything other than Publishing or Unpublishing - which is precisely
- // what we want to do here - throws
- content.Published = content.Published;
- }
- }
-
- // won't happen in a branch
- if (unpublishing)
- {
- IContent? newest = GetById(content.Id); // ensure we have the newest version - in scope
- if (content.VersionId != newest?.VersionId)
- {
- return new PublishResult(PublishResultType.FailedPublishConcurrencyViolation, eventMessages, content);
- }
-
- if (content.Published)
- {
- // ensure that the document can be unpublished, and unpublish
- // handling events, business rules, etc
- // note: StrategyUnpublish flips the PublishedState to Unpublishing!
- // note: This unpublishes the entire document (not different variants)
- unpublishResult = StrategyCanUnpublish(scope, content, eventMessages, notificationState);
- if (unpublishResult.Success)
- {
- unpublishResult = StrategyUnpublish(content, eventMessages);
- }
- else
- {
- // reset published state from temp values (publishing, unpublishing) to original value
- // (published, unpublished) in order to save the document, unchanged - yes, this is odd,
- // but: (a) it means we don't reproduce the PublishState logic here and (b) setting the
- // PublishState to anything other than Publishing or Unpublishing - which is precisely
- // what we want to do here - throws
- content.Published = content.Published;
- return unpublishResult;
- }
- }
- else
- {
- // already unpublished - optimistic concurrency collision, really,
- // and I am not sure at all what we should do, better die fast, else
- // we may end up corrupting the db
- throw new InvalidOperationException("Concurrency collision.");
- }
- }
-
- // Persist the document
- SaveDocument(content);
-
- // we have tried to unpublish - won't happen in a branch
- if (unpublishing)
- {
- // and succeeded, trigger events
- if (unpublishResult?.Success ?? false)
- {
- // events and audit
- scope.Notifications.Publish(
- new ContentUnpublishedNotification(content, eventMessages).WithState(notificationState));
- scope.Notifications.Publish(new ContentTreeChangeNotification(
- content,
- TreeChangeTypes.RefreshBranch,
- variesByCulture ? culturesPublishing.IsCollectionEmpty() ? null : culturesPublishing : null,
- variesByCulture ? culturesUnpublishing.IsCollectionEmpty() ? null : culturesUnpublishing : ["*"],
- eventMessages));
-
- if (culturesUnpublishing != null)
- {
- // This will mean that that we unpublished a mandatory culture or we unpublished the last culture.
- var langs = GetLanguageDetailsForAuditEntry(allLangs, culturesUnpublishing);
- Audit(AuditType.UnpublishVariant, userId, content.Id, $"Unpublished languages: {langs}", langs);
-
- if (publishResult == null)
- {
- throw new PanicException("publishResult == null - should not happen");
- }
-
- switch (publishResult.Result)
- {
- case PublishResultType.FailedPublishMandatoryCultureMissing:
- // Occurs when a mandatory culture was unpublished (which means we tried publishing the document without a mandatory culture)
-
- // Log that the whole content item has been unpublished due to mandatory culture unpublished
- Audit(AuditType.Unpublish, userId, content.Id, "Unpublished (mandatory language unpublished)");
- return new PublishResult(PublishResultType.SuccessUnpublishMandatoryCulture, eventMessages, content);
- case PublishResultType.SuccessUnpublishCulture:
- // Occurs when the last culture is unpublished
- Audit(AuditType.Unpublish, userId, content.Id, "Unpublished (last language unpublished)");
- return new PublishResult(PublishResultType.SuccessUnpublishLastCulture, eventMessages, content);
- }
- }
-
- Audit(AuditType.Unpublish, userId, content.Id);
- return new PublishResult(PublishResultType.SuccessUnpublish, eventMessages, content);
- }
-
- // or, failed
- scope.Notifications.Publish(new ContentTreeChangeNotification(content, changeType, eventMessages));
- return new PublishResult(PublishResultType.FailedUnpublish, eventMessages, content); // bah
- }
-
- // we have tried to publish
- if (publishing)
- {
- // and succeeded, trigger events
- if (publishResult?.Success ?? false)
- {
- if (isNew == false && previouslyPublished == false)
- {
- changeType = TreeChangeTypes.RefreshBranch; // whole branch
- }
- else if (isNew == false && previouslyPublished)
- {
- changeType = TreeChangeTypes.RefreshNode; // single node
- }
-
- // invalidate the node/branch
- // for branches, handled by SaveAndPublishBranch
- if (!branchOne)
- {
- scope.Notifications.Publish(
- new ContentTreeChangeNotification(
- content,
- changeType,
- variesByCulture ? culturesPublishing.IsCollectionEmpty() ? null : culturesPublishing : ["*"],
- variesByCulture ? culturesUnpublishing.IsCollectionEmpty() ? null : culturesUnpublishing : null,
- eventMessages));
- scope.Notifications.Publish(
- new ContentPublishedNotification(content, eventMessages).WithState(notificationState));
- }
-
- // it was not published and now is... descendants that were 'published' (but
- // had an unpublished ancestor) are 're-published' ie not explicitly published
- // but back as 'published' nevertheless
- if (!branchOne && isNew == false && previouslyPublished == false && HasChildren(content.Id))
- {
- IContent[] descendants = GetPublishedDescendantsLocked(content).ToArray();
- scope.Notifications.Publish(
- new ContentPublishedNotification(descendants, eventMessages).WithState(notificationState));
- }
-
- switch (publishResult.Result)
- {
- case PublishResultType.SuccessPublish:
- Audit(AuditType.Publish, userId, content.Id);
- break;
- case PublishResultType.SuccessPublishCulture:
- if (culturesPublishing != null)
- {
- var langs = GetLanguageDetailsForAuditEntry(allLangs, culturesPublishing);
- Audit(AuditType.PublishVariant, userId, content.Id, $"Published languages: {langs}", langs);
- }
-
- break;
- case PublishResultType.SuccessUnpublishCulture:
- if (culturesUnpublishing != null)
- {
- var langs = GetLanguageDetailsForAuditEntry(allLangs, culturesUnpublishing);
- Audit(AuditType.UnpublishVariant, userId, content.Id, $"Unpublished languages: {langs}", langs);
- }
-
- break;
- }
-
- return publishResult;
- }
- }
-
- // should not happen
- if (branchOne && !branchRoot)
- {
- throw new PanicException("branchOne && !branchRoot - should not happen");
- }
-
- // if publishing didn't happen or if it has failed, we still need to log which cultures were saved
- if (!branchOne && (publishResult == null || !publishResult.Success))
- {
- if (culturesChanging != null)
- {
- var langs = GetLanguageDetailsForAuditEntry(allLangs, culturesChanging);
- Audit(AuditType.SaveVariant, userId, content.Id, $"Saved languages: {langs}", langs);
- }
- else
- {
- Audit(AuditType.Save, userId, content.Id);
- }
- }
-
- // or, failed
- scope.Notifications.Publish(new ContentTreeChangeNotification(content, changeType, eventMessages));
- return publishResult!;
- }
///
public IEnumerable PerformScheduledPublish(DateTime date)
- {
- var allLangs = new Lazy>(() => _languageRepository.GetMany().ToList());
- EventMessages evtMsgs = EventMessagesFactory.Get();
- var results = new List();
-
- PerformScheduledPublishingRelease(date, results, evtMsgs, allLangs);
- PerformScheduledPublishingExpiration(date, results, evtMsgs, allLangs);
-
- return results;
- }
-
- private void PerformScheduledPublishingExpiration(DateTime date, List results, EventMessages evtMsgs, Lazy> allLangs)
- {
- using ICoreScope scope = ScopeProvider.CreateCoreScope();
-
- // do a fast read without any locks since this executes often to see if we even need to proceed
- if (_documentRepository.HasContentForExpiration(date))
- {
- // now take a write lock since we'll be updating
- scope.WriteLock(Constants.Locks.ContentTree);
-
- foreach (IContent d in _documentRepository.GetContentForExpiration(date))
- {
- ContentScheduleCollection contentSchedule = _documentRepository.GetContentSchedule(d.Id);
- if (d.ContentType.VariesByCulture())
- {
- // find which cultures have pending schedules
- var pendingCultures = contentSchedule.GetPending(ContentScheduleAction.Expire, date)
- .Select(x => x.Culture)
- .Distinct()
- .ToList();
-
- if (pendingCultures.Count == 0)
- {
- continue; // shouldn't happen but no point in processing this document if there's nothing there
- }
-
- var savingNotification = new ContentSavingNotification(d, evtMsgs);
- if (scope.Notifications.PublishCancelable(savingNotification))
- {
- results.Add(new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, d));
- continue;
- }
-
- foreach (var c in pendingCultures)
- {
- // Clear this schedule for this culture
- contentSchedule.Clear(c, ContentScheduleAction.Expire, date);
-
- // set the culture to be published
- d.UnpublishCulture(c);
- }
-
- _documentRepository.PersistContentSchedule(d, contentSchedule);
- PublishResult result = CommitDocumentChangesInternal(scope, d, evtMsgs, allLangs.Value, savingNotification.State, d.WriterId);
- if (result.Success == false)
- {
- _logger.LogError(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result);
- }
-
- results.Add(result);
- }
- else
- {
- // Clear this schedule for this culture
- contentSchedule.Clear(ContentScheduleAction.Expire, date);
- _documentRepository.PersistContentSchedule(d, contentSchedule);
- PublishResult result = Unpublish(d, userId: d.WriterId);
- if (result.Success == false)
- {
- _logger.LogError(null, "Failed to unpublish document id={DocumentId}, reason={Reason}.", d.Id, result.Result);
- }
-
- results.Add(result);
- }
- }
-
- _documentRepository.ClearSchedule(date, ContentScheduleAction.Expire);
- }
-
- scope.Complete();
- }
-
- private void PerformScheduledPublishingRelease(DateTime date, List results, EventMessages evtMsgs, Lazy> allLangs)
- {
- using ICoreScope scope = ScopeProvider.CreateCoreScope();
-
- // do a fast read without any locks since this executes often to see if we even need to proceed
- if (_documentRepository.HasContentForRelease(date))
- {
- // now take a write lock since we'll be updating
- scope.WriteLock(Constants.Locks.ContentTree);
-
- foreach (IContent d in _documentRepository.GetContentForRelease(date))
- {
- ContentScheduleCollection contentSchedule = _documentRepository.GetContentSchedule(d.Id);
- if (d.ContentType.VariesByCulture())
- {
- // find which cultures have pending schedules
- var pendingCultures = contentSchedule.GetPending(ContentScheduleAction.Release, date)
- .Select(x => x.Culture)
- .Distinct()
- .ToList();
-
- if (pendingCultures.Count == 0)
- {
- continue; // shouldn't happen but no point in processing this document if there's nothing there
- }
- var savingNotification = new ContentSavingNotification(d, evtMsgs);
- if (scope.Notifications.PublishCancelable(savingNotification))
- {
- results.Add(new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, d));
- continue;
- }
-
-
- var publishing = true;
- foreach (var culture in pendingCultures)
- {
- // Clear this schedule for this culture
- contentSchedule.Clear(culture, ContentScheduleAction.Release, date);
-
- if (d.Trashed)
- {
- continue; // won't publish
- }
-
- // publish the culture values and validate the property values, if validation fails, log the invalid properties so the develeper has an idea of what has failed
- IProperty[]? invalidProperties = null;
- CultureImpact impact = _cultureImpactFactory.ImpactExplicit(culture, IsDefaultCulture(allLangs.Value, culture));
- var tryPublish = d.PublishCulture(impact, date, _propertyEditorCollection) &&
- _propertyValidationService.Value.IsPropertyDataValid(d, out invalidProperties, impact);
- if (invalidProperties != null && invalidProperties.Length > 0)
- {
- _logger.LogWarning(
- "Scheduled publishing will fail for document {DocumentId} and culture {Culture} because of invalid properties {InvalidProperties}",
- d.Id,
- culture,
- string.Join(",", invalidProperties.Select(x => x.Alias)));
- }
-
- publishing &= tryPublish; // set the culture to be published
- if (!publishing)
- {
- }
- }
-
- PublishResult result;
-
- if (d.Trashed)
- {
- result = new PublishResult(PublishResultType.FailedPublishIsTrashed, evtMsgs, d);
- }
- else if (!publishing)
- {
- result = new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, d);
- }
- else
- {
- _documentRepository.PersistContentSchedule(d, contentSchedule);
- result = CommitDocumentChangesInternal(scope, d, evtMsgs, allLangs.Value, savingNotification.State, d.WriterId);
- }
-
- if (result.Success == false)
- {
- _logger.LogError(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result);
- }
-
- results.Add(result);
- }
- else
- {
- // Clear this schedule
- contentSchedule.Clear(ContentScheduleAction.Release, date);
-
- PublishResult? result = null;
-
- if (d.Trashed)
- {
- result = new PublishResult(PublishResultType.FailedPublishIsTrashed, evtMsgs, d);
- }
- else
- {
- _documentRepository.PersistContentSchedule(d, contentSchedule);
- result = Publish(d, d.AvailableCultures.ToArray(), userId: d.WriterId);
- }
-
- if (result.Success == false)
- {
- _logger.LogError(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result);
- }
-
- results.Add(result);
- }
- }
-
- _documentRepository.ClearSchedule(date, ContentScheduleAction.Release);
- }
-
- scope.Complete();
- }
-
- // utility 'PublishCultures' func used by SaveAndPublishBranch
- private bool PublishBranch_PublishCultures(IContent content, HashSet culturesToPublish, IReadOnlyCollection allLangs)
- {
- // variant content type - publish specified cultures
- // invariant content type - publish only the invariant culture
-
- var publishTime = DateTime.UtcNow;
- if (content.ContentType.VariesByCulture())
- {
- return culturesToPublish.All(culture =>
- {
- CultureImpact? impact = _cultureImpactFactory.Create(culture, IsDefaultCulture(allLangs, culture), content);
- return content.PublishCulture(impact, publishTime, _propertyEditorCollection) &&
- _propertyValidationService.Value.IsPropertyDataValid(content, out _, impact);
- });
- }
-
- return content.PublishCulture(_cultureImpactFactory.ImpactInvariant(), publishTime, _propertyEditorCollection)
- && _propertyValidationService.Value.IsPropertyDataValid(content, out _, _cultureImpactFactory.ImpactInvariant());
- }
-
- // utility 'ShouldPublish' func used by PublishBranch
- private static HashSet? PublishBranch_ShouldPublish(ref HashSet? cultures, string c, bool published, bool edited, bool isRoot, PublishBranchFilter publishBranchFilter)
- {
- // if published, republish
- if (published)
- {
- cultures ??= new HashSet(); // empty means 'already published'
-
- if (edited || publishBranchFilter.HasFlag(PublishBranchFilter.ForceRepublish))
- {
- cultures.Add(c); // means 'republish this culture'
- }
-
- return cultures;
- }
-
- // if not published, publish if force/root else do nothing
- if (!publishBranchFilter.HasFlag(PublishBranchFilter.IncludeUnpublished) && !isRoot)
- {
- return cultures; // null means 'nothing to do'
- }
-
- cultures ??= new HashSet();
-
- cultures.Add(c); // means 'publish this culture'
- return cultures;
- }
+ => PublishOperationService.PerformScheduledPublish(date);
///
public IEnumerable PublishBranch(IContent content, PublishBranchFilter publishBranchFilter, string[] cultures, int userId = Constants.Security.SuperUserId)
- {
- // note: EditedValue and PublishedValue are objects here, so it is important to .Equals()
- // and not to == them, else we would be comparing references, and that is a bad thing
-
- cultures = EnsureCultures(content, cultures);
-
- string? defaultCulture;
- using (ICoreScope scope = ScopeProvider.CreateCoreScope())
- {
- defaultCulture = _languageRepository.GetDefaultIsoCode();
- scope.Complete();
- }
-
- // determines cultures to be published
- // can be: null (content is not impacted), an empty set (content is impacted but already published), or cultures
- HashSet? ShouldPublish(IContent c)
- {
- var isRoot = c.Id == content.Id;
- HashSet? culturesToPublish = null;
-
- // invariant content type
- if (!c.ContentType.VariesByCulture())
- {
- return PublishBranch_ShouldPublish(ref culturesToPublish, "*", c.Published, c.Edited, isRoot, publishBranchFilter);
- }
-
- // variant content type, specific cultures
- if (c.Published)
- {
- // then some (and maybe all) cultures will be 'already published' (unless forcing),
- // others will have to 'republish this culture'
- foreach (var culture in cultures)
- {
- // We could be publishing a parent invariant page, with descendents that are variant.
- // So convert the invariant request to a request for the default culture.
- var specificCulture = culture == "*" ? defaultCulture : culture;
-
- PublishBranch_ShouldPublish(ref culturesToPublish, specificCulture, c.IsCulturePublished(specificCulture), c.IsCultureEdited(specificCulture), isRoot, publishBranchFilter);
- }
-
- return culturesToPublish;
- }
-
- // if not published, publish if forcing unpublished/root else do nothing
- return publishBranchFilter.HasFlag(PublishBranchFilter.IncludeUnpublished) || isRoot
- ? new HashSet(cultures) // means 'publish specified cultures'
- : null; // null means 'nothing to do'
- }
-
- return PublishBranch(content, ShouldPublish, PublishBranch_PublishCultures, userId);
- }
-
- private static string[] EnsureCultures(IContent content, string[] cultures)
- {
- // Ensure consistent indication of "all cultures" for variant content.
- if (content.ContentType.VariesByCulture() is false && ProvidedCulturesIndicatePublishAll(cultures))
- {
- cultures = ["*"];
- }
-
- return cultures.Select(x => x.EnsureCultureCode()!).ToArray();
- }
-
- private static bool ProvidedCulturesIndicatePublishAll(string[] cultures) => cultures.Length == 0 || (cultures.Length == 1 && cultures[0] == "invariant");
-
- internal IEnumerable PublishBranch(
- IContent document,
- Func?> shouldPublish,
- Func, IReadOnlyCollection, bool> publishCultures,
- int userId = Constants.Security.SuperUserId)
- {
- if (shouldPublish == null)
- {
- throw new ArgumentNullException(nameof(shouldPublish));
- }
-
- if (publishCultures == null)
- {
- throw new ArgumentNullException(nameof(publishCultures));
- }
-
- EventMessages eventMessages = EventMessagesFactory.Get();
- var results = new List();
- var publishedDocuments = new List();
-
- using (ICoreScope scope = ScopeProvider.CreateCoreScope())
- {
- scope.WriteLock(Constants.Locks.ContentTree);
-
- var allLangs = _languageRepository.GetMany().ToList();
-
- if (!document.HasIdentity)
- {
- throw new InvalidOperationException("Cannot not branch-publish a new document.");
- }
-
- PublishedState publishedState = document.PublishedState;
- if (publishedState == PublishedState.Publishing)
- {
- throw new InvalidOperationException("Cannot mix PublishCulture and SaveAndPublishBranch.");
- }
-
- // deal with the branch root - if it fails, abort
- HashSet? culturesToPublish = shouldPublish(document);
- PublishResult? result = PublishBranchItem(scope, document, culturesToPublish, publishCultures, true, publishedDocuments, eventMessages, userId, allLangs, out IDictionary? notificationState);
- if (result != null)
- {
- results.Add(result);
- if (!result.Success)
- {
- return results;
- }
- }
-
- HashSet culturesPublished = culturesToPublish ?? [];
-
- // deal with descendants
- // if one fails, abort its branch
- var exclude = new HashSet();
-
- int count;
- var page = 0;
- const int pageSize = 100;
- do
- {
- count = 0;
-
- // important to order by Path ASC so make it explicit in case defaults change
- // ReSharper disable once RedundantArgumentDefaultValue
- foreach (IContent d in GetPagedDescendants(document.Id, page, pageSize, out _, ordering: Ordering.By("Path", Direction.Ascending)))
- {
- count++;
-
- // if parent is excluded, exclude child too
- if (exclude.Contains(d.ParentId))
- {
- exclude.Add(d.Id);
- continue;
- }
-
- // no need to check path here, parent has to be published here
- culturesToPublish = shouldPublish(d);
- result = PublishBranchItem(scope, d, culturesToPublish, publishCultures, false, publishedDocuments, eventMessages, userId, allLangs, out _);
- if (result != null)
- {
- results.Add(result);
- if (result.Success)
- {
- culturesPublished.UnionWith(culturesToPublish ?? []);
- continue;
- }
- }
-
- // if we could not publish the document, cut its branch
- exclude.Add(d.Id);
- }
-
- page++;
- }
- while (count > 0);
-
- Audit(AuditType.Publish, userId, document.Id, "Branch published");
-
- // trigger events for the entire branch
- // (SaveAndPublishBranchOne does *not* do it)
- var variesByCulture = document.ContentType.VariesByCulture();
- scope.Notifications.Publish(
- new ContentTreeChangeNotification(
- document,
- TreeChangeTypes.RefreshBranch,
- variesByCulture ? culturesPublished.IsCollectionEmpty() ? null : culturesPublished : ["*"],
- null,
- eventMessages));
- scope.Notifications.Publish(new ContentPublishedNotification(publishedDocuments, eventMessages, true).WithState(notificationState));
-
- scope.Complete();
- }
-
- return results;
- }
-
- // shouldPublish: a function determining whether the document has changes that need to be published
- // note - 'force' is handled by 'editing'
- // publishValues: a function publishing values (using the appropriate PublishCulture calls)
- private PublishResult? PublishBranchItem(
- ICoreScope scope,
- IContent document,
- HashSet? culturesToPublish,
- Func, IReadOnlyCollection,
- bool> publishCultures,
- bool isRoot,
- ICollection publishedDocuments,
- EventMessages evtMsgs,
- int userId,
- IReadOnlyCollection allLangs,
- out IDictionary? initialNotificationState)
- {
- initialNotificationState = new Dictionary();
-
- // we need to guard against unsaved changes before proceeding; the document will be saved, but we're not firing any saved notifications
- if (HasUnsavedChanges(document))
- {
- return new PublishResult(PublishResultType.FailedPublishUnsavedChanges, evtMsgs, document);
- }
-
- // null = do not include
- if (culturesToPublish == null)
- {
- return null;
- }
-
- // empty = already published
- if (culturesToPublish.Count == 0)
- {
- return new PublishResult(PublishResultType.SuccessPublishAlready, evtMsgs, document);
- }
-
- var savingNotification = new ContentSavingNotification(document, evtMsgs);
- if (scope.Notifications.PublishCancelable(savingNotification))
- {
- return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, document);
- }
-
- // publish & check if values are valid
- if (!publishCultures(document, culturesToPublish, allLangs))
- {
- // TODO: Based on this callback behavior there is no way to know which properties may have been invalid if this failed, see other results of FailedPublishContentInvalid
- return new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, document);
- }
-
- PublishResult result = CommitDocumentChangesInternal(scope, document, evtMsgs, allLangs, savingNotification.State, userId, true, isRoot);
- if (result.Success)
- {
- publishedDocuments.Add(document);
- }
-
- return result;
- }
+ => PublishOperationService.PublishBranch(content, publishBranchFilter, cultures, userId);
#endregion
@@ -2173,67 +1024,9 @@ public class ContentService : RepositoryService, IContentService
return parentKeyAttempt.Success;
}
- ///
- /// Sends an to Publication, which executes handlers and events for the 'Send to Publication'
- /// action.
- ///
- /// The to send to publication
- /// Optional Id of the User issuing the send to publication
- /// True if sending publication was successful otherwise false
+ ///
public bool SendToPublication(IContent? content, int userId = Constants.Security.SuperUserId)
- {
- if (content is null)
- {
- return false;
- }
-
- EventMessages evtMsgs = EventMessagesFactory.Get();
-
- using (ICoreScope scope = ScopeProvider.CreateCoreScope())
- {
- var sendingToPublishNotification = new ContentSendingToPublishNotification(content, evtMsgs);
- if (scope.Notifications.PublishCancelable(sendingToPublishNotification))
- {
- scope.Complete();
- return false;
- }
-
- // track the cultures changing for auditing
- var culturesChanging = content.ContentType.VariesByCulture()
- ? string.Join(",", content.CultureInfos!.Values.Where(x => x.IsDirty()).Select(x => x.Culture))
- : null;
-
- // TODO: Currently there's no way to change track which variant properties have changed, we only have change
- // tracking enabled on all values on the Property which doesn't allow us to know which variants have changed.
- // in this particular case, determining which cultures have changed works with the above with names since it will
- // have always changed if it's been saved in the back office but that's not really fail safe.
-
- // Save before raising event
- OperationResult saveResult = Save(content, userId);
-
- // always complete (but maybe return a failed status)
- scope.Complete();
-
- if (!saveResult.Success)
- {
- return saveResult.Success;
- }
-
- scope.Notifications.Publish(
- new ContentSentToPublishNotification(content, evtMsgs).WithStateFrom(sendingToPublishNotification));
-
- if (culturesChanging != null)
- {
- Audit(AuditType.SendToPublishVariant, userId, content.Id, $"Send To Publish for cultures: {culturesChanging}", culturesChanging);
- }
- else
- {
- Audit(AuditType.SendToPublish, userId, content.Id);
- }
-
- return saveResult.Success;
- }
- }
+ => PublishOperationService.SendToPublication(content, userId);
///
/// Sorts a collection of objects by updating the SortOrder according
@@ -2288,48 +1081,6 @@ public class ContentService : RepositoryService, IContentService
#endregion
- #region Internal Methods
-
- ///
- /// Gets a collection of descendants by the first Parent.
- ///
- /// item to retrieve Descendants from
- /// An Enumerable list of objects
- internal IEnumerable GetPublishedDescendants(IContent content)
- {
- using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
- {
- scope.ReadLock(Constants.Locks.ContentTree);
- return GetPublishedDescendantsLocked(content).ToArray(); // ToArray important in uow!
- }
- }
-
- internal IEnumerable GetPublishedDescendantsLocked(IContent content)
- {
- var pathMatch = content.Path + ",";
- IQuery query = Query()
- .Where(x => x.Id != content.Id && x.Path.StartsWith(pathMatch) /*&& culture.Trashed == false*/);
- IEnumerable contents = _documentRepository.Get(query);
-
- // beware! contents contains all published version below content
- // including those that are not directly published because below an unpublished content
- // these must be filtered out here
- var parents = new List { content.Id };
- if (contents is not null)
- {
- foreach (IContent c in contents)
- {
- if (parents.Contains(c.ParentId))
- {
- yield return c;
- parents.Add(c.Id);
- }
- }
- }
- }
-
- #endregion
-
#region Private Methods
private void Audit(AuditType type, int userId, int objectId, string? message = null, string? parameters = null) =>
@@ -2348,350 +1099,6 @@ public class ContentService : RepositoryService, IContentService
parameters);
}
- private string GetLanguageDetailsForAuditEntry(IEnumerable affectedCultures)
- => GetLanguageDetailsForAuditEntry(_languageRepository.GetMany(), affectedCultures);
-
- private static string GetLanguageDetailsForAuditEntry(IEnumerable languages, IEnumerable affectedCultures)
- {
- IEnumerable languageIsoCodes = languages
- .Where(x => affectedCultures.InvariantContains(x.IsoCode))
- .Select(x => x.IsoCode);
- return string.Join(", ", languageIsoCodes);
- }
-
- private static bool IsDefaultCulture(IReadOnlyCollection? langs, string culture) =>
- langs?.Any(x => x.IsDefault && x.IsoCode.InvariantEquals(culture)) ?? false;
-
- private bool IsMandatoryCulture(IReadOnlyCollection langs, string culture) =>
- langs.Any(x => x.IsMandatory && x.IsoCode.InvariantEquals(culture));
-
- #endregion
-
- #region Publishing Strategies
-
- ///
- /// Ensures that a document can be published
- ///
- ///
- ///
- ///
- ///
- ///
- ///
- ///
- ///
- ///
- private PublishResult StrategyCanPublish(
- ICoreScope scope,
- IContent content,
- bool checkPath,
- IReadOnlyList? culturesPublishing,
- IReadOnlyCollection? culturesUnpublishing,
- EventMessages evtMsgs,
- IReadOnlyCollection allLangs,
- IDictionary? notificationState)
- {
- var variesByCulture = content.ContentType.VariesByCulture();
-
- // If it's null it's invariant
- CultureImpact[] impactsToPublish = culturesPublishing == null
- ? new[] { _cultureImpactFactory.ImpactInvariant() }
- : culturesPublishing.Select(x =>
- _cultureImpactFactory.ImpactExplicit(
- x,
- allLangs.Any(lang => lang.IsoCode.InvariantEquals(x) && lang.IsMandatory)))
- .ToArray();
-
- // publish the culture(s)
- var publishTime = DateTime.UtcNow;
- if (!impactsToPublish.All(impact => content.PublishCulture(impact, publishTime, _propertyEditorCollection)))
- {
- return new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, content);
- }
-
- // Validate the property values
- IProperty[]? invalidProperties = null;
- if (!impactsToPublish.All(x =>
- _propertyValidationService.Value.IsPropertyDataValid(content, out invalidProperties, x)))
- {
- return new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, content)
- {
- InvalidProperties = invalidProperties,
- };
- }
-
- // Check if mandatory languages fails, if this fails it will mean anything that the published flag on the document will
- // be changed to Unpublished and any culture currently published will not be visible.
- if (variesByCulture)
- {
- if (culturesPublishing == null)
- {
- throw new InvalidOperationException(
- "Internal error, variesByCulture but culturesPublishing is null.");
- }
-
- if (content.Published && culturesPublishing.Count == 0 && culturesUnpublishing?.Count == 0)
- {
- // no published cultures = cannot be published
- // This will occur if for example, a culture that is already unpublished is sent to be unpublished again, or vice versa, in that case
- // there will be nothing to publish/unpublish.
- return new PublishResult(PublishResultType.FailedPublishNothingToPublish, evtMsgs, content);
- }
-
- // missing mandatory culture = cannot be published
- IEnumerable mandatoryCultures = allLangs.Where(x => x.IsMandatory).Select(x => x.IsoCode);
- var mandatoryMissing = mandatoryCultures.Any(x =>
- !content.PublishedCultures.Contains(x, StringComparer.OrdinalIgnoreCase));
- if (mandatoryMissing)
- {
- return new PublishResult(PublishResultType.FailedPublishMandatoryCultureMissing, evtMsgs, content);
- }
-
- if (culturesPublishing.Count == 0 && culturesUnpublishing?.Count > 0)
- {
- return new PublishResult(PublishResultType.SuccessUnpublishCulture, evtMsgs, content);
- }
- }
-
- // ensure that the document has published values
- // either because it is 'publishing' or because it already has a published version
- if (content.PublishedState != PublishedState.Publishing && content.PublishedVersionId == 0)
- {
- _logger.LogInformation(
- "Document {ContentName} (id={ContentId}) cannot be published: {Reason}",
- content.Name,
- content.Id,
- "document does not have published values");
- return new PublishResult(PublishResultType.FailedPublishNothingToPublish, evtMsgs, content);
- }
-
- ContentScheduleCollection contentSchedule = _documentRepository.GetContentSchedule(content.Id);
-
- // loop over each culture publishing - or InvariantCulture for invariant
- foreach (var culture in culturesPublishing ?? new[] { Constants.System.InvariantCulture })
- {
- // ensure that the document status is correct
- // note: culture will be string.Empty for invariant
- switch (content.GetStatus(contentSchedule, culture))
- {
- case ContentStatus.Expired:
- if (!variesByCulture)
- {
- _logger.LogInformation(
- "Document {ContentName} (id={ContentId}) cannot be published: {Reason}", content.Name, content.Id, "document has expired");
- }
- else
- {
- _logger.LogInformation(
- "Document {ContentName} (id={ContentId}) culture {Culture} cannot be published: {Reason}", content.Name, content.Id, culture, "document culture has expired");
- }
-
- return new PublishResult(
- !variesByCulture
- ? PublishResultType.FailedPublishHasExpired : PublishResultType.FailedPublishCultureHasExpired,
- evtMsgs,
- content);
-
- case ContentStatus.AwaitingRelease:
- if (!variesByCulture)
- {
- _logger.LogInformation(
- "Document {ContentName} (id={ContentId}) cannot be published: {Reason}",
- content.Name,
- content.Id,
- "document is awaiting release");
- }
- else
- {
- _logger.LogInformation(
- "Document {ContentName} (id={ContentId}) culture {Culture} cannot be published: {Reason}",
- content.Name,
- content.Id,
- culture,
- "document has culture awaiting release");
- }
-
- return new PublishResult(
- !variesByCulture
- ? PublishResultType.FailedPublishAwaitingRelease
- : PublishResultType.FailedPublishCultureAwaitingRelease,
- evtMsgs,
- content);
-
- case ContentStatus.Trashed:
- _logger.LogInformation(
- "Document {ContentName} (id={ContentId}) cannot be published: {Reason}",
- content.Name,
- content.Id,
- "document is trashed");
- return new PublishResult(PublishResultType.FailedPublishIsTrashed, evtMsgs, content);
- }
- }
-
- if (checkPath)
- {
- // check if the content can be path-published
- // root content can be published
- // else check ancestors - we know we are not trashed
- var pathIsOk = content.ParentId == Constants.System.Root || IsPathPublished(GetParent(content));
- if (!pathIsOk)
- {
- _logger.LogInformation(
- "Document {ContentName} (id={ContentId}) cannot be published: {Reason}",
- content.Name,
- content.Id,
- "parent is not published");
- return new PublishResult(PublishResultType.FailedPublishPathNotPublished, evtMsgs, content);
- }
- }
-
- // If we are both publishing and unpublishing cultures, then return a mixed status
- if (variesByCulture && culturesPublishing?.Count > 0 && culturesUnpublishing?.Count > 0)
- {
- return new PublishResult(PublishResultType.SuccessMixedCulture, evtMsgs, content);
- }
-
- return new PublishResult(evtMsgs, content);
- }
-
- ///
- /// Publishes a document
- ///
- ///
- ///
- ///
- ///
- ///
- ///
- /// It is assumed that all publishing checks have passed before calling this method like
- ///
- ///
- private PublishResult StrategyPublish(
- IContent content,
- IReadOnlyCollection? culturesPublishing,
- IReadOnlyCollection? culturesUnpublishing,
- EventMessages evtMsgs)
- {
- // change state to publishing
- content.PublishedState = PublishedState.Publishing;
-
- // if this is a variant then we need to log which cultures have been published/unpublished and return an appropriate result
- if (content.ContentType.VariesByCulture())
- {
- if (content.Published && culturesUnpublishing?.Count == 0 && culturesPublishing?.Count == 0)
- {
- return new PublishResult(PublishResultType.FailedPublishNothingToPublish, evtMsgs, content);
- }
-
- if (culturesUnpublishing?.Count > 0)
- {
- _logger.LogInformation(
- "Document {ContentName} (id={ContentId}) cultures: {Cultures} have been unpublished.",
- content.Name,
- content.Id,
- string.Join(",", culturesUnpublishing));
- }
-
- if (culturesPublishing?.Count > 0)
- {
- _logger.LogInformation(
- "Document {ContentName} (id={ContentId}) cultures: {Cultures} have been published.",
- content.Name,
- content.Id,
- string.Join(",", culturesPublishing));
- }
-
- if (culturesUnpublishing?.Count > 0 && culturesPublishing?.Count > 0)
- {
- return new PublishResult(PublishResultType.SuccessMixedCulture, evtMsgs, content);
- }
-
- if (culturesUnpublishing?.Count > 0 && culturesPublishing?.Count == 0)
- {
- return new PublishResult(PublishResultType.SuccessUnpublishCulture, evtMsgs, content);
- }
-
- return new PublishResult(PublishResultType.SuccessPublishCulture, evtMsgs, content);
- }
-
- _logger.LogInformation("Document {ContentName} (id={ContentId}) has been published.", content.Name, content.Id);
- return new PublishResult(evtMsgs, content);
- }
-
- ///
- /// Ensures that a document can be unpublished
- ///
- ///
- ///
- ///
- ///
- ///
- private PublishResult StrategyCanUnpublish(
- ICoreScope scope,
- IContent content,
- EventMessages evtMsgs,
- IDictionary? notificationState)
- {
- // raise Unpublishing notification
- ContentUnpublishingNotification notification = new ContentUnpublishingNotification(content, evtMsgs).WithState(notificationState);
- var notificationResult = scope.Notifications.PublishCancelable(notification);
-
- if (notificationResult)
- {
- _logger.LogInformation(
- "Document {ContentName} (id={ContentId}) cannot be unpublished: unpublishing was cancelled.", content.Name, content.Id);
- return new PublishResult(PublishResultType.FailedUnpublishCancelledByEvent, evtMsgs, content);
- }
-
- return new PublishResult(PublishResultType.SuccessUnpublish, evtMsgs, content);
- }
-
- ///
- /// Unpublishes a document
- ///
- ///
- ///
- ///
- ///
- /// It is assumed that all unpublishing checks have passed before calling this method like
- ///
- ///
- private PublishResult StrategyUnpublish(IContent content, EventMessages evtMsgs)
- {
- var attempt = new PublishResult(PublishResultType.SuccessUnpublish, evtMsgs, content);
-
- // TODO: What is this check?? we just created this attempt and of course it is Success?!
- if (attempt.Success == false)
- {
- return attempt;
- }
-
- // if the document has any release dates set to before now,
- // they should be removed so they don't interrupt an unpublish
- // otherwise it would remain released == published
- ContentScheduleCollection contentSchedule = _documentRepository.GetContentSchedule(content.Id);
- IReadOnlyList pastReleases =
- contentSchedule.GetPending(ContentScheduleAction.Expire, DateTime.UtcNow);
- foreach (ContentSchedule p in pastReleases)
- {
- contentSchedule.Remove(p);
- }
-
- if (pastReleases.Count > 0)
- {
- _logger.LogInformation(
- "Document {ContentName} (id={ContentId}) had its release date removed, because it was unpublished.", content.Name, content.Id);
- }
-
- _documentRepository.PersistContentSchedule(content, contentSchedule);
-
- // change state to unpublishing
- content.PublishedState = PublishedState.Unpublishing;
-
- _logger.LogInformation("Document {ContentName} (id={ContentId}) has been unpublished.", content.Name, content.Id);
- return attempt;
- }
-
#endregion
#region Content Types