Merge branch 'v15/dev' into v16/merge-from-15
# Conflicts: # src/Umbraco.Cms.Api.Management/Controllers/PublishedCache/RebuildPublishedCacheController.cs # src/Umbraco.Cms.Api.Management/Factories/UserPresentationFactory.cs # src/Umbraco.Core/Persistence/Repositories/ITrackedReferencesRepository.cs # src/Umbraco.Core/Services/ContentEditingService.cs # src/Umbraco.Core/Services/DataTypeService.cs # src/Umbraco.Core/Services/IContentEditingService.cs # src/Umbraco.Core/Services/IDataTypeService.cs # src/Umbraco.Core/Services/ITrackedReferencesService.cs # src/Umbraco.Core/Services/RelationService.cs # src/Umbraco.Core/Services/TrackedReferencesService.cs # src/Umbraco.Infrastructure/Examine/Deferred/DeliveryApiContentIndexHandleContentTypeChanges.cs # src/Umbraco.Infrastructure/Examine/DeliveryApiIndexingHandler.cs # src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TrackedReferencesRepository.cs # src/Umbraco.Web.UI.Client/src/external/backend-api/src/sdk.gen.ts # src/Umbraco.Web.UI.Client/src/mocks/data/document-blueprint/document-blueprint.data.ts # src/Umbraco.Web.UI.Client/src/mocks/data/document/document.db.ts # src/Umbraco.Web.UI.Client/src/packages/core/router/modal-registration/modal-route-registration.controller.ts # src/Umbraco.Web.UI.Client/src/packages/core/router/route/route.context.ts # src/Umbraco.Web.UI.Client/src/packages/core/router/route/route.interface.ts # src/Umbraco.Web.UI.Client/src/packages/core/router/route/router-slot.element.ts # src/Umbraco.Web.UI.Client/src/packages/core/router/router-slot/model.ts # src/Umbraco.Web.UI.Client/src/packages/data-type/reference/repository/data-type-reference.server.data.ts # src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/repository/document-publishing.server.data-source.ts # src/Umbraco.Web.UI.Client/src/packages/documents/documents/rollback/entity-action/rollback.action.ts # tests/Umbraco.Tests.AcceptanceTest/package-lock.json # tests/Umbraco.Tests.AcceptanceTest/package.json # tests/Umbraco.Tests.Common/Builders/UserGroupBuilder.cs # tests/Umbraco.Tests.Integration/Umbraco.Core/Services/TemporaryFileServiceTests.cs
This commit is contained in:
@@ -5,6 +5,7 @@ using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Models.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.Navigation;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
@@ -19,6 +20,7 @@ public sealed class ApiContentRouteBuilder : IApiContentRouteBuilder
|
||||
private readonly IPublishedContentCache _contentCache;
|
||||
private readonly IDocumentNavigationQueryService _navigationQueryService;
|
||||
private readonly IPublishStatusQueryService _publishStatusQueryService;
|
||||
private readonly IDocumentUrlService _documentUrlService;
|
||||
private RequestHandlerSettings _requestSettings;
|
||||
|
||||
public ApiContentRouteBuilder(
|
||||
@@ -29,7 +31,8 @@ public sealed class ApiContentRouteBuilder : IApiContentRouteBuilder
|
||||
IOptionsMonitor<RequestHandlerSettings> requestSettings,
|
||||
IPublishedContentCache contentCache,
|
||||
IDocumentNavigationQueryService navigationQueryService,
|
||||
IPublishStatusQueryService publishStatusQueryService)
|
||||
IPublishStatusQueryService publishStatusQueryService,
|
||||
IDocumentUrlService documentUrlService)
|
||||
{
|
||||
_apiContentPathProvider = apiContentPathProvider;
|
||||
_variationContextAccessor = variationContextAccessor;
|
||||
@@ -37,11 +40,35 @@ public sealed class ApiContentRouteBuilder : IApiContentRouteBuilder
|
||||
_contentCache = contentCache;
|
||||
_navigationQueryService = navigationQueryService;
|
||||
_publishStatusQueryService = publishStatusQueryService;
|
||||
_documentUrlService = documentUrlService;
|
||||
_globalSettings = globalSettings.Value;
|
||||
_requestSettings = requestSettings.CurrentValue;
|
||||
requestSettings.OnChange(settings => _requestSettings = settings);
|
||||
}
|
||||
|
||||
[Obsolete("Use the non-obsolete constructor, scheduled for removal in v17")]
|
||||
public ApiContentRouteBuilder(
|
||||
IApiContentPathProvider apiContentPathProvider,
|
||||
IOptions<GlobalSettings> globalSettings,
|
||||
IVariationContextAccessor variationContextAccessor,
|
||||
IRequestPreviewService requestPreviewService,
|
||||
IOptionsMonitor<RequestHandlerSettings> requestSettings,
|
||||
IPublishedContentCache contentCache,
|
||||
IDocumentNavigationQueryService navigationQueryService,
|
||||
IPublishStatusQueryService publishStatusQueryService)
|
||||
: this(
|
||||
apiContentPathProvider,
|
||||
globalSettings,
|
||||
variationContextAccessor,
|
||||
requestPreviewService,
|
||||
requestSettings,
|
||||
contentCache,
|
||||
navigationQueryService,
|
||||
publishStatusQueryService,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IDocumentUrlService>())
|
||||
{
|
||||
}
|
||||
|
||||
[Obsolete("Use the non-obsolete constructor, scheduled for removal in v17")]
|
||||
public ApiContentRouteBuilder(
|
||||
IApiContentPathProvider apiContentPathProvider,
|
||||
@@ -59,7 +86,8 @@ public sealed class ApiContentRouteBuilder : IApiContentRouteBuilder
|
||||
requestSettings,
|
||||
contentCache,
|
||||
navigationQueryService,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IPublishStatusQueryService>())
|
||||
StaticServiceProvider.Instance.GetRequiredService<IPublishStatusQueryService>(),
|
||||
StaticServiceProvider.Instance.GetRequiredService<IDocumentUrlService>())
|
||||
{
|
||||
}
|
||||
|
||||
@@ -113,7 +141,7 @@ public sealed class ApiContentRouteBuilder : IApiContentRouteBuilder
|
||||
// we can perform fallback to the content route.
|
||||
if (IsInvalidContentPath(contentPath))
|
||||
{
|
||||
contentPath = _contentCache.GetRouteById(content.Id, culture) ?? contentPath;
|
||||
contentPath = _documentUrlService.GetLegacyRouteFormat(content.Key, culture ?? _variationContextAccessor.VariationContext?.Culture, isPreview);
|
||||
}
|
||||
|
||||
// if the content path has still not been resolved as a valid path, the content is un-routable in this culture
|
||||
@@ -125,7 +153,9 @@ public sealed class ApiContentRouteBuilder : IApiContentRouteBuilder
|
||||
: null;
|
||||
}
|
||||
|
||||
return contentPath;
|
||||
return _requestSettings.AddTrailingSlash
|
||||
? contentPath?.EnsureEndsWith('/')
|
||||
: contentPath?.TrimEnd('/');
|
||||
}
|
||||
|
||||
private string ContentPreviewPath(IPublishedContent content) => $"{Constants.DeliveryApi.Routing.PreviewContentPathPrefix}{content.Key:D}{(_requestSettings.AddTrailingSlash ? "/" : string.Empty)}";
|
||||
|
||||
@@ -86,10 +86,35 @@ public sealed class ApiPublishedContentCache : IApiPublishedContentCache
|
||||
_variationContextAccessor.VariationContext?.Culture,
|
||||
_requestPreviewService.IsPreview());
|
||||
|
||||
// in multi-root settings, we've historically resolved all but the first root by their ID + URL segment,
|
||||
// e.g. "1234/second-root-url-segment". in V15+, IDocumentUrlService won't resolve this anymore; it will
|
||||
// however resolve "1234/" correctly, so to remain backwards compatible, we need to perform this extra step.
|
||||
var verifyUrlSegment = false;
|
||||
if (documentKey is null && route.TrimEnd('/').CountOccurrences("/") is 1)
|
||||
{
|
||||
documentKey = _apiDocumentUrlService.GetDocumentKeyByRoute(
|
||||
route[..(route.IndexOf('/') + 1)],
|
||||
_variationContextAccessor.VariationContext?.Culture,
|
||||
_requestPreviewService.IsPreview());
|
||||
verifyUrlSegment = true;
|
||||
}
|
||||
|
||||
IPublishedContent? content = documentKey.HasValue
|
||||
? _publishedContentCache.GetById(isPreviewMode, documentKey.Value)
|
||||
: null;
|
||||
|
||||
// the additional look-up above can result in false positives; if attempting to request a non-existing child to
|
||||
// the currently contextualized request root (either by start item or by domain), the root content key might
|
||||
// get resolved. to counter for this, we compare the requested URL segment with the resolved content URL segment.
|
||||
if (content is not null && verifyUrlSegment)
|
||||
{
|
||||
var expectedUrlSegment = route[(route.IndexOf('/') + 1)..];
|
||||
if (content.UrlSegment != expectedUrlSegment)
|
||||
{
|
||||
content = null;
|
||||
}
|
||||
}
|
||||
|
||||
return ContentOrNullIfDisallowed(content);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
using Umbraco.Cms.Core.Events;
|
||||
using Umbraco.Cms.Core.Models.Membership;
|
||||
|
||||
namespace Umbraco.Cms.Core.Notifications;
|
||||
|
||||
public class UserPasswordResettingNotification : CancelableObjectNotification<IUser>
|
||||
{
|
||||
public UserPasswordResettingNotification(IUser target, EventMessages messages) : base(target, messages)
|
||||
{
|
||||
}
|
||||
|
||||
public IUser User => Target;
|
||||
}
|
||||
@@ -41,11 +41,8 @@ public interface ITrackedReferencesRepository
|
||||
long skip,
|
||||
long take,
|
||||
bool filterMustBeIsDependency,
|
||||
out long totalRecords)
|
||||
{
|
||||
totalRecords = 0;
|
||||
return [];
|
||||
}
|
||||
out long totalRecords);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets a page of items used in any kind of relation from selected integer ids.
|
||||
|
||||
@@ -29,8 +29,7 @@ public interface ITrackedReferencesService
|
||||
/// dependencies (isDependency field is set to true).
|
||||
/// </param>
|
||||
/// <returns>A paged result of <see cref="RelationItemModel" /> objects.</returns>
|
||||
Task<PagedModel<RelationItemModel>> GetPagedRelationsForRecycleBinAsync(UmbracoObjectTypes objectType, long skip, long take, bool filterMustBeIsDependency)
|
||||
=> Task.FromResult(new PagedModel<RelationItemModel>(0, []));
|
||||
Task<PagedModel<RelationItemModel>> GetPagedRelationsForRecycleBinAsync(UmbracoObjectTypes objectType, long skip, long take, bool filterMustBeIsDependency);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a paged result of the descending items that have any references, given a parent id.
|
||||
|
||||
@@ -10,7 +10,6 @@ public class TrackedReferencesService : ITrackedReferencesService
|
||||
private readonly IEntityService _entityService;
|
||||
private readonly ITrackedReferencesRepository _trackedReferencesRepository;
|
||||
|
||||
|
||||
public TrackedReferencesService(
|
||||
ITrackedReferencesRepository trackedReferencesRepository,
|
||||
ICoreScopeProvider scopeProvider,
|
||||
@@ -30,7 +29,7 @@ public class TrackedReferencesService : ITrackedReferencesService
|
||||
return Task.FromResult(pagedModel);
|
||||
}
|
||||
|
||||
public async Task<PagedModel<RelationItemModel>> GetPagedRelationsForRecycleBinAsync(UmbracoObjectTypes objectType, long skip, long take, bool filterMustBeIsDependency)
|
||||
public Task<PagedModel<RelationItemModel>> GetPagedRelationsForRecycleBinAsync(UmbracoObjectTypes objectType, long skip, long take, bool filterMustBeIsDependency)
|
||||
{
|
||||
Guid objectTypeKey = objectType switch
|
||||
{
|
||||
@@ -42,7 +41,7 @@ public class TrackedReferencesService : ITrackedReferencesService
|
||||
using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true);
|
||||
IEnumerable<RelationItemModel> items = _trackedReferencesRepository.GetPagedRelationsForRecycleBin(objectTypeKey, skip, take, filterMustBeIsDependency, out var totalItems);
|
||||
var pagedModel = new PagedModel<RelationItemModel>(totalItems, items);
|
||||
return await Task.FromResult(pagedModel);
|
||||
return Task.FromResult(pagedModel);
|
||||
}
|
||||
|
||||
public Task<PagedModel<RelationItemModel>> GetPagedDescendantsInReferencesAsync(Guid parentKey, long skip, long take, bool filterMustBeIsDependency)
|
||||
|
||||
@@ -1130,7 +1130,21 @@ internal partial class UserService : RepositoryService, IUserService
|
||||
return keys;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<Attempt<PasswordChangedModel, UserOperationStatus>> ChangePasswordAsync(Guid performingUserKey, ChangeUserPasswordModel model)
|
||||
{
|
||||
IServiceScope serviceScope = _serviceScopeFactory.CreateScope();
|
||||
IBackOfficeUserStore userStore = serviceScope.ServiceProvider.GetRequiredService<IBackOfficeUserStore>();
|
||||
IUser? performingUser = await userStore.GetAsync(performingUserKey);
|
||||
if (performingUser is null)
|
||||
{
|
||||
return Attempt.FailWithStatus(UserOperationStatus.MissingUser, new PasswordChangedModel());
|
||||
}
|
||||
|
||||
return await ChangePasswordAsync(performingUser, model);
|
||||
}
|
||||
|
||||
private async Task<Attempt<PasswordChangedModel, UserOperationStatus>> ChangePasswordAsync(IUser performingUser, ChangeUserPasswordModel model)
|
||||
{
|
||||
IServiceScope serviceScope = _serviceScopeFactory.CreateScope();
|
||||
using ICoreScope scope = ScopeProvider.CreateCoreScope();
|
||||
@@ -1147,12 +1161,6 @@ internal partial class UserService : RepositoryService, IUserService
|
||||
return Attempt.FailWithStatus(UserOperationStatus.InvalidUserType, new PasswordChangedModel());
|
||||
}
|
||||
|
||||
IUser? performingUser = await userStore.GetAsync(performingUserKey);
|
||||
if (performingUser is null)
|
||||
{
|
||||
return Attempt.FailWithStatus(UserOperationStatus.MissingUser, new PasswordChangedModel());
|
||||
}
|
||||
|
||||
// require old password for self change when outside of invite or resetByToken flows
|
||||
if (performingUser.UserState != UserState.Invited && performingUser.Username == user.Username && string.IsNullOrEmpty(model.OldPassword) && string.IsNullOrEmpty(model.ResetPasswordToken))
|
||||
{
|
||||
@@ -1176,12 +1184,13 @@ internal partial class UserService : RepositoryService, IUserService
|
||||
IBackOfficePasswordChanger passwordChanger = serviceScope.ServiceProvider.GetRequiredService<IBackOfficePasswordChanger>();
|
||||
Attempt<PasswordChangedModel?> result = await passwordChanger.ChangeBackOfficePassword(
|
||||
new ChangeBackOfficeUserPasswordModel
|
||||
{
|
||||
NewPassword = model.NewPassword,
|
||||
OldPassword = model.OldPassword,
|
||||
User = user,
|
||||
ResetPasswordToken = model.ResetPasswordToken,
|
||||
}, performingUser);
|
||||
{
|
||||
NewPassword = model.NewPassword,
|
||||
OldPassword = model.OldPassword,
|
||||
User = user,
|
||||
ResetPasswordToken = model.ResetPasswordToken,
|
||||
},
|
||||
performingUser);
|
||||
|
||||
if (result.Success is false)
|
||||
{
|
||||
@@ -1967,9 +1976,26 @@ internal partial class UserService : RepositoryService, IUserService
|
||||
public async Task<Attempt<PasswordChangedModel, UserOperationStatus>> ResetPasswordAsync(Guid userKey, string token, string password)
|
||||
{
|
||||
using ICoreScope scope = ScopeProvider.CreateCoreScope();
|
||||
IServiceScope serviceScope = _serviceScopeFactory.CreateScope();
|
||||
|
||||
EventMessages evtMsgs = EventMessagesFactory.Get();
|
||||
IBackOfficeUserStore userStore = serviceScope.ServiceProvider.GetRequiredService<IBackOfficeUserStore>();
|
||||
|
||||
IUser? user = await userStore.GetAsync(userKey);
|
||||
if (user is null)
|
||||
{
|
||||
return Attempt.FailWithStatus(UserOperationStatus.UserNotFound, new PasswordChangedModel());
|
||||
}
|
||||
|
||||
var savingNotification = new UserPasswordResettingNotification(user, evtMsgs);
|
||||
if (await scope.Notifications.PublishCancelableAsync(savingNotification))
|
||||
{
|
||||
scope.Complete();
|
||||
return Attempt.FailWithStatus(UserOperationStatus.CancelledByNotification, new PasswordChangedModel());
|
||||
}
|
||||
|
||||
Attempt<PasswordChangedModel, UserOperationStatus> changePasswordAttempt =
|
||||
await ChangePasswordAsync(userKey, new ChangeUserPasswordModel
|
||||
await ChangePasswordAsync(user, new ChangeUserPasswordModel
|
||||
{
|
||||
NewPassword = password,
|
||||
UserKey = userKey,
|
||||
|
||||
Reference in New Issue
Block a user