From 48f4d8b7d15042c63adc47d47555da9c76a0c7ad Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Fri, 16 Dec 2022 11:17:56 +0100 Subject: [PATCH 01/13] Set redirect URL when building model (#12969) Co-authored-by: Elitsa Marinovska (cherry picked from commit 544cb306023482fb03acfaef2ac63343fbc13786) --- src/Umbraco.Web.Website/Models/RegisterModelBuilder.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Umbraco.Web.Website/Models/RegisterModelBuilder.cs b/src/Umbraco.Web.Website/Models/RegisterModelBuilder.cs index 8ce6ab5595..b3736e63e8 100644 --- a/src/Umbraco.Web.Website/Models/RegisterModelBuilder.cs +++ b/src/Umbraco.Web.Website/Models/RegisterModelBuilder.cs @@ -56,6 +56,7 @@ public class RegisterModelBuilder : MemberModelBuilderBase var model = new RegisterModel { + RedirectUrl = _redirectUrl, MemberTypeAlias = providedOrDefaultMemberTypeAlias, UsernameIsEmail = _usernameIsEmail, MemberProperties = _lookupProperties From a897e3bdd3165a0674db6a9866a4ed7f9fb4a0f5 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Fri, 16 Dec 2022 12:41:19 +0100 Subject: [PATCH 02/13] Improved support for TinyMCE custom style formats (#13582) --- .../src/common/services/tinymce.service.js | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js b/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js index fb8cf12ea2..36c1259cc3 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js @@ -170,9 +170,6 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s })); }); } - else { - styleFormats = fallbackStyles; - } return $q.all(promises).then(function () { // Always push our Umbraco RTE stylesheet @@ -375,7 +372,6 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s autoresize_bottom_margin: 10, content_css: styles.stylesheets, style_formats: styles.styleFormats, - style_formats_autohide: true, language: getLanguage(), //this would be for a theme other than inlite @@ -450,9 +446,20 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s } } + // if we have style_formats at this point they originate from the RTE CSS config. we don't want any custom + // style_formats to interfere with the RTE CSS config, so let's explicitly remove the custom style_formats. + if(tinyMceConfig.customConfig.style_formats && config.style_formats && config.style_formats.length){ + delete tinyMceConfig.customConfig.style_formats; + } + Utilities.extend(config, tinyMceConfig.customConfig); } + if(!config.style_formats || !config.style_formats.length){ + // if we have no style_formats at this point we'll revert to using the default ones (fallbackStyles) + config.style_formats = fallbackStyles; + } + return config; }); From a5a80fdd41d3c319dac75c7f06ec60a169729541 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Fri, 16 Dec 2022 13:06:27 +0100 Subject: [PATCH 03/13] Multi URL picker improvements for V11 (#13586) * Use bulk lookup to find URLs for multi URL picker items * Make sure URLs and anchors are loaded correctly when no culture is supplied --- .../Controllers/EntityController.cs | 2 +- .../multiurlpicker.controller.js | 26 +++++++++++-------- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/Umbraco.Web.BackOffice/Controllers/EntityController.cs b/src/Umbraco.Web.BackOffice/Controllers/EntityController.cs index ba9809f657..42019f7c99 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/EntityController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/EntityController.cs @@ -568,7 +568,7 @@ public class EntityController : UmbracoAuthorizedJsonController [HttpGet] public UrlAndAnchors GetUrlAndAnchors(int id, string? culture = "*") { - culture ??= ClientCulture(); + culture = culture is null or "*" ? ClientCulture() : culture; var url = _publishedUrlProvider.GetUrl(id, culture: culture); IEnumerable anchorValues = _contentService.GetAnchorValuesFromRTEs(id, culture); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/multiurlpicker/multiurlpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/multiurlpicker/multiurlpicker.controller.js index dfdc9aa779..09facf96b5 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/multiurlpicker/multiurlpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/multiurlpicker/multiurlpicker.controller.js @@ -21,10 +21,6 @@ function multiUrlPickerController($scope, localizationService, entityResource, i $scope.renderModel = []; - if ($scope.preview) { - return; - } - if ($scope.model.config && parseInt($scope.model.config.maxNumber) !== 1 && $scope.umbProperty) { var propertyActions = [ removeAllEntriesAction @@ -83,7 +79,7 @@ function multiUrlPickerController($scope, localizationService, entityResource, i $scope.sortableOptions.disabled = $scope.renderModel.length === 1 || $scope.readonly; removeAllEntriesAction.isDisabled = $scope.renderModel.length === 0 || $scope.readonly; - + //Update value $scope.model.value = $scope.renderModel; } @@ -93,7 +89,7 @@ function multiUrlPickerController($scope, localizationService, entityResource, i if (!$scope.allowRemove) return; $scope.renderModel.splice($index, 1); - + setDirty(); }; @@ -208,15 +204,23 @@ function multiUrlPickerController($scope, localizationService, entityResource, i $scope.model.config.minNumber = 1; } - _.each($scope.model.value, function (item) { + const ids = []; + $scope.model.value.forEach(item => { // we must reload the "document" link URLs to match the current editor culture - if (item.udi && item.udi.indexOf("/document/") > 0) { + if (item.udi && item.udi.indexOf("/document/") > 0 && ids.indexOf(item.udi) < 0) { + ids.push(item.udi); item.url = null; - entityResource.getUrlByUdi(item.udi).then(data => { - item.url = data; - }); } }); + + if(ids.length){ + entityResource.getUrlsByIds(ids, "Document").then(function(urlMap){ + Object.keys(urlMap).forEach((udi) => { + const items = $scope.model.value.filter(item => item.udi === udi); + items.forEach(item => item.url = urlMap[udi]); + }) + }); + } } init(); From d173a99fc83d14e7ffaf78f894fb8474dd49fd55 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Fri, 16 Dec 2022 14:23:31 +0100 Subject: [PATCH 04/13] Support space (and more) in tag groups (#13589) --- .../directives/components/tags/umbtagseditor.directive.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/tags/umbtagseditor.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/tags/umbtagseditor.directive.js index 8238a2be66..7ae96e0cd5 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/tags/umbtagseditor.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/tags/umbtagseditor.directive.js @@ -92,7 +92,7 @@ var sources = { //see: https://github.com/twitter/typeahead.js/blob/master/doc/jquery_typeahead.md#options // name = the data set name, we'll make this the tag group name + culture - name: vm.config.group + (vm.culture ? vm.culture : ""), + name: (vm.config.group + (vm.culture ? vm.culture : "")).replace(/\W/g, '-'), display: "text", //source: tagsHound source: function (query, syncCallback, asyncCallback) { From e375ab4b1b3444bdff13cdc74b41ec48f53d32b2 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Tue, 20 Dec 2022 07:00:07 +0100 Subject: [PATCH 05/13] Added new ContentAndWebRootFileProviderFactory and use that for IManifestFileProviderFactory, so manifests can be found in /app_plugins folder too (#13597) Fixes https://github.com/umbraco/Umbraco-CMS/issues/13565 --- .../UmbracoBuilderExtensions.cs | 2 +- .../ContentAndWebRootFileProviderFactory.cs | 27 +++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 src/Umbraco.Web.Common/FileProviders/ContentAndWebRootFileProviderFactory.cs diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs index edfe999d20..dc94fa3dc8 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs @@ -149,7 +149,7 @@ public static partial class UmbracoBuilderExtensions // WebRootFileProviderFactory is just a wrapper around the IWebHostEnvironment.WebRootFileProvider, // therefore no need to register it as singleton - builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.Services.AddSingleton(); // Must be added here because DbProviderFactories is netstandard 2.1 so cannot exist in Infra for now diff --git a/src/Umbraco.Web.Common/FileProviders/ContentAndWebRootFileProviderFactory.cs b/src/Umbraco.Web.Common/FileProviders/ContentAndWebRootFileProviderFactory.cs new file mode 100644 index 0000000000..7c3831b919 --- /dev/null +++ b/src/Umbraco.Web.Common/FileProviders/ContentAndWebRootFileProviderFactory.cs @@ -0,0 +1,27 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.FileProviders; +using Umbraco.Cms.Core.IO; + +namespace Umbraco.Cms.Web.Common.FileProviders; + +public class ContentAndWebRootFileProviderFactory : IManifestFileProviderFactory +{ + private readonly IWebHostEnvironment _webHostEnvironment; + + /// + /// Initializes a new instance of the class. + /// + /// The web hosting environment an application is running in. + public ContentAndWebRootFileProviderFactory(IWebHostEnvironment webHostEnvironment) + { + _webHostEnvironment = webHostEnvironment; + } + + /// + /// Creates a new instance, pointing at WebRootPath and ContentRootPath. + /// + /// + /// The newly created instance. + /// + public IFileProvider Create() => new CompositeFileProvider(_webHostEnvironment.WebRootFileProvider, _webHostEnvironment.ContentRootFileProvider); +} From 751c1883ce316d2ac8a974db5315ac099ebc80f6 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Fri, 9 Sep 2022 09:45:38 +0200 Subject: [PATCH 06/13] Fix GetBackOfficeUrl extension method --- .../Extensions/LinkGeneratorExtensions.cs | 41 ++++++++----------- 1 file changed, 17 insertions(+), 24 deletions(-) diff --git a/src/Umbraco.Web.Common/Extensions/LinkGeneratorExtensions.cs b/src/Umbraco.Web.Common/Extensions/LinkGeneratorExtensions.cs index ce6e8c7816..70f8d1c2fe 100644 --- a/src/Umbraco.Web.Common/Extensions/LinkGeneratorExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/LinkGeneratorExtensions.cs @@ -13,32 +13,25 @@ namespace Umbraco.Extensions; public static class LinkGeneratorExtensions { /// - /// Return the back office url if the back office is installed + /// Gets the backoffice URL (if the back office is installed). /// - public static string? GetBackOfficeUrl(this LinkGenerator linkGenerator, IHostingEnvironment hostingEnvironment) - { - Type? backOfficeControllerType; - try - { - backOfficeControllerType = Assembly.Load("Umbraco.Web.BackOffice") - .GetType("Umbraco.Web.BackOffice.Controllers.BackOfficeController"); - if (backOfficeControllerType == null) - { - return "/"; // this would indicate that the installer is installed without the back office - } - } - catch - { - return - hostingEnvironment - .ApplicationVirtualPath; // this would indicate that the installer is installed without the back office - } + /// The link generator. + /// + /// The backoffice URL. + /// + public static string? GetBackOfficeUrl(this LinkGenerator linkGenerator) + => linkGenerator.GetPathByAction("Default", "BackOffice", new { area = Constants.Web.Mvc.BackOfficeArea }); - return linkGenerator.GetPathByAction( - "Default", - ControllerExtensions.GetControllerName(backOfficeControllerType), - new { area = Constants.Web.Mvc.BackOfficeApiArea }); - } + /// + /// Gets the backoffice URL (if the back office is installed) or application virtual path (in most cases just "/"). + /// + /// The link generator. + /// The hosting environment. + /// + /// The backoffice URL. + /// + public static string GetBackOfficeUrl(this LinkGenerator linkGenerator, IHostingEnvironment hostingEnvironment) + => GetBackOfficeUrl(linkGenerator) ?? hostingEnvironment.ApplicationVirtualPath; /// /// Return the Url for a Web Api service From 58da7f754b78434623d028180943f87bced383ac Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Mon, 19 Dec 2022 11:52:48 +0100 Subject: [PATCH 07/13] Rename fixed extension method to GetUmbracoBackOfficeUrl and obsolete old one --- .../Install/InstallAuthorizeAttribute.cs | 2 +- .../Extensions/LinkGeneratorExtensions.cs | 24 +++++++++++++------ 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Web.BackOffice/Install/InstallAuthorizeAttribute.cs b/src/Umbraco.Web.BackOffice/Install/InstallAuthorizeAttribute.cs index 2c6d5102e8..6d58e902d4 100644 --- a/src/Umbraco.Web.BackOffice/Install/InstallAuthorizeAttribute.cs +++ b/src/Umbraco.Web.BackOffice/Install/InstallAuthorizeAttribute.cs @@ -42,7 +42,7 @@ public class InstallAuthorizeAttribute : TypeFilterAttribute // Only authorize when the installer is enabled context.Result = new ForbidResult(new AuthenticationProperties() { - RedirectUri = _linkGenerator.GetBackOfficeUrl(_hostingEnvironment) + RedirectUri = _linkGenerator.GetUmbracoBackOfficeUrl(_hostingEnvironment) }); } else if (_runtimeState.Level == RuntimeLevel.Upgrade && (await context.HttpContext.AuthenticateBackOfficeAsync()).Succeeded == false) diff --git a/src/Umbraco.Web.Common/Extensions/LinkGeneratorExtensions.cs b/src/Umbraco.Web.Common/Extensions/LinkGeneratorExtensions.cs index 70f8d1c2fe..1c589ebe2a 100644 --- a/src/Umbraco.Web.Common/Extensions/LinkGeneratorExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/LinkGeneratorExtensions.cs @@ -13,25 +13,35 @@ namespace Umbraco.Extensions; public static class LinkGeneratorExtensions { /// - /// Gets the backoffice URL (if the back office is installed). + /// Gets the Umbraco backoffice URL (if Umbraco is installed). /// /// The link generator. /// - /// The backoffice URL. + /// The Umbraco backoffice URL. /// - public static string? GetBackOfficeUrl(this LinkGenerator linkGenerator) + public static string? GetUmbracoBackOfficeUrl(this LinkGenerator linkGenerator) => linkGenerator.GetPathByAction("Default", "BackOffice", new { area = Constants.Web.Mvc.BackOfficeArea }); /// - /// Gets the backoffice URL (if the back office is installed) or application virtual path (in most cases just "/"). + /// Gets the Umbraco backoffice URL (if Umbraco is installed) or application virtual path (in most cases just "/"). /// /// The link generator. /// The hosting environment. /// - /// The backoffice URL. + /// The Umbraco backoffice URL. /// - public static string GetBackOfficeUrl(this LinkGenerator linkGenerator, IHostingEnvironment hostingEnvironment) - => GetBackOfficeUrl(linkGenerator) ?? hostingEnvironment.ApplicationVirtualPath; + public static string GetUmbracoBackOfficeUrl(this LinkGenerator linkGenerator, IHostingEnvironment hostingEnvironment) + => GetUmbracoBackOfficeUrl(linkGenerator) ?? hostingEnvironment.ApplicationVirtualPath; + + /// + /// Return the back office url if the back office is installed + /// + /// + /// This method contained a bug that would result in always returning "/". + /// + [Obsolete("Use the GetUmbracoBackOfficeUrl extension method instead. This method will be removed in Umbraco 13.")] + public static string? GetBackOfficeUrl(this LinkGenerator linkGenerator, IHostingEnvironment hostingEnvironment) + => "/"; /// /// Return the Url for a Web Api service From 832e44cb01612cdf26355971c69d669a8d193c97 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Mon, 19 Dec 2022 12:04:35 +0100 Subject: [PATCH 08/13] Also fix GetBackOfficeUrl in UrlHelperExtensions --- .../Extensions/UrlHelperExtensions.cs | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/Umbraco.Web.Common/Extensions/UrlHelperExtensions.cs b/src/Umbraco.Web.Common/Extensions/UrlHelperExtensions.cs index 6eacc2ef24..972dc67e19 100644 --- a/src/Umbraco.Web.Common/Extensions/UrlHelperExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/UrlHelperExtensions.cs @@ -22,21 +22,27 @@ namespace Umbraco.Extensions; public static class UrlHelperExtensions { + /// + /// Gets the Umbraco backoffice URL (if Umbraco is installed). + /// + /// The URL helper. + /// + /// The Umbraco backoffice URL. + /// + public static string? GetUmbracoBackOfficeUrl(this IUrlHelper urlHelper) + => urlHelper.Action("Default", "BackOffice", new { area = Constants.Web.Mvc.BackOfficeArea }); + /// /// Return the back office url if the back office is installed /// /// /// + /// + /// This method contained a bug that would result in always returning "/". + /// + [Obsolete("Use the GetUmbracoBackOfficeUrl extension method instead. This method will be removed in Umbraco 13.")] public static string? GetBackOfficeUrl(this IUrlHelper url) - { - var backOfficeControllerType = Type.GetType("Umbraco.Web.BackOffice.Controllers"); - if (backOfficeControllerType == null) - { - return "/"; // this would indicate that the installer is installed without the back office - } - - return url.Action("Default", ControllerExtensions.GetControllerName(backOfficeControllerType), new { area = Constants.Web.Mvc.BackOfficeApiArea }); - } + => "/"; /// /// Return the Url for a Web Api service From 9c087f584d48a4af646e29f7f4f98e2a94742dbd Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Tue, 20 Dec 2022 08:53:37 +0100 Subject: [PATCH 09/13] Fixed build warnings in ManagementApi --- ...troller.cs => ByIdDictionaryController.cs} | 0 .../Dictionary/ImportDictionaryController.cs | 6 +- .../Controllers/Security/Paths.cs | 4 +- ...e.cs => TrackedReferenceControllerBase.cs} | 0 .../ManagementApiComposer.cs | 91 +++++++++++-------- .../Services/UploadFileService.cs | 12 ++- .../Umbraco.Cms.ManagementApi.csproj | 3 - 7 files changed, 65 insertions(+), 51 deletions(-) rename src/Umbraco.Cms.ManagementApi/Controllers/Dictionary/{ByKeyDictionaryController.cs => ByIdDictionaryController.cs} (100%) rename src/Umbraco.Cms.ManagementApi/Controllers/TrackedReference/{TrackedReferencesControllerBase.cs => TrackedReferenceControllerBase.cs} (100%) diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Dictionary/ByKeyDictionaryController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Dictionary/ByIdDictionaryController.cs similarity index 100% rename from src/Umbraco.Cms.ManagementApi/Controllers/Dictionary/ByKeyDictionaryController.cs rename to src/Umbraco.Cms.ManagementApi/Controllers/Dictionary/ByIdDictionaryController.cs diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Dictionary/ImportDictionaryController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Dictionary/ImportDictionaryController.cs index cbf9a0b9c7..b3c21446c9 100644 --- a/src/Umbraco.Cms.ManagementApi/Controllers/Dictionary/ImportDictionaryController.cs +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Dictionary/ImportDictionaryController.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Extensions; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services; using Umbraco.Cms.ManagementApi.Services; @@ -13,18 +14,15 @@ namespace Umbraco.Cms.ManagementApi.Controllers.Dictionary; public class ImportDictionaryController : DictionaryControllerBase { - private readonly IHostingEnvironment _hostingEnvironment; private readonly IDictionaryService _dictionaryService; private readonly IWebHostEnvironment _webHostEnvironment; private readonly ILoadDictionaryItemService _loadDictionaryItemService; public ImportDictionaryController( - IHostingEnvironment hostingEnvironment, IDictionaryService dictionaryService, IWebHostEnvironment webHostEnvironment, ILoadDictionaryItemService loadDictionaryItemService) { - _hostingEnvironment = hostingEnvironment; _dictionaryService = dictionaryService; _webHostEnvironment = webHostEnvironment; _loadDictionaryItemService = loadDictionaryItemService; @@ -41,7 +39,7 @@ public class ImportDictionaryController : DictionaryControllerBase return NotFound(); } - var filePath = Path.Combine(_hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Data), file); + var filePath = Path.Combine(_webHostEnvironment.MapPathContentRoot(Constants.SystemDirectories.Data), file); if (_webHostEnvironment.ContentRootFileProvider.GetFileInfo(filePath) is null) { return await Task.FromResult(NotFound()); diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Security/Paths.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Security/Paths.cs index 3ce1b7c3c6..77d69d90ab 100644 --- a/src/Umbraco.Cms.ManagementApi/Controllers/Security/Paths.cs +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Security/Paths.cs @@ -4,9 +4,9 @@ public static class Paths { public const string BackOfficeApiEndpointTemplate = "security/back-office"; - public static string BackOfficeApiAuthorizationEndpoint = BackOfficeApiEndpointPath($"{BackOfficeApiEndpointTemplate}/authorize"); + public static readonly string BackOfficeApiAuthorizationEndpoint = BackOfficeApiEndpointPath($"{BackOfficeApiEndpointTemplate}/authorize"); - public static string BackOfficeApiTokenEndpoint = BackOfficeApiEndpointPath($"{BackOfficeApiEndpointTemplate}/token"); + public static readonly string BackOfficeApiTokenEndpoint = BackOfficeApiEndpointPath($"{BackOfficeApiEndpointTemplate}/token"); private static string BackOfficeApiEndpointPath(string relativePath) => $"/umbraco/management/api/v1.0/{relativePath}"; } diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/TrackedReference/TrackedReferencesControllerBase.cs b/src/Umbraco.Cms.ManagementApi/Controllers/TrackedReference/TrackedReferenceControllerBase.cs similarity index 100% rename from src/Umbraco.Cms.ManagementApi/Controllers/TrackedReference/TrackedReferencesControllerBase.cs rename to src/Umbraco.Cms.ManagementApi/Controllers/TrackedReference/TrackedReferenceControllerBase.cs diff --git a/src/Umbraco.Cms.ManagementApi/ManagementApiComposer.cs b/src/Umbraco.Cms.ManagementApi/ManagementApiComposer.cs index f11290e76e..88d7602fdd 100644 --- a/src/Umbraco.Cms.ManagementApi/ManagementApiComposer.cs +++ b/src/Umbraco.Cms.ManagementApi/ManagementApiComposer.cs @@ -10,15 +10,16 @@ using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Options; using Microsoft.OpenApi.Models; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.ManagementApi.Configuration; +using Umbraco.Cms.ManagementApi.Controllers.Security; using Umbraco.Cms.ManagementApi.DependencyInjection; using Umbraco.Cms.ManagementApi.OpenApi; using Umbraco.Cms.Web.Common.ApplicationBuilder; using Umbraco.Extensions; -using Umbraco.New.Cms.Core; using Umbraco.New.Cms.Core.Models.Configuration; using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment; @@ -77,18 +78,22 @@ public class ManagementApiComposer : IComposer if (string.IsNullOrWhiteSpace(relativePath)) { - throw new Exception($"There is no relative path for controller action {e.ActionDescriptor.RouteValues["controller"]}"); + throw new Exception( + $"There is no relative path for controller action {e.ActionDescriptor.RouteValues["controller"]}"); } // Remove the prefixed base path with version, e.g. /umbraco/management/api/v1/tracked-reference/{id} => tracked-reference/{id} - var unprefixedRelativePath = OperationIdRegexes.VersionPrefixRegex().Replace(relativePath, string.Empty); + var unprefixedRelativePath = + OperationIdRegexes.VersionPrefixRegex().Replace(relativePath, string.Empty); // Remove template placeholders, e.g. tracked-reference/{id} => tracked-reference/Id - var formattedOperationId = OperationIdRegexes.TemplatePlaceholdersRegex().Replace(unprefixedRelativePath, m => $"By{m.Groups[1].Value.ToFirstUpper()}"); + var formattedOperationId = OperationIdRegexes.TemplatePlaceholdersRegex() + .Replace(unprefixedRelativePath, m => $"By{m.Groups[1].Value.ToFirstUpper()}"); // Remove dashes (-) and slashes (/) and convert the following letter to uppercase with // the word "By" in front, e.g. tracked-reference/Id => TrackedReferenceById - formattedOperationId = OperationIdRegexes.ToCamelCaseRegex().Replace(formattedOperationId, m => m.Groups[1].Value.ToUpper()); + formattedOperationId = OperationIdRegexes.ToCamelCaseRegex() + .Replace(formattedOperationId, m => m.Groups[1].Value.ToUpper()); // Return the operation ID with the formatted http method verb in front, e.g. GetTrackedReferenceById return $"{httpMethod}{formattedOperationId.ToFirstUpper()}"; @@ -99,33 +104,41 @@ public class ManagementApiComposer : IComposer { Title = ApiTitle, Version = DefaultApiVersion.ToString(), - Description = "This shows all APIs available in this version of Umbraco - including all the legacy apis that are available for backward compatibility" + Description = + "This shows all APIs available in this version of Umbraco - including all the legacy apis that are available for backward compatibility" }); swaggerGenOptions.DocInclusionPredicate((_, api) => !string.IsNullOrWhiteSpace(api.GroupName)); - swaggerGenOptions.TagActionsBy(api => new [] { api.GroupName }); + swaggerGenOptions.TagActionsBy(api => new[] { api.GroupName }); // see https://github.com/domaindrivendev/Swashbuckle.AspNetCore#change-operation-sort-order-eg-for-ui-sorting string ActionSortKeySelector(ApiDescription apiDesc) - => $"{apiDesc.GroupName}_{apiDesc.ActionDescriptor.AttributeRouteInfo?.Template ?? apiDesc.ActionDescriptor.RouteValues["controller"]}_{apiDesc.ActionDescriptor.RouteValues["action"]}_{apiDesc.HttpMethod}"; + { + return + $"{apiDesc.GroupName}_{apiDesc.ActionDescriptor.AttributeRouteInfo?.Template ?? apiDesc.ActionDescriptor.RouteValues["controller"]}_{apiDesc.ActionDescriptor.RouteValues["action"]}_{apiDesc.HttpMethod}"; + } + swaggerGenOptions.OrderActionsBy(ActionSortKeySelector); - swaggerGenOptions.AddSecurityDefinition("OAuth", new OpenApiSecurityScheme - { - In = ParameterLocation.Header, - Name = "Umbraco", - Type = SecuritySchemeType.OAuth2, - Description = "Umbraco Authentication", - Flows = new OpenApiOAuthFlows + swaggerGenOptions.AddSecurityDefinition( + "OAuth", + new OpenApiSecurityScheme { - AuthorizationCode = new OpenApiOAuthFlow + In = ParameterLocation.Header, + Name = "Umbraco", + Type = SecuritySchemeType.OAuth2, + Description = "Umbraco Authentication", + Flows = new OpenApiOAuthFlows { - AuthorizationUrl = new Uri(Controllers.Security.Paths.BackOfficeApiAuthorizationEndpoint, UriKind.Relative), - TokenUrl = new Uri(Controllers.Security.Paths.BackOfficeApiTokenEndpoint, UriKind.Relative) + AuthorizationCode = new OpenApiOAuthFlow + { + AuthorizationUrl = + new Uri(Paths.BackOfficeApiAuthorizationEndpoint, UriKind.Relative), + TokenUrl = new Uri(Paths.BackOfficeApiTokenEndpoint, UriKind.Relative) + } } - } - }); + }); swaggerGenOptions.AddSecurityRequirement(new OpenApiSecurityRequirement { @@ -134,13 +147,9 @@ public class ManagementApiComposer : IComposer { new OpenApiSecurityScheme { - Reference = new OpenApiReference - { - Id = "OAuth", - Type = ReferenceType.SecurityScheme - } + Reference = new OpenApiReference { Id = "OAuth", Type = ReferenceType.SecurityScheme } }, - new List { } + new List() } }); @@ -175,8 +184,10 @@ public class ManagementApiComposer : IComposer applicationBuilder.UseWhen( httpContext => { - GlobalSettings? settings = httpContext.RequestServices.GetRequiredService>().Value; - IHostingEnvironment hostingEnvironment = httpContext.RequestServices.GetRequiredService(); + GlobalSettings? settings = httpContext.RequestServices + .GetRequiredService>().Value; + IHostingEnvironment hostingEnvironment = + httpContext.RequestServices.GetRequiredService(); var officePath = settings.GetBackOfficePath(hostingEnvironment); return httpContext.Request.Path.Value?.StartsWith($"{officePath}/management/api/") ?? false; @@ -197,7 +208,7 @@ public class ManagementApiComposer : IComposer Detail = exception.StackTrace, Status = StatusCodes.Status500InternalServerError, Instance = exception.GetType().Name, - Type = "Error", + Type = "Error" }; await context.Response.WriteAsJsonAsync(response); })); @@ -216,14 +227,19 @@ public class ManagementApiComposer : IComposer applicationBuilder.UseSwagger(swaggerOptions => { - swaggerOptions.RouteTemplate = $"{officePath.TrimStart(Core.Constants.CharArrays.ForwardSlash)}/swagger/{{documentName}}/swagger.json"; + swaggerOptions.RouteTemplate = + $"{officePath.TrimStart(Constants.CharArrays.ForwardSlash)}/swagger/{{documentName}}/swagger.json"; }); - applicationBuilder.UseSwaggerUI(swaggerUiOptions => + applicationBuilder.UseSwaggerUI( + swaggerUiOptions => { - swaggerUiOptions.SwaggerEndpoint($"{officePath}/swagger/v1/swagger.json", $"{ApiTitle} {DefaultApiVersion}"); - swaggerUiOptions.RoutePrefix = $"{officePath.TrimStart(Core.Constants.CharArrays.ForwardSlash)}/swagger"; + swaggerUiOptions.SwaggerEndpoint( + $"{officePath}/swagger/v1/swagger.json", + $"{ApiTitle} {DefaultApiVersion}"); + swaggerUiOptions.RoutePrefix = + $"{officePath.TrimStart(Constants.CharArrays.ForwardSlash)}/swagger"; - swaggerUiOptions.OAuthClientId(Constants.OauthClientIds.Swagger); + swaggerUiOptions.OAuthClientId(New.Cms.Core.Constants.OauthClientIds.Swagger); swaggerUiOptions.OAuthUsePkce(); }); } @@ -243,11 +259,12 @@ public class ManagementApiComposer : IComposer // Serve contract endpoints.MapGet($"{officePath}/management/api/openapi.json", async context => { - await context.Response.SendFileAsync(new EmbeddedFileProvider(GetType().Assembly).GetFileInfo("OpenApi.json")); + await context.Response.SendFileAsync( + new EmbeddedFileProvider(GetType().Assembly).GetFileInfo("OpenApi.json")); }); }); - } - )); + })); }); } } + diff --git a/src/Umbraco.Cms.ManagementApi/Services/UploadFileService.cs b/src/Umbraco.Cms.ManagementApi/Services/UploadFileService.cs index 20a43511ea..27de5dc545 100644 --- a/src/Umbraco.Cms.ManagementApi/Services/UploadFileService.cs +++ b/src/Umbraco.Cms.ManagementApi/Services/UploadFileService.cs @@ -1,22 +1,24 @@ using System.Xml; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Hosting; using Umbraco.Cms.Core; -using Umbraco.Cms.Core.Hosting; +using Umbraco.Cms.Core.Extensions; using Umbraco.Cms.Core.Services; using Umbraco.Cms.ManagementApi.Models; using Umbraco.Extensions; +using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment; namespace Umbraco.Cms.ManagementApi.Services; public class UploadFileService : IUploadFileService { - private readonly IHostingEnvironment _hostingEnvironment; + private readonly IHostEnvironment _hostEnvironment; private readonly ILocalizedTextService _localizedTextService; - public UploadFileService(IHostingEnvironment hostingEnvironment, ILocalizedTextService localizedTextService) + public UploadFileService(IHostEnvironment hostEnvironment, ILocalizedTextService localizedTextService) { - _hostingEnvironment = hostingEnvironment; + _hostEnvironment = hostEnvironment; _localizedTextService = localizedTextService; } @@ -25,7 +27,7 @@ public class UploadFileService : IUploadFileService var formFileUploadResult = new FormFileUploadResult(); var fileName = file.FileName.Trim(Constants.CharArrays.DoubleQuote); var ext = fileName.Substring(fileName.LastIndexOf('.') + 1).ToLower(); - var root = _hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.TempFileUploads); + var root = _hostEnvironment.MapPathContentRoot(Constants.SystemDirectories.TempFileUploads); formFileUploadResult.TemporaryPath = Path.Combine(root, fileName); if (!Path.GetFullPath(formFileUploadResult.TemporaryPath).StartsWith(Path.GetFullPath(root))) diff --git a/src/Umbraco.Cms.ManagementApi/Umbraco.Cms.ManagementApi.csproj b/src/Umbraco.Cms.ManagementApi/Umbraco.Cms.ManagementApi.csproj index 3fe326cf44..8c2d96457e 100644 --- a/src/Umbraco.Cms.ManagementApi/Umbraco.Cms.ManagementApi.csproj +++ b/src/Umbraco.Cms.ManagementApi/Umbraco.Cms.ManagementApi.csproj @@ -13,9 +13,6 @@ - - - From a364141efa0003f155b905d298390ebe3ca06663 Mon Sep 17 00:00:00 2001 From: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Date: Tue, 20 Dec 2022 09:53:54 +0100 Subject: [PATCH 10/13] V10: Dont disable invited users (#13600) * Dont allow disable user when invited * Use data instead of selection * return succesfully disabled users * Disable disable button when invited * Add integration tests * Remove unused usings * Update src/Umbraco.Web.BackOffice/Controllers/UsersController.cs Co-authored-by: Kenn Jacobsen * Create DisabledUsersModel * use data.disabledUsers * Return OK if no users to be saved * User disabledUsersModel Co-authored-by: Zeegaan Co-authored-by: Kenn Jacobsen --- .../Controllers/UsersController.cs | 35 +++++++-- .../Models/DisabledUsersModel.cs | 13 ++++ .../users/views/users/users.controller.js | 3 +- .../Controllers/UsersControllerTests.cs | 72 +++++++++++++++++-- 4 files changed, 110 insertions(+), 13 deletions(-) create mode 100644 src/Umbraco.Web.Common/Models/DisabledUsersModel.cs diff --git a/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs b/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs index f734d8626b..04e4c64645 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs @@ -35,6 +35,7 @@ using Umbraco.Cms.Web.BackOffice.Security; using Umbraco.Cms.Web.Common.ActionsResults; using Umbraco.Cms.Web.Common.Attributes; using Umbraco.Cms.Web.Common.Authorization; +using Umbraco.Cms.Web.Common.Models; using Umbraco.Cms.Web.Common.Security; using Umbraco.Extensions; @@ -785,22 +786,44 @@ public class UsersController : BackOfficeNotificationsController return ValidationProblem("The current user cannot disable itself"); } - IUser[] users = _userService.GetUsersById(userIds).ToArray(); + var users = _userService.GetUsersById(userIds).ToList(); + List skippedUsers = new(); foreach (IUser u in users) { + if (u.UserState is UserState.Invited) + { + _logger.LogWarning("Could not disable invited user {Username}", u.Name); + skippedUsers.Add(u); + continue; + } + u.IsApproved = false; u.InvitedDate = null; } - _userService.Save(users); + users = users.Except(skippedUsers).ToList(); - if (users.Length > 1) + if (users.Any()) { - return Ok(_localizedTextService.Localize("speechBubbles", "disableUsersSuccess", - new[] { userIds.Length.ToString() })); + _userService.Save(users); + } + else + { + return Ok(new DisabledUsersModel()); } - return Ok(_localizedTextService.Localize("speechBubbles", "disableUserSuccess", new[] { users[0].Name })); + var disabledUsersModel = new DisabledUsersModel + { + DisabledUserIds = users.Select(x => x.Id), + }; + + var message= users.Count > 1 + ? _localizedTextService.Localize("speechBubbles", "disableUsersSuccess", new[] { userIds.Length.ToString() }) + : _localizedTextService.Localize("speechBubbles", "disableUserSuccess", new[] { users[0].Name }); + + var header = _localizedTextService.Localize("general", "success"); + disabledUsersModel.Notifications.Add(new BackOfficeNotification(header, message, NotificationStyle.Success)); + return Ok(disabledUsersModel); } /// diff --git a/src/Umbraco.Web.Common/Models/DisabledUsersModel.cs b/src/Umbraco.Web.Common/Models/DisabledUsersModel.cs new file mode 100644 index 0000000000..f6712ab193 --- /dev/null +++ b/src/Umbraco.Web.Common/Models/DisabledUsersModel.cs @@ -0,0 +1,13 @@ +using System.Runtime.Serialization; +using Umbraco.Cms.Core.Models.ContentEditing; + +namespace Umbraco.Cms.Web.Common.Models; + +[DataContract] +public class DisabledUsersModel : INotificationModel +{ + public List Notifications { get; } = new(); + + [DataMember(Name = "disabledUserIds")] + public IEnumerable DisabledUserIds { get; set; } = Enumerable.Empty(); +} diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js index 99cbe31f66..823f7ee6fd 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js @@ -334,7 +334,7 @@ vm.disableUserButtonState = "busy"; usersResource.disableUsers(vm.selection).then(function (data) { // update userState - vm.selection.forEach(function (userId) { + data.disabledUserIds.forEach(function (userId) { var user = getUserFromArrayById(userId, vm.users); if (user) { user.userState = "Disabled"; @@ -808,6 +808,7 @@ if (user.userDisplayState && user.userDisplayState.key === "Invited") { vm.allowEnableUser = false; + vm.allowDisableUser = false; } if (user.userDisplayState && user.userDisplayState.key === "LockedOut") { diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/Controllers/UsersControllerTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/Controllers/UsersControllerTests.cs index 6fcd1d7eee..c89841260a 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/Controllers/UsersControllerTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/Controllers/UsersControllerTests.cs @@ -1,14 +1,9 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; -using System.Linq; using System.Net; -using System.Net.Http; using System.Net.Mime; using System.Text; -using System.Threading.Tasks; using Newtonsoft.Json; using NUnit.Framework; using Umbraco.Cms.Core; @@ -21,7 +16,7 @@ using Umbraco.Cms.Tests.Common.Builders.Extensions; using Umbraco.Cms.Tests.Integration.TestServerTest; using Umbraco.Cms.Web.BackOffice.Controllers; using Umbraco.Cms.Web.Common.Formatters; -using Umbraco.Extensions; +using Umbraco.Cms.Web.Common.Models; namespace Umbraco.Cms.Tests.Integration.Umbraco.Web.BackOffice.Controllers; @@ -231,4 +226,69 @@ public class UsersControllerTests : UmbracoTestServerTestBase Assert.AreEqual($"Unlocked {users.Count()} users", actual.Message); }); } + + [Test] + public async Task Cannot_Disable_Invited_User() + { + var userService = GetRequiredService(); + + var user = new UserBuilder() + .AddUserGroup() + .WithAlias("writer") // Needs to be an existing alias + .Done() + .Build(); + + user.LastLoginDate = default; + user.InvitedDate = DateTime.Now; + userService.Save(user); + var createdUser = userService.GetByEmail("test@test.com"); + + // Act + var url = PrepareApiControllerUrl(x => x.PostDisableUsers(new []{createdUser.Id})); + var response = await Client.PostAsync(url, null); + + // Assert + Assert.Multiple(() => + { + Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); + var body = response.Content.ReadAsStringAsync().GetAwaiter().GetResult(); + + body = body.TrimStart(AngularJsonMediaTypeFormatter.XsrfPrefix); + }); + } + + [Test] + public async Task Can_Disable_Active_User() + { + var userService = GetRequiredService(); + + var user = new UserBuilder() + .AddUserGroup() + .WithAlias("writer") // Needs to be an existing alias + .Done() + .Build(); + + user.IsApproved = true; + userService.Save(user); + + var createdUser = userService.GetByEmail("test@test.com"); + + // Act + var url = PrepareApiControllerUrl(x => x.PostDisableUsers(new[] { createdUser.Id })); + var response = await Client.PostAsync(url, null); + + // Assert + Assert.Multiple(() => + { + Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); + var body = response.Content.ReadAsStringAsync().GetAwaiter().GetResult(); + + body = body.TrimStart(AngularJsonMediaTypeFormatter.XsrfPrefix); + var affectedUsers = JsonConvert.DeserializeObject(body, new JsonSerializerSettings { ContractResolver = new IgnoreRequiredAttributesResolver() }); + Assert.AreEqual(affectedUsers!.DisabledUserIds.First(), createdUser!.Id); + + var disabledUser = userService.GetByEmail("test@test.com"); + Assert.AreEqual(disabledUser!.UserState, UserState.Disabled); + }); + } } From f37c02ea79532b1953f85235f1e3fd983c42cb71 Mon Sep 17 00:00:00 2001 From: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Date: Tue, 20 Dec 2022 09:53:54 +0100 Subject: [PATCH 11/13] V10: Dont disable invited users (#13600) * Dont allow disable user when invited * Use data instead of selection * return succesfully disabled users * Disable disable button when invited * Add integration tests * Remove unused usings * Update src/Umbraco.Web.BackOffice/Controllers/UsersController.cs Co-authored-by: Kenn Jacobsen * Create DisabledUsersModel * use data.disabledUsers * Return OK if no users to be saved * User disabledUsersModel Co-authored-by: Zeegaan Co-authored-by: Kenn Jacobsen --- .../Controllers/UsersController.cs | 35 +++++++-- .../Models/DisabledUsersModel.cs | 13 ++++ .../users/views/users/users.controller.js | 3 +- .../Controllers/UsersControllerTests.cs | 72 +++++++++++++++++-- 4 files changed, 110 insertions(+), 13 deletions(-) create mode 100644 src/Umbraco.Web.Common/Models/DisabledUsersModel.cs diff --git a/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs b/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs index 916ff3d495..02eb9cda8e 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs @@ -35,6 +35,7 @@ using Umbraco.Cms.Web.BackOffice.Security; using Umbraco.Cms.Web.Common.ActionsResults; using Umbraco.Cms.Web.Common.Attributes; using Umbraco.Cms.Web.Common.Authorization; +using Umbraco.Cms.Web.Common.Models; using Umbraco.Cms.Web.Common.Security; using Umbraco.Extensions; @@ -795,22 +796,44 @@ public class UsersController : BackOfficeNotificationsController return ValidationProblem("The current user cannot disable itself"); } - IUser[] users = _userService.GetUsersById(userIds).ToArray(); + var users = _userService.GetUsersById(userIds).ToList(); + List skippedUsers = new(); foreach (IUser u in users) { + if (u.UserState is UserState.Invited) + { + _logger.LogWarning("Could not disable invited user {Username}", u.Name); + skippedUsers.Add(u); + continue; + } + u.IsApproved = false; u.InvitedDate = null; } - _userService.Save(users); + users = users.Except(skippedUsers).ToList(); - if (users.Length > 1) + if (users.Any()) { - return Ok(_localizedTextService.Localize("speechBubbles", "disableUsersSuccess", - new[] { userIds.Length.ToString() })); + _userService.Save(users); + } + else + { + return Ok(new DisabledUsersModel()); } - return Ok(_localizedTextService.Localize("speechBubbles", "disableUserSuccess", new[] { users[0].Name })); + var disabledUsersModel = new DisabledUsersModel + { + DisabledUserIds = users.Select(x => x.Id), + }; + + var message= users.Count > 1 + ? _localizedTextService.Localize("speechBubbles", "disableUsersSuccess", new[] { userIds.Length.ToString() }) + : _localizedTextService.Localize("speechBubbles", "disableUserSuccess", new[] { users[0].Name }); + + var header = _localizedTextService.Localize("general", "success"); + disabledUsersModel.Notifications.Add(new BackOfficeNotification(header, message, NotificationStyle.Success)); + return Ok(disabledUsersModel); } /// diff --git a/src/Umbraco.Web.Common/Models/DisabledUsersModel.cs b/src/Umbraco.Web.Common/Models/DisabledUsersModel.cs new file mode 100644 index 0000000000..f6712ab193 --- /dev/null +++ b/src/Umbraco.Web.Common/Models/DisabledUsersModel.cs @@ -0,0 +1,13 @@ +using System.Runtime.Serialization; +using Umbraco.Cms.Core.Models.ContentEditing; + +namespace Umbraco.Cms.Web.Common.Models; + +[DataContract] +public class DisabledUsersModel : INotificationModel +{ + public List Notifications { get; } = new(); + + [DataMember(Name = "disabledUserIds")] + public IEnumerable DisabledUserIds { get; set; } = Enumerable.Empty(); +} diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js index 99cbe31f66..823f7ee6fd 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js @@ -334,7 +334,7 @@ vm.disableUserButtonState = "busy"; usersResource.disableUsers(vm.selection).then(function (data) { // update userState - vm.selection.forEach(function (userId) { + data.disabledUserIds.forEach(function (userId) { var user = getUserFromArrayById(userId, vm.users); if (user) { user.userState = "Disabled"; @@ -808,6 +808,7 @@ if (user.userDisplayState && user.userDisplayState.key === "Invited") { vm.allowEnableUser = false; + vm.allowDisableUser = false; } if (user.userDisplayState && user.userDisplayState.key === "LockedOut") { diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/Controllers/UsersControllerTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/Controllers/UsersControllerTests.cs index 6fcd1d7eee..c89841260a 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/Controllers/UsersControllerTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/Controllers/UsersControllerTests.cs @@ -1,14 +1,9 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; -using System.Linq; using System.Net; -using System.Net.Http; using System.Net.Mime; using System.Text; -using System.Threading.Tasks; using Newtonsoft.Json; using NUnit.Framework; using Umbraco.Cms.Core; @@ -21,7 +16,7 @@ using Umbraco.Cms.Tests.Common.Builders.Extensions; using Umbraco.Cms.Tests.Integration.TestServerTest; using Umbraco.Cms.Web.BackOffice.Controllers; using Umbraco.Cms.Web.Common.Formatters; -using Umbraco.Extensions; +using Umbraco.Cms.Web.Common.Models; namespace Umbraco.Cms.Tests.Integration.Umbraco.Web.BackOffice.Controllers; @@ -231,4 +226,69 @@ public class UsersControllerTests : UmbracoTestServerTestBase Assert.AreEqual($"Unlocked {users.Count()} users", actual.Message); }); } + + [Test] + public async Task Cannot_Disable_Invited_User() + { + var userService = GetRequiredService(); + + var user = new UserBuilder() + .AddUserGroup() + .WithAlias("writer") // Needs to be an existing alias + .Done() + .Build(); + + user.LastLoginDate = default; + user.InvitedDate = DateTime.Now; + userService.Save(user); + var createdUser = userService.GetByEmail("test@test.com"); + + // Act + var url = PrepareApiControllerUrl(x => x.PostDisableUsers(new []{createdUser.Id})); + var response = await Client.PostAsync(url, null); + + // Assert + Assert.Multiple(() => + { + Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); + var body = response.Content.ReadAsStringAsync().GetAwaiter().GetResult(); + + body = body.TrimStart(AngularJsonMediaTypeFormatter.XsrfPrefix); + }); + } + + [Test] + public async Task Can_Disable_Active_User() + { + var userService = GetRequiredService(); + + var user = new UserBuilder() + .AddUserGroup() + .WithAlias("writer") // Needs to be an existing alias + .Done() + .Build(); + + user.IsApproved = true; + userService.Save(user); + + var createdUser = userService.GetByEmail("test@test.com"); + + // Act + var url = PrepareApiControllerUrl(x => x.PostDisableUsers(new[] { createdUser.Id })); + var response = await Client.PostAsync(url, null); + + // Assert + Assert.Multiple(() => + { + Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); + var body = response.Content.ReadAsStringAsync().GetAwaiter().GetResult(); + + body = body.TrimStart(AngularJsonMediaTypeFormatter.XsrfPrefix); + var affectedUsers = JsonConvert.DeserializeObject(body, new JsonSerializerSettings { ContractResolver = new IgnoreRequiredAttributesResolver() }); + Assert.AreEqual(affectedUsers!.DisabledUserIds.First(), createdUser!.Id); + + var disabledUser = userService.GetByEmail("test@test.com"); + Assert.AreEqual(disabledUser!.UserState, UserState.Disabled); + }); + } } From d1fd1f16fdbc8abb23cf05bb63c77dd32d4c66fb Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Tue, 3 Jan 2023 15:34:23 +0100 Subject: [PATCH 12/13] Move resources and implementations for System.Text.Json for reuse across projects (#13626) * Moved resources and implementations for System.Text.Json for reuse across projects. * Move MvcBuilderExtensions as well --- .../Configuration/ConfigureMvcJsonOptions.cs | 4 ++-- .../DependencyInjection/MvcBuilderExtensions.cs | 4 ++-- .../Filters/JsonOptionsNameAttribute.cs | 2 +- .../Json/HttpContextJsonExtensions.cs | 4 ++-- .../Json/NamedSystemTextJsonInputFormatter.cs | 2 +- .../Json/NamedSystemTextJsonOutputFormatter.cs | 2 +- .../Controllers/ManagementApiControllerBase.cs | 2 +- src/Umbraco.Cms.Api.Management/ManagementApiComposer.cs | 3 ++- .../Serialization}/JsonObjectConverter.cs | 2 +- 9 files changed, 13 insertions(+), 12 deletions(-) rename src/{Umbraco.Cms.Api.Management => Umbraco.Cms.Api.Common}/Configuration/ConfigureMvcJsonOptions.cs (92%) rename src/{Umbraco.Cms.Api.Management => Umbraco.Cms.Api.Common}/DependencyInjection/MvcBuilderExtensions.cs (88%) rename src/{Umbraco.Cms.Api.Management => Umbraco.Cms.Api.Common}/Filters/JsonOptionsNameAttribute.cs (82%) rename src/{Umbraco.Cms.Api.Management => Umbraco.Cms.Api.Common}/Json/HttpContextJsonExtensions.cs (75%) rename src/{Umbraco.Cms.Api.Management => Umbraco.Cms.Api.Common}/Json/NamedSystemTextJsonInputFormatter.cs (93%) rename src/{Umbraco.Cms.Api.Management => Umbraco.Cms.Api.Common}/Json/NamedSystemTextJsonOutputFormatter.cs (93%) rename src/{Umbraco.Cms.Api.Management/Json => Umbraco.Infrastructure/Serialization}/JsonObjectConverter.cs (97%) diff --git a/src/Umbraco.Cms.Api.Management/Configuration/ConfigureMvcJsonOptions.cs b/src/Umbraco.Cms.Api.Common/Configuration/ConfigureMvcJsonOptions.cs similarity index 92% rename from src/Umbraco.Cms.Api.Management/Configuration/ConfigureMvcJsonOptions.cs rename to src/Umbraco.Cms.Api.Common/Configuration/ConfigureMvcJsonOptions.cs index 2bc51fa404..8edebec2db 100644 --- a/src/Umbraco.Cms.Api.Management/Configuration/ConfigureMvcJsonOptions.cs +++ b/src/Umbraco.Cms.Api.Common/Configuration/ConfigureMvcJsonOptions.cs @@ -1,9 +1,9 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Umbraco.Cms.Api.Management.Json; +using Umbraco.Cms.Api.Common.Json; -namespace Umbraco.Cms.Api.Management.Configuration; +namespace Umbraco.Cms.Api.Common.Configuration; public class ConfigureMvcJsonOptions : IConfigureOptions { diff --git a/src/Umbraco.Cms.Api.Management/DependencyInjection/MvcBuilderExtensions.cs b/src/Umbraco.Cms.Api.Common/DependencyInjection/MvcBuilderExtensions.cs similarity index 88% rename from src/Umbraco.Cms.Api.Management/DependencyInjection/MvcBuilderExtensions.cs rename to src/Umbraco.Cms.Api.Common/DependencyInjection/MvcBuilderExtensions.cs index 6faf2e46eb..483c30d54e 100644 --- a/src/Umbraco.Cms.Api.Management/DependencyInjection/MvcBuilderExtensions.cs +++ b/src/Umbraco.Cms.Api.Common/DependencyInjection/MvcBuilderExtensions.cs @@ -2,9 +2,9 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Umbraco.Cms.Api.Management.Configuration; +using Umbraco.Cms.Api.Common.Configuration; -namespace Umbraco.Cms.Api.Management.DependencyInjection; +namespace Umbraco.Cms.Api.Common.DependencyInjection; public static class MvcBuilderExtensions { diff --git a/src/Umbraco.Cms.Api.Management/Filters/JsonOptionsNameAttribute.cs b/src/Umbraco.Cms.Api.Common/Filters/JsonOptionsNameAttribute.cs similarity index 82% rename from src/Umbraco.Cms.Api.Management/Filters/JsonOptionsNameAttribute.cs rename to src/Umbraco.Cms.Api.Common/Filters/JsonOptionsNameAttribute.cs index 2c92a577e1..e9899531c0 100644 --- a/src/Umbraco.Cms.Api.Management/Filters/JsonOptionsNameAttribute.cs +++ b/src/Umbraco.Cms.Api.Common/Filters/JsonOptionsNameAttribute.cs @@ -1,4 +1,4 @@ -namespace Umbraco.Cms.Api.Management.Filters; +namespace Umbraco.Cms.Api.Common.Filters; [AttributeUsage(AttributeTargets.Class)] public class JsonOptionsNameAttribute : Attribute diff --git a/src/Umbraco.Cms.Api.Management/Json/HttpContextJsonExtensions.cs b/src/Umbraco.Cms.Api.Common/Json/HttpContextJsonExtensions.cs similarity index 75% rename from src/Umbraco.Cms.Api.Management/Json/HttpContextJsonExtensions.cs rename to src/Umbraco.Cms.Api.Common/Json/HttpContextJsonExtensions.cs index d891e4e82e..4b3a28ccc8 100644 --- a/src/Umbraco.Cms.Api.Management/Json/HttpContextJsonExtensions.cs +++ b/src/Umbraco.Cms.Api.Common/Json/HttpContextJsonExtensions.cs @@ -1,7 +1,7 @@ using Microsoft.AspNetCore.Http; -using Umbraco.Cms.Api.Management.Filters; +using Umbraco.Cms.Api.Common.Filters; -namespace Umbraco.Cms.Api.Management.Json; +namespace Umbraco.Cms.Api.Common.Json; public static class HttpContextJsonExtensions { diff --git a/src/Umbraco.Cms.Api.Management/Json/NamedSystemTextJsonInputFormatter.cs b/src/Umbraco.Cms.Api.Common/Json/NamedSystemTextJsonInputFormatter.cs similarity index 93% rename from src/Umbraco.Cms.Api.Management/Json/NamedSystemTextJsonInputFormatter.cs rename to src/Umbraco.Cms.Api.Common/Json/NamedSystemTextJsonInputFormatter.cs index 42548a1b33..47d2e33c0c 100644 --- a/src/Umbraco.Cms.Api.Management/Json/NamedSystemTextJsonInputFormatter.cs +++ b/src/Umbraco.Cms.Api.Common/Json/NamedSystemTextJsonInputFormatter.cs @@ -2,7 +2,7 @@ using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.Extensions.Logging; -namespace Umbraco.Cms.Api.Management.Json; +namespace Umbraco.Cms.Api.Common.Json; public class NamedSystemTextJsonInputFormatter : SystemTextJsonInputFormatter { diff --git a/src/Umbraco.Cms.Api.Management/Json/NamedSystemTextJsonOutputFormatter.cs b/src/Umbraco.Cms.Api.Common/Json/NamedSystemTextJsonOutputFormatter.cs similarity index 93% rename from src/Umbraco.Cms.Api.Management/Json/NamedSystemTextJsonOutputFormatter.cs rename to src/Umbraco.Cms.Api.Common/Json/NamedSystemTextJsonOutputFormatter.cs index fd5f6e158f..9759542828 100644 --- a/src/Umbraco.Cms.Api.Management/Json/NamedSystemTextJsonOutputFormatter.cs +++ b/src/Umbraco.Cms.Api.Common/Json/NamedSystemTextJsonOutputFormatter.cs @@ -1,7 +1,7 @@ using System.Text.Json; using Microsoft.AspNetCore.Mvc.Formatters; -namespace Umbraco.Cms.Api.Management.Json; +namespace Umbraco.Cms.Api.Common.Json; public class NamedSystemTextJsonOutputFormatter : SystemTextJsonOutputFormatter { diff --git a/src/Umbraco.Cms.Api.Management/Controllers/ManagementApiControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/ManagementApiControllerBase.cs index 9447deff07..70b5328517 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/ManagementApiControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/ManagementApiControllerBase.cs @@ -1,5 +1,5 @@ using Microsoft.AspNetCore.Mvc; -using Umbraco.Cms.Api.Management.Filters; +using Umbraco.Cms.Api.Common.Filters; using Umbraco.New.Cms.Core; namespace Umbraco.Cms.Api.Management.Controllers; diff --git a/src/Umbraco.Cms.Api.Management/ManagementApiComposer.cs b/src/Umbraco.Cms.Api.Management/ManagementApiComposer.cs index 513a8c0dfe..3a9da9b507 100644 --- a/src/Umbraco.Cms.Api.Management/ManagementApiComposer.cs +++ b/src/Umbraco.Cms.Api.Management/ManagementApiComposer.cs @@ -16,9 +16,10 @@ using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Api.Common.Configuration; +using Umbraco.Cms.Api.Common.DependencyInjection; using Umbraco.Cms.Api.Management.DependencyInjection; -using Umbraco.Cms.Api.Management.Json; using Umbraco.Cms.Api.Management.OpenApi; +using Umbraco.Cms.Infrastructure.Serialization; using Umbraco.Cms.Web.Common.ApplicationBuilder; using Umbraco.Extensions; using Umbraco.New.Cms.Core; diff --git a/src/Umbraco.Cms.Api.Management/Json/JsonObjectConverter.cs b/src/Umbraco.Infrastructure/Serialization/JsonObjectConverter.cs similarity index 97% rename from src/Umbraco.Cms.Api.Management/Json/JsonObjectConverter.cs rename to src/Umbraco.Infrastructure/Serialization/JsonObjectConverter.cs index 6f475e045a..a763c21a1c 100644 --- a/src/Umbraco.Cms.Api.Management/Json/JsonObjectConverter.cs +++ b/src/Umbraco.Infrastructure/Serialization/JsonObjectConverter.cs @@ -2,7 +2,7 @@ using System.Text.Json.Nodes; using System.Text.Json.Serialization; -namespace Umbraco.Cms.Api.Management.Json; +namespace Umbraco.Cms.Infrastructure.Serialization; public class JsonObjectConverter : JsonConverter { From b4ca2a66360d8f8e657b71229eeb327a68b319c4 Mon Sep 17 00:00:00 2001 From: Elitsa Marinovska <21998037+elit0451@users.noreply.github.com> Date: Wed, 4 Jan 2023 09:24:43 +0100 Subject: [PATCH 13/13] Fixing a few nullable reference types for log viewer (#13634) --- .../Logging/Viewer/ILogViewer.cs | 8 ++++---- .../Logging/Viewer/ILogViewerConfig.cs | 6 +++--- .../Logging/Viewer/LogViewerConfig.cs | 12 ++++++------ .../Logging/Viewer/SerilogLogViewerSourceBase.cs | 12 ++++-------- .../Controllers/LogViewerController.cs | 6 +++--- 5 files changed, 20 insertions(+), 24 deletions(-) diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/ILogViewer.cs b/src/Umbraco.Infrastructure/Logging/Viewer/ILogViewer.cs index 3fc763d92f..e16f06b231 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/ILogViewer.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/ILogViewer.cs @@ -1,4 +1,4 @@ -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models; namespace Umbraco.Cms.Core.Logging.Viewer; @@ -9,17 +9,17 @@ public interface ILogViewer /// /// Get all saved searches from your chosen data source /// - IReadOnlyList? GetSavedSearches(); + IReadOnlyList GetSavedSearches(); /// /// Adds a new saved search to chosen data source and returns the updated searches /// - IReadOnlyList? AddSavedSearch(string? name, string? query); + IReadOnlyList AddSavedSearch(string? name, string? query); /// /// Deletes a saved search to chosen data source and returns the remaining searches /// - IReadOnlyList? DeleteSavedSearch(string? name, string? query); + IReadOnlyList DeleteSavedSearch(string? name, string? query); /// /// A count of number of errors diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/ILogViewerConfig.cs b/src/Umbraco.Infrastructure/Logging/Viewer/ILogViewerConfig.cs index bdcbf64a94..a6b98291c9 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/ILogViewerConfig.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/ILogViewerConfig.cs @@ -2,9 +2,9 @@ namespace Umbraco.Cms.Core.Logging.Viewer; public interface ILogViewerConfig { - IReadOnlyList? GetSavedSearches(); + IReadOnlyList GetSavedSearches(); - IReadOnlyList? AddSavedSearch(string? name, string? query); + IReadOnlyList AddSavedSearch(string? name, string? query); - IReadOnlyList? DeleteSavedSearch(string? name, string? query); + IReadOnlyList DeleteSavedSearch(string? name, string? query); } diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/LogViewerConfig.cs b/src/Umbraco.Infrastructure/Logging/Viewer/LogViewerConfig.cs index e8b9de36d7..15f7f8996c 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/LogViewerConfig.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/LogViewerConfig.cs @@ -1,4 +1,4 @@ -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; using IScope = Umbraco.Cms.Infrastructure.Scoping.IScope; @@ -16,15 +16,15 @@ public class LogViewerConfig : ILogViewerConfig _scopeProvider = scopeProvider; } - public IReadOnlyList? GetSavedSearches() + public IReadOnlyList GetSavedSearches() { using IScope scope = _scopeProvider.CreateScope(autoComplete: true); - IEnumerable? logViewerQueries = _logViewerQueryRepository.GetMany(); - SavedLogSearch[]? result = logViewerQueries?.Select(x => new SavedLogSearch() { Name = x.Name, Query = x.Query }).ToArray(); + IEnumerable logViewerQueries = _logViewerQueryRepository.GetMany(); + SavedLogSearch[] result = logViewerQueries.Select(x => new SavedLogSearch() { Name = x.Name, Query = x.Query }).ToArray(); return result; } - public IReadOnlyList? AddSavedSearch(string? name, string? query) + public IReadOnlyList AddSavedSearch(string? name, string? query) { using IScope scope = _scopeProvider.CreateScope(autoComplete: true); _logViewerQueryRepository.Save(new LogViewerQuery(name, query)); @@ -32,7 +32,7 @@ public class LogViewerConfig : ILogViewerConfig return GetSavedSearches(); } - public IReadOnlyList? DeleteSavedSearch(string? name, string? query) + public IReadOnlyList DeleteSavedSearch(string? name, string? query) { using IScope scope = _scopeProvider.CreateScope(autoComplete: true); ILogViewerQuery? item = name is null ? null : _logViewerQueryRepository.GetByName(name); diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/SerilogLogViewerSourceBase.cs b/src/Umbraco.Infrastructure/Logging/Viewer/SerilogLogViewerSourceBase.cs index 4b41df90f7..c41d384640 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/SerilogLogViewerSourceBase.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/SerilogLogViewerSourceBase.cs @@ -1,8 +1,6 @@ -using System.Collections.ObjectModel; -using Microsoft.Extensions.DependencyInjection; +using System.Collections.ObjectModel; using Serilog; using Serilog.Events; -using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Models; using Umbraco.Extensions; @@ -12,26 +10,24 @@ public abstract class SerilogLogViewerSourceBase : ILogViewer { private readonly ILogLevelLoader _logLevelLoader; private readonly ILogViewerConfig _logViewerConfig; - private readonly ILogger _serilogLog; protected SerilogLogViewerSourceBase(ILogViewerConfig logViewerConfig, ILogLevelLoader logLevelLoader, ILogger serilogLog) { _logViewerConfig = logViewerConfig; _logLevelLoader = logLevelLoader; - _serilogLog = serilogLog; } public abstract bool CanHandleLargeLogs { get; } public abstract bool CheckCanOpenLogs(LogTimePeriod logTimePeriod); - public virtual IReadOnlyList? GetSavedSearches() + public virtual IReadOnlyList GetSavedSearches() => _logViewerConfig.GetSavedSearches(); - public virtual IReadOnlyList? AddSavedSearch(string? name, string? query) + public virtual IReadOnlyList AddSavedSearch(string? name, string? query) => _logViewerConfig.AddSavedSearch(name, query); - public virtual IReadOnlyList? DeleteSavedSearch(string? name, string? query) + public virtual IReadOnlyList DeleteSavedSearch(string? name, string? query) => _logViewerConfig.DeleteSavedSearch(name, query); public int GetNumberOfErrors(LogTimePeriod logTimePeriod) diff --git a/src/Umbraco.Web.BackOffice/Controllers/LogViewerController.cs b/src/Umbraco.Web.BackOffice/Controllers/LogViewerController.cs index 57ffac3f2f..ed3994e6df 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/LogViewerController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/LogViewerController.cs @@ -129,14 +129,14 @@ public class LogViewerController : BackOfficeNotificationsController } [HttpGet] - public IEnumerable? GetSavedSearches() => _logViewer.GetSavedSearches(); + public IEnumerable GetSavedSearches() => _logViewer.GetSavedSearches(); [HttpPost] - public IEnumerable? PostSavedSearch(SavedLogSearch item) => + public IEnumerable PostSavedSearch(SavedLogSearch item) => _logViewer.AddSavedSearch(item.Name, item.Query); [HttpPost] - public IEnumerable? DeleteSavedSearch(SavedLogSearch item) => + public IEnumerable DeleteSavedSearch(SavedLogSearch item) => _logViewer.DeleteSavedSearch(item.Name, item.Query); [HttpGet]