diff --git a/src/OrchardCore.Modules/OrchardCore.AdminDashboard/Controllers/DashboardController.cs b/src/OrchardCore.Modules/OrchardCore.AdminDashboard/Controllers/DashboardController.cs index 3abf427168d..6fc1b3d8ee1 100644 --- a/src/OrchardCore.Modules/OrchardCore.AdminDashboard/Controllers/DashboardController.cs +++ b/src/OrchardCore.Modules/OrchardCore.AdminDashboard/Controllers/DashboardController.cs @@ -150,7 +150,7 @@ public async Task Update([FromForm] DashboardPartViewModel[] part return Unauthorized(); } - var contentItemIds = parts.Select(i => i.ContentItemId).ToList(); + var contentItemIds = parts.Select(i => i.ContentItemId).ToArray(); // Load the latest version first if any. var latestItems = await _contentManager.GetAsync(contentItemIds, VersionOptions.Latest); diff --git a/src/OrchardCore.Modules/OrchardCore.ContentFields/Controllers/ContentPickerAdminController.cs b/src/OrchardCore.Modules/OrchardCore.ContentFields/Controllers/ContentPickerAdminController.cs index e46628f75fd..9f4fb5fe130 100644 --- a/src/OrchardCore.Modules/OrchardCore.ContentFields/Controllers/ContentPickerAdminController.cs +++ b/src/OrchardCore.Modules/OrchardCore.ContentFields/Controllers/ContentPickerAdminController.cs @@ -91,6 +91,7 @@ public async Task SearchContentItems(string part, string field, s .GetAsync(results.Select(r => r.ContentItemId)); var selectedItems = new List(); + var user = _httpContextAccessor.HttpContext?.User; foreach (var contentItem in contentItems) { selectedItems.Add(new VueMultiselectItemViewModel() @@ -98,8 +99,7 @@ public async Task SearchContentItems(string part, string field, s Id = contentItem.ContentItemId, DisplayText = contentItem.ToString(), HasPublished = contentItem.IsPublished(), - IsViewable = await _authorizationService.AuthorizeAsync(_httpContextAccessor.HttpContext?.User, - CommonPermissions.EditContent, contentItem) + IsViewable = await _authorizationService.AuthorizeAsync(user, CommonPermissions.EditContent, contentItem) }); } diff --git a/src/OrchardCore.Modules/OrchardCore.ContentFields/Views/ContentPickerField.cshtml b/src/OrchardCore.Modules/OrchardCore.ContentFields/Views/ContentPickerField.cshtml index 4f51d37248c..5ccc59323e6 100644 --- a/src/OrchardCore.Modules/OrchardCore.ContentFields/Views/ContentPickerField.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.ContentFields/Views/ContentPickerField.cshtml @@ -1,4 +1,5 @@ @model OrchardCore.ContentFields.ViewModels.DisplayContentPickerFieldViewModel +@using OrchardCore.ContentManagement @using OrchardCore.Mvc.Utilities @using OrchardCore.ContentManagement.Metadata.Models diff --git a/src/OrchardCore.Modules/OrchardCore.Contents/Liquid/ContentItemFilter.cs b/src/OrchardCore.Modules/OrchardCore.Contents/Liquid/ContentItemFilter.cs index a999d8ebb3a..b9deac61e8e 100644 --- a/src/OrchardCore.Modules/OrchardCore.Contents/Liquid/ContentItemFilter.cs +++ b/src/OrchardCore.Modules/OrchardCore.Contents/Liquid/ContentItemFilter.cs @@ -20,17 +20,15 @@ public async ValueTask ProcessAsync(FluidValue input, FilterArgument { if (input.Type == FluidValues.Array) { - // List of content item ids - var contentItemIds = input.Enumerate(ctx).Select(x => x.ToStringValue()).ToArray(); + // List of content item ids to return. + var contentItemIds = input.Enumerate(ctx).Select(x => x.ToStringValue()); return FluidValue.Create(await _contentManager.GetAsync(contentItemIds), ctx.Options); } - else - { - var contentItemId = input.ToStringValue(); - return FluidValue.Create(await _contentManager.GetAsync(contentItemId), ctx.Options); - } + var contentItemId = input.ToStringValue(); + + return FluidValue.Create(await _contentManager.GetAsync(contentItemId), ctx.Options); } } } diff --git a/src/OrchardCore.Modules/OrchardCore.Contents/Razor/ContentRazorHelperExtensions.cs b/src/OrchardCore.Modules/OrchardCore.Contents/Razor/ContentRazorHelperExtensions.cs index c5e3925e28d..3a44e1d8398 100644 --- a/src/OrchardCore.Modules/OrchardCore.Contents/Razor/ContentRazorHelperExtensions.cs +++ b/src/OrchardCore.Modules/OrchardCore.Contents/Razor/ContentRazorHelperExtensions.cs @@ -2,14 +2,13 @@ using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; -using OrchardCore; using OrchardCore.ContentManagement; using OrchardCore.ContentManagement.Records; using YesSql; -#pragma warning disable CA1050 // Declare types in namespaces +namespace OrchardCore; + public static class ContentRazorHelperExtensions -#pragma warning restore CA1050 // Declare types in namespaces { /// /// Returns a content item id by its handle. @@ -30,15 +29,13 @@ public static Task GetContentItemIdByHandleAsync(this IOrchardHelper orc /// /// The . /// The handle to load. - /// Whether a draft should be loaded if available. false by default. - /// GetContentItemByHandleAsync("alias:carousel"). - /// GetContentItemByHandleAsync("slug:myblog/my-blog-post", true). + /// A specific version to load or the default version. /// A content item with the specific name, or null if it doesn't exist. - public static async Task GetContentItemByHandleAsync(this IOrchardHelper orchardHelper, string handle, bool latest = false) + public static async Task GetContentItemByHandleAsync(this IOrchardHelper orchardHelper, string handle, VersionOptions option = null) { var contentItemId = await GetContentItemIdByHandleAsync(orchardHelper, handle); var contentManager = orchardHelper.HttpContext.RequestServices.GetService(); - return await contentManager.GetAsync(contentItemId, latest ? VersionOptions.Latest : VersionOptions.Published); + return await contentManager.GetAsync(contentItemId, option); } /// @@ -46,13 +43,13 @@ public static async Task GetContentItemByHandleAsync(this IOrchardH /// /// The . /// The content item id to load. - /// Whether a draft should be loaded if available. false by default. - /// GetContentItemByIdAsync("4xxxxxxxxxxxxxxxx"). + /// A specific version to load or the default version. /// A content item with the specific id, or null if it doesn't exist. - public static Task GetContentItemByIdAsync(this IOrchardHelper orchardHelper, string contentItemId, bool latest = false) + public static Task GetContentItemByIdAsync(this IOrchardHelper orchardHelper, string contentItemId, VersionOptions option = null) { var contentManager = orchardHelper.HttpContext.RequestServices.GetService(); - return contentManager.GetAsync(contentItemId, latest ? VersionOptions.Latest : VersionOptions.Published); + + return contentManager.GetAsync(contentItemId, option); } /// @@ -60,12 +57,13 @@ public static Task GetContentItemByIdAsync(this IOrchardHelper orch /// /// The . /// The content item ids to load. - /// Whether a draft should be loaded if available. false by default. + /// A specific version to load or the default version. /// A list of content items with the specific ids. - public static Task> GetContentItemsByIdAsync(this IOrchardHelper orchardHelper, IEnumerable contentItemIds, bool latest = false) + public static Task> GetContentItemsByIdAsync(this IOrchardHelper orchardHelper, IEnumerable contentItemIds, VersionOptions option = null) { var contentManager = orchardHelper.HttpContext.RequestServices.GetService(); - return contentManager.GetAsync(contentItemIds, latest); + + return contentManager.GetAsync(contentItemIds, option); } /// @@ -73,11 +71,11 @@ public static Task> GetContentItemsByIdAsync(this IOrch /// /// The . /// The content item version id to load. - /// GetContentItemByVersionIdAsync("4xxxxxxxxxxxxxxxx"). /// A content item with the specific version id, or null if it doesn't exist. public static Task GetContentItemByVersionIdAsync(this IOrchardHelper orchardHelper, string contentItemVersionId) { var contentManager = orchardHelper.HttpContext.RequestServices.GetService(); + return contentManager.GetVersionAsync(contentItemVersionId); } @@ -102,6 +100,8 @@ public static async Task> QueryContentItemsAsync(this I /// The maximum content items to return. public static Task> GetRecentContentItemsByContentTypeAsync(this IOrchardHelper orchardHelper, string contentType, int maxContentItems = 10) { - return orchardHelper.QueryContentItemsAsync(query => query.Where(x => x.ContentType == contentType && x.Published == true).OrderByDescending(x => x.CreatedUtc).Take(maxContentItems)); + return orchardHelper.QueryContentItemsAsync(query => query.Where(x => x.ContentType == contentType && x.Published == true) + .OrderByDescending(x => x.CreatedUtc) + .Take(maxContentItems)); } } diff --git a/src/OrchardCore.Modules/OrchardCore.Markdown/Razor/ContentRazorHelperExtensions.cs b/src/OrchardCore.Modules/OrchardCore.Markdown/Razor/ContentRazorHelperExtensions.cs index b685aa89932..d5f33cd2f90 100644 --- a/src/OrchardCore.Modules/OrchardCore.Markdown/Razor/ContentRazorHelperExtensions.cs +++ b/src/OrchardCore.Modules/OrchardCore.Markdown/Razor/ContentRazorHelperExtensions.cs @@ -2,22 +2,21 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Html; using Microsoft.Extensions.DependencyInjection; -using OrchardCore; using OrchardCore.Infrastructure.Html; using OrchardCore.Liquid; using OrchardCore.Markdown.Services; using OrchardCore.Shortcodes.Services; -#pragma warning disable CA1050 // Declare types in namespaces +namespace OrchardCore; + public static class ContentRazorHelperExtensions -#pragma warning restore CA1050 // Declare types in namespaces { /// /// Converts Markdown string to HTML. /// /// The . /// The markdown to convert. - /// Whether to sanitze the markdown. Defaults to . + /// Whether to sanitize the markdown. Defaults to . public static async Task MarkdownToHtmlAsync(this IOrchardHelper orchardHelper, string markdown, bool sanitize = true) { var shortcodeService = orchardHelper.HttpContext.RequestServices.GetRequiredService(); @@ -25,9 +24,9 @@ public static async Task MarkdownToHtmlAsync(this IOrchardHelper o // The default Markdown option is to entity escape html // so filters must be run after the markdown has been processed. - markdown = markdownService.ToHtml(markdown ?? ""); + markdown = markdownService.ToHtml(markdown ?? string.Empty); - // The liquid rendering is for backwards compatability and can be removed in a future version. + // The liquid rendering is for backwards compatibility and can be removed in a future version. if (!sanitize) { var liquidTemplateManager = orchardHelper.HttpContext.RequestServices.GetRequiredService(); diff --git a/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Services/LuceneIndexingService.cs b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Services/LuceneIndexingService.cs index 75bfd7e39aa..415452028fe 100644 --- a/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Services/LuceneIndexingService.cs +++ b/src/OrchardCore.Modules/OrchardCore.Search.Lucene/Services/LuceneIndexingService.cs @@ -128,7 +128,7 @@ await shellScope.UsingAsync(async scope => var allPublishedContentItems = await contentManager.GetAsync(updatedContentItemIds); allPublished = allPublishedContentItems.DistinctBy(x => x.ContentItemId).ToDictionary(k => k.ContentItemId, v => v); - var allLatestContentItems = await contentManager.GetAsync(updatedContentItemIds, latest: true); + var allLatestContentItems = await contentManager.GetAsync(updatedContentItemIds, VersionOptions.Latest); allLatest = allLatestContentItems.DistinctBy(x => x.ContentItemId).ToDictionary(k => k.ContentItemId, v => v); // Group all DocumentIndex by index to batch update them. diff --git a/src/OrchardCore/OrchardCore.ContentManagement.Abstractions/ContentManagerExtensions.cs b/src/OrchardCore/OrchardCore.ContentManagement.Abstractions/ContentManagerExtensions.cs new file mode 100644 index 00000000000..b24d4d31403 --- /dev/null +++ b/src/OrchardCore/OrchardCore.ContentManagement.Abstractions/ContentManagerExtensions.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json.Nodes; +using System.Threading.Tasks; +using OrchardCore.ContentManagement.Handlers; + +namespace OrchardCore.ContentManagement; + +public static class ContentManagerExtensions +{ + public static Task PopulateAspectAsync(this IContentManager contentManager, IContent content) where TAspect : new() + { + return contentManager.PopulateAspectAsync(content, new TAspect()); + } + + public static async Task HasPublishedVersionAsync(this IContentManager contentManager, IContent content) + { + if (content?.ContentItem == null) + { + return false; + } + + return content.ContentItem.IsPublished() || + (await contentManager.GetAsync(content.ContentItem.ContentItemId, VersionOptions.Published) != null); + } + + public static Task GetContentItemMetadataAsync(this IContentManager contentManager, IContent content) + { + return contentManager.PopulateAspectAsync(content); + } + + public static async Task> LoadAsync(this IContentManager contentManager, IEnumerable contentItems) + { + ArgumentNullException.ThrowIfNull(contentItems); + + var results = new List(contentItems.Count()); + + foreach (var contentItem in contentItems) + { + results.Add(await contentManager.LoadAsync(contentItem)); + } + + return results; + } + + public static async IAsyncEnumerable LoadAsync(this IContentManager contentManager, IAsyncEnumerable contentItems) + { + ArgumentNullException.ThrowIfNull(contentItems); + + await foreach (var contentItem in contentItems) + { + yield return await contentManager.LoadAsync(contentItem); + } + } + + public static async Task UpdateValidateAndCreateAsync(this IContentManager contentManager, ContentItem contentItem, VersionOptions options) + { + await contentManager.UpdateAsync(contentItem); + var result = await contentManager.ValidateAsync(contentItem); + + if (result.Succeeded) + { + await contentManager.CreateAsync(contentItem, options); + } + + return result; + } + + /// + /// Gets either the container content item with the specified id and version, or if the json path supplied gets the contained content item. + /// + /// The instance. + /// The id content item id to load. + /// The version option. + /// The json path of the contained content item. + public static async Task GetAsync(this IContentManager contentManager, string contentItemId, string jsonPath, VersionOptions options = null) + { + var contentItem = await contentManager.GetAsync(contentItemId, options); + + // It represents a contained content item. + if (!string.IsNullOrEmpty(jsonPath)) + { + var root = (JsonObject)contentItem.Content; + contentItem = root.SelectNode(jsonPath)?.ToObject(); + + return contentItem; + } + + return contentItem; + } +} diff --git a/src/OrchardCore/OrchardCore.ContentManagement.Abstractions/IContentManager.cs b/src/OrchardCore/OrchardCore.ContentManagement.Abstractions/IContentManager.cs index 395f2d9afec..a91a2fb50be 100644 --- a/src/OrchardCore/OrchardCore.ContentManagement.Abstractions/IContentManager.cs +++ b/src/OrchardCore/OrchardCore.ContentManagement.Abstractions/IContentManager.cs @@ -1,7 +1,4 @@ -using System; using System.Collections.Generic; -using System.Linq; -using System.Text.Json.Nodes; using System.Threading.Tasks; using OrchardCore.ContentManagement.Handlers; @@ -32,8 +29,7 @@ public interface IContentManager /// /// The content instance filled with all necessary data. /// The version to create the item with. - Task CreateAsync(ContentItem contentItem, VersionOptions options); - + Task CreateAsync(ContentItem contentItem, VersionOptions options = null); /// /// Creates (puts) a new content item and manages removing and updating existing published or draft items. @@ -70,29 +66,12 @@ public interface IContentManager /// The validation result. Task RestoreAsync(ContentItem contentItem); - /// - /// Gets the published content item with the specified id. - /// - /// The content item id to load. - Task GetAsync(string id); - /// /// Gets the content item with the specified id and version. /// - /// The id content item id to load. + /// The id of the content item to load. /// The version option. - Task GetAsync(string id, VersionOptions options); - - /// - /// Gets the published content items with the specified ids. - /// - /// The content item ids to load. - /// Whether a draft should be loaded if available. false by default. - /// - /// This method will always issue a database query. - /// This means that it should be used only to get a list of content items that have not been loaded. - /// - Task> GetAsync(IEnumerable contentItemIds, bool latest = false); + Task GetAsync(string contentItemId, VersionOptions options = null); /// /// Gets the published content items with the specified ids. @@ -103,7 +82,7 @@ public interface IContentManager /// This method will always issue a database query. /// This means that it should be used only to get a list of content items that have not been loaded. /// - Task> GetAsync(IEnumerable contentItemIds, VersionOptions options); + Task> GetAsync(IEnumerable contentItemIds, VersionOptions options = null); /// /// Gets the content item with the specified version id. @@ -111,6 +90,12 @@ public interface IContentManager /// The content item version id. Task GetVersionAsync(string contentItemVersionId); + /// + /// Gets all versions of the given content item id. + /// + /// The content item id. + Task> GetAllVersionsAsync(string contentItemId); + /// /// Triggers the Load events for a content item that was queried directly from the database. /// @@ -138,7 +123,9 @@ public interface IContentManager Task SaveDraftAsync(ContentItem contentItem); Task PublishAsync(ContentItem contentItem); + Task UnpublishAsync(ContentItem contentItem); + Task PopulateAspectAsync(IContent content, TAspect aspect); /// @@ -148,138 +135,4 @@ public interface IContentManager /// Clone of the item. Task CloneAsync(ContentItem contentItem); } - - public static class ContentManagerExtensions - { - /// - /// Creates (persists) a new Published content item. - /// - /// The instance. - /// The content instance filled with all necessary data. - public static Task CreateAsync(this IContentManager contentManager, ContentItem contentItem) - { - return contentManager.CreateAsync(contentItem, VersionOptions.Published); - } - - public static Task PopulateAspectAsync(this IContentManager contentManager, IContent content) where TAspect : new() - { - return contentManager.PopulateAspectAsync(content, new TAspect()); - } - - public static async Task HasPublishedVersionAsync(this IContentManager contentManager, IContent content) - { - if (content.ContentItem == null) - { - return false; - } - - return content.ContentItem.IsPublished() || (await contentManager.GetAsync(content.ContentItem.ContentItemId, VersionOptions.Published) != null); - } - - public static Task GetContentItemMetadataAsync(this IContentManager contentManager, IContent content) - { - return contentManager.PopulateAspectAsync(content); - } - - public static async Task> LoadAsync(this IContentManager contentManager, IEnumerable contentItems) - { - var results = new List(contentItems.Count()); - - foreach (var contentItem in contentItems) - { - results.Add(await contentManager.LoadAsync(contentItem)); - } - - return results; - } - - public static async IAsyncEnumerable LoadAsync(this IContentManager contentManager, IAsyncEnumerable contentItems) - { - await foreach (var contentItem in contentItems) - { - yield return await contentManager.LoadAsync(contentItem); - } - } - - public static async Task UpdateValidateAndCreateAsync(this IContentManager contentManager, ContentItem contentItem, VersionOptions options) - { - await contentManager.UpdateAsync(contentItem); - var result = await contentManager.ValidateAsync(contentItem); - - if (result.Succeeded) - { - await contentManager.CreateAsync(contentItem, options); - } - - return result; - } - - /// - /// Gets either the published container content item with the specified id, or if the json path supplied gets the contained content item. - /// - /// The instance. - /// The content item id to load. - /// The json path of the contained content item. - public static Task GetAsync(this IContentManager contentManager, string id, string jsonPath) - { - return contentManager.GetAsync(id, jsonPath, VersionOptions.Published); - } - - /// - /// Gets either the container content item with the specified id and version, or if the json path supplied gets the contained content item. - /// - /// The instance. - /// The id content item id to load. - /// The version option. - /// The json path of the contained content item. - public static async Task GetAsync(this IContentManager contentManager, string id, string jsonPath, VersionOptions options) - { - var contentItem = await contentManager.GetAsync(id, options); - - // It represents a contained content item - if (!string.IsNullOrEmpty(jsonPath)) - { - var root = (JsonObject)contentItem.Content; - contentItem = root.SelectNode(jsonPath)?.ToObject(); - - return contentItem; - } - - return contentItem; - } - } - - public class VersionOptions - { - /// - /// Gets the latest version. - /// - public static VersionOptions Latest { get { return new VersionOptions { IsLatest = true }; } } - - /// - /// Gets the latest published version. - /// - public static VersionOptions Published { get { return new VersionOptions { IsPublished = true }; } } - - /// - /// Gets the latest draft version. - /// - public static VersionOptions Draft { get { return new VersionOptions { IsDraft = true }; } } - - /// - /// Gets the latest version and creates a new version draft based on it. - /// - public static VersionOptions DraftRequired { get { return new VersionOptions { IsDraft = true, IsDraftRequired = true }; } } - - /// - /// Gets all versions. - /// - public static VersionOptions AllVersions { get { return new VersionOptions { IsAllVersions = true }; } } - - public bool IsLatest { get; private set; } - public bool IsPublished { get; private set; } - public bool IsDraft { get; private set; } - public bool IsDraftRequired { get; private set; } - public bool IsAllVersions { get; private set; } - } } diff --git a/src/OrchardCore/OrchardCore.ContentManagement.Abstractions/VersionOptions.cs b/src/OrchardCore/OrchardCore.ContentManagement.Abstractions/VersionOptions.cs new file mode 100644 index 00000000000..6de61f90b2f --- /dev/null +++ b/src/OrchardCore/OrchardCore.ContentManagement.Abstractions/VersionOptions.cs @@ -0,0 +1,45 @@ +namespace OrchardCore.ContentManagement; + +public record VersionOptions +{ + /// + /// Gets the latest version. + /// + public static readonly VersionOptions Latest = new() + { + IsLatest = true, + }; + + /// + /// Gets the latest published version. + /// + public static readonly VersionOptions Published = new() + { + IsPublished = true, + }; + + /// + /// Gets the latest draft version. + /// + public static readonly VersionOptions Draft = new() + { + IsDraft = true, + }; + + /// + /// Gets the latest version and creates a new version draft based on it. + /// + public static readonly VersionOptions DraftRequired = new() + { + IsDraft = true, + IsDraftRequired = true, + }; + + public bool IsLatest { get; private set; } + + public bool IsPublished { get; private set; } + + public bool IsDraft { get; private set; } + + public bool IsDraftRequired { get; private set; } +} diff --git a/src/OrchardCore/OrchardCore.ContentManagement.GraphQL/Queries/ContentItemsFieldType.cs b/src/OrchardCore/OrchardCore.ContentManagement.GraphQL/Queries/ContentItemsFieldType.cs index 484494cf969..363bc219b48 100644 --- a/src/OrchardCore/OrchardCore.ContentManagement.GraphQL/Queries/ContentItemsFieldType.cs +++ b/src/OrchardCore/OrchardCore.ContentManagement.GraphQL/Queries/ContentItemsFieldType.cs @@ -68,13 +68,6 @@ public ContentItemsFieldType(string contentItemName, ISchema schema, IOptions> ResolveAsync(IResolveFieldContext context) { - var versionOption = VersionOptions.Published; - - if (context.HasPopulatedArgument("status")) - { - versionOption = GetVersionOption(context.GetArgument("status")); - } - JsonObject where = null; if (context.HasArgument("where")) { @@ -95,7 +88,7 @@ private async ValueTask> ResolveAsync(IResolveFieldCont var query = preQuery.With(); - query = FilterVersion(query, versionOption); + query = FilterVersion(query, GetVersionOptions(context)); query = FilterContentType(query, context); query = OrderBy(query, context); @@ -130,7 +123,7 @@ private IQuery FilterWhereArguments( propertyProviders: fieldContext.RequestServices.GetServices()); // Create the default table alias. - predicateQuery.CreateAlias("", nameof(ContentItemIndex)); + predicateQuery.CreateAlias(string.Empty, nameof(ContentItemIndex)); predicateQuery.CreateTableAlias(nameof(ContentItemIndex), defaultTableAlias); // Add all provided table alias to the current predicate query. @@ -206,7 +199,6 @@ private static VersionOptions GetVersionOption(PublicationStatusEnum status) PublicationStatusEnum.Published => VersionOptions.Published, PublicationStatusEnum.Draft => VersionOptions.Draft, PublicationStatusEnum.Latest => VersionOptions.Latest, - PublicationStatusEnum.All => VersionOptions.AllVersions, _ => VersionOptions.Published, }; } @@ -219,6 +211,16 @@ private static IQuery FilterContentType(IQuery q.ContentType == contentType); } + private static VersionOptions GetVersionOptions(IResolveFieldContext context) + { + if (context.HasPopulatedArgument("status")) + { + return GetVersionOption(context.GetArgument("status")); + } + + return VersionOptions.Published; + } + private static IQuery FilterVersion(IQuery query, VersionOptions versionOption) { if (versionOption.IsPublished) diff --git a/src/OrchardCore/OrchardCore.ContentManagement.GraphQL/Queries/PublicationStatusEnum.cs b/src/OrchardCore/OrchardCore.ContentManagement.GraphQL/Queries/PublicationStatusEnum.cs index 10615e20c7e..05b4aa6c5c1 100644 --- a/src/OrchardCore/OrchardCore.ContentManagement.GraphQL/Queries/PublicationStatusEnum.cs +++ b/src/OrchardCore/OrchardCore.ContentManagement.GraphQL/Queries/PublicationStatusEnum.cs @@ -5,6 +5,6 @@ public enum PublicationStatusEnum Published, Draft, Latest, - All + All, } } diff --git a/src/OrchardCore/OrchardCore.ContentManagement/DefaultContentManager.cs b/src/OrchardCore/OrchardCore.ContentManagement/DefaultContentManager.cs index cb1bd57ac49..4cefbae3906 100644 --- a/src/OrchardCore/OrchardCore.ContentManagement/DefaultContentManager.cs +++ b/src/OrchardCore/OrchardCore.ContentManagement/DefaultContentManager.cs @@ -20,9 +20,12 @@ namespace OrchardCore.ContentManagement { public class DefaultContentManager : IContentManager { - private const int ImportBatchSize = 500; + private const int _importBatchSize = 500; - private static readonly JsonMergeSettings _updateJsonMergeSettings = new() { MergeArrayHandling = MergeArrayHandling.Replace }; + private static readonly JsonMergeSettings _updateJsonMergeSettings = new() + { + MergeArrayHandling = MergeArrayHandling.Replace, + }; private readonly IContentDefinitionManager _contentDefinitionManager; private readonly ISession _session; @@ -42,7 +45,6 @@ public DefaultContentManager( { _contentDefinitionManager = contentDefinitionManager; Handlers = handlers; - ReversedHandlers = handlers.Reverse().ToArray(); _session = session; _idGenerator = idGenerator; _contentManagerSession = contentManagerSession; @@ -51,10 +53,16 @@ public DefaultContentManager( } public IEnumerable Handlers { get; private set; } - public IEnumerable ReversedHandlers { get; private set; } + + public IEnumerable _reversedHandlers; + + public IEnumerable ReversedHandlers + => _reversedHandlers ??= Handlers.Reverse().ToArray(); public async Task NewAsync(string contentType) { + ArgumentException.ThrowIfNullOrEmpty(contentType); + var contentTypeDefinition = await _contentDefinitionManager.GetTypeDefinitionAsync(contentType); contentTypeDefinition ??= new ContentTypeDefinitionBuilder().Named(contentType).Build(); @@ -83,93 +91,15 @@ public async Task NewAsync(string contentType) return context3.ContentItem; } - public Task GetAsync(string contentItemId) - { - return GetAsync(contentItemId, VersionOptions.Published); - } - - public async Task> GetAsync(IEnumerable contentItemIds, bool latest = false) - { - ArgumentNullException.ThrowIfNull(contentItemIds); - - var itemIds = contentItemIds - .Where(id => id is not null) - .Distinct() - .ToArray(); - - if (itemIds.Length == 0) - { - return []; - } - - List contentItems = null; - List storedItems = null; - if (latest) - { - contentItems = (await _session - .Query() - .Where(i => i.ContentItemId.IsIn(itemIds) && i.Latest == true) - .ListAsync() - ).ToList(); - } - else - { - foreach (var itemId in itemIds) - { - // If the published version is already stored, we can return it. - if (_contentManagerSession.RecallPublishedItemId(itemId, out var contentItem)) - { - storedItems ??= []; - storedItems.Add(contentItem); - } - } - - // Only query the ids not already stored. - var itemIdsToQuery = storedItems is not null - ? itemIds.Except(storedItems.Select(c => c.ContentItemId)).ToArray() - : itemIds; - - if (itemIdsToQuery.Length > 0) - { - contentItems = (await _session - .Query() - .Where(i => i.ContentItemId.IsIn(itemIdsToQuery) && i.Published == true) - .ListAsync() - ).ToList(); - } - } - - if (contentItems is not null) - { - for (var i = 0; i < contentItems.Count; i++) - { - contentItems[i] = await LoadAsync(contentItems[i]); - } - - if (storedItems is not null) - { - contentItems.AddRange(storedItems); - } - } - else if (storedItems is not null) - { - contentItems = storedItems; - } - else - { - return []; - } - - return contentItems.OrderBy(c => Array.IndexOf(itemIds, c.ContentItemId)); - } - - public async Task GetAsync(string contentItemId, VersionOptions options) + public async Task GetAsync(string contentItemId, VersionOptions options = null) { if (string.IsNullOrEmpty(contentItemId)) { return null; } + options ??= VersionOptions.Published; + ContentItem contentItem = null; if (options.IsLatest) @@ -202,7 +132,7 @@ public async Task GetAsync(string contentItemId, VersionOptions opt else if (options.IsPublished) { // If the published version is requested and is already loaded, we can - // return it right away + // return it right away. if (_contentManagerSession.RecallPublishedItemId(contentItemId, out contentItem)) { return contentItem; @@ -220,20 +150,20 @@ public async Task GetAsync(string contentItemId, VersionOptions opt if (options.IsDraftRequired) { - // When draft is required and latest is published a new version is added + // When draft is required and latest is published a new version is added. if (contentItem.Published) { // We save the previous version further because this call might do a session query. var contentTypeDefinition = await _contentDefinitionManager.GetTypeDefinitionAsync(contentItem.ContentType); - // Check if not versionable, meaning we use only one version + // Check if not versionable, meaning we use only one version. if (contentTypeDefinition != null && !contentTypeDefinition.IsVersionable()) { contentItem.Published = false; } else { - // Save the previous version + // Save the previous version. await _session.SaveAsync(contentItem, checkConcurrency: true); contentItem = await BuildNewVersionAsync(contentItem); @@ -247,14 +177,36 @@ public async Task GetAsync(string contentItemId, VersionOptions opt return contentItem; } - public async Task> GetAsync(IEnumerable contentItemIds, VersionOptions options) + public async Task> GetAllVersionsAsync(string contentItemId) + { + ArgumentException.ThrowIfNullOrEmpty(contentItemId); + + var contentItems = await _session + .Query() + .Where(x => x.ContentItemId == contentItemId) + .ListAsync(); + + foreach (var contentItem in contentItems) + { + await LoadAsync(contentItem); + } + + return contentItems; + } + + public async Task> GetAsync(IEnumerable contentItemIds, VersionOptions options = null) { - if (contentItemIds == null || !contentItemIds.Any()) + var ids = contentItemIds? + .Where(id => id is not null) + .Distinct() + .ToArray(); + + if (ids?.Length == 0) { return []; } - var ids = new List(contentItemIds); + options ??= VersionOptions.Published; var contentItems = new List(); @@ -275,7 +227,7 @@ public async Task> GetAsync(IEnumerable content } else if (options.IsDraft || options.IsDraftRequired) { - // Loaded whatever is the latest as it will be cloned + // Loaded whatever is the latest as it will be cloned. contentItems = (await _session .Query() .Where(x => x.ContentItemId.IsIn(ids) && x.Latest) @@ -362,6 +314,8 @@ public async Task> GetAsync(IEnumerable content public async Task LoadAsync(ContentItem contentItem) { + ArgumentNullException.ThrowIfNull(contentItem); + if (!_contentManagerSession.RecallVersionId(contentItem.Id, out var loaded)) { // store in session prior to loading to avoid some problems with simple circular dependencies @@ -382,6 +336,8 @@ public async Task LoadAsync(ContentItem contentItem) public async Task GetVersionAsync(string contentItemVersionId) { + ArgumentException.ThrowIfNullOrEmpty(contentItemVersionId); + var contentItem = await _session .Query(x => x.ContentItemVersionId == contentItemVersionId) .FirstOrDefaultAsync(); @@ -396,6 +352,8 @@ public async Task GetVersionAsync(string contentItemVersionId) public async Task SaveDraftAsync(ContentItem contentItem) { + ArgumentNullException.ThrowIfNull(contentItem); + if (!contentItem.Latest || contentItem.Published) { return; @@ -412,13 +370,15 @@ public async Task SaveDraftAsync(ContentItem contentItem) public async Task PublishAsync(ContentItem contentItem) { + ArgumentNullException.ThrowIfNull(contentItem); + if (contentItem.Published) { return; } // Create a context for the item and it's previous published record - // Because of this query the content item will need to be re-enlisted + // because of this query the content item will need to be re-enlisted // to be saved. var previous = await _session .Query(x => @@ -427,7 +387,7 @@ public async Task PublishAsync(ContentItem contentItem) var context = new PublishContentContext(contentItem, previous); - // invoke handlers to acquire state, or at least establish lazy loading callbacks + // Invoke handlers to acquire state, or at least establish lazy loading callbacks. await Handlers.InvokeAsync((handler, context) => handler.PublishingAsync(context), context, _logger); if (context.Cancel) @@ -449,6 +409,8 @@ public async Task PublishAsync(ContentItem contentItem) public async Task UnpublishAsync(ContentItem contentItem) { + ArgumentNullException.ThrowIfNull(contentItem); + // This method needs to be called using the latest version if (!contentItem.Latest) { @@ -575,7 +537,7 @@ protected async Task> BuildNewVersionsAsync(IEnumerable await _session.SaveAsync(existingContentItem); // We are not invoking NewAsync as we are cloning an existing item - // This will also prevent the Elements (parts) from being allocated unnecessarily + // This will also prevent the Elements (parts) from being allocated unnecessarily. var buildingContentItem = new ContentItem { ContentType = existingContentItem.ContentType, @@ -597,7 +559,7 @@ protected async Task> BuildNewVersionsAsync(IEnumerable return finalVersions; } - public async Task CreateAsync(ContentItem contentItem, VersionOptions options) + public async Task CreateAsync(ContentItem contentItem, VersionOptions options = null) { if (string.IsNullOrEmpty(contentItem.ContentItemVersionId)) { @@ -606,6 +568,8 @@ public async Task CreateAsync(ContentItem contentItem, VersionOptions options) contentItem.Latest = true; } + options ??= VersionOptions.Published; + // Draft flag on create is required for explicitly-published content items if (options.IsDraft) { @@ -647,11 +611,13 @@ public Task UpdateContentItemVersionAsync(ContentItem upd public async Task ImportAsync(IEnumerable contentItems) { + ArgumentNullException.ThrowIfNull(contentItems); + var skip = 0; var importedVersionIds = new HashSet(); - var batchedContentItems = contentItems.Take(ImportBatchSize); + var batchedContentItems = contentItems.Take(_importBatchSize); while (batchedContentItems.Any()) { @@ -772,13 +738,15 @@ public async Task ImportAsync(IEnumerable contentItems) } } - skip += ImportBatchSize; - batchedContentItems = contentItems.Skip(skip).Take(ImportBatchSize); + skip += _importBatchSize; + batchedContentItems = contentItems.Skip(skip).Take(_importBatchSize); } } public async Task UpdateAsync(ContentItem contentItem) { + ArgumentNullException.ThrowIfNull(contentItem); + var context = new UpdateContentContext(contentItem); await Handlers.InvokeAsync((handler, context) => handler.UpdatingAsync(context), context, _logger); @@ -790,6 +758,8 @@ public async Task UpdateAsync(ContentItem contentItem) public async Task ValidateAsync(ContentItem contentItem) { + ArgumentNullException.ThrowIfNull(contentItem); + var validateContext = new ValidateContentContext(contentItem); await Handlers.InvokeAsync((handler, context) => handler.ValidatingAsync(context), validateContext, _logger); @@ -806,11 +776,13 @@ public async Task ValidateAsync(ContentItem contentItem) public async Task RestoreAsync(ContentItem contentItem) { + ArgumentNullException.ThrowIfNull(contentItem); + // Prepare record for restore. // So that a new record will be created. contentItem.Id = 0; // So that a new version id will be generated. - contentItem.ContentItemVersionId = ""; + contentItem.ContentItemVersionId = string.Empty; contentItem.Latest = contentItem.Published = false; var context = new RestoreContentContext(contentItem); @@ -846,6 +818,9 @@ public async Task RestoreAsync(ContentItem contentItem) public async Task PopulateAspectAsync(IContent content, TAspect aspect) { + ArgumentNullException.ThrowIfNull(content); + ArgumentNullException.ThrowIfNull(aspect); + var context = new ContentItemAspectContext { ContentItem = content.ContentItem, @@ -859,6 +834,8 @@ public async Task PopulateAspectAsync(IContent content, TAspec public async Task RemoveAsync(ContentItem contentItem) { + ArgumentNullException.ThrowIfNull(contentItem); + var activeVersions = await _session.Query() .Where(x => x.ContentItemId == contentItem.ContentItemId && @@ -885,6 +862,8 @@ public async Task RemoveAsync(ContentItem contentItem) public async Task DiscardDraftAsync(ContentItem contentItem) { + ArgumentNullException.ThrowIfNull(contentItem); + if (contentItem.Published || !contentItem.Latest) { throw new InvalidOperationException("Not a draft version."); @@ -910,6 +889,8 @@ public async Task DiscardDraftAsync(ContentItem contentItem) public async Task CloneAsync(ContentItem contentItem) { + ArgumentNullException.ThrowIfNull(contentItem); + var cloneContentItem = await NewAsync(contentItem.ContentType); cloneContentItem.DisplayText = contentItem.DisplayText; await CreateAsync(cloneContentItem, VersionOptions.Draft); @@ -1014,6 +995,7 @@ private async Task CreateContentItemVersionAsync(ContentI { contentItem.Owner = owner; } + if (!string.IsNullOrEmpty(author)) { contentItem.Author = author; @@ -1030,7 +1012,7 @@ private async Task UpdateContentItemVersionAsync(ContentI var modifiedUtc = updatedVersion.ModifiedUtc; var publishedUtc = updatedVersion.PublishedUtc; - // Remove previous published or draft items if necesary or they will continue to be listed as published or draft. + // Remove previous published or draft items if necessary or they will continue to be listed as published or draft. var discardLatest = false; var removePublished = false; @@ -1098,6 +1080,7 @@ private async Task UpdateContentItemVersionAsync(ContentI { updatingVersion.ModifiedUtc = modifiedUtc; } + if (publishedUtc.HasValue) { updatingVersion.PublishedUtc = publishedUtc; diff --git a/src/docs/releases/2.0.0.md b/src/docs/releases/2.0.0.md index 545ceeb058f..5a1f1bfd675 100644 --- a/src/docs/releases/2.0.0.md +++ b/src/docs/releases/2.0.0.md @@ -194,6 +194,15 @@ public class RegisterUserFormDisplayDriver : DisplayDriver } } ``` + +### Contents + +The `IContentManager` interface was modified. The method `Task> GetAsync(IEnumerable contentItemIds, bool latest = false)` was removed. Instead use the method that accepts `VersionOptions` by providing either `VersionOptions.Latest` or `VersionOptions.Published` will be used by default. + +Additionally, the `GetContentItemByHandleAsync(string handle, bool latest = false)` and `GetContentItemByIdAsync(string contentItemId, bool latest = false)` were removed from `IOrchardHelper`. Instead use the method that accepts `VersionOptions` by providing either `VersionOptions.Latest` or `VersionOptions.Published` will be used by default. + +Lastly, we dropped support for AllVersions option in `VersionOptions`. Instead, you can use the new `GetAllVersionsAsync(string contentItemId)` method on `IContentManager`. + ### Media Indexing Previously, `.pdf` files were automatically indexed in the search providers (Elasticsearch, Lucene or Azure AI Search). Now, if you want to continue to index `.PDF` file you'll need to enable the `OrchardCore.Media.Indexing.Pdf` feature. diff --git a/test/OrchardCore.Tests/Modules/OrchardCore.Media/SecureMedia/ViewMediaFolderAuthorizationHandlerTests.cs b/test/OrchardCore.Tests/Modules/OrchardCore.Media/SecureMedia/ViewMediaFolderAuthorizationHandlerTests.cs index 0e6f6cbf288..6f7bf447f24 100644 --- a/test/OrchardCore.Tests/Modules/OrchardCore.Media/SecureMedia/ViewMediaFolderAuthorizationHandlerTests.cs +++ b/test/OrchardCore.Tests/Modules/OrchardCore.Media/SecureMedia/ViewMediaFolderAuthorizationHandlerTests.cs @@ -106,7 +106,7 @@ public async Task DoesNotGrantRootViewPermission(string permission, string resou [InlineData("ManageMediaFolder", "non-existent-folder")] [InlineData("ManageMediaFolder", "non-existent-folder/filename.png")] - public async Task GrantsAllFoldersViewPermission(string permission, string resource) + public async Task GrantsAllFoldersViewPermission(string permission, string resource) { // Arrange var handler = CreateHandler(); @@ -320,13 +320,13 @@ private static ViewMediaFolderAuthorizationHandler CreateHandler() mockUserAssetFolderNameProvider.Setup(afp => afp.GetUserAssetFolderName(It.Is(ci => ci.Identity.AuthenticationType == "Test"))).Returns("user-folder"); var mockContentManager = new Mock(); - mockContentManager.Setup(cm => cm.GetAsync(It.IsAny())).ReturnsAsync(Mock.Of()); // Pretends an existing content item. + mockContentManager.Setup(cm => cm.GetAsync(It.IsAny(), It.IsAny())).ReturnsAsync(Mock.Of()); // Pretends an existing content item. var attachedMediaFieldFileService = new AttachedMediaFieldFileService( mockMediaFileStore.Object, httpContextAccessor, mockUserAssetFolderNameProvider.Object, - NullLogger< AttachedMediaFieldFileService>.Instance); + NullLogger.Instance); // Create an IAuthorizationService mock that mimics how OC is granting permissions. var mockAuthorizationService = new Mock();