diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Document/Item/SearchDocumentItemController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Document/Item/SearchDocumentItemController.cs index 0ab15f0e7f..1e8bb5054b 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Document/Item/SearchDocumentItemController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Document/Item/SearchDocumentItemController.cs @@ -30,14 +30,33 @@ public class SearchDocumentItemController : DocumentItemControllerBase [NonAction] [Obsolete("Scheduled to be removed in v16, use the non obsoleted method instead")] public async Task SearchFromParent(CancellationToken cancellationToken, string query, int skip = 0, int take = 100, Guid? parentId = null) - => await SearchFromParentWithAllowedTypes(cancellationToken, query, skip, take, parentId); + => await SearchWithTrashed(cancellationToken, query, null, skip, take, parentId); + + [NonAction] + [Obsolete("Scheduled to be removed in v16, use the non obsoleted method instead")] + [ProducesResponseType(typeof(PagedModel), StatusCodes.Status200OK)] + public async Task SearchFromParentWithAllowedTypes( + CancellationToken cancellationToken, + string query, + int skip = 0, + int take = 100, + Guid? parentId = null, + [FromQuery] IEnumerable? allowedDocumentTypes = null) => + await SearchWithTrashed(cancellationToken, query, null, skip, take, parentId, allowedDocumentTypes); [HttpGet("search")] [MapToApiVersion("1.0")] [ProducesResponseType(typeof(PagedModel), StatusCodes.Status200OK)] - public async Task SearchFromParentWithAllowedTypes(CancellationToken cancellationToken, string query, int skip = 0, int take = 100, Guid? parentId = null, [FromQuery]IEnumerable? allowedDocumentTypes = null) + public async Task SearchWithTrashed( + CancellationToken cancellationToken, + string query, + bool? trashed = null, + int skip = 0, + int take = 100, + Guid? parentId = null, + [FromQuery] IEnumerable? allowedDocumentTypes = null) { - PagedModel searchResult = _indexedEntitySearchService.Search(UmbracoObjectTypes.Document, query, parentId, allowedDocumentTypes, skip, take); + PagedModel searchResult = _indexedEntitySearchService.Search(UmbracoObjectTypes.Document, query, parentId, allowedDocumentTypes, trashed, skip, take); var result = new PagedModel { Items = searchResult.Items.OfType().Select(_documentPresentationFactory.CreateItemResponseModel), diff --git a/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/Item/SearchDocumentTypeItemController.cs b/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/Item/SearchDocumentTypeItemController.cs index 5069df0d80..0582c8e3e8 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/Item/SearchDocumentTypeItemController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/Item/SearchDocumentTypeItemController.cs @@ -1,45 +1,60 @@ using Asp.Versioning; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Management.ViewModels.DocumentType.Item; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Services; -using Umbraco.Extensions; namespace Umbraco.Cms.Api.Management.Controllers.DocumentType.Item; [ApiVersion("1.0")] public class SearchDocumentTypeItemController : DocumentTypeItemControllerBase { - private readonly IEntitySearchService _entitySearchService; - private readonly IContentTypeService _contentTypeService; private readonly IUmbracoMapper _mapper; + private readonly IContentTypeSearchService _contentTypeSearchService; + [Obsolete("Please use ctor that only accepts IUmbracoMapper & IContentTypeSearchService, scheduled for removal in v17")] public SearchDocumentTypeItemController(IEntitySearchService entitySearchService, IContentTypeService contentTypeService, IUmbracoMapper mapper) + : this(mapper, StaticServiceProvider.Instance.GetRequiredService()) { - _entitySearchService = entitySearchService; - _contentTypeService = contentTypeService; - _mapper = mapper; } + [Obsolete("Please use ctor that only accepts IUmbracoMapper & IContentTypeSearchService, scheduled for removal in v17")] + // We need to have this constructor, or else we get ambiguous constructor error + public SearchDocumentTypeItemController( + IEntitySearchService entitySearchService, + IContentTypeService contentTypeService, + IUmbracoMapper mapper, + IContentTypeSearchService contentTypeSearchService) + : this(mapper, contentTypeSearchService) + { + } + + [ActivatorUtilitiesConstructor] + public SearchDocumentTypeItemController(IUmbracoMapper mapper, IContentTypeSearchService contentTypeSearchService) + { + _mapper = mapper; + _contentTypeSearchService = contentTypeSearchService; + } + + [NonAction] + [Obsolete("Scheduled to be removed in v16, use the non obsoleted method instead")] + public async Task Search(CancellationToken cancellationToken, string query, int skip = 0, int take = 100) + => await SearchDocumentType(cancellationToken, query, null, skip, take); + [HttpGet("search")] [MapToApiVersion("1.0")] [ProducesResponseType(typeof(PagedModel), StatusCodes.Status200OK)] - public async Task Search(CancellationToken cancellationToken, string query, int skip = 0, int take = 100) + public async Task SearchDocumentType(CancellationToken cancellationToken, string query, bool? isElement = null, int skip = 0, int take = 100) { - PagedModel searchResult = _entitySearchService.Search(UmbracoObjectTypes.DocumentType, query, skip, take); - if (searchResult.Items.Any() is false) - { - return await Task.FromResult(Ok(new PagedModel { Total = searchResult.Total })); - } - - IEnumerable contentTypes = _contentTypeService.GetMany(searchResult.Items.Select(item => item.Key).ToArray().EmptyNull()); + PagedModel contentTypes = await _contentTypeSearchService.SearchAsync(query, isElement, cancellationToken, skip, take); var result = new PagedModel { - Items = _mapper.MapEnumerable(contentTypes), - Total = searchResult.Total + Items = _mapper.MapEnumerable(contentTypes.Items), + Total = contentTypes.Total }; return Ok(result); diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Media/Item/SearchMediaItemController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Media/Item/SearchMediaItemController.cs index c7cccd0cea..6db5950179 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Media/Item/SearchMediaItemController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Media/Item/SearchMediaItemController.cs @@ -32,12 +32,17 @@ public class SearchMediaItemController : MediaItemControllerBase public async Task SearchFromParent(CancellationToken cancellationToken, string query, int skip = 0, int take = 100, Guid? parentId = null) => await SearchFromParentWithAllowedTypes(cancellationToken, query, skip, take, parentId); + [NonAction] + [Obsolete("Scheduled to be removed in v16, use the non obsoleted method instead")] + public async Task SearchFromParentWithAllowedTypes(CancellationToken cancellationToken, string query, int skip = 0, int take = 100, Guid? parentId = null, [FromQuery]IEnumerable? allowedMediaTypes = null) + => await SearchFromParentWithAllowedTypes(cancellationToken, query, null, skip, take, parentId); + [HttpGet("search")] [MapToApiVersion("1.0")] [ProducesResponseType(typeof(PagedModel), StatusCodes.Status200OK)] - public async Task SearchFromParentWithAllowedTypes(CancellationToken cancellationToken, string query, int skip = 0, int take = 100, Guid? parentId = null, [FromQuery]IEnumerable? allowedMediaTypes = null) + public async Task SearchFromParentWithAllowedTypes(CancellationToken cancellationToken, string query, bool? trashed = null, int skip = 0, int take = 100, Guid? parentId = null, [FromQuery]IEnumerable? allowedMediaTypes = null) { - PagedModel searchResult = _indexedEntitySearchService.Search(UmbracoObjectTypes.Media, query, parentId, allowedMediaTypes, skip, take); + PagedModel searchResult = _indexedEntitySearchService.Search(UmbracoObjectTypes.Media, query, parentId, allowedMediaTypes, trashed, skip, take); var result = new PagedModel { Items = searchResult.Items.OfType().Select(_mediaPresentationFactory.CreateItemResponseModel), diff --git a/src/Umbraco.Core/Services/ContentTypeService.cs b/src/Umbraco.Core/Services/ContentTypeService.cs index 3566696af4..1a4ea449c0 100644 --- a/src/Umbraco.Core/Services/ContentTypeService.cs +++ b/src/Umbraco.Core/Services/ContentTypeService.cs @@ -4,6 +4,7 @@ using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services.Changes; @@ -147,6 +148,16 @@ public class ContentTypeService : ContentTypeServiceBase> GetByQueryAsync(IQuery query, CancellationToken cancellationToken) + { + using ICoreScope scope = ScopeProvider.CreateCoreScope(); + // that one is special because it works across content, media and member types + scope.ReadLock(Constants.Locks.ContentTypes); + IEnumerable contentTypes = Repository.Get(query); + scope.Complete(); + return contentTypes; + } + protected override void DeleteItemsOfTypes(IEnumerable typeIds) { using (ICoreScope scope = ScopeProvider.CreateCoreScope()) diff --git a/src/Umbraco.Core/Services/IContentTypeSearchService.cs b/src/Umbraco.Core/Services/IContentTypeSearchService.cs new file mode 100644 index 0000000000..1cb41ad573 --- /dev/null +++ b/src/Umbraco.Core/Services/IContentTypeSearchService.cs @@ -0,0 +1,8 @@ +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Core.Services; + +public interface IContentTypeSearchService +{ + Task> SearchAsync(string query, bool? isElement, CancellationToken cancellationToken, int skip = 0, int take = 100); +} diff --git a/src/Umbraco.Core/Services/IContentTypeService.cs b/src/Umbraco.Core/Services/IContentTypeService.cs index 7dbd0834ec..aa093a4192 100644 --- a/src/Umbraco.Core/Services/IContentTypeService.cs +++ b/src/Umbraco.Core/Services/IContentTypeService.cs @@ -1,4 +1,5 @@ using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Services.OperationStatus; namespace Umbraco.Cms.Core.Services; @@ -30,4 +31,6 @@ public interface IContentTypeService : IContentTypeBaseService /// /// IEnumerable GetAllContentTypeIds(string[] aliases); + + Task> GetByQueryAsync(IQuery query, CancellationToken cancellationToken) => Task.FromResult(Enumerable.Empty()); } diff --git a/src/Umbraco.Core/Services/IIndexedEntitySearchService.cs b/src/Umbraco.Core/Services/IIndexedEntitySearchService.cs index 12d4c3e72c..1404d3e946 100644 --- a/src/Umbraco.Core/Services/IIndexedEntitySearchService.cs +++ b/src/Umbraco.Core/Services/IIndexedEntitySearchService.cs @@ -20,7 +20,19 @@ public interface IIndexedEntitySearchService PagedModel Search(UmbracoObjectTypes objectType, string query, Guid? parentId, int skip = 0, int take = 100, bool ignoreUserStartNodes = false) => Search(objectType,query, skip, take, ignoreUserStartNodes); - // default implementation to avoid breaking changes falls back to old behaviour + [Obsolete("Please use the method that accepts all parameters. Will be removed in V17.")] PagedModel Search(UmbracoObjectTypes objectType, string query, Guid? parentId, IEnumerable? contentTypeIds, int skip = 0, int take = 100, bool ignoreUserStartNodes = false) => Search(objectType,query, skip, take, ignoreUserStartNodes); + + // default implementation to avoid breaking changes falls back to old behaviour + PagedModel Search( + UmbracoObjectTypes objectType, + string query, + Guid? parentId, + IEnumerable? contentTypeIds, + bool? trashed, + int skip = 0, + int take = 100, + bool ignoreUserStartNodes = false) + => Search(objectType,query, skip, take, ignoreUserStartNodes); } diff --git a/src/Umbraco.Examine.Lucene/BackOfficeExamineSearcher.cs b/src/Umbraco.Examine.Lucene/BackOfficeExamineSearcher.cs index 08eda5fe74..86fab12b74 100644 --- a/src/Umbraco.Examine.Lucene/BackOfficeExamineSearcher.cs +++ b/src/Umbraco.Examine.Lucene/BackOfficeExamineSearcher.cs @@ -1,6 +1,7 @@ // Copyright (c) Umbraco. // See LICENSE for more details. +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Text; using System.Text.RegularExpressions; @@ -61,7 +62,19 @@ public class BackOfficeExamineSearcher : IBackOfficeExamineSearcher out long totalFound, string? searchFrom = null, bool ignoreUserStartNodes = false) - => Search(query, entityType, pageSize, pageIndex, out totalFound, null, searchFrom, ignoreUserStartNodes); + => Search(query, entityType, pageSize, pageIndex, out totalFound, null, null, searchFrom, ignoreUserStartNodes); + + [Obsolete("Please use the method that accepts all parameters. Will be removed in V17.")] + public IEnumerable Search( + string query, + UmbracoEntityTypes entityType, + int pageSize, + long pageIndex, + out long totalFound, + string[]? contentTypeAliases, + string? searchFrom = null, + bool ignoreUserStartNodes = false) + => Search(query, entityType, pageSize, pageIndex, out totalFound, contentTypeAliases, null, searchFrom, ignoreUserStartNodes); public IEnumerable Search( string query, @@ -70,6 +83,7 @@ public class BackOfficeExamineSearcher : IBackOfficeExamineSearcher long pageIndex, out long totalFound, string[]? contentTypeAliases, + bool? trashed, string? searchFrom = null, bool ignoreUserStartNodes = false) { @@ -133,6 +147,12 @@ public class BackOfficeExamineSearcher : IBackOfficeExamineSearcher ? currentUser.CalculateMediaStartNodeIds(_entityService, _appCaches) : Array.Empty(); AppendPath(sb, UmbracoObjectTypes.Media, allMediaStartNodes, searchFrom, ignoreUserStartNodes, _entityService); + + if (trashed.HasValue) + { + AppendRequiredTrashPath(trashed.Value, sb, Constants.System.RecycleBinMedia); + } + break; case UmbracoEntityTypes.Document: type = "content"; @@ -146,6 +166,12 @@ public class BackOfficeExamineSearcher : IBackOfficeExamineSearcher ? currentUser.CalculateContentStartNodeIds(_entityService, _appCaches) : Array.Empty(); AppendPath(sb, UmbracoObjectTypes.Document, allContentStartNodes, searchFrom, ignoreUserStartNodes, _entityService); + + if (trashed.HasValue) + { + AppendRequiredTrashPath(trashed.Value, sb, Constants.System.RecycleBinContent); + } + break; default: throw new NotSupportedException("The " + typeof(BackOfficeExamineSearcher) + @@ -181,6 +207,14 @@ public class BackOfficeExamineSearcher : IBackOfficeExamineSearcher return result; } + private void AppendRequiredTrashPath(bool trashed, StringBuilder sb, int recycleBinId) + { + var requiredOrNotString = trashed ? "+" : "!"; + var trashPath = $"-1,{recycleBinId}"; + trashPath = trashPath.Replace("-", "\\-").Replace(",", "\\,"); + sb.Append($"{requiredOrNotString}__Path:{trashPath}\\,* "); + } + private bool BuildQuery(StringBuilder sb, string query, string? searchFrom, List fields, string type) { //build a lucene query: @@ -394,6 +428,7 @@ public class BackOfficeExamineSearcher : IBackOfficeExamineSearcher searchFromId > 0 ? entityService.GetAllPaths(objectType, searchFromId).FirstOrDefault() : null; + if (entityPath != null) { // find... only what's underneath diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs index 042bf82e45..6f2beb4ef9 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs @@ -78,6 +78,7 @@ public static partial class UmbracoBuilderExtensions builder.Services.AddUnique(); builder.Services.AddUnique(); builder.Services.AddUnique(); + builder.Services.AddUnique(); builder.Services.AddUnique(); builder.Services.TryAddTransient(); diff --git a/src/Umbraco.Infrastructure/Examine/IBackOfficeExamineSearcher.cs b/src/Umbraco.Infrastructure/Examine/IBackOfficeExamineSearcher.cs index 555059e385..1f840328a5 100644 --- a/src/Umbraco.Infrastructure/Examine/IBackOfficeExamineSearcher.cs +++ b/src/Umbraco.Infrastructure/Examine/IBackOfficeExamineSearcher.cs @@ -18,7 +18,7 @@ public interface IBackOfficeExamineSearcher string? searchFrom = null, bool ignoreUserStartNodes = false); - // default implementation to avoid breaking changes falls back to old behaviour + [Obsolete("Please use the method that accepts all parameters. Will be removed in V17.")] IEnumerable Search( string query, UmbracoEntityTypes entityType, @@ -28,5 +28,18 @@ public interface IBackOfficeExamineSearcher string[]? contentTypeAliases, string? searchFrom = null, bool ignoreUserStartNodes = false) + => Search(query, entityType, pageSize, pageIndex, out totalFound, searchFrom, ignoreUserStartNodes); + + // default implementation to avoid breaking changes falls back to old behaviour + IEnumerable Search( + string query, + UmbracoEntityTypes entityType, + int pageSize, + long pageIndex, + out long totalFound, + string[]? contentTypeAliases, + bool? trashed, + string? searchFrom = null, + bool ignoreUserStartNodes = false) => Search(query, entityType, pageSize, pageIndex, out totalFound, null, searchFrom, ignoreUserStartNodes); } diff --git a/src/Umbraco.Infrastructure/Search/UmbracoTreeSearcherFields.cs b/src/Umbraco.Infrastructure/Search/UmbracoTreeSearcherFields.cs index 8845e6e371..c1631a9b5d 100644 --- a/src/Umbraco.Infrastructure/Search/UmbracoTreeSearcherFields.cs +++ b/src/Umbraco.Infrastructure/Search/UmbracoTreeSearcherFields.cs @@ -18,6 +18,7 @@ public class UmbracoTreeSearcherFields : IUmbracoTreeSearcherFields UmbracoExamineFieldNames.CategoryFieldName, "parentID", UmbracoExamineFieldNames.ItemTypeFieldName, + UmbracoExamineFieldNames.IndexPathFieldName, }; private readonly ISet _backOfficeMediaFieldsToLoad = diff --git a/src/Umbraco.Infrastructure/Services/ContentTypeSearchService.cs b/src/Umbraco.Infrastructure/Services/ContentTypeSearchService.cs new file mode 100644 index 0000000000..45e2f1da8a --- /dev/null +++ b/src/Umbraco.Infrastructure/Services/ContentTypeSearchService.cs @@ -0,0 +1,37 @@ +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Persistence.Querying; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Infrastructure.Persistence; + +namespace Umbraco.Cms.Infrastructure.Services; + +internal sealed class ContentTypeSearchService : IContentTypeSearchService +{ + private readonly ISqlContext _sqlContext; + private readonly IContentTypeService _contentTypeService; + + public ContentTypeSearchService(ISqlContext sqlContext, IContentTypeService contentTypeService) + { + _sqlContext = sqlContext; + _contentTypeService = contentTypeService; + } + + public async Task> SearchAsync(string query, bool? isElement, CancellationToken cancellationToken, int skip = 0, int take = 100) + { + // if the query is a GUID, search for that explicitly + Guid.TryParse(query, out Guid guidQuery); + + IQuery nameQuery = isElement is not null ? + _sqlContext.Query().Where(x => (x.Name!.Contains(query) || x.Key == guidQuery) && x.IsElement == isElement) : + _sqlContext.Query().Where(x => x.Name!.Contains(query) || x.Key == guidQuery); + + IContentType[] contentTypes = (await _contentTypeService.GetByQueryAsync(nameQuery, cancellationToken)).ToArray(); + + return new PagedModel + { + Items = contentTypes.Skip(skip).Take(take), + Total = contentTypes.Count() + }; + } +} diff --git a/src/Umbraco.Infrastructure/Services/Implement/IndexedEntitySearchService.cs b/src/Umbraco.Infrastructure/Services/Implement/IndexedEntitySearchService.cs index 5ac85ad0fd..43fe982df9 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/IndexedEntitySearchService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/IndexedEntitySearchService.cs @@ -52,7 +52,7 @@ internal sealed class IndexedEntitySearchService : IIndexedEntitySearchService int skip = 0, int take = 100, bool ignoreUserStartNodes = false) - => Search(objectType, query, parentId, null, skip, take, ignoreUserStartNodes); + => Search(objectType, query, parentId, null, null, skip, take, ignoreUserStartNodes); public PagedModel Search( UmbracoObjectTypes objectType, @@ -62,6 +62,17 @@ internal sealed class IndexedEntitySearchService : IIndexedEntitySearchService int skip = 0, int take = 100, bool ignoreUserStartNodes = false) + => Search(objectType, query, parentId, contentTypeIds, null, skip, take, ignoreUserStartNodes); + + public PagedModel Search( + UmbracoObjectTypes objectType, + string query, + Guid? parentId, + IEnumerable? contentTypeIds, + bool? trashed, + int skip = 0, + int take = 100, + bool ignoreUserStartNodes = false) { UmbracoEntityTypes entityType = objectType switch { @@ -91,6 +102,7 @@ internal sealed class IndexedEntitySearchService : IIndexedEntitySearchService pageNumber, out var totalFound, contentTypeAliases, + trashed, ignoreUserStartNodes: ignoreUserStartNodes, searchFrom: parentId?.ToString());