diff --git a/src/Umbraco.Cms.Api.Management/Controllers/AuditLog/AuditLogControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/AuditLog/AuditLogControllerBase.cs new file mode 100644 index 0000000000..29293c67b7 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/AuditLog/AuditLogControllerBase.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.Routing; + +namespace Umbraco.Cms.Api.Management.Controllers.AuditLog; + +[ApiController] +[VersionedApiBackOfficeRoute("audit-log")] +[ApiExplorerSettings(GroupName = "Audit Log")] +[ApiVersion("1.0")] +public class AuditLogControllerBase : ManagementApiControllerBase +{ +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/AuditLog/ByKeyAuditLogController.cs b/src/Umbraco.Cms.Api.Management/Controllers/AuditLog/ByKeyAuditLogController.cs new file mode 100644 index 0000000000..b94139dd8c --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/AuditLog/ByKeyAuditLogController.cs @@ -0,0 +1,39 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Common.ViewModels.Pagination; +using Umbraco.Cms.Api.Management.Factories; +using Umbraco.Cms.Api.Management.ViewModels.AuditLogs; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.New.Cms.Core.Models; + +namespace Umbraco.Cms.Api.Management.Controllers.AuditLog; + +public class ByKeyAuditLogController : AuditLogControllerBase +{ + private readonly IAuditService _auditService; + private readonly IAuditLogViewModelFactory _auditLogViewModelFactory; + + public ByKeyAuditLogController(IAuditService auditService, IAuditLogViewModelFactory auditLogViewModelFactory) + { + _auditService = auditService; + _auditLogViewModelFactory = auditLogViewModelFactory; + } + + [HttpGet("{key:guid}")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task ByKey(Guid key, Direction orderDirection = Direction.Descending, DateTime? sinceDate = null, int skip = 0, int take = 100) + { + PagedModel result = await _auditService.GetItemsByKeyAsync(key, skip, take, orderDirection, sinceDate); + IEnumerable mapped = _auditLogViewModelFactory.CreateAuditLogViewModel(result.Items); + var viewModel = new PagedViewModel + { + Total = result.Total, + Items = mapped, + }; + + return Ok(viewModel); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/AuditLog/ByTypeAuditLogController.cs b/src/Umbraco.Cms.Api.Management/Controllers/AuditLog/ByTypeAuditLogController.cs new file mode 100644 index 0000000000..0fd14ea891 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/AuditLog/ByTypeAuditLogController.cs @@ -0,0 +1,37 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Common.ViewModels.Pagination; +using Umbraco.Cms.Api.Management.Factories; +using Umbraco.Cms.Api.Management.ViewModels.AuditLogs; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Api.Management.Controllers.AuditLog; + +public class ByTypeAuditLogController : AuditLogControllerBase +{ + private readonly IAuditService _auditService; + private readonly IAuditLogViewModelFactory _auditLogViewModelFactory; + + public ByTypeAuditLogController(IAuditService auditService, IAuditLogViewModelFactory auditLogViewModelFactory) + { + _auditService = auditService; + _auditLogViewModelFactory = auditLogViewModelFactory; + } + + [HttpGet("type/{logType}")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task ByType(AuditType logType, DateTime? sinceDate = null, int skip = 0, int take = 100) + { + IAuditItem[] result = _auditService.GetLogs(logType, sinceDate).ToArray(); + IEnumerable mapped = _auditLogViewModelFactory.CreateAuditLogViewModel(result.Skip(skip).Take(take)); + var viewModel = new PagedViewModel + { + Total = result.Length, + Items = mapped, + }; + + return await Task.FromResult(Ok(viewModel)); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/AuditLog/CurrentUserAuditLogController.cs b/src/Umbraco.Cms.Api.Management/Controllers/AuditLog/CurrentUserAuditLogController.cs new file mode 100644 index 0000000000..d34192756a --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/AuditLog/CurrentUserAuditLogController.cs @@ -0,0 +1,69 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Common.ViewModels.Pagination; +using Umbraco.Cms.Api.Management.Factories; +using Umbraco.Cms.Api.Management.ViewModels.AuditLogs; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Exceptions; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.New.Cms.Core.Models; + +namespace Umbraco.Cms.Api.Management.Controllers.AuditLog; + +public class CurrentUserAuditLogController : AuditLogControllerBase +{ + private readonly IAuditService _auditService; + private readonly IAuditLogViewModelFactory _auditLogViewModelFactory; + private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; + private readonly IUserService _userService; + + public CurrentUserAuditLogController( + IAuditService auditService, + IAuditLogViewModelFactory auditLogViewModelFactory, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor, + IUserService userService) + { + _auditService = auditService; + _auditLogViewModelFactory = auditLogViewModelFactory; + _backOfficeSecurityAccessor = backOfficeSecurityAccessor; + _userService = userService; + } + + [HttpGet] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task CurrentUser(Direction orderDirection = Direction.Descending, DateTime? sinceDate = null, int skip = 0, int take = 100) + { + // FIXME: Pull out current backoffice user when its implemented. + // var userId = _backOfficeSecurityAccessor.BackOfficeSecurity?.GetUserId().Result ?? -1; + var userId = Constants.Security.SuperUserId; + + IUser? user = _userService.GetUserById(userId); + + if (user is null) + { + throw new PanicException("Could not find current user"); + } + + PagedModel result = await _auditService.GetPagedItemsByUserAsync( + user.Key, + skip, + take, + orderDirection, + null, + sinceDate); + + IEnumerable mapped = _auditLogViewModelFactory.CreateAuditLogWithUsernameViewModels(result.Items.Skip(skip).Take(take)); + var viewModel = new PagedViewModel + { + Total = result.Total, + Items = mapped, + }; + + return Ok(viewModel); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Dictionary/DictionaryControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Dictionary/DictionaryControllerBase.cs index 02e49021d2..5f417acf76 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Dictionary/DictionaryControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Dictionary/DictionaryControllerBase.cs @@ -1,4 +1,4 @@ -using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Api.Common.Builders; using Umbraco.Cms.Api.Management.Routing; diff --git a/src/Umbraco.Cms.Api.Management/Controllers/LogViewer/LogLevelCountLogViewerController.cs b/src/Umbraco.Cms.Api.Management/Controllers/LogViewer/LogLevelCountLogViewerController.cs index 145bcd1eb8..942deffbbf 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/LogViewer/LogLevelCountLogViewerController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/LogViewer/LogLevelCountLogViewerController.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Common.ViewModels.Pagination; using Umbraco.Cms.Api.Management.ViewModels.LogViewer; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Logging.Viewer; @@ -29,7 +30,7 @@ public class LogLevelCountLogViewerController : LogViewerControllerBase [HttpGet("level-count")] [MapToApiVersion("1.0")] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(typeof(LogLevelCountsViewModel), StatusCodes.Status200OK)] public async Task LogLevelCounts(DateTime? startDate = null, DateTime? endDate = null) { Attempt logLevelCountsAttempt = diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Package/AllMigrationStatusPackageController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Package/AllMigrationStatusPackageController.cs new file mode 100644 index 0000000000..afb7ffda69 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/Package/AllMigrationStatusPackageController.cs @@ -0,0 +1,44 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Common.ViewModels.Pagination; +using Umbraco.Cms.Api.Management.ViewModels.Package; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Packaging; +using Umbraco.Cms.Core.Services; +using Umbraco.New.Cms.Core.Models; + +namespace Umbraco.Cms.Api.Management.Controllers.Package; + +public class AllMigrationStatusPackageController : PackageControllerBase +{ + private readonly IPackagingService _packagingService; + private readonly IUmbracoMapper _umbracoMapper; + + public AllMigrationStatusPackageController(IPackagingService packagingService, IUmbracoMapper umbracoMapper) + { + _packagingService = packagingService; + _umbracoMapper = umbracoMapper; + } + + /// + /// Gets a paginated list of the migration status of each installed package. + /// + /// The amount of items to skip. + /// The amount of items to take. + /// The paged result of the installed packages migration status. + [HttpGet("migration-status")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task>> AllMigrationStatuses(int skip = 0, int take = 100) + { + PagedModel migrationPlans = await _packagingService.GetInstalledPackagesFromMigrationPlansAsync(skip, take); + + IEnumerable viewModels = _umbracoMapper.MapEnumerable(migrationPlans.Items); + + return Ok(new PagedViewModel() + { + Total = migrationPlans.Total, + Items = viewModels, + }); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Package/Created/AllCreatedPackageController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Package/Created/AllCreatedPackageController.cs new file mode 100644 index 0000000000..2500071124 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/Package/Created/AllCreatedPackageController.cs @@ -0,0 +1,42 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Common.ViewModels.Pagination; +using Umbraco.Cms.Api.Management.ViewModels.Package; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Packaging; +using Umbraco.Cms.Core.Services; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Api.Management.Controllers.Package.Created; + +public class AllCreatedPackageController : CreatedPackageControllerBase +{ + private readonly IPackagingService _packagingService; + private readonly IUmbracoMapper _umbracoMapper; + + public AllCreatedPackageController(IPackagingService packagingService, IUmbracoMapper umbracoMapper) + { + _packagingService = packagingService; + _umbracoMapper = umbracoMapper; + } + + /// + /// Gets a paginated list of all created packages. + /// + /// The amount of items to skip. + /// The amount of items to take. + /// The paged result of the created packages. + [HttpGet] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task>> All(int skip = 0, int take = 100) + { + IEnumerable createdPackages = _packagingService + .GetAllCreatedPackages() + .WhereNotNull() + .Skip(skip) + .Take(take); + + return await Task.FromResult(Ok(_umbracoMapper.Map>(createdPackages))); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Package/Created/ByKeyCreatedPackageController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Package/Created/ByKeyCreatedPackageController.cs new file mode 100644 index 0000000000..5d53ef3548 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/Package/Created/ByKeyCreatedPackageController.cs @@ -0,0 +1,41 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.ViewModels.Package; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Packaging; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Api.Management.Controllers.Package.Created; + +public class ByKeyCreatedPackageController : CreatedPackageControllerBase +{ + private readonly IPackagingService _packagingService; + private readonly IUmbracoMapper _umbracoMapper; + + public ByKeyCreatedPackageController(IPackagingService packagingService, IUmbracoMapper umbracoMapper) + { + _packagingService = packagingService; + _umbracoMapper = umbracoMapper; + } + + /// + /// Gets a package by key. + /// + /// The key of the package. + /// The package or not found result. + [HttpGet("{key:guid}")] + [MapToApiVersion("1.0")] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(PackageDefinitionViewModel), StatusCodes.Status200OK)] + public async Task> ByKey(Guid key) + { + PackageDefinition? package = await _packagingService.GetCreatedPackageByKeyAsync(key); + + if (package is null) + { + return NotFound(); + } + + return Ok(_umbracoMapper.Map(package)); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Package/Created/CreateCreatedPackageController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Package/Created/CreateCreatedPackageController.cs new file mode 100644 index 0000000000..9edeb024a9 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/Package/Created/CreateCreatedPackageController.cs @@ -0,0 +1,49 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.Factories; +using Umbraco.Cms.Api.Management.ViewModels.Package; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Packaging; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Api.Management.Controllers.Package.Created; + +public class CreateCreatedPackageController : CreatedPackageControllerBase +{ + private readonly IPackagingService _packagingService; + private readonly IPackageDefinitionFactory _packageDefinitionFactory; + private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; + + public CreateCreatedPackageController( + IPackagingService packagingService, + IPackageDefinitionFactory packageDefinitionFactory, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor) + { + _packagingService = packagingService; + _packageDefinitionFactory = packageDefinitionFactory; + _backOfficeSecurityAccessor = backOfficeSecurityAccessor; + } + + /// + /// Creates a package. + /// + /// The model containing the data for a new package. + /// The created package. + [HttpPost] + [MapToApiVersion("1.0")] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status201Created)] + public async Task Create(PackageCreateModel packageCreateModel) + { + PackageDefinition packageDefinition = _packageDefinitionFactory.CreatePackageDefinition(packageCreateModel); + + Attempt result = await _packagingService.CreateCreatedPackageAsync(packageDefinition, CurrentUserId(_backOfficeSecurityAccessor)); + + return result.Success + ? CreatedAtAction(controller => nameof(controller.ByKey), packageDefinition.PackageId) + : PackageOperationStatusResult(result.Status); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Package/Created/CreatedPackageControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Package/Created/CreatedPackageControllerBase.cs new file mode 100644 index 0000000000..a1d9bd9cd9 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/Package/Created/CreatedPackageControllerBase.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.Routing; + +namespace Umbraco.Cms.Api.Management.Controllers.Package.Created; + +[ApiController] +[VersionedApiBackOfficeRoute("package/created")] +[ApiExplorerSettings(GroupName = "Package")] +[ApiVersion("1.0")] +public class CreatedPackageControllerBase : PackageControllerBase +{ +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Package/Created/DeleteCreatedPackageController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Package/Created/DeleteCreatedPackageController.cs new file mode 100644 index 0000000000..cbc71b7d20 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/Package/Created/DeleteCreatedPackageController.cs @@ -0,0 +1,40 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Packaging; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Api.Management.Controllers.Package.Created; + +public class DeleteCreatedPackageController : CreatedPackageControllerBase +{ + private readonly IPackagingService _packagingService; + private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; + + public DeleteCreatedPackageController(IPackagingService packagingService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor) + { + _packagingService = packagingService; + _backOfficeSecurityAccessor = backOfficeSecurityAccessor; + } + + /// + /// Deletes a package with a given key. + /// + /// The key of the package. + /// The result of the deletion. + [HttpDelete("{key:guid}")] + [MapToApiVersion("1.0")] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task Delete(Guid key) + { + Attempt result = + await _packagingService.DeleteCreatedPackageAsync(key, CurrentUserId(_backOfficeSecurityAccessor)); + + return result.Success + ? Ok() + : PackageOperationStatusResult(result.Status); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Package/Created/DownloadCreatedPackageController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Package/Created/DownloadCreatedPackageController.cs new file mode 100644 index 0000000000..1c13d0596b --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/Package/Created/DownloadCreatedPackageController.cs @@ -0,0 +1,59 @@ +using System.Net; +using System.Net.Mime; +using System.Text; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Net.Http.Headers; +using Umbraco.Cms.Core.Packaging; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Api.Management.Controllers.Package.Created; + +public class DownloadCreatedPackageController : CreatedPackageControllerBase +{ + private readonly IPackagingService _packagingService; + + public DownloadCreatedPackageController(IPackagingService packagingService) => _packagingService = packagingService; + + /// + /// Downloads a package XML or ZIP file. + /// + /// The key of the package. + /// The XML or ZIP file of the package or not found result. + [HttpGet("{key:guid}/download")] + [MapToApiVersion("1.0")] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)] + public async Task Download(Guid key) + { + PackageDefinition? package = await _packagingService.GetCreatedPackageByKeyAsync(key); + + if (package is null) + { + return NotFound(); + } + + Stream? fileStream = _packagingService.GetPackageFileStream(package); + if (fileStream is null) + { + return NotFound(); + } + + var fileName = Path.GetFileName(package.PackagePath); + Encoding encoding = Encoding.UTF8; + + var contentDisposition = new ContentDisposition + { + FileName = WebUtility.UrlEncode(fileName), + DispositionType = DispositionTypeNames.Attachment + }; + + Response.Headers.Add("Content-Disposition", contentDisposition.ToString()); + + var result = new FileStreamResult( + fileStream, + new MediaTypeHeaderValue(MediaTypeNames.Application.Octet) { Charset = encoding.WebName }); + + return result; + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Package/Created/UpdateCreatedPackageController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Package/Created/UpdateCreatedPackageController.cs new file mode 100644 index 0000000000..895d889193 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/Package/Created/UpdateCreatedPackageController.cs @@ -0,0 +1,57 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.ViewModels.Package; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Packaging; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Api.Management.Controllers.Package.Created; + +public class UpdateCreatedPackageController : CreatedPackageControllerBase +{ + private readonly IPackagingService _packagingService; + private readonly IUmbracoMapper _umbracoMapper; + private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; + + public UpdateCreatedPackageController( + IPackagingService packagingService, + IUmbracoMapper umbracoMapper, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor) + { + _packagingService = packagingService; + _umbracoMapper = umbracoMapper; + _backOfficeSecurityAccessor = backOfficeSecurityAccessor; + } + + /// + /// Updates a package. + /// + /// The key of the package. + /// The model containing the data for updating a package. + /// The created package. + [HttpPut("{key:guid}")] + [MapToApiVersion("1.0")] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task Update(Guid key, PackageUpdateModel packageUpdateModel) + { + PackageDefinition? package = await _packagingService.GetCreatedPackageByKeyAsync(key); + + if (package is null) + { + return NotFound(); + } + + // Macros are not included! + PackageDefinition packageDefinition = _umbracoMapper.Map(packageUpdateModel, package); + + Attempt result = await _packagingService.UpdateCreatedPackageAsync(packageDefinition, CurrentUserId(_backOfficeSecurityAccessor)); + + return result.Success + ? Ok() + : PackageOperationStatusResult(result.Status); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Package/PackageControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Package/PackageControllerBase.cs new file mode 100644 index 0000000000..0ec683d7b3 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/Package/PackageControllerBase.cs @@ -0,0 +1,40 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Common.Builders; +using Umbraco.Cms.Api.Management.Routing; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Api.Management.Controllers.Package; + +[ApiController] +[VersionedApiBackOfficeRoute("package")] +[ApiExplorerSettings(GroupName = "Package")] +[ApiVersion("1.0")] +public abstract class PackageControllerBase : ManagementApiControllerBase +{ + protected IActionResult PackageOperationStatusResult(PackageOperationStatus status) => + status switch + { + PackageOperationStatus.NotFound => NotFound("The package could not be found"), + PackageOperationStatus.DuplicateItemName => Conflict(new ProblemDetailsBuilder() + .WithTitle("Duplicate package name") + .WithDetail("Another package already exists with the attempted name.") + .Build()), + PackageOperationStatus.InvalidName => BadRequest(new ProblemDetailsBuilder() + .WithTitle("Invalid package name") + .WithDetail("The attempted package name does not represent a valid name for a package.") + .Build()), + _ => StatusCode(StatusCodes.Status500InternalServerError, "Unknown package operation status") + }; + + protected IActionResult PackageMigrationOperationStatusResult(PackageMigrationOperationStatus status) => + status switch + { + PackageMigrationOperationStatus.NotFound => NotFound("No migration plans were found for that package"), + PackageMigrationOperationStatus.CancelledByFailedMigration => Conflict(new ProblemDetailsBuilder() + .WithTitle("Package migration failed") + .WithDetail("Check log for full details about the failed migration.") + .Build()), + _ => StatusCode(StatusCodes.Status500InternalServerError, "Unknown package migration operation status") + }; +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Package/RunMigrationPackageController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Package/RunMigrationPackageController.cs new file mode 100644 index 0000000000..1c3fc6fd31 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/Package/RunMigrationPackageController.cs @@ -0,0 +1,34 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.Cms.Infrastructure.Install; + +namespace Umbraco.Cms.Api.Management.Controllers.Package; + +public class RunMigrationPackageController : PackageControllerBase +{ + private readonly PackageMigrationRunner _packageMigrationRunner; + + public RunMigrationPackageController(PackageMigrationRunner packageMigrationRunner) + => _packageMigrationRunner = packageMigrationRunner; + + /// + /// Runs all migration plans for a package with a given name if any are pending. + /// + /// The name of the package. + /// The result of running the package migrations. + [HttpPost("{name}/run-migration")] + [MapToApiVersion("1.0")] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status409Conflict)] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task RunMigrations(string name) + { + Attempt result = await _packageMigrationRunner.RunPendingPackageMigrations(name); + + return result.Success + ? Ok() + : PackageMigrationOperationStatusResult(result.Status); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Profiling/GetStatusProfilingController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Profiling/GetStatusProfilingController.cs new file mode 100644 index 0000000000..700689f0a3 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/Profiling/GetStatusProfilingController.cs @@ -0,0 +1,25 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.ViewModels.Profiling; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Api.Management.Controllers.Profiling; + +public class GetStatusProfilingController : ProfilingControllerBase +{ + private readonly IWebProfilerService _webProfilerService; + + public GetStatusProfilingController(IWebProfilerService webProfilerService) => _webProfilerService = webProfilerService; + + [HttpGet("status")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(ProfilingStatusViewModel), StatusCodes.Status200OK)] + public async Task Status() + { + var result = await _webProfilerService.GetStatus(); + return result.Success + ? Ok(new ProfilingStatusViewModel(result.Result)) + : WebProfilerOperationStatusResult(result.Status); + } +} + diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Profiling/ProfilingControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Profiling/ProfilingControllerBase.cs index 07b068f5c9..49be02c341 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Profiling/ProfilingControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Profiling/ProfilingControllerBase.cs @@ -1,5 +1,8 @@ -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Common.Builders; using Umbraco.Cms.Api.Management.Routing; +using Umbraco.Cms.Core.Services.OperationStatus; namespace Umbraco.Cms.Api.Management.Controllers.Profiling; @@ -9,4 +12,15 @@ namespace Umbraco.Cms.Api.Management.Controllers.Profiling; [ApiExplorerSettings(GroupName = "Profiling")] public class ProfilingControllerBase : ManagementApiControllerBase { + + protected IActionResult WebProfilerOperationStatusResult(WebProfilerOperationStatus status) => + status switch + { + WebProfilerOperationStatus.ExecutingUserNotFound => Unauthorized(new ProblemDetailsBuilder() + .WithTitle("Executing user not found") + .WithDetail("Executing this action requires a signed in user.") + .Build()), + + _ => StatusCode(StatusCodes.Status500InternalServerError, "Unknown profiling operation status") + }; } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Profiling/StatusProfilingController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Profiling/StatusProfilingController.cs deleted file mode 100644 index 5738655e14..0000000000 --- a/src/Umbraco.Cms.Api.Management/Controllers/Profiling/StatusProfilingController.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Umbraco.Cms.Core.Hosting; -using Umbraco.Cms.Api.Management.ViewModels.Profiling; - -namespace Umbraco.Cms.Api.Management.Controllers.Profiling; - -public class StatusProfilingController : ProfilingControllerBase -{ - private readonly IHostingEnvironment _hosting; - - public StatusProfilingController(IHostingEnvironment hosting) => _hosting = hosting; - - [HttpGet("status")] - [MapToApiVersion("1.0")] - [ProducesResponseType(typeof(ProfilingStatusViewModel), StatusCodes.Status200OK)] - public async Task> Status() - => await Task.FromResult(Ok(new ProfilingStatusViewModel(_hosting.IsDebugMode))); -} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Profiling/UpdateStatusProfilingController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Profiling/UpdateStatusProfilingController.cs new file mode 100644 index 0000000000..15ed5bae45 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/Profiling/UpdateStatusProfilingController.cs @@ -0,0 +1,25 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.ViewModels.Profiling; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Api.Management.Controllers.Profiling; + +public class UpdateStatusProfilingController : ProfilingControllerBase +{ + private readonly IWebProfilerService _webProfilerService; + + public UpdateStatusProfilingController(IWebProfilerService webProfilerService) => _webProfilerService = webProfilerService; + + [HttpPut("status")] + [MapToApiVersion("1.0")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task Status(ProfilingStatusViewModel model) + { + var result = await _webProfilerService.SetStatus(model.Enabled); + return result.Success + ? Ok() + : WebProfilerOperationStatusResult(result.Status); + } +} + diff --git a/src/Umbraco.Cms.Api.Management/Controllers/TrackedReference/ByIdTrackedReferenceController.cs b/src/Umbraco.Cms.Api.Management/Controllers/TrackedReference/ByIdTrackedReferenceController.cs index 39d2cfe3c2..57b24dd4b4 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/TrackedReference/ByIdTrackedReferenceController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/TrackedReference/ByIdTrackedReferenceController.cs @@ -27,16 +27,16 @@ public class ByIdTrackedReferenceController : TrackedReferenceControllerBase /// Used by info tabs on content, media etc. and for the delete and unpublish of single items. /// This is basically finding parents of relations. /// - [HttpGet("{id:int}")] + [HttpGet("{key:guid}")] [MapToApiVersion("1.0")] [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] public async Task>> Get( - int id, - long skip, - long take, - bool? filterMustBeIsDependency) + Guid key, + long skip = 0, + long take = 20, + bool filterMustBeIsDependency = false) { - PagedModel relationItems = _trackedReferencesService.GetPagedRelationsForItem(id, skip, take, filterMustBeIsDependency ?? false); + PagedModel relationItems = await _trackedReferencesService.GetPagedRelationsForItemAsync(key, skip, take, filterMustBeIsDependency); var pagedViewModel = new PagedViewModel { diff --git a/src/Umbraco.Cms.Api.Management/Controllers/TrackedReference/DescendantsTrackedReferenceController.cs b/src/Umbraco.Cms.Api.Management/Controllers/TrackedReference/DescendantsTrackedReferenceController.cs index 752b6d8846..899b8ff500 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/TrackedReference/DescendantsTrackedReferenceController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/TrackedReference/DescendantsTrackedReferenceController.cs @@ -28,12 +28,12 @@ public class DescendantsTrackedReferenceController : TrackedReferenceControllerB /// kind of relation. /// This is basically finding the descending items which are children in relations. /// - [HttpGet("descendants/{parentId:int}")] + [HttpGet("descendants/{parentKey:guid}")] [MapToApiVersion("1.0")] [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] - public async Task>> Descendants(int parentId, long skip, long take, bool? filterMustBeIsDependency) + public async Task>> Descendants(Guid parentKey, long skip, long take, bool filterMustBeIsDependency = true) { - PagedModel relationItems = _trackedReferencesSkipTakeService.GetPagedDescendantsInReferences(parentId, skip, take, filterMustBeIsDependency ?? true); + PagedModel relationItems = await _trackedReferencesSkipTakeService.GetPagedDescendantsInReferencesAsync(parentKey, skip, take, filterMustBeIsDependency); var pagedViewModel = new PagedViewModel { Total = relationItems.Total, diff --git a/src/Umbraco.Cms.Api.Management/Controllers/TrackedReference/ItemsTrackedReferenceController.cs b/src/Umbraco.Cms.Api.Management/Controllers/TrackedReference/ItemsTrackedReferenceController.cs index ecc0e3434d..59e3ba0503 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/TrackedReference/ItemsTrackedReferenceController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/TrackedReference/ItemsTrackedReferenceController.cs @@ -21,7 +21,7 @@ public class ItemsTrackedReferenceController : TrackedReferenceControllerBase } /// - /// Gets a page list of the items used in any kind of relation from selected integer ids. + /// Gets a page list of the items used in any kind of relation from selected keys. /// /// /// Used when bulk deleting content/media and bulk unpublishing content (delete and unpublish on List view). @@ -30,9 +30,9 @@ public class ItemsTrackedReferenceController : TrackedReferenceControllerBase [HttpGet("item")] [MapToApiVersion("1.0")] [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] - public async Task>> GetPagedReferencedItems([FromQuery]int[] ids, long skip, long take, bool? filterMustBeIsDependency) + public async Task>> GetPagedReferencedItems([FromQuery(Name="key")]SortedSet keys, long skip = 0, long take = 20, bool filterMustBeIsDependency = true) { - PagedModel relationItems = _trackedReferencesSkipTakeService.GetPagedItemsWithRelations(ids, skip, take, filterMustBeIsDependency ?? true); + PagedModel relationItems = await _trackedReferencesSkipTakeService.GetPagedItemsWithRelationsAsync(keys, skip, take, filterMustBeIsDependency); var pagedViewModel = new PagedViewModel { Total = relationItems.Total, diff --git a/src/Umbraco.Cms.Api.Management/DependencyInjection/AuditLogBuilderExtensions.cs b/src/Umbraco.Cms.Api.Management/DependencyInjection/AuditLogBuilderExtensions.cs new file mode 100644 index 0000000000..392e9ecf57 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/DependencyInjection/AuditLogBuilderExtensions.cs @@ -0,0 +1,15 @@ +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Api.Management.Factories; +using Umbraco.Cms.Core.DependencyInjection; + +namespace Umbraco.Cms.Api.Management.DependencyInjection; + +internal static class AuditLogBuilderExtensions +{ + internal static IUmbracoBuilder AddAuditLogs(this IUmbracoBuilder builder) + { + builder.Services.AddTransient(); + + return builder; + } +} diff --git a/src/Umbraco.Cms.Api.Management/DependencyInjection/PackageBuilderExtensions.cs b/src/Umbraco.Cms.Api.Management/DependencyInjection/PackageBuilderExtensions.cs new file mode 100644 index 0000000000..3fad1574f4 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/DependencyInjection/PackageBuilderExtensions.cs @@ -0,0 +1,19 @@ +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Api.Management.Factories; +using Umbraco.Cms.Api.Management.Mapping.Package; +using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.Mapping; + +namespace Umbraco.Cms.Api.Management.DependencyInjection; + +internal static class PackageBuilderExtensions +{ + internal static IUmbracoBuilder AddPackages(this IUmbracoBuilder builder) + { + builder.Services.AddTransient(); + + builder.WithCollectionBuilder().Add(); + + return builder; + } +} diff --git a/src/Umbraco.Cms.Api.Management/Factories/AuditLogViewModelFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/AuditLogViewModelFactory.cs new file mode 100644 index 0000000000..6cbaa5a794 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Factories/AuditLogViewModelFactory.cs @@ -0,0 +1,81 @@ +using Umbraco.Cms.Api.Management.ViewModels.AuditLogs; +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.IO; +using Umbraco.Cms.Core.Media; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Api.Management.Factories; + +public class AuditLogViewModelFactory : IAuditLogViewModelFactory +{ + private readonly IUserService _userService; + private readonly AppCaches _appCaches; + private readonly MediaFileManager _mediaFileManager; + private readonly IImageUrlGenerator _imageUrlGenerator; + private readonly IEntityService _entityService; + + public AuditLogViewModelFactory(IUserService userService, AppCaches appCaches, MediaFileManager mediaFileManager, IImageUrlGenerator imageUrlGenerator, IEntityService entityService) + { + _userService = userService; + _appCaches = appCaches; + _mediaFileManager = mediaFileManager; + _imageUrlGenerator = imageUrlGenerator; + _entityService = entityService; + } + + public IEnumerable CreateAuditLogViewModel(IEnumerable auditItems) => auditItems.Select(CreateAuditLogViewModel); + + public IEnumerable CreateAuditLogWithUsernameViewModels(IEnumerable auditItems) => auditItems.Select(CreateAuditLogWithUsernameViewModel); + + private AuditLogWithUsernameResponseModel CreateAuditLogWithUsernameViewModel(IAuditItem auditItem) + { + IEntitySlim? entitySlim = _entityService.Get(auditItem.Id); + + var target = new AuditLogWithUsernameResponseModel + { + Comment = auditItem.Comment, + EntityType = auditItem.EntityType, + EntityKey = entitySlim?.Key, + LogType = auditItem.AuditType, + Parameters = auditItem.Parameters, + Timestamp = auditItem.CreateDate, + }; + + IUser? user = _userService.GetUserById(auditItem.UserId); + if (user is null) + { + throw new ArgumentException($"Could not find user with id {auditItem.UserId}"); + } + + target.UserKey = user.Key; + target.UserAvatars = user.GetUserAvatarUrls(_appCaches.RuntimeCache, _mediaFileManager, _imageUrlGenerator); + target.UserName = user.Name; + return target; + } + + private AuditLogResponseModel CreateAuditLogViewModel(IAuditItem auditItem) + { + IEntitySlim? entitySlim = _entityService.Get(auditItem.Id); + var target = new AuditLogResponseModel + { + Comment = auditItem.Comment, + EntityType = auditItem.EntityType, + EntityKey = entitySlim?.Key, + LogType = auditItem.AuditType, + Parameters = auditItem.Parameters, + Timestamp = auditItem.CreateDate, + }; + + IUser? user = _userService.GetUserById(auditItem.UserId); + if (user is null) + { + throw new ArgumentException($"Could not find user with id {auditItem.UserId}"); + } + + target.UserKey = user.Key; + return target; + } +} diff --git a/src/Umbraco.Cms.Api.Management/Factories/IAuditLogViewModelFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/IAuditLogViewModelFactory.cs new file mode 100644 index 0000000000..798eb47e76 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Factories/IAuditLogViewModelFactory.cs @@ -0,0 +1,11 @@ +using Umbraco.Cms.Api.Management.ViewModels.AuditLogs; +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Api.Management.Factories; + +public interface IAuditLogViewModelFactory +{ + IEnumerable CreateAuditLogViewModel(IEnumerable auditItems); + + IEnumerable CreateAuditLogWithUsernameViewModels(IEnumerable auditItems); +} diff --git a/src/Umbraco.Cms.Api.Management/Factories/IPackageDefinitionFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/IPackageDefinitionFactory.cs new file mode 100644 index 0000000000..e6c207004a --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Factories/IPackageDefinitionFactory.cs @@ -0,0 +1,9 @@ +using Umbraco.Cms.Api.Management.ViewModels.Package; +using Umbraco.Cms.Core.Packaging; + +namespace Umbraco.Cms.Api.Management.Factories; + +public interface IPackageDefinitionFactory +{ + PackageDefinition CreatePackageDefinition(PackageCreateModel packageCreateModel); +} diff --git a/src/Umbraco.Cms.Api.Management/Factories/PackageDefinitionFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/PackageDefinitionFactory.cs new file mode 100644 index 0000000000..cce54c35d5 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Factories/PackageDefinitionFactory.cs @@ -0,0 +1,25 @@ +using Umbraco.Cms.Api.Management.ViewModels.Package; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Packaging; + +namespace Umbraco.Cms.Api.Management.Factories; + +internal class PackageDefinitionFactory : IPackageDefinitionFactory +{ + private readonly IUmbracoMapper _umbracoMapper; + + public PackageDefinitionFactory(IUmbracoMapper umbracoMapper) => _umbracoMapper = umbracoMapper; + + public PackageDefinition CreatePackageDefinition(PackageCreateModel packageCreateModel) + { + // Macros are not included! + PackageDefinition packageDefinition = _umbracoMapper.Map(packageCreateModel)!; + + // Temp Id, PackageId and PackagePath for the newly created package + packageDefinition.Id = 0; + packageDefinition.PackageId = Guid.Empty; + packageDefinition.PackagePath = string.Empty; + + return packageDefinition; + } +} diff --git a/src/Umbraco.Cms.Api.Management/ManagementApiComposer.cs b/src/Umbraco.Cms.Api.Management/ManagementApiComposer.cs index 1bb5f1ff4c..1123847943 100644 --- a/src/Umbraco.Cms.Api.Management/ManagementApiComposer.cs +++ b/src/Umbraco.Cms.Api.Management/ManagementApiComposer.cs @@ -25,6 +25,7 @@ public class ManagementApiComposer : IComposer .AddUpgrader() .AddSearchManagement() .AddTrees() + .AddAuditLogs() .AddDocuments() .AddDocumentTypes() .AddLanguages() @@ -38,6 +39,7 @@ public class ManagementApiComposer : IComposer .AddTemplates() .AddLogViewer() .AddUserGroups() + .AddPackages() .AddBackOfficeAuthentication() .AddApiVersioning() .AddSwaggerGen(); diff --git a/src/Umbraco.Cms.Api.Management/Mapping/Package/PackageViewModelMapDefinition.cs b/src/Umbraco.Cms.Api.Management/Mapping/Package/PackageViewModelMapDefinition.cs new file mode 100644 index 0000000000..873848c356 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Mapping/Package/PackageViewModelMapDefinition.cs @@ -0,0 +1,83 @@ +using Umbraco.Cms.Api.Common.ViewModels.Pagination; +using Umbraco.Cms.Api.Management.ViewModels.Package; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Packaging; + +namespace Umbraco.Cms.Api.Management.Mapping.Package; + +public class PackageViewModelMapDefinition : IMapDefinition +{ + public void DefineMaps(IUmbracoMapper mapper) + { + mapper.Define((_, _) => new PackageDefinition(), Map); + mapper.Define( + (_, _) => new PackageDefinitionViewModel + { + Name = string.Empty, + PackagePath = string.Empty + }, + Map); + mapper.Define((_, _) => new PackageMigrationStatusViewModel { PackageName = string.Empty }, Map); + mapper.Define, PagedViewModel>((_, _) => new PagedViewModel(), Map); + } + + // Umbraco.Code.MapAll -Id -PackageId -PackagePath -Macros + private static void Map(PackageModelBase source, PackageDefinition target, MapperContext context) + { + target.Name = source.Name; + target.ContentLoadChildNodes = source.ContentLoadChildNodes; + target.ContentNodeId = source.ContentNodeId; + target.Languages = source.Languages; + target.DictionaryItems = source.DictionaryItems; + target.Templates = source.Templates; + target.PartialViews = source.PartialViews; + target.DocumentTypes = source.DocumentTypes; + target.MediaTypes = source.MediaTypes; + target.Stylesheets = source.Stylesheets; + target.Scripts = source.Scripts; + target.DataTypes = source.DataTypes; + target.MediaUdis = source.MediaKeys.Select(x => new GuidUdi(Constants.UdiEntityType.Media, x)).ToList(); + target.MediaLoadChildNodes = source.MediaLoadChildNodes; + } + + // Umbraco.Code.MapAll + private static void Map(PackageDefinition source, PackageDefinitionViewModel target, MapperContext context) + { + target.Key = source.PackageId; + target.Name = source.Name; + target.PackagePath = source.PackagePath; + target.ContentNodeId = source.ContentNodeId; + target.ContentLoadChildNodes = source.ContentLoadChildNodes; + target.MediaKeys = source.MediaUdis.Select(x => x.Guid).ToList(); + target.MediaLoadChildNodes = source.MediaLoadChildNodes; + target.DocumentTypes = source.DocumentTypes; + target.MediaTypes = source.MediaTypes; + target.DataTypes = source.DataTypes; + target.Templates = source.Templates; + target.PartialViews = source.PartialViews; + target.Stylesheets = source.Stylesheets; + target.Scripts = source.Scripts; + target.Languages = source.Languages; + target.DictionaryItems = source.DictionaryItems; + } + + // Umbraco.Code.MapAll + private static void Map(InstalledPackage source, PackageMigrationStatusViewModel target, MapperContext context) + { + if (source.PackageName is not null) + { + target.PackageName = source.PackageName; + } + + target.HasPendingMigrations = source.HasPendingMigrations; + } + + // Umbraco.Code.MapAll + private static void Map(IEnumerable source, PagedViewModel target, MapperContext context) + { + PackageDefinition[] definitions = source.ToArray(); + target.Items = context.MapEnumerable(definitions); + target.Total = definitions.Length; + } +} diff --git a/src/Umbraco.Cms.Api.Management/Mapping/TrackedReferences/TrackedReferenceViewModelsMapDefinition.cs b/src/Umbraco.Cms.Api.Management/Mapping/TrackedReferences/TrackedReferenceViewModelsMapDefinition.cs index 9b36669fcf..f7b75cc692 100644 --- a/src/Umbraco.Cms.Api.Management/Mapping/TrackedReferences/TrackedReferenceViewModelsMapDefinition.cs +++ b/src/Umbraco.Cms.Api.Management/Mapping/TrackedReferences/TrackedReferenceViewModelsMapDefinition.cs @@ -23,6 +23,7 @@ public class TrackedReferenceViewModelsMapDefinition : IMapDefinition target.RelationTypeIsBidirectional = source.RelationTypeIsBidirectional; target.RelationTypeIsDependency = source.RelationTypeIsDependency; target.RelationTypeName = source.RelationTypeName; + target.NodePublished = source.NodePublished; } } diff --git a/src/Umbraco.Cms.Api.Management/OpenApi.json b/src/Umbraco.Cms.Api.Management/OpenApi.json index fb12d7a5c4..52824318c7 100644 --- a/src/Umbraco.Cms.Api.Management/OpenApi.json +++ b/src/Umbraco.Cms.Api.Management/OpenApi.json @@ -6,6 +6,181 @@ "version": "1.0" }, "paths": { + "/umbraco/management/api/v1/audit-log": { + "get": { + "tags": [ + "Audit Log" + ], + "operationId": "GetAuditLog", + "parameters": [ + { + "name": "orderDirection", + "in": "query", + "schema": { + "$ref": "#/components/schemas/DirectionModel" + } + }, + { + "name": "sinceDate", + "in": "query", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "skip", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + } + }, + { + "name": "take", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 100 + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedAuditLogWithUsernameResponseModel" + } + } + } + } + } + } + }, + "/umbraco/management/api/v1/audit-log/{key}": { + "get": { + "tags": [ + "Audit Log" + ], + "operationId": "GetAuditLogByKey", + "parameters": [ + { + "name": "key", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "orderDirection", + "in": "query", + "schema": { + "$ref": "#/components/schemas/DirectionModel" + } + }, + { + "name": "sinceDate", + "in": "query", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "skip", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + } + }, + { + "name": "take", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 100 + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedAuditLogResponseModel" + } + } + } + } + } + } + }, + "/umbraco/management/api/v1/audit-log/type/{logType}": { + "get": { + "tags": [ + "Audit Log" + ], + "operationId": "GetAuditLogTypeByLogType", + "parameters": [ + { + "name": "logType", + "in": "path", + "required": true, + "schema": { + "$ref": "#/components/schemas/AuditTypeModel" + } + }, + { + "name": "sinceDate", + "in": "query", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "skip", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + } + }, + { + "name": "take", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 100 + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedAuditLogResponseModel" + } + } + } + } + } + } + }, "/umbraco/management/api/v1/culture": { "get": { "tags": [ @@ -702,6 +877,9 @@ } } }, + "404": { + "description": "Not Found" + }, "400": { "description": "Bad Request", "content": { @@ -712,9 +890,6 @@ } } }, - "404": { - "description": "Not Found" - }, "409": { "description": "Conflict", "content": { @@ -998,9 +1173,7 @@ ], "operationId": "PostDictionaryUpload", "requestBody": { - "content": { - - } + "content": { } }, "responses": { "200": { @@ -1496,6 +1669,9 @@ } ], "responses": { + "401": { + "description": "Unauthorized" + }, "200": { "description": "Success", "content": { @@ -1505,9 +1681,6 @@ } } } - }, - "401": { - "description": "Unauthorized" } } } @@ -1539,6 +1712,9 @@ } ], "responses": { + "401": { + "description": "Unauthorized" + }, "200": { "description": "Success", "content": { @@ -1548,9 +1724,6 @@ } } } - }, - "401": { - "description": "Unauthorized" } } } @@ -1785,6 +1958,9 @@ } ], "responses": { + "404": { + "description": "Not Found" + }, "200": { "description": "Success", "content": { @@ -1798,9 +1974,6 @@ } } } - }, - "404": { - "description": "Not Found" } } } @@ -1822,6 +1995,9 @@ } ], "responses": { + "404": { + "description": "Not Found" + }, "200": { "description": "Success", "content": { @@ -1835,9 +2011,6 @@ } } } - }, - "404": { - "description": "Not Found" } } } @@ -1862,6 +2035,16 @@ } }, "responses": { + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetailsModel" + } + } + } + }, "200": { "description": "Success", "content": { @@ -1875,16 +2058,6 @@ } } } - }, - "400": { - "description": "Bad Request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetailsModel" - } - } - } } } } @@ -1936,16 +2109,6 @@ } ], "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PagedHelpPageModel" - } - } - } - }, "400": { "description": "Bad Request", "content": { @@ -1955,6 +2118,16 @@ } } } + }, + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedHelpPageModel" + } + } + } } } } @@ -2014,6 +2187,16 @@ } ], "responses": { + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetailsModel" + } + } + } + }, "200": { "description": "Success", "content": { @@ -2027,16 +2210,6 @@ } } } - }, - "400": { - "description": "Bad Request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetailsModel" - } - } - } } } } @@ -2058,16 +2231,6 @@ } ], "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/OkResultModel" - } - } - } - }, "400": { "description": "Bad Request", "content": { @@ -2077,6 +2240,16 @@ } } } + }, + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OkResultModel" + } + } + } } } } @@ -2088,20 +2261,6 @@ ], "operationId": "GetInstallSettings", "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "$ref": "#/components/schemas/InstallSettingsModel" - } - ] - } - } - } - }, "400": { "description": "Bad Request", "content": { @@ -2121,6 +2280,20 @@ } } } + }, + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/InstallSettingsModel" + } + ] + } + } + } } } } @@ -2145,9 +2318,6 @@ } }, "responses": { - "200": { - "description": "Success" - }, "400": { "description": "Bad Request", "content": { @@ -2167,6 +2337,9 @@ } } } + }, + "200": { + "description": "Success" } } } @@ -2191,9 +2364,6 @@ } }, "responses": { - "200": { - "description": "Success" - }, "400": { "description": "Bad Request", "content": { @@ -2203,6 +2373,9 @@ } } } + }, + "200": { + "description": "Success" } } } @@ -2265,6 +2438,19 @@ } }, "responses": { + "404": { + "description": "Not Found" + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetailsModel" + } + } + } + }, "201": { "description": "Created", "headers": { @@ -2277,19 +2463,6 @@ } } } - }, - "400": { - "description": "Bad Request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetailsModel" - } - } - } - }, - "404": { - "description": "Not Found" } } } @@ -2311,6 +2484,9 @@ } ], "responses": { + "404": { + "description": "Not Found" + }, "200": { "description": "Success", "content": { @@ -2324,9 +2500,6 @@ } } } - }, - "404": { - "description": "Not Found" } } }, @@ -2346,9 +2519,6 @@ } ], "responses": { - "200": { - "description": "Success" - }, "400": { "description": "Bad Request", "content": { @@ -2368,6 +2538,9 @@ } } } + }, + "200": { + "description": "Success" } } }, @@ -2400,8 +2573,8 @@ } }, "responses": { - "200": { - "description": "Success" + "404": { + "description": "Not Found" }, "400": { "description": "Bad Request", @@ -2413,8 +2586,8 @@ } } }, - "404": { - "description": "Not Found" + "200": { + "description": "Success" } } } @@ -2484,9 +2657,6 @@ } ], "responses": { - "200": { - "description": "Success" - }, "400": { "description": "Bad Request", "content": { @@ -2496,6 +2666,20 @@ } } } + }, + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/LogLevelCountsModel" + } + ] + } + } + } } } } @@ -2623,16 +2807,6 @@ } ], "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PagedLogTemplateModel" - } - } - } - }, "400": { "description": "Bad Request", "content": { @@ -2642,6 +2816,16 @@ } } } + }, + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedLogTemplateModel" + } + } + } } } } @@ -2704,6 +2888,16 @@ } }, "responses": { + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetailsModel" + } + } + } + }, "201": { "description": "Created", "headers": { @@ -2716,16 +2910,6 @@ } } } - }, - "400": { - "description": "Bad Request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetailsModel" - } - } - } } } } @@ -2747,6 +2931,9 @@ } ], "responses": { + "404": { + "description": "Not Found" + }, "200": { "description": "Success", "content": { @@ -2760,9 +2947,6 @@ } } } - }, - "404": { - "description": "Not Found" } } }, @@ -2782,11 +2966,11 @@ } ], "responses": { - "200": { - "description": "Success" - }, "404": { "description": "Not Found" + }, + "200": { + "description": "Success" } } } @@ -2816,9 +3000,6 @@ } ], "responses": { - "200": { - "description": "Success" - }, "400": { "description": "Bad Request", "content": { @@ -2828,6 +3009,9 @@ } } } + }, + "200": { + "description": "Success" } } } @@ -3014,6 +3198,9 @@ } ], "responses": { + "401": { + "description": "Unauthorized" + }, "200": { "description": "Success", "content": { @@ -3023,9 +3210,6 @@ } } } - }, - "401": { - "description": "Unauthorized" } } } @@ -3057,6 +3241,9 @@ } ], "responses": { + "401": { + "description": "Unauthorized" + }, "200": { "description": "Success", "content": { @@ -3066,9 +3253,6 @@ } } } - }, - "401": { - "description": "Unauthorized" } } } @@ -3489,6 +3673,305 @@ } } }, + "/umbraco/management/api/v1/package/{name}/run-migration": { + "post": { + "tags": [ + "Package" + ], + "operationId": "PostPackageByNameRunMigration", + "parameters": [ + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "404": { + "description": "Not Found" + }, + "409": { + "description": "Conflict", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetailsModel" + } + } + } + }, + "200": { + "description": "Success" + } + } + } + }, + "/umbraco/management/api/v1/package/created": { + "get": { + "tags": [ + "Package" + ], + "operationId": "GetPackageCreated", + "parameters": [ + { + "name": "skip", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + } + }, + { + "name": "take", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 100 + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedPackageDefinitionModel" + } + } + } + } + } + }, + "post": { + "tags": [ + "Package" + ], + "operationId": "PostPackageCreated", + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/PackageCreateModel" + } + ] + } + } + } + }, + "responses": { + "404": { + "description": "Not Found" + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetailsModel" + } + } + } + }, + "201": { + "description": "Created", + "headers": { + "Location": { + "description": "Location of the newly created resource", + "schema": { + "type": "string", + "description": "Location of the newly created resource", + "format": "uri" + } + } + } + } + } + } + }, + "/umbraco/management/api/v1/package/created/{key}": { + "get": { + "tags": [ + "Package" + ], + "operationId": "GetPackageCreatedByKey", + "parameters": [ + { + "name": "key", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "404": { + "description": "Not Found" + }, + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/PackageDefinitionModel" + } + ] + } + } + } + } + } + }, + "delete": { + "tags": [ + "Package" + ], + "operationId": "DeletePackageCreatedByKey", + "parameters": [ + { + "name": "key", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "404": { + "description": "Not Found" + }, + "200": { + "description": "Success" + } + } + }, + "put": { + "tags": [ + "Package" + ], + "operationId": "PutPackageCreatedByKey", + "parameters": [ + { + "name": "key", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/PackageUpdateModel" + } + ] + } + } + } + }, + "responses": { + "404": { + "description": "Not Found" + }, + "200": { + "description": "Success" + } + } + } + }, + "/umbraco/management/api/v1/package/created/{key}/download": { + "get": { + "tags": [ + "Package" + ], + "operationId": "GetPackageCreatedByKeyDownload", + "parameters": [ + { + "name": "key", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "404": { + "description": "Not Found" + }, + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + } + } + } + }, + "/umbraco/management/api/v1/package/migration-status": { + "get": { + "tags": [ + "Package" + ], + "operationId": "GetPackageMigrationStatus", + "parameters": [ + { + "name": "skip", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + } + }, + { + "name": "take", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 100 + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedPackageMigrationStatusModel" + } + } + } + } + } + } + }, "/umbraco/management/api/v1/tree/partial-view/children": { "get": { "tags": [ @@ -3637,6 +4120,30 @@ } } } + }, + "put": { + "tags": [ + "Profiling" + ], + "operationId": "PutProfilingStatus", + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ProfilingStatusModel" + } + ] + } + } + } + }, + "responses": { + "200": { + "description": "Success" + } + } } }, "/umbraco/management/api/v1/published-cache/collect": { @@ -3730,16 +4237,6 @@ } ], "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PagedRedirectUrlModel" - } - } - } - }, "400": { "description": "Bad Request", "content": { @@ -3749,6 +4246,16 @@ } } } + }, + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedRedirectUrlModel" + } + } + } } } } @@ -4300,6 +4807,16 @@ ], "operationId": "GetServerStatus", "responses": { + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetailsModel" + } + } + } + }, "200": { "description": "Success", "content": { @@ -4313,16 +4830,6 @@ } } } - }, - "400": { - "description": "Bad Request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetailsModel" - } - } - } } } } @@ -4334,6 +4841,16 @@ ], "operationId": "GetServerVersion", "responses": { + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetailsModel" + } + } + } + }, "200": { "description": "Success", "content": { @@ -4347,16 +4864,6 @@ } } } - }, - "400": { - "description": "Bad Request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetailsModel" - } - } - } } } } @@ -4693,9 +5200,6 @@ } }, "responses": { - "200": { - "description": "Success" - }, "400": { "description": "Bad Request", "content": { @@ -4705,6 +5209,9 @@ } } } + }, + "200": { + "description": "Success" } } } @@ -5110,20 +5617,20 @@ } } }, - "/umbraco/management/api/v1/tracked-reference/{id}": { + "/umbraco/management/api/v1/tracked-reference/{key}": { "get": { "tags": [ "Tracked Reference" ], - "operationId": "GetTrackedReferenceById", + "operationId": "GetTrackedReferenceByKey", "parameters": [ { - "name": "id", + "name": "key", "in": "path", "required": true, "schema": { - "type": "integer", - "format": "int32" + "type": "string", + "format": "uuid" } }, { @@ -5131,7 +5638,8 @@ "in": "query", "schema": { "type": "integer", - "format": "int64" + "format": "int64", + "default": 0 } }, { @@ -5139,14 +5647,16 @@ "in": "query", "schema": { "type": "integer", - "format": "int64" + "format": "int64", + "default": 20 } }, { "name": "filterMustBeIsDependency", "in": "query", "schema": { - "type": "boolean" + "type": "boolean", + "default": false } } ], @@ -5164,20 +5674,20 @@ } } }, - "/umbraco/management/api/v1/tracked-reference/descendants/{parentId}": { + "/umbraco/management/api/v1/tracked-reference/descendants/{parentKey}": { "get": { "tags": [ "Tracked Reference" ], - "operationId": "GetTrackedReferenceDescendantsByParentId", + "operationId": "GetTrackedReferenceDescendantsByParentKey", "parameters": [ { - "name": "parentId", + "name": "parentKey", "in": "path", "required": true, "schema": { - "type": "integer", - "format": "int32" + "type": "string", + "format": "uuid" } }, { @@ -5200,7 +5710,8 @@ "name": "filterMustBeIsDependency", "in": "query", "schema": { - "type": "boolean" + "type": "boolean", + "default": true } } ], @@ -5226,13 +5737,14 @@ "operationId": "GetTrackedReferenceItem", "parameters": [ { - "name": "ids", + "name": "key", "in": "query", "schema": { + "uniqueItems": true, "type": "array", "items": { - "type": "integer", - "format": "int32" + "type": "string", + "format": "uuid" } } }, @@ -5241,7 +5753,8 @@ "in": "query", "schema": { "type": "integer", - "format": "int64" + "format": "int64", + "default": 0 } }, { @@ -5249,14 +5762,16 @@ "in": "query", "schema": { "type": "integer", - "format": "int64" + "format": "int64", + "default": 20 } }, { "name": "filterMustBeIsDependency", "in": "query", "schema": { - "type": "boolean" + "type": "boolean", + "default": true } } ], @@ -5361,6 +5876,16 @@ } }, "responses": { + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetailsModel" + } + } + } + }, "201": { "description": "Created", "headers": { @@ -5373,16 +5898,6 @@ } } } - }, - "400": { - "description": "Bad Request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetailsModel" - } - } - } } } }, @@ -5529,6 +6044,101 @@ }, "components": { "schemas": { + "AuditLogBaseModel": { + "type": "object", + "properties": { + "userKey": { + "type": "string", + "format": "uuid" + }, + "entityKey": { + "type": "string", + "format": "uuid", + "nullable": true + }, + "timestamp": { + "type": "string", + "format": "date-time" + }, + "logType": { + "$ref": "#/components/schemas/AuditTypeModel" + }, + "entityType": { + "type": "string", + "nullable": true + }, + "comment": { + "type": "string", + "nullable": true + }, + "parameters": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "AuditLogResponseModel": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/AuditLogBaseModel" + } + ], + "additionalProperties": false + }, + "AuditLogWithUsernameResponseModel": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/AuditLogBaseModel" + } + ], + "properties": { + "userName": { + "type": "string", + "nullable": true + }, + "userAvatars": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + } + }, + "additionalProperties": false + }, + "AuditTypeModel": { + "enum": [ + "New", + "Save", + "SaveVariant", + "Open", + "Delete", + "Publish", + "PublishVariant", + "SendToPublish", + "SendToPublishVariant", + "Unpublish", + "UnpublishVariant", + "Move", + "Copy", + "AssignDomain", + "PublicAccess", + "Sort", + "Notify", + "System", + "RollBack", + "PackagerInstall", + "PackagerUninstall", + "Custom", + "ContentVersionPreventCleanup", + "ContentVersionEnableCleanup" + ], + "type": "integer", + "format": "int32" + }, "ConsentLevelModel": { "type": "object", "properties": { @@ -6765,9 +7375,7 @@ }, "providerProperties": { "type": "object", - "additionalProperties": { - - }, + "additionalProperties": { }, "nullable": true } }, @@ -6879,6 +7487,32 @@ ], "additionalProperties": false }, + "LogLevelCountsModel": { + "type": "object", + "properties": { + "information": { + "type": "integer", + "format": "int32" + }, + "debug": { + "type": "integer", + "format": "int32" + }, + "warning": { + "type": "integer", + "format": "int32" + }, + "error": { + "type": "integer", + "format": "int32" + }, + "fatal": { + "type": "integer", + "format": "int32" + } + }, + "additionalProperties": false + }, "LogLevelModel": { "enum": [ "Verbose", @@ -7047,6 +7681,187 @@ "type": "integer", "format": "int32" }, + "PackageCreateModel": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/PackageModelBaseModel" + } + ], + "additionalProperties": false + }, + "PackageDefinitionModel": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/PackageModelBaseModel" + } + ], + "properties": { + "key": { + "type": "string", + "format": "uuid" + }, + "packagePath": { + "type": "string" + } + }, + "additionalProperties": false + }, + "PackageMigrationStatusModel": { + "type": "object", + "properties": { + "packageName": { + "type": "string" + }, + "hasPendingMigrations": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "PackageModelBaseModel": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "contentNodeId": { + "type": "string", + "nullable": true + }, + "contentLoadChildNodes": { + "type": "boolean" + }, + "mediaKeys": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + }, + "mediaLoadChildNodes": { + "type": "boolean" + }, + "documentTypes": { + "type": "array", + "items": { + "type": "string" + } + }, + "mediaTypes": { + "type": "array", + "items": { + "type": "string" + } + }, + "dataTypes": { + "type": "array", + "items": { + "type": "string" + } + }, + "templates": { + "type": "array", + "items": { + "type": "string" + } + }, + "partialViews": { + "type": "array", + "items": { + "type": "string" + } + }, + "stylesheets": { + "type": "array", + "items": { + "type": "string" + } + }, + "scripts": { + "type": "array", + "items": { + "type": "string" + } + }, + "languages": { + "type": "array", + "items": { + "type": "string" + } + }, + "dictionaryItems": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "PackageUpdateModel": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/PackageModelBaseModel" + } + ], + "properties": { + "packagePath": { + "type": "string" + } + }, + "additionalProperties": false + }, + "PagedAuditLogResponseModel": { + "required": [ + "items", + "total" + ], + "type": "object", + "properties": { + "total": { + "type": "integer", + "format": "int64" + }, + "items": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/AuditLogResponseModel" + } + ] + } + } + }, + "additionalProperties": false + }, + "PagedAuditLogWithUsernameResponseModel": { + "required": [ + "items", + "total" + ], + "type": "object", + "properties": { + "total": { + "type": "integer", + "format": "int64" + }, + "items": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/AuditLogWithUsernameResponseModel" + } + ] + } + } + }, + "additionalProperties": false + }, "PagedContentTreeItemModel": { "required": [ "items", @@ -7455,6 +8270,54 @@ }, "additionalProperties": false }, + "PagedPackageDefinitionModel": { + "required": [ + "items", + "total" + ], + "type": "object", + "properties": { + "total": { + "type": "integer", + "format": "int64" + }, + "items": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/PackageDefinitionModel" + } + ] + } + } + }, + "additionalProperties": false + }, + "PagedPackageMigrationStatusModel": { + "required": [ + "items", + "total" + ], + "type": "object", + "properties": { + "total": { + "type": "integer", + "format": "int64" + }, + "items": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/PackageMigrationStatusModel" + } + ] + } + } + }, + "additionalProperties": false + }, "PagedRecycleBinItemModel": { "required": [ "items", @@ -7696,9 +8559,7 @@ "nullable": true } }, - "additionalProperties": { - - } + "additionalProperties": { } }, "ProfilingStatusModel": { "type": "object", @@ -7919,6 +8780,10 @@ "type": "string", "nullable": true }, + "nodePublished": { + "type": "boolean", + "nullable": true + }, "contentTypeIcon": { "type": "string", "nullable": true @@ -8561,9 +9426,7 @@ "authorizationCode": { "authorizationUrl": "/umbraco/management/api/v1.0/security/back-office/authorize", "tokenUrl": "/umbraco/management/api/v1.0/security/back-office/token", - "scopes": { - - } + "scopes": { } } } } @@ -8571,9 +9434,7 @@ }, "security": [ { - "OAuth": [ - - ] + "OAuth": [ ] } ] } diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/AuditLogs/AuditLogBaseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/AuditLogs/AuditLogBaseModel.cs new file mode 100644 index 0000000000..7168fd9dfd --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/AuditLogs/AuditLogBaseModel.cs @@ -0,0 +1,20 @@ +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Api.Management.ViewModels.AuditLogs; + +public class AuditLogBaseModel +{ + public Guid UserKey { get; set; } + + public Guid? EntityKey { get; set; } + + public DateTime Timestamp { get; set; } + + public AuditType LogType { get; set; } + + public string? EntityType { get; set; } + + public string? Comment { get; set; } + + public string? Parameters { get; set; } +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/AuditLogs/AuditLogResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/AuditLogs/AuditLogResponseModel.cs new file mode 100644 index 0000000000..7b0f43b4e1 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/AuditLogs/AuditLogResponseModel.cs @@ -0,0 +1,8 @@ +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Api.Management.ViewModels.AuditLogs; + +/// +public class AuditLogResponseModel : AuditLogBaseModel +{ +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/AuditLogs/AuditLogWithUsernameResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/AuditLogs/AuditLogWithUsernameResponseModel.cs new file mode 100644 index 0000000000..d1b0933ae7 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/AuditLogs/AuditLogWithUsernameResponseModel.cs @@ -0,0 +1,10 @@ +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Api.Management.ViewModels.AuditLogs; + +public class AuditLogWithUsernameResponseModel : AuditLogBaseModel +{ + public string? UserName { get; set; } + + public string[]? UserAvatars { get; set; } +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Package/PackageCreateModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Package/PackageCreateModel.cs new file mode 100644 index 0000000000..e4ece8fe4f --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Package/PackageCreateModel.cs @@ -0,0 +1,5 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.Package; + +public class PackageCreateModel : PackageModelBase +{ +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Package/PackageDefinitionViewModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Package/PackageDefinitionViewModel.cs new file mode 100644 index 0000000000..7e51bd801b --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Package/PackageDefinitionViewModel.cs @@ -0,0 +1,17 @@ +using System.ComponentModel; + +namespace Umbraco.Cms.Api.Management.ViewModels.Package; + +public class PackageDefinitionViewModel : PackageModelBase +{ + /// + /// Gets or sets the key. + /// + public Guid Key { get; set; } + + /// + /// Gets or sets the full path to the package's XML file. + /// + [ReadOnly(true)] + public required string PackagePath { get; set; } +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Package/PackageMigrationStatusViewModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Package/PackageMigrationStatusViewModel.cs new file mode 100644 index 0000000000..5b4c7d8ece --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Package/PackageMigrationStatusViewModel.cs @@ -0,0 +1,14 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.Package; + +public class PackageMigrationStatusViewModel +{ + /// + /// Gets or sets the name of the package. + /// + public required string PackageName { get; set; } + + /// + /// Gets or sets a value indicating whether the package has any pending migrations to run. + /// + public bool HasPendingMigrations { get; set; } +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Package/PackageModelBase.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Package/PackageModelBase.cs new file mode 100644 index 0000000000..c3c1fa9460 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Package/PackageModelBase.cs @@ -0,0 +1,74 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.Package; + +public class PackageModelBase +{ + /// + /// Gets or sets the name. + /// + public required string Name { get; set; } + + /// + /// Gets or sets the id of the selected content node. + /// + public string? ContentNodeId { get; set; } + + /// + /// Gets or sets a value indicating whether to load all child nodes of the selected content node. + /// + public bool ContentLoadChildNodes { get; set; } + + /// + /// Gets or sets the list of media keys for the selected media items. + /// + public IList MediaKeys { get; set; } = new List(); + + /// + /// Gets or sets a value indicating whether to load all child nodes of the selected media items. + /// + public bool MediaLoadChildNodes { get; set; } + + /// + /// Gets or sets the list of ids for the selected document types. + /// + public IList DocumentTypes { get; set; } = new List(); + + /// + /// Gets or sets the list of ids for the selected media types. + /// + public IList MediaTypes { get; set; } = new List(); + + /// + /// Gets or sets the list of ids for the selected data types. + /// + public IList DataTypes { get; set; } = new List(); + + /// + /// Gets or sets the list of ids for the selected templates. + /// + public IList Templates { get; set; } = new List(); + + /// + /// Gets or sets the list of relative paths for the selected partial views. + /// + public IList PartialViews { get; set; } = new List(); + + /// + /// Gets or sets the list of names for the selected stylesheets. + /// + public IList Stylesheets { get; set; } = new List(); + + /// + /// Gets or sets the list of names for the selected scripts. + /// + public IList Scripts { get; set; } = new List(); + + /// + /// Gets or sets the list of ids for the selected languages. + /// + public IList Languages { get; set; } = new List(); + + /// + /// Gets or sets the list of ids for the selected dictionary items. + /// + public IList DictionaryItems { get; set; } = new List(); +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Package/PackageUpdateModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Package/PackageUpdateModel.cs new file mode 100644 index 0000000000..548f22fd8c --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Package/PackageUpdateModel.cs @@ -0,0 +1,12 @@ +using System.ComponentModel; + +namespace Umbraco.Cms.Api.Management.ViewModels.Package; + +public class PackageUpdateModel : PackageModelBase +{ + /// + /// Gets or sets the full path to the package's XML file. + /// + [ReadOnly(true)] + public required string PackagePath { get; set; } +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/TrackedReferences/RelationItemViewModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/TrackedReferences/RelationItemViewModel.cs index f27c81f3eb..572da9d9ea 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/TrackedReferences/RelationItemViewModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/TrackedReferences/RelationItemViewModel.cs @@ -8,6 +8,8 @@ public class RelationItemViewModel public string? NodeType { get; set; } + public bool? NodePublished { get; set; } + public string? ContentTypeIcon { get; set; } public string? ContentTypeAlias { get; set; } @@ -19,4 +21,5 @@ public class RelationItemViewModel public bool RelationTypeIsBidirectional { get; set; } public bool RelationTypeIsDependency { get; set; } + } diff --git a/src/Umbraco.Cms.StaticAssets/umbraco/UmbracoBackOffice/Default.cshtml b/src/Umbraco.Cms.StaticAssets/umbraco/UmbracoBackOffice/Default.cshtml index 51a9d3d9fa..5662c164a6 100644 --- a/src/Umbraco.Cms.StaticAssets/umbraco/UmbracoBackOffice/Default.cshtml +++ b/src/Umbraco.Cms.StaticAssets/umbraco/UmbracoBackOffice/Default.cshtml @@ -20,8 +20,10 @@ @inject IProfilerHtml profilerHtml @inject IIconService IconService @inject IBackOfficeExternalLoginProviders externalLogins +@inject IWebProfilerService WebProfilerService @{ - bool.TryParse(Context.Request.Query["umbDebug"], out bool isDebug); + var webProfilingStatus = await WebProfilerService.GetStatus(); + var isDebug = (webProfilingStatus.Success && webProfilingStatus.Result) ? true : false; var backOfficePath = globalSettings.Value.GetBackOfficePath(hostingEnvironment); } diff --git a/src/Umbraco.Core/CompatibilitySuppressions.xml b/src/Umbraco.Core/CompatibilitySuppressions.xml index 213a91ab24..e4376074a2 100644 --- a/src/Umbraco.Core/CompatibilitySuppressions.xml +++ b/src/Umbraco.Core/CompatibilitySuppressions.xml @@ -420,6 +420,13 @@ lib/net7.0/Umbraco.Core.dll true + + CP0006 + M:Umbraco.Cms.Core.Packaging.IPackageDefinitionRepository.GetByKey(System.Guid) + lib/net7.0/Umbraco.Core.dll + lib/net7.0/Umbraco.Core.dll + true + CP0006 M:Umbraco.Cms.Core.PropertyEditors.IConfigurationEditor.FromConfigurationEditor(System.Collections.Generic.IDictionary{System.String,System.Object}) @@ -581,6 +588,20 @@ lib/net7.0/Umbraco.Core.dll true + + CP0006 + M:Umbraco.Cms.Core.Services.IPackagingService.GetCreatedPackageByKey(System.Guid) + lib/net7.0/Umbraco.Core.dll + lib/net7.0/Umbraco.Core.dll + true + + + CP0006 + M:Umbraco.Cms.Core.Services.IPackagingService.GetInstalledPackagesFromMigrationPlans + lib/net7.0/Umbraco.Core.dll + lib/net7.0/Umbraco.Core.dll + true + CP0006 P:Umbraco.Cms.Core.Models.IDataType.ConfigurationData diff --git a/src/Umbraco.Core/Configuration/Models/SecuritySettings.cs b/src/Umbraco.Core/Configuration/Models/SecuritySettings.cs index eca2501a63..d002cc2111 100644 --- a/src/Umbraco.Core/Configuration/Models/SecuritySettings.cs +++ b/src/Umbraco.Core/Configuration/Models/SecuritySettings.cs @@ -68,11 +68,13 @@ public class SecuritySettings /// /// Gets or sets a value for the user password settings. /// + [Obsolete("This no longer works. You can now inject this by using IOptions instead, scheduled for removal in v13")] public UserPasswordConfigurationSettings? UserPassword { get; set; } /// /// Gets or sets a value for the member password settings. /// + [Obsolete("This no longer works. You can now inject this by using IOptions instead, scheduled for removal in v13")] public MemberPasswordConfigurationSettings? MemberPassword { get; set; } /// diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs index 0f7967e76d..ffbbfebd4a 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs @@ -282,6 +282,7 @@ namespace Umbraco.Cms.Core.DependencyInjection Services.AddTransient(); Services.AddUnique(); Services.AddUnique(); + Services.AddUnique(); Services.AddUnique(); Services.AddUnique(); Services.AddUnique(); diff --git a/src/Umbraco.Core/Models/AuditItem.cs b/src/Umbraco.Core/Models/AuditItem.cs index bbfca724aa..cfbe676dac 100644 --- a/src/Umbraco.Core/Models/AuditItem.cs +++ b/src/Umbraco.Core/Models/AuditItem.cs @@ -7,7 +7,7 @@ public sealed class AuditItem : EntityBase, IAuditItem /// /// Initializes a new instance of the class. /// - public AuditItem(int objectId, AuditType type, int userId, string? entityType, string? comment = null, string? parameters = null) + public AuditItem(int objectId, AuditType type, int userId, string? entityType, string? comment = null, string? parameters = null, DateTime? createDate = null) { DisableChangeTracking(); @@ -17,6 +17,7 @@ public sealed class AuditItem : EntityBase, IAuditItem UserId = userId; EntityType = entityType; Parameters = parameters; + CreateDate = createDate ?? default; EnableChangeTracking(); } diff --git a/src/Umbraco.Core/Models/RelationItemModel.cs b/src/Umbraco.Core/Models/RelationItemModel.cs index ee45422586..a05c8f6591 100644 --- a/src/Umbraco.Core/Models/RelationItemModel.cs +++ b/src/Umbraco.Core/Models/RelationItemModel.cs @@ -8,6 +8,8 @@ public class RelationItemModel public string? NodeType { get; set; } + public bool? NodePublished { get; set; } + public string? ContentTypeIcon { get; set; } public string? ContentTypeAlias { get; set; } diff --git a/src/Umbraco.Core/Packaging/IPackageDefinitionRepository.cs b/src/Umbraco.Core/Packaging/IPackageDefinitionRepository.cs index b66f4884af..0764152515 100644 --- a/src/Umbraco.Core/Packaging/IPackageDefinitionRepository.cs +++ b/src/Umbraco.Core/Packaging/IPackageDefinitionRepository.cs @@ -9,6 +9,8 @@ public interface IPackageDefinitionRepository PackageDefinition? GetById(int id); + PackageDefinition? GetByKey(Guid key); + void Delete(int id); /// diff --git a/src/Umbraco.Core/Packaging/PackagesRepository.cs b/src/Umbraco.Core/Packaging/PackagesRepository.cs index a5982aef7e..a130712c07 100644 --- a/src/Umbraco.Core/Packaging/PackagesRepository.cs +++ b/src/Umbraco.Core/Packaging/PackagesRepository.cs @@ -129,6 +129,9 @@ public class PackagesRepository : ICreatedPackagesRepository return packageXml == null ? null : _parser.ToPackageDefinition(packageXml); } + // Default implementation as the class is obsolete + public PackageDefinition? GetByKey(Guid key) => null; + public void Delete(int id) { XDocument packagesXml = EnsureStorage(out var packagesFile); diff --git a/src/Umbraco.Core/PaginationHelper.cs b/src/Umbraco.Core/PaginationHelper.cs new file mode 100644 index 0000000000..2fbf6ff771 --- /dev/null +++ b/src/Umbraco.Core/PaginationHelper.cs @@ -0,0 +1,15 @@ +namespace Umbraco.Cms.Core; + +public static class PaginationHelper +{ + internal static void ConvertSkipTakeToPaging(int skip, int take, out long pageNumber, out int pageSize) + { + if (skip % take != 0) + { + throw new ArgumentException("Invalid skip/take, Skip must be a multiple of take - i.e. skip = 10, take = 5"); + } + + pageSize = take; + pageNumber = skip / take; + } +} diff --git a/src/Umbraco.Core/Persistence/Repositories/ITrackedReferencesRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ITrackedReferencesRepository.cs index bd6723e674..9466a1fcd8 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ITrackedReferencesRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ITrackedReferencesRepository.cs @@ -17,6 +17,7 @@ public interface ITrackedReferencesRepository /// /// The total count of the items with reference to the current item. /// An enumerable list of objects. + [Obsolete("Use overload that takes key instead of id. This will be removed in Umbraco 15.")] IEnumerable GetPagedRelationsForItem(int id, long pageIndex, int pageSize, bool filterMustBeIsDependency, out long totalRecords); /// @@ -31,6 +32,7 @@ public interface ITrackedReferencesRepository /// /// The total count of the items in any kind of relation. /// An enumerable list of objects. + [Obsolete("Use overload that takes key instead of id. This will be removed in Umbraco 15.")] IEnumerable GetPagedItemsWithRelations(int[] ids, long pageIndex, int pageSize, bool filterMustBeIsDependency, out long totalRecords); /// @@ -45,6 +47,7 @@ public interface ITrackedReferencesRepository /// /// The total count of descending items. /// An enumerable list of objects. + [Obsolete("Use overload that takes key instead of id. This will be removed in Umbraco 15.")] IEnumerable GetPagedDescendantsInReferences(int parentId, long pageIndex, int pageSize, bool filterMustBeIsDependency, out long totalRecords); /// @@ -60,6 +63,15 @@ public interface ITrackedReferencesRepository /// /// The total count of the items with reference to the current item. /// An enumerable list of objects. + IEnumerable GetPagedRelationsForItem( + Guid key, + long skip, + long take, + bool filterMustBeIsDependency, + out long totalRecords) => + throw new NotImplementedException(); + + [Obsolete("Use overload that takes key instead of id. This will be removed in Umbraco 15.")] IEnumerable GetPagedRelationsForItem( int id, long skip, @@ -80,18 +92,25 @@ public interface ITrackedReferencesRepository /// /// The total count of the items in any kind of relation. /// An enumerable list of objects. + IEnumerable GetPagedItemsWithRelations( + ISet keys, + long skip, + long take, + bool filterMustBeIsDependency, + out long totalRecords); + + [Obsolete("Use overload that takes key instead of id. This will be removed in Umbraco 15.")] IEnumerable GetPagedItemsWithRelations( int[] ids, long skip, long take, bool filterMustBeIsDependency, - out long totalRecords) => - throw new NotImplementedException(); + out long totalRecords); /// /// Gets a page of the descending items that have any references, given a parent id. /// - /// The unique identifier of the parent to retrieve descendants for. + /// The unique identifier of the parent to retrieve descendants for. /// The amount of items to skip. /// The amount of items to take. /// @@ -100,11 +119,18 @@ public interface ITrackedReferencesRepository /// /// The total count of descending items. /// An enumerable list of objects. + IEnumerable GetPagedDescendantsInReferences( + Guid parentKey, + long skip, + long take, + bool filterMustBeIsDependency, + out long totalRecords); + + [Obsolete("Use overload that takes key instead of id. This will be removed in Umbraco 15.")] IEnumerable GetPagedDescendantsInReferences( int parentId, long skip, long take, bool filterMustBeIsDependency, - out long totalRecords) => - throw new NotImplementedException(); + out long totalRecords); } diff --git a/src/Umbraco.Core/Persistence/Repositories/IUserRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IUserRepository.cs index 35458d6eba..dbb96478d6 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IUserRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IUserRepository.cs @@ -81,6 +81,39 @@ public interface IUserRepository : IReadWriteQueryRepository /// IUser? GetByUsername(string username, bool includeSecurityData); + /// + /// Gets a user by username for upgrade purposes, this will only return a result if the current runtime state is upgrade. + /// + /// + /// This only resolves the minimum amount of fields required to authorize for an upgrade. + /// We need this to be able to add new columns to the user table. + /// + /// The username to find the user by. + /// An uncached instance. + IUser? GetForUpgradeByUsername(string username) => GetByUsername(username, false); + + /// + /// Gets a user by email for upgrade purposes, this will only return a result if the current runtime state is upgrade. + /// + /// + /// This only resolves the minimum amount of fields required to authorize for an upgrade. + /// We need this to be able to add new columns to the user table. + /// + /// The email to find the user by. + /// An uncached instance. + IUser? GetForUpgradeByEmail(string email) => GetMany().FirstOrDefault(x=>x.Email == email); + + /// + /// Gets a user for upgrade purposes, this will only return a result if the current runtime state is upgrade. + /// + /// + /// This only resolves the minimum amount of fields required to authorize for an upgrade. + /// We need this to be able to add new columns to the user table. + /// + /// The id to find the user by. + /// An uncached instance. + IUser? GetForUpgrade(int id) => Get(id, false); + /// /// Returns a user by id /// diff --git a/src/Umbraco.Core/Persistence/Repositories/IWebProfilerRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IWebProfilerRepository.cs new file mode 100644 index 0000000000..d78bcdcd2c --- /dev/null +++ b/src/Umbraco.Core/Persistence/Repositories/IWebProfilerRepository.cs @@ -0,0 +1,7 @@ +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface IWebProfilerRepository +{ + void SetStatus(int userId, bool status); + bool GetStatus(int userId); +} diff --git a/src/Umbraco.Core/Services/AuditService.cs b/src/Umbraco.Core/Services/AuditService.cs index 046c5fff3d..bbfaf70076 100644 --- a/src/Umbraco.Core/Services/AuditService.cs +++ b/src/Umbraco.Core/Services/AuditService.cs @@ -1,15 +1,23 @@ using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Exceptions; using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.Extensions; +using Umbraco.New.Cms.Core.Models; namespace Umbraco.Cms.Core.Services.Implement; public sealed class AuditService : RepositoryService, IAuditService { private readonly IAuditEntryRepository _auditEntryRepository; + private readonly IUserService _userService; + private readonly IEntityRepository _entityRepository; private readonly IAuditRepository _auditRepository; private readonly Lazy _isAvailable; @@ -18,11 +26,15 @@ public sealed class AuditService : RepositoryService, IAuditService ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory, IAuditRepository auditRepository, - IAuditEntryRepository auditEntryRepository) + IAuditEntryRepository auditEntryRepository, + IUserService userService, + IEntityRepository entityRepository) : base(provider, loggerFactory, eventMessagesFactory) { _auditRepository = auditRepository; _auditEntryRepository = auditEntryRepository; + _userService = userService; + _entityRepository = entityRepository; _isAvailable = new Lazy(DetermineIsAvailable); } @@ -184,6 +196,77 @@ public sealed class AuditService : RepositoryService, IAuditService } } + public async Task> GetItemsByKeyAsync( + Guid entityKey, + int skip, + int take, + Direction orderDirection = Direction.Descending, + DateTime? sinceDate = null, + AuditType[]? auditTypeFilter = null) + { + if (skip < 0) + { + throw new ArgumentOutOfRangeException(nameof(skip)); + } + + if (take <= 0) + { + throw new ArgumentOutOfRangeException(nameof(take)); + } + + using (ScopeProvider.CreateCoreScope(autoComplete: true)) + { + IEntitySlim? entity = _entityRepository.Get(entityKey); + if (entity is null) + { + throw new ArgumentNullException($"Could not find user with key {entityKey}"); + } + + IQuery query = Query().Where(x => x.Id == entity.Id); + IQuery? customFilter = sinceDate.HasValue ? Query().Where(x => x.CreateDate >= sinceDate) : null; + PaginationHelper.ConvertSkipTakeToPaging(skip, take, out var pageNumber, out var pageSize); + + IEnumerable auditItems = _auditRepository.GetPagedResultsByQuery(query, pageNumber, pageSize, out var totalRecords, orderDirection, auditTypeFilter, customFilter); + return await Task.FromResult(new PagedModel { Items = auditItems, Total = totalRecords }); + } + } + + public async Task> GetPagedItemsByUserAsync( + Guid userKey, + int skip, + int take, + Direction orderDirection = Direction.Descending, + AuditType[]? auditTypeFilter = null, + DateTime? sinceDate = null) + { + if (skip < 0) + { + throw new ArgumentOutOfRangeException(nameof(skip)); + } + + if (take <= 0) + { + throw new ArgumentOutOfRangeException(nameof(take)); + } + + IUser? user = await _userService.GetAsync(userKey); + + if (user is null) + { + return await Task.FromResult(new PagedModel()); + } + + using (ScopeProvider.CreateCoreScope(autoComplete: true)) + { + IQuery query = Query().Where(x => x.UserId == user.Id); + IQuery? customFilter = sinceDate.HasValue ? Query().Where(x => x.CreateDate >= sinceDate) : null; + PaginationHelper.ConvertSkipTakeToPaging(skip, take, out var pageNumber, out var pageSize); + + IEnumerable auditItems = _auditRepository.GetPagedResultsByQuery(query, pageNumber, pageSize, out var totalRecords, orderDirection, auditTypeFilter, customFilter); + return await Task.FromResult(new PagedModel { Items = auditItems, Total = totalRecords }); + } + } + /// public IAuditEntry Write(int performingUserId, string perfomingDetails, string performingIp, DateTime eventDateUtc, int affectedUserId, string? affectedDetails, string eventType, string eventDetails) { diff --git a/src/Umbraco.Core/Services/IAuditService.cs b/src/Umbraco.Core/Services/IAuditService.cs index f58da53174..ba6d6aa4ff 100644 --- a/src/Umbraco.Core/Services/IAuditService.cs +++ b/src/Umbraco.Core/Services/IAuditService.cs @@ -1,5 +1,7 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Querying; +using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.New.Cms.Core.Models; namespace Umbraco.Cms.Core.Services; @@ -74,6 +76,60 @@ public interface IAuditService : IService AuditType[]? auditTypeFilter = null, IQuery? customFilter = null); + /// + /// Returns paged items in the audit trail for a given user + /// + /// The key of the user + /// The amount of entries to skip + /// The amount of entiries to take + /// The total amount of entires + /// + /// By default this will always be ordered descending (newest first) + /// + /// + /// Since we currently do not have enum support with our expression parser, we cannot query on AuditType in the query + /// or the custom filter + /// so we need to do that here + /// + /// + /// If populated, will only return entries after this time. + /// + /// + Task> GetItemsByKeyAsync( + Guid entityKey, + int skip, + int take, + Direction orderDirection = Direction.Descending, + DateTime? sinceDate = null, + AuditType[]? auditTypeFilter = null) => throw new NotImplementedException(); + + /// + /// Returns paged items in the audit trail for a given user + /// + /// + /// + /// + /// + /// + /// By default this will always be ordered descending (newest first) + /// + /// + /// Since we currently do not have enum support with our expression parser, we cannot query on AuditType in the query + /// or the custom filter + /// so we need to do that here + /// + /// + /// Optional filter to be applied + /// + /// + Task> GetPagedItemsByUserAsync( + Guid userKey, + int skip, + int take, + Direction orderDirection = Direction.Descending, + AuditType[]? auditTypeFilter = null, + DateTime? sinceDate = null) => throw new NotImplementedException(); + /// /// Writes an audit entry for an audited event. /// diff --git a/src/Umbraco.Core/Services/IPackagingService.cs b/src/Umbraco.Core/Services/IPackagingService.cs index 40f39628be..4c3849a789 100644 --- a/src/Umbraco.Core/Services/IPackagingService.cs +++ b/src/Umbraco.Core/Services/IPackagingService.cs @@ -1,6 +1,8 @@ using System.Xml.Linq; using Umbraco.Cms.Core.Models.Packaging; using Umbraco.Cms.Core.Packaging; +using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.New.Cms.Core.Models; namespace Umbraco.Cms.Core.Services; @@ -28,6 +30,11 @@ public interface IPackagingService : IService /// IEnumerable GetAllInstalledPackages(); + /// + /// Returns installed packages collected from the package migration plans. + /// + Task> GetInstalledPackagesFromMigrationPlansAsync(int skip, int take); + InstalledPackage? GetInstalledPackageByName(string packageName); /// @@ -43,17 +50,51 @@ public interface IPackagingService : IService /// PackageDefinition? GetCreatedPackageById(int id); + /// + /// Returns a created package by key. + /// + /// The key of the package. + /// The package or null if the package was not found. + Task GetCreatedPackageByKeyAsync(Guid key); + + [Obsolete("Use DeleteCreatedPackageAsync instead. Scheduled for removal in Umbraco 15.")] void DeleteCreatedPackage(int id, int userId = Constants.Security.SuperUserId); + /// + /// Deletes a created package by key. + /// + /// The key of the package. + /// Optional id of the user deleting the package. + Task> DeleteCreatedPackageAsync(Guid key, int userId = Constants.Security.SuperUserId); + /// /// Persists a package definition to storage /// /// + [Obsolete("Use CreateCreatedPackageAsync or UpdateCreatedPackageAsync instead. Scheduled for removal in Umbraco 15.")] bool SaveCreatedPackage(PackageDefinition definition); + /// + /// Creates a new package. + /// + /// model for the package to create. + Task> CreateCreatedPackageAsync(PackageDefinition package, int userId); + + /// + /// Updates a created package. + /// + /// model for the package to update. + Task> UpdateCreatedPackageAsync(PackageDefinition package, int userId); + /// /// Creates the package file and returns it's physical path /// /// string ExportCreatedPackage(PackageDefinition definition); + + /// + /// Gets the package file stream. + /// + /// + Stream? GetPackageFileStream(PackageDefinition definition) => null; } diff --git a/src/Umbraco.Core/Services/ITrackedReferencesService.cs b/src/Umbraco.Core/Services/ITrackedReferencesService.cs index 94a8871e7f..50730c4f07 100644 --- a/src/Umbraco.Core/Services/ITrackedReferencesService.cs +++ b/src/Umbraco.Core/Services/ITrackedReferencesService.cs @@ -17,6 +17,7 @@ public interface ITrackedReferencesService /// dependencies (isDependency field is set to true). /// /// A paged result of objects. + [Obsolete("Use overload that takes key instead of id. This will be removed in Umbraco 15.")] PagedResult GetPagedRelationsForItem(int id, long pageIndex, int pageSize, bool filterMustBeIsDependency); /// @@ -30,6 +31,7 @@ public interface ITrackedReferencesService /// dependencies (isDependency field is set to true). /// /// A paged result of objects. + [Obsolete("Use overload that takes key instead of id. This will be removed in Umbraco 15.")] PagedResult GetPagedDescendantsInReferences(int parentId, long pageIndex, int pageSize, bool filterMustBeIsDependency); /// @@ -43,9 +45,13 @@ public interface ITrackedReferencesService /// dependencies (isDependency field is set to true). /// /// A paged result of objects. + [Obsolete("Use method that takes key (Guid) instead of id (int). This will be removed in Umbraco 15.")] PagedResult GetPagedItemsWithRelations(int[] ids, long pageIndex, int pageSize, bool filterMustBeIsDependency); - /// + [Obsolete("Use method that takes key (Guid) instead of id (int). This will be removed in Umbraco 15.")] + PagedModel GetPagedRelationsForItem(int id, long skip, long take, bool filterMustBeIsDependency); + + /// /// Gets a paged result of items which are in relation with the current item. /// Basically, shows the items which depend on the current item. /// @@ -58,26 +64,32 @@ public interface ITrackedReferencesService /// /// The total amount of items. /// A paged result of objects. - PagedModel GetPagedRelationsForItem(int id, long skip, long take, bool filterMustBeIsDependency) => throw new NotImplementedException(); + Task> GetPagedRelationsForItemAsync(Guid key, long skip, long take, bool filterMustBeIsDependency); + + [Obsolete("Use method that takes key (Guid) instead of id (int). This will be removed in Umbraco 15.")] + PagedModel GetPagedDescendantsInReferences(int parentId, long skip, long take, bool filterMustBeIsDependency); /// /// Gets a paged result of the descending items that have any references, given a parent id. /// - /// The unique identifier of the parent to retrieve descendants for. + /// The unique identifier of the parent to retrieve descendants for. /// The amount of items to skip /// The amount of items to take. /// /// A boolean indicating whether to filter only the RelationTypes which are /// dependencies (isDependency field is set to true). /// - /// The total amount of items. /// A paged result of objects. - PagedModel GetPagedDescendantsInReferences(int parentId, long skip, long take, bool filterMustBeIsDependency) => throw new NotImplementedException(); + Task> GetPagedDescendantsInReferencesAsync(Guid parentKey, long skip, long take, bool filterMustBeIsDependency); + + [Obsolete("Use method that takes keys (Guid) instead of ids (int). This will be removed in Umbraco 15.")] + PagedModel GetPagedItemsWithRelations(int[] ids, long skip, long take, + bool filterMustBeIsDependency); /// /// Gets a paged result of items used in any kind of relation from selected integer ids. /// - /// The identifiers of the entities to check for relations. + /// The identifiers of the entities to check for relations. /// The amount of items to skip /// The amount of items to take. /// @@ -86,5 +98,6 @@ public interface ITrackedReferencesService /// /// The total amount of items. /// A paged result of objects. - PagedModel GetPagedItemsWithRelations(int[] ids, long skip, long take, bool filterMustBeIsDependency) => throw new NotImplementedException(); + Task> GetPagedItemsWithRelationsAsync(ISet keys, long skip, long take, + bool filterMustBeIsDependency); } diff --git a/src/Umbraco.Core/Services/IUserService.cs b/src/Umbraco.Core/Services/IUserService.cs index b04e9d8850..095585c0e7 100644 --- a/src/Umbraco.Core/Services/IUserService.cs +++ b/src/Umbraco.Core/Services/IUserService.cs @@ -119,6 +119,13 @@ public interface IUserService : IMembershipUserService /// IProfile? GetProfileByUserName(string username); + /// + /// Get a user by its key. + /// + /// The GUID key of the user. + /// The found user, or null if nothing was found. + Task GetAsync(Guid key) => Task.FromResult(GetAll(0, int.MaxValue, out _).FirstOrDefault(x=>x.Key == key)); + /// /// Gets a user by Id /// diff --git a/src/Umbraco.Core/Services/IWebProfilerService.cs b/src/Umbraco.Core/Services/IWebProfilerService.cs new file mode 100644 index 0000000000..5366c8781d --- /dev/null +++ b/src/Umbraco.Core/Services/IWebProfilerService.cs @@ -0,0 +1,9 @@ +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Core.Services; + +public interface IWebProfilerService +{ + Task> GetStatus(); + Task> SetStatus(bool status); +} diff --git a/src/Umbraco.Core/Services/OperationStatus/AuditLogOperationStatus.cs b/src/Umbraco.Core/Services/OperationStatus/AuditLogOperationStatus.cs new file mode 100644 index 0000000000..c21a412e49 --- /dev/null +++ b/src/Umbraco.Core/Services/OperationStatus/AuditLogOperationStatus.cs @@ -0,0 +1,7 @@ +namespace Umbraco.Cms.Core.Services.OperationStatus; + +public enum AuditLogOperationStatus +{ + Success, + UserNotFound, +} diff --git a/src/Umbraco.Core/Services/OperationStatus/PackageMigrationOperationStatus.cs b/src/Umbraco.Core/Services/OperationStatus/PackageMigrationOperationStatus.cs new file mode 100644 index 0000000000..10806fe378 --- /dev/null +++ b/src/Umbraco.Core/Services/OperationStatus/PackageMigrationOperationStatus.cs @@ -0,0 +1,8 @@ +namespace Umbraco.Cms.Core.Services.OperationStatus; + +public enum PackageMigrationOperationStatus +{ + Success, + NotFound, + CancelledByFailedMigration +} diff --git a/src/Umbraco.Core/Services/OperationStatus/PackageOperationStatus.cs b/src/Umbraco.Core/Services/OperationStatus/PackageOperationStatus.cs new file mode 100644 index 0000000000..6b6fcb54a6 --- /dev/null +++ b/src/Umbraco.Core/Services/OperationStatus/PackageOperationStatus.cs @@ -0,0 +1,9 @@ +namespace Umbraco.Cms.Core.Services.OperationStatus; + +public enum PackageOperationStatus +{ + Success, + NotFound, + DuplicateItemName, + InvalidName +} diff --git a/src/Umbraco.Core/Services/OperationStatus/WebProfilerOperationStatus.cs b/src/Umbraco.Core/Services/OperationStatus/WebProfilerOperationStatus.cs new file mode 100644 index 0000000000..2a3a0bc3f2 --- /dev/null +++ b/src/Umbraco.Core/Services/OperationStatus/WebProfilerOperationStatus.cs @@ -0,0 +1,7 @@ +namespace Umbraco.Cms.Core.Services.OperationStatus; + +public enum WebProfilerOperationStatus +{ + Success, + ExecutingUserNotFound +} diff --git a/src/Umbraco.Core/Services/TrackedReferencesService.cs b/src/Umbraco.Core/Services/TrackedReferencesService.cs index 280e648327..3b2a89a531 100644 --- a/src/Umbraco.Core/Services/TrackedReferencesService.cs +++ b/src/Umbraco.Core/Services/TrackedReferencesService.cs @@ -1,3 +1,5 @@ +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; @@ -8,28 +10,33 @@ namespace Umbraco.Cms.Core.Services; public class TrackedReferencesService : ITrackedReferencesService { private readonly ICoreScopeProvider _scopeProvider; + private readonly IEntityService _entityService; private readonly ITrackedReferencesRepository _trackedReferencesRepository; - [Obsolete("Please use ctor that does not take an IEntityService, scheduled for removal in V12")] + public TrackedReferencesService( ITrackedReferencesRepository trackedReferencesRepository, ICoreScopeProvider scopeProvider, - IEntityService entityService) : this(trackedReferencesRepository, scopeProvider) - { - } - - public TrackedReferencesService( - ITrackedReferencesRepository trackedReferencesRepository, - ICoreScopeProvider scopeProvider) + IEntityService entityService) { _trackedReferencesRepository = trackedReferencesRepository; _scopeProvider = scopeProvider; + _entityService = entityService; + } + + [Obsolete("Please use ctor that does not take an IEntityService, scheduled for removal in V15")] + public TrackedReferencesService( + ITrackedReferencesRepository trackedReferencesRepository, + ICoreScopeProvider scopeProvider): this(trackedReferencesRepository, scopeProvider, StaticServiceProvider.Instance.GetRequiredService()) + { + } /// /// Gets a paged result of items which are in relation with the current item. /// Basically, shows the items which depend on the current item. /// + [Obsolete("Use overload that takes key instead of id. This will be removed in Umbraco 15.")] public PagedResult GetPagedRelationsForItem(int id, long pageIndex, int pageSize, bool filterMustBeIsDependency) { using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true); @@ -41,6 +48,7 @@ public class TrackedReferencesService : ITrackedReferencesService /// /// Gets a paged result of items used in any kind of relation from selected integer ids. /// + [Obsolete("Use overload that takes key instead of id. This will be removed in Umbraco 15.")] public PagedResult GetPagedItemsWithRelations(int[] ids, long pageIndex, int pageSize, bool filterMustBeIsDependency) { using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true); @@ -52,6 +60,7 @@ public class TrackedReferencesService : ITrackedReferencesService /// /// Gets a paged result of the descending items that have any references, given a parent id. /// + [Obsolete("Use overload that takes key instead of id. This will be removed in Umbraco 15.")] public PagedResult GetPagedDescendantsInReferences(int parentId, long pageIndex, int pageSize, bool filterMustBeIsDependency) { using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true); @@ -65,6 +74,7 @@ public class TrackedReferencesService : ITrackedReferencesService return new PagedResult(totalItems, pageIndex + 1, pageSize) { Items = items }; } + [Obsolete("Use overload that takes key instead of id. This will be removed in Umbraco 15.")] public PagedModel GetPagedRelationsForItem(int id, long skip, long take, bool filterMustBeIsDependency) { using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true); @@ -74,6 +84,16 @@ public class TrackedReferencesService : ITrackedReferencesService return pagedModel; } + public async Task> GetPagedRelationsForItemAsync(Guid key, long skip, long take, bool filterMustBeIsDependency) + { + using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true); + IEnumerable items = _trackedReferencesRepository.GetPagedRelationsForItem(key, skip, take, filterMustBeIsDependency, out var totalItems); + var pagedModel = new PagedModel(totalItems, items); + + return await Task.FromResult(pagedModel); + } + + [Obsolete("Use overload that takes key instead of id. This will be removed in Umbraco 15.")] public PagedModel GetPagedDescendantsInReferences(int parentId, long skip, long take, bool filterMustBeIsDependency) { using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true); @@ -89,6 +109,22 @@ public class TrackedReferencesService : ITrackedReferencesService return pagedModel; } + public async Task> GetPagedDescendantsInReferencesAsync(Guid parentKey, long skip, long take, bool filterMustBeIsDependency) + { + using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true); + + IEnumerable items = _trackedReferencesRepository.GetPagedDescendantsInReferences( + parentKey, + skip, + take, + filterMustBeIsDependency, + out var totalItems); + var pagedModel = new PagedModel(totalItems, items); + + return await Task.FromResult(pagedModel); + } + + [Obsolete("Use overload that takes key instead of id. This will be removed in Umbraco 15.")] public PagedModel GetPagedItemsWithRelations(int[] ids, long skip, long take, bool filterMustBeIsDependency) { using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true); @@ -97,4 +133,13 @@ public class TrackedReferencesService : ITrackedReferencesService return pagedModel; } + + public async Task> GetPagedItemsWithRelationsAsync(ISet keys, long skip, long take, bool filterMustBeIsDependency) + { + using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true); + IEnumerable items = _trackedReferencesRepository.GetPagedItemsWithRelations(keys, skip, take, filterMustBeIsDependency, out var totalItems); + var pagedModel = new PagedModel(totalItems, items); + + return await Task.FromResult(pagedModel); + } } diff --git a/src/Umbraco.Core/Services/UserService.cs b/src/Umbraco.Core/Services/UserService.cs index 0782431e91..dc3acad3c5 100644 --- a/src/Umbraco.Core/Services/UserService.cs +++ b/src/Umbraco.Core/Services/UserService.cs @@ -251,8 +251,22 @@ internal class UserService : RepositoryService, IUserService { using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) { - IQuery query = Query().Where(x => x.Email.Equals(email)); - return _userRepository.Get(query)?.FirstOrDefault(); + try + { + IQuery query = Query().Where(x => x.Email.Equals(email)); + return _userRepository.Get(query)?.FirstOrDefault(); + } + catch(DbException) + { + // We also need to catch upgrade state here, because the framework will try to call this to validate the email. + if (IsUpgrading) + { + return _userRepository.GetForUpgradeByEmail(email); + } + + throw; + } + } } @@ -285,7 +299,7 @@ internal class UserService : RepositoryService, IUserService if (IsUpgrading) { // NOTE: this will not be cached - return _userRepository.GetByUsername(username, false); + return _userRepository.GetForUpgradeByUsername(username); } throw; @@ -802,7 +816,7 @@ internal class UserService : RepositoryService, IUserService if (IsUpgrading) { // NOTE: this will not be cached - return _userRepository.Get(id, false); + return _userRepository.GetForUpgrade(id); } throw; @@ -810,6 +824,15 @@ internal class UserService : RepositoryService, IUserService } } + public Task GetAsync(Guid key) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + IQuery query = Query().Where(x => x.Key == key); + return Task.FromResult(_userRepository.Get(query).FirstOrDefault()); + } + } + public IEnumerable GetUsersById(params int[]? ids) { if (ids?.Length <= 0) diff --git a/src/Umbraco.Core/Services/UserServiceExtensions.cs b/src/Umbraco.Core/Services/UserServiceExtensions.cs index f17a266616..761b924ac5 100644 --- a/src/Umbraco.Core/Services/UserServiceExtensions.cs +++ b/src/Umbraco.Core/Services/UserServiceExtensions.cs @@ -81,9 +81,6 @@ public static class UserServiceExtensions }); } - public static IUser? GetByKey(this IUserService userService, Guid key) - { - var id = BitConverter.ToInt32(key.ToByteArray(), 0); - return userService.GetUserById(id); - } + [Obsolete("Use IUserService.Get that takes a Guid instead. Scheduled for removal in V15.")] + public static IUser? GetByKey(this IUserService userService, Guid key) => userService.GetAsync(key).GetAwaiter().GetResult(); } diff --git a/src/Umbraco.Core/Services/WebProfilerService.cs b/src/Umbraco.Core/Services/WebProfilerService.cs new file mode 100644 index 0000000000..026c2afd38 --- /dev/null +++ b/src/Umbraco.Core/Services/WebProfilerService.cs @@ -0,0 +1,55 @@ +using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Core.Services; + +internal sealed class WebProfilerService : IWebProfilerService +{ + private readonly IWebProfilerRepository _webProfilerRepository; + private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; + + public WebProfilerService(IWebProfilerRepository webProfilerRepository, IBackOfficeSecurityAccessor backOfficeSecurityAccessor) + { + _webProfilerRepository = webProfilerRepository; + _backOfficeSecurityAccessor = backOfficeSecurityAccessor; + } + + public async Task> GetStatus() + { + Attempt userIdAttempt = GetExecutingUserId(); + + if (userIdAttempt.Success is false) + { + return Attempt.FailWithStatus(WebProfilerOperationStatus.ExecutingUserNotFound, false); + } + + var result = _webProfilerRepository.GetStatus(userIdAttempt.Result); + return await Task.FromResult(Attempt.SucceedWithStatus(WebProfilerOperationStatus.Success, result)); + } + + public async Task> SetStatus(bool status) + { + Attempt userIdAttempt = GetExecutingUserId(); + + if (userIdAttempt.Success is false) + { + return Attempt.FailWithStatus(WebProfilerOperationStatus.ExecutingUserNotFound, false); + } + + _webProfilerRepository.SetStatus(userIdAttempt.Result, status); + return await Task.FromResult(Attempt.SucceedWithStatus(WebProfilerOperationStatus.Success, status)); + } + + private Attempt GetExecutingUserId() + { + //FIXME when we can get current user + return Attempt.Succeed(-1); + + Attempt? userIdAttempt = _backOfficeSecurityAccessor?.BackOfficeSecurity?.GetUserId(); + + return (userIdAttempt.HasValue && userIdAttempt.Value.Success) + ? Attempt.Succeed(userIdAttempt.Value.Result) + : Attempt.Fail(0); + } +} diff --git a/src/Umbraco.Infrastructure/Extensions/GuidExtensions.cs b/src/Umbraco.Infrastructure/Extensions/GuidExtensions.cs new file mode 100644 index 0000000000..d2a189bd14 --- /dev/null +++ b/src/Umbraco.Infrastructure/Extensions/GuidExtensions.cs @@ -0,0 +1,24 @@ +namespace Umbraco.Cms.Infrastructure.Extensions; + +public static class GuidExtensions +{ + internal static bool IsFakeGuid(this Guid guid) + { + var bytes = guid.ToByteArray(); + + // Our fake guid is a 32 bit int, converted to a byte representation, + // so we can check if everything but the first 4 bytes are 0, if so, we know it's a fake guid. + if (bytes[4..].All(x => x == 0)) + { + return true; + } + + return false; + } + + internal static int ToInt(this Guid guid) + { + var bytes = guid.ToByteArray(); + return BitConverter.ToInt32(bytes, 0); + } +} diff --git a/src/Umbraco.Infrastructure/Install/PackageMigrationRunner.cs b/src/Umbraco.Infrastructure/Install/PackageMigrationRunner.cs index a6741b74fc..4f8e393b6a 100644 --- a/src/Umbraco.Infrastructure/Install/PackageMigrationRunner.cs +++ b/src/Umbraco.Infrastructure/Install/PackageMigrationRunner.cs @@ -12,6 +12,7 @@ using Umbraco.Cms.Infrastructure.Migrations; using Umbraco.Cms.Infrastructure.Migrations.Notifications; using Umbraco.Cms.Infrastructure.Migrations.Upgrade; using Umbraco.Extensions; +using Umbraco.Cms.Core.Services.OperationStatus; namespace Umbraco.Cms.Infrastructure.Install; @@ -89,6 +90,28 @@ public class PackageMigrationRunner return RunPackagePlans(packagePlans); } + /// + /// Checks if all executed package migrations succeeded for a package. + /// + public async Task> RunPendingPackageMigrations(string packageName) + { + // Check if there are any migrations + if (_packageMigrationPlans.ContainsKey(packageName) == false) + { + return Attempt.FailWithStatus(PackageMigrationOperationStatus.NotFound, false); + } + + // Run the migrations + IEnumerable executedMigrationPlans = RunPackageMigrationsIfPending(packageName); + + if (executedMigrationPlans.Any(plan => plan.Successful == false)) + { + return Attempt.FailWithStatus(PackageMigrationOperationStatus.CancelledByFailedMigration, false); + } + + return Attempt.SucceedWithStatus(PackageMigrationOperationStatus.Success, true); + } + /// /// Runs the all specified package migration plans and publishes a /// if all are successful. diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs index e77d26f088..decf0b984c 100644 --- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs @@ -1152,6 +1152,7 @@ internal class DatabaseDataCreator new UserDto { Id = Constants.Security.SuperUserId, + Key = new Guid("1E70F841-C261-413B-ABB2-2D68CDB96094"), Disabled = false, NoConsole = false, UserName = "Administrator", @@ -1172,7 +1173,7 @@ internal class DatabaseDataCreator new UserGroupDto { Id = 1, - Key = Guid.NewGuid(), + Key = new Guid("F3120A06-78D0-404F-8218-0C41F70C5A56"), StartMediaId = -1, StartContentId = -1, Alias = Constants.Security.AdminGroupAlias, @@ -1190,7 +1191,7 @@ internal class DatabaseDataCreator new UserGroupDto { Id = 2, - Key = Guid.NewGuid(), + Key = new Guid("95F812FB-B401-46C3-9314-60996F414B29"), StartMediaId = -1, StartContentId = -1, Alias = Constants.Security.WriterGroupAlias, @@ -1208,7 +1209,7 @@ internal class DatabaseDataCreator new UserGroupDto { Id = 3, - Key = Guid.NewGuid(), + Key = new Guid("98BCC501-AC7F-4EB0-B10B-1C8FA6F91141"), StartMediaId = -1, StartContentId = -1, Alias = Constants.Security.EditorGroupAlias, @@ -1226,7 +1227,7 @@ internal class DatabaseDataCreator new UserGroupDto { Id = 4, - Key = Guid.NewGuid(), + Key = new Guid("AA1EC438-7810-4C72-A636-87A840D8D57F"), StartMediaId = -1, StartContentId = -1, Alias = Constants.Security.TranslatorGroupAlias, @@ -1244,7 +1245,7 @@ internal class DatabaseDataCreator new UserGroupDto { Id = 5, - Key = Guid.NewGuid(), + Key = new Guid("17627245-521E-4871-9F20-81C809B714FD"), Alias = Constants.Security.SensitiveDataGroupAlias, Name = "Sensitive data", DefaultPermissions = string.Empty, @@ -1407,7 +1408,7 @@ internal class DatabaseDataCreator new PropertyTypeDto { Id = 6, - UniqueId = 6.ToGuid(), + UniqueId = new Guid("B646CA8F-E469-4FC2-A48A-D4DC1AA64A53"), DataTypeId = Constants.DataTypes.ImageCropper, ContentTypeId = 1032, PropertyTypeGroupId = 3, @@ -1423,7 +1424,7 @@ internal class DatabaseDataCreator new PropertyTypeDto { Id = 7, - UniqueId = 7.ToGuid(), + UniqueId = new Guid("A68D453B-1F62-44F4-9F71-0B6BBD43C355"), DataTypeId = Constants.DataTypes.LabelInt, ContentTypeId = 1032, PropertyTypeGroupId = 3, @@ -1439,7 +1440,7 @@ internal class DatabaseDataCreator new PropertyTypeDto { Id = 8, - UniqueId = 8.ToGuid(), + UniqueId = new Guid("854087F6-648B-40ED-BC98-B8A9789E80B9"), DataTypeId = Constants.DataTypes.LabelInt, ContentTypeId = 1032, PropertyTypeGroupId = 3, @@ -1455,7 +1456,7 @@ internal class DatabaseDataCreator new PropertyTypeDto { Id = 9, - UniqueId = 9.ToGuid(), + UniqueId = new Guid("BD4C5ACE-26E3-4A8B-AF1A-E8206A35FA07"), DataTypeId = Constants.DataTypes.LabelBigint, ContentTypeId = 1032, PropertyTypeGroupId = 3, @@ -1471,7 +1472,7 @@ internal class DatabaseDataCreator new PropertyTypeDto { Id = 10, - UniqueId = 10.ToGuid(), + UniqueId = new Guid("F7786FE8-724A-4ED0-B244-72546DB32A92"), DataTypeId = -92, ContentTypeId = 1032, PropertyTypeGroupId = 3, @@ -1491,7 +1492,7 @@ internal class DatabaseDataCreator new PropertyTypeDto { Id = 24, - UniqueId = 24.ToGuid(), + UniqueId = new Guid("A0FB68F3-F427-47A6-AFCE-536FFA5B64E9"), DataTypeId = Constants.DataTypes.Upload, ContentTypeId = 1033, PropertyTypeGroupId = 4, @@ -1507,7 +1508,7 @@ internal class DatabaseDataCreator new PropertyTypeDto { Id = 25, - UniqueId = 25.ToGuid(), + UniqueId = new Guid("3531C0A3-4E0A-4324-A621-B9D3822B071F"), DataTypeId = -92, ContentTypeId = 1033, PropertyTypeGroupId = 4, @@ -1523,7 +1524,7 @@ internal class DatabaseDataCreator new PropertyTypeDto { Id = 26, - UniqueId = 26.ToGuid(), + UniqueId = new Guid("F9527050-59BC-43E4-8FA8-1658D1319FF5"), DataTypeId = Constants.DataTypes.LabelBigint, ContentTypeId = 1033, PropertyTypeGroupId = 4, @@ -1543,7 +1544,7 @@ internal class DatabaseDataCreator new PropertyTypeDto { Id = 40, - UniqueId = 40.ToGuid(), + UniqueId = new Guid("BED8AB97-D85F-44D2-A8B9-AEF6893F9610"), DataTypeId = Constants.DataTypes.UploadVideo, ContentTypeId = 1034, PropertyTypeGroupId = 52, @@ -1559,7 +1560,7 @@ internal class DatabaseDataCreator new PropertyTypeDto { Id = 41, - UniqueId = 41.ToGuid(), + UniqueId = new Guid("EDD2B3FD-1E57-4E57-935E-096DEFCCDC9B"), DataTypeId = -92, ContentTypeId = 1034, PropertyTypeGroupId = 52, @@ -1575,7 +1576,7 @@ internal class DatabaseDataCreator new PropertyTypeDto { Id = 42, - UniqueId = 42.ToGuid(), + UniqueId = new Guid("180EEECF-1F00-409E-8234-BBA967E08B0A"), DataTypeId = Constants.DataTypes.LabelBigint, ContentTypeId = 1034, PropertyTypeGroupId = 52, @@ -1595,7 +1596,7 @@ internal class DatabaseDataCreator new PropertyTypeDto { Id = 43, - UniqueId = 43.ToGuid(), + UniqueId = new Guid("1F48D730-F174-4684-AFAD-A335E59D84A0"), DataTypeId = Constants.DataTypes.UploadAudio, ContentTypeId = 1035, PropertyTypeGroupId = 53, @@ -1611,7 +1612,7 @@ internal class DatabaseDataCreator new PropertyTypeDto { Id = 44, - UniqueId = 44.ToGuid(), + UniqueId = new Guid("1BEE433F-A21A-4031-8E03-AF01BB8D2DE9"), DataTypeId = -92, ContentTypeId = 1035, PropertyTypeGroupId = 53, @@ -1627,7 +1628,7 @@ internal class DatabaseDataCreator new PropertyTypeDto { Id = 45, - UniqueId = 45.ToGuid(), + UniqueId = new Guid("3CBF538A-29AB-4317-A9EB-BBCDF1A54260"), DataTypeId = Constants.DataTypes.LabelBigint, ContentTypeId = 1035, PropertyTypeGroupId = 53, @@ -1647,7 +1648,7 @@ internal class DatabaseDataCreator new PropertyTypeDto { Id = 46, - UniqueId = 46.ToGuid(), + UniqueId = new Guid("E5C8C2D0-2D82-4F01-B53A-45A1D1CBF19C"), DataTypeId = Constants.DataTypes.UploadArticle, ContentTypeId = 1036, PropertyTypeGroupId = 54, @@ -1663,7 +1664,7 @@ internal class DatabaseDataCreator new PropertyTypeDto { Id = 47, - UniqueId = 47.ToGuid(), + UniqueId = new Guid("EF1B4AF7-36DE-45EB-8C18-A2DE07319227"), DataTypeId = -92, ContentTypeId = 1036, PropertyTypeGroupId = 54, @@ -1679,7 +1680,7 @@ internal class DatabaseDataCreator new PropertyTypeDto { Id = 48, - UniqueId = 48.ToGuid(), + UniqueId = new Guid("AAB7D00C-7209-4337-BE3F-A4421C8D79A0"), DataTypeId = Constants.DataTypes.LabelBigint, ContentTypeId = 1036, PropertyTypeGroupId = 54, @@ -1699,7 +1700,7 @@ internal class DatabaseDataCreator new PropertyTypeDto { Id = 49, - UniqueId = 49.ToGuid(), + UniqueId = new Guid("E2A2BDF2-971B-483E-95A1-4104CC06AF26"), DataTypeId = Constants.DataTypes.UploadVectorGraphics, ContentTypeId = 1037, PropertyTypeGroupId = 55, @@ -1715,7 +1716,7 @@ internal class DatabaseDataCreator new PropertyTypeDto { Id = 50, - UniqueId = 50.ToGuid(), + UniqueId = new Guid("0F25A89E-2EB7-49BC-A7B4-759A7E4C69F2"), DataTypeId = -92, ContentTypeId = 1037, PropertyTypeGroupId = 55, @@ -1731,7 +1732,7 @@ internal class DatabaseDataCreator new PropertyTypeDto { Id = 51, - UniqueId = 51.ToGuid(), + UniqueId = new Guid("09A07AFF-861D-4769-A2B0-C165EBD43D39"), DataTypeId = Constants.DataTypes.LabelBigint, ContentTypeId = 1037, PropertyTypeGroupId = 55, @@ -1752,7 +1753,7 @@ internal class DatabaseDataCreator new PropertyTypeDto { Id = 28, - UniqueId = 28.ToGuid(), + UniqueId = new Guid("70F24C26-1C0E-4053-BD8E-E9E6E4EC4C01"), DataTypeId = Constants.DataTypes.Textarea, ContentTypeId = 1044, PropertyTypeGroupId = 11, diff --git a/src/Umbraco.Infrastructure/Migrations/MigrationPlanExecutor.cs b/src/Umbraco.Infrastructure/Migrations/MigrationPlanExecutor.cs index 8c7e1c2f9a..ebe22d518e 100644 --- a/src/Umbraco.Infrastructure/Migrations/MigrationPlanExecutor.cs +++ b/src/Umbraco.Infrastructure/Migrations/MigrationPlanExecutor.cs @@ -137,7 +137,8 @@ public class MigrationPlanExecutor : IMigrationPlanExecutor } catch (Exception exception) { - _logger.LogError("Plan failed at step {TargetState}", transition.TargetState); + _logger.LogError(exception, "Plan {PlanName} failed at step {TargetState}", plan.Name, transition.TargetState); + // We have to always return something, so whatever running this has a chance to save the state we got to. return new ExecutedMigrationPlan { diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs index 5e9ef82a13..b5df9be73e 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs @@ -84,5 +84,7 @@ public class UmbracoPlan : MigrationPlan To("{5F15A1CC-353D-4889-8C7E-F303B4766196}"); To("{69E12556-D9B3-493A-8E8A-65EC89FB658D}"); To("{F2B16CD4-F181-4BEE-81C9-11CF384E6025}"); + To("{A8E01644-9F2E-4988-8341-587EF5B7EA69}"); + To("{E073DBC0-9E8E-4C92-8210-9CB18364F46E}"); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_13_0_0/AddGuidsToUserGroups.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_13_0_0/AddGuidsToUserGroups.cs index 3f75caabee..24a9714a06 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_13_0_0/AddGuidsToUserGroups.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_13_0_0/AddGuidsToUserGroups.cs @@ -37,6 +37,11 @@ public class AddGuidsToUserGroups : UnscopedMigrationBase using IDisposable notificationSuppression = scope.Notifications.Suppress(); ScopeDatabase(scope); + if (ColumnExists(Constants.DatabaseSchema.Tables.UserGroup, NewColumnName)) + { + return; + } + var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToList(); AddColumnIfNotExists(columns, NewColumnName); scope.Complete(); diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_13_0_0/AddGuidsToUsers.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_13_0_0/AddGuidsToUsers.cs new file mode 100644 index 0000000000..064d4e6190 --- /dev/null +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_13_0_0/AddGuidsToUsers.cs @@ -0,0 +1,271 @@ +using NPoco; +using Umbraco.Cms.Core; +using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; +using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Infrastructure.Scoping; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_13_0_0; + +/// +/// This is an unscoped migration to support migrating sqlite, since it doesn't support adding columns. +/// See for more information. +/// +public class AddGuidsToUsers : UnscopedMigrationBase +{ + private const string NewColumnName = "key"; + private readonly IScopeProvider _scopeProvider; + + public AddGuidsToUsers(IMigrationContext context, IScopeProvider scopeProvider) + : base(context) + { + _scopeProvider = scopeProvider; + } + + protected override void Migrate() + { + using IScope scope = _scopeProvider.CreateScope(); + using IDisposable notificationSuppression = scope.Notifications.Suppress(); + ScopeDatabase(scope); + + if (DatabaseType != DatabaseType.SQLite) + { + MigrateSqlServer(); + scope.Complete(); + return; + } + + MigrateSqlite(); + scope.Complete(); + } + + private void MigrateSqlServer() + { + var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToList(); + AddColumnIfNotExists(columns, NewColumnName); + List? dtos = Database.Fetch(); + if (dtos is null) + { + return; + } + + MigrateExternalLogins(dtos); + MigrateTwoFactorLogins(dtos); + } + + private void MigrateSqlite() + { + if (ColumnExists(Constants.DatabaseSchema.Tables.User, NewColumnName)) + { + return; + } + + /* + * We commit the initial transaction started by the scope. This is required in order to disable the foreign keys. + * We then begin a new transaction, this transaction will be committed or rolled back by the scope, like normal. + * We don't have to worry about re-enabling the foreign keys, since these are enabled by default every time a connection is established. + * + * Ideally we'd want to do this with the unscoped database we get, however, this cannot be done, + * since our scoped database cannot share a connection with the unscoped database, so a new one will be created, which enables the foreign keys. + * Similarly we cannot use Database.CompleteTransaction(); since this also closes the connection, + * so starting a new transaction would re-enable foreign keys. + */ + Database.Execute("COMMIT;"); + Database.Execute("PRAGMA foreign_keys=off;"); + Database.Execute("BEGIN TRANSACTION;"); + + List users = Database.Fetch().Select(x => new UserDto + { + Id = x.Id, + Key = Guid.NewGuid(), + Disabled = x.Disabled, + NoConsole = x.NoConsole, + UserName = x.UserName, + Login = x.Login, + Password = x.Password, + PasswordConfig = x.PasswordConfig, + Email = x.Email, + UserLanguage = x.UserLanguage, + SecurityStampToken = x.SecurityStampToken, + FailedLoginAttempts = x.FailedLoginAttempts, + LastLockoutDate = x.LastLockoutDate, + LastPasswordChangeDate = x.LastPasswordChangeDate, + LastLoginDate = x.LastLoginDate, + EmailConfirmedDate = x.EmailConfirmedDate, + InvitedDate = x.InvitedDate, + CreateDate = x.CreateDate, + UpdateDate = x.UpdateDate, + Avatar = x.Avatar, + TourData = x.TourData, + }).ToList(); + + Delete.Table(Constants.DatabaseSchema.Tables.User).Do(); + Create.Table().Do(); + + foreach (UserDto user in users) + { + Database.Insert(Constants.DatabaseSchema.Tables.User, "id", false, user); + } + + MigrateExternalLogins(users); + MigrateTwoFactorLogins(users); + } + + private void MigrateExternalLogins(List userDtos) + { + List? externalLogins = Database.Fetch(); + if (externalLogins is null) + { + return; + } + + foreach (ExternalLoginDto externalLogin in externalLogins) + { + UserDto? associatedUser = userDtos.FirstOrDefault(x => x.Id.ToGuid() == externalLogin.UserOrMemberKey); + if (associatedUser is null) + { + continue; + } + + externalLogin.UserOrMemberKey = associatedUser.Key; + Database.Update(externalLogin); + } + } + + private void MigrateTwoFactorLogins(List userDtos) + { + // TODO: TEST ME! + List? twoFactorLoginDtos = Database.Fetch(); + if (twoFactorLoginDtos is null) + { + return; + } + + foreach (TwoFactorLoginDto twoFactorLoginDto in twoFactorLoginDtos) + { + UserDto? associatedUser = userDtos.FirstOrDefault(x => x.Id.ToGuid() == twoFactorLoginDto.UserOrMemberKey); + + if (associatedUser is null) + { + continue; + } + + twoFactorLoginDto.UserOrMemberKey = associatedUser.Key; + Database.Update(twoFactorLoginDto); + } + } + + [TableName(TableName)] + [PrimaryKey("id", AutoIncrement = true)] + [ExplicitColumns] + public class OldUserDto + { + public const string TableName = Constants.DatabaseSchema.Tables.User; + + public OldUserDto() + { + UserGroupDtos = new List(); + UserStartNodeDtos = new HashSet(); + } + + [Column("id")] + [PrimaryKeyColumn(Name = "PK_user")] + public int Id { get; set; } + + [Column("userDisabled")] + [Constraint(Default = "0")] + public bool Disabled { get; set; } + + [Column("userNoConsole")] + [Constraint(Default = "0")] + public bool NoConsole { get; set; } + + [Column("userName")] public string UserName { get; set; } = null!; + + [Column("userLogin")] + [Length(125)] + [Index(IndexTypes.NonClustered)] + public string? Login { get; set; } + + [Column("userPassword")] [Length(500)] public string? Password { get; set; } + + /// + /// This will represent a JSON structure of how the password has been created (i.e hash algorithm, iterations) + /// + [Column("passwordConfig")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(500)] + public string? PasswordConfig { get; set; } + + [Column("userEmail")] public string Email { get; set; } = null!; + + [Column("userLanguage")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(10)] + public string? UserLanguage { get; set; } + + [Column("securityStampToken")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(255)] + public string? SecurityStampToken { get; set; } + + [Column("failedLoginAttempts")] + [NullSetting(NullSetting = NullSettings.Null)] + public int? FailedLoginAttempts { get; set; } + + [Column("lastLockoutDate")] + [NullSetting(NullSetting = NullSettings.Null)] + public DateTime? LastLockoutDate { get; set; } + + [Column("lastPasswordChangeDate")] + [NullSetting(NullSetting = NullSettings.Null)] + public DateTime? LastPasswordChangeDate { get; set; } + + [Column("lastLoginDate")] + [NullSetting(NullSetting = NullSettings.Null)] + public DateTime? LastLoginDate { get; set; } + + [Column("emailConfirmedDate")] + [NullSetting(NullSetting = NullSettings.Null)] + public DateTime? EmailConfirmedDate { get; set; } + + [Column("invitedDate")] + [NullSetting(NullSetting = NullSettings.Null)] + public DateTime? InvitedDate { get; set; } + + [Column("createDate")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Constraint(Default = SystemMethods.CurrentDateTime)] + public DateTime CreateDate { get; set; } = DateTime.Now; + + [Column("updateDate")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Constraint(Default = SystemMethods.CurrentDateTime)] + public DateTime UpdateDate { get; set; } = DateTime.Now; + + /// + /// Will hold the media file system relative path of the users custom avatar if they uploaded one + /// + [Column("avatar")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(500)] + public string? Avatar { get; set; } + + /// + /// A Json blob stored for recording tour data for a user + /// + [Column("tourData")] + [NullSetting(NullSetting = NullSettings.Null)] + [SpecialDbType(SpecialDbTypes.NVARCHARMAX)] + public string? TourData { get; set; } + + [ResultColumn] + [Reference(ReferenceType.Many, ReferenceMemberName = "UserId")] + public List UserGroupDtos { get; set; } + + [ResultColumn] + [Reference(ReferenceType.Many, ReferenceMemberName = "UserId")] + public HashSet UserStartNodeDtos { get; set; } + } +} diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_13_0_0/UpdateDefaultGuidsOfCreatedPackages.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_13_0_0/UpdateDefaultGuidsOfCreatedPackages.cs new file mode 100644 index 0000000000..93ec0c4b62 --- /dev/null +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_13_0_0/UpdateDefaultGuidsOfCreatedPackages.cs @@ -0,0 +1,44 @@ +using System.Xml.Linq; +using Umbraco.Cms.Core.Packaging; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; + +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_13_0_0; + +public class UpdateDefaultGuidsOfCreatedPackages : MigrationBase +{ + private readonly PackageDefinitionXmlParser _xmlParser; + + public UpdateDefaultGuidsOfCreatedPackages(IMigrationContext context) + : base(context) + { + _xmlParser = new PackageDefinitionXmlParser(); + } + + protected override void Migrate() + { + IEnumerable createdPackages = Database.Fetch(); + + foreach (CreatedPackageSchemaDto package in createdPackages) + { + if (package.PackageId != default) + { + continue; + } + + var guid = Guid.NewGuid(); + package.PackageId = guid; + + var packageDefinition = _xmlParser.ToPackageDefinition(XElement.Parse(package.Value)); + + if (packageDefinition is not null) + { + packageDefinition.PackageId = guid; + + // Update the package XML value with the correct GUID + package.Value = _xmlParser.ToXml(packageDefinition).ToString(); + } + + Database.Update(package); + } + } +} diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/UserDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/UserDto.cs index 21c6afde38..f71a8bc60d 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/UserDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/UserDto.cs @@ -18,7 +18,6 @@ public class UserDto UserStartNodeDtos = new HashSet(); } - // TODO: We need to add a GUID for users and track external logins with that instead of the INT [Column("id")] [PrimaryKeyColumn(Name = "PK_user")] public int Id { get; set; } @@ -27,6 +26,12 @@ public class UserDto [Constraint(Default = "0")] public bool Disabled { get; set; } + [Column("key")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Constraint(Default = SystemMethods.NewGuid)] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoUser_userKey")] + public Guid Key { get; set; } + [Column("userNoConsole")] [Constraint(Default = "0")] public bool NoConsole { get; set; } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/UserFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/UserFactory.cs index 17c445576d..4051c0d198 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/UserFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/UserFactory.cs @@ -9,7 +9,12 @@ internal static class UserFactory { public static IUser BuildEntity(GlobalSettings globalSettings, UserDto dto) { - var guidId = dto.Id.ToGuid(); + Guid key = dto.Key; + // This should only happen if the user is still not migrated to have a true key. + if (key == Guid.Empty) + { + key = dto.Id.ToGuid(); + } var user = new User(globalSettings, dto.Id, dto.UserName, dto.Email, dto.Login, dto.Password, dto.PasswordConfig, @@ -23,7 +28,7 @@ internal static class UserFactory { user.DisableChangeTracking(); - user.Key = guidId; + user.Key = key; user.IsLockedOut = dto.NoConsole; user.IsApproved = dto.Disabled == false; user.Language = dto.UserLanguage; @@ -54,6 +59,7 @@ internal static class UserFactory { var dto = new UserDto { + Key = entity.Key, Disabled = entity.IsApproved == false, Email = entity.Email, Login = entity.Username, @@ -66,8 +72,7 @@ internal static class UserFactory FailedLoginAttempts = entity.FailedPasswordAttempts, LastLockoutDate = entity.LastLockoutDate == DateTime.MinValue ? null : entity.LastLockoutDate, LastLoginDate = entity.LastLoginDate == DateTime.MinValue ? null : entity.LastLoginDate, - LastPasswordChangeDate = - entity.LastPasswordChangeDate == DateTime.MinValue ? null : entity.LastPasswordChangeDate, + LastPasswordChangeDate = entity.LastPasswordChangeDate == DateTime.MinValue ? null : entity.LastPasswordChangeDate, CreateDate = entity.CreateDate, UpdateDate = entity.UpdateDate, Avatar = entity.Avatar, diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/UserMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/UserMapper.cs index 92af2773cf..276a522f62 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/UserMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/UserMapper.cs @@ -14,6 +14,7 @@ public sealed class UserMapper : BaseMapper protected override void DefineMaps() { + DefineMap(nameof(User.Key), nameof(UserDto.Key)); DefineMap(nameof(User.Id), nameof(UserDto.Id)); DefineMap(nameof(User.Email), nameof(UserDto.Email)); DefineMap(nameof(User.Username), nameof(UserDto.Login)); diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/AuditRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/AuditRepository.cs index f11e17a236..9f0df27897 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/AuditRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/AuditRepository.cs @@ -29,7 +29,7 @@ internal class AuditRepository : EntityRepositoryBase, IAuditRe List? dtos = Database.Fetch(sql); - return dtos.Select(x => new AuditItem(x.NodeId, Enum.Parse(x.Header), x.UserId ?? Constants.Security.UnknownUserId, x.EntityType, x.Comment, x.Parameters)).ToList(); + return dtos.Select(x => new AuditItem(x.NodeId, Enum.Parse(x.Header), x.UserId ?? Constants.Security.UnknownUserId, x.EntityType, x.Comment, x.Parameters, x.Datestamp)).ToList(); } public void CleanLogs(int maximumAgeOfLogsInMinutes) diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/CreatedPackageSchemaRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/CreatedPackageSchemaRepository.cs index 9f921266ca..79a65479e2 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/CreatedPackageSchemaRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/CreatedPackageSchemaRepository.cs @@ -1,4 +1,5 @@ using System.ComponentModel.DataAnnotations; +using System.Data; using System.Globalization; using System.IO.Compression; using System.Xml.Linq; @@ -88,12 +89,9 @@ public class CreatedPackageSchemaRepository : ICreatedPackagesRepository List xmlSchemas = _umbracoDatabase.Fetch(query); foreach (CreatedPackageSchemaDto packageSchema in xmlSchemas) { - var packageDefinition = _xmlParser.ToPackageDefinition(XElement.Parse(packageSchema.Value)); + PackageDefinition? packageDefinition = CreatePackageDefinitionFromSchema(packageSchema); if (packageDefinition is not null) { - packageDefinition.Id = packageSchema.Id; - packageDefinition.Name = packageSchema.Name; - packageDefinition.PackageId = packageSchema.PackageId; packageDefinitions.Add(packageDefinition); } } @@ -107,6 +105,7 @@ public class CreatedPackageSchemaRepository : ICreatedPackagesRepository .Select() .From() .Where(x => x.Id == id); + List schemaDtos = _umbracoDatabase.Fetch(query); if (schemaDtos.IsCollectionEmpty()) @@ -114,16 +113,24 @@ public class CreatedPackageSchemaRepository : ICreatedPackagesRepository return null; } - CreatedPackageSchemaDto packageSchema = schemaDtos.First(); - var packageDefinition = _xmlParser.ToPackageDefinition(XElement.Parse(packageSchema.Value)); - if (packageDefinition is not null) + return CreatePackageDefinitionFromSchema(schemaDtos.First()); + } + + public PackageDefinition? GetByKey(Guid key) + { + Sql query = new Sql(_umbracoDatabase!.SqlContext) + .Select() + .From() + .Where(x => x.PackageId == key); + + List schemaDtos = _umbracoDatabase.Fetch(query); + + if (schemaDtos.IsCollectionEmpty()) { - packageDefinition.Id = packageSchema.Id; - packageDefinition.Name = packageSchema.Name; - packageDefinition.PackageId = packageSchema.PackageId; + return null; } - return packageDefinition; + return CreatePackageDefinitionFromSchema(schemaDtos.First()); } public void Delete(int id) @@ -149,7 +156,7 @@ public class CreatedPackageSchemaRepository : ICreatedPackagesRepository throw new NullReferenceException("PackageDefinition cannot be null when saving"); } - if (string.IsNullOrEmpty(definition.Name) || definition.PackagePath == null) + if (string.IsNullOrEmpty(definition.Name)) { return false; } @@ -159,6 +166,17 @@ public class CreatedPackageSchemaRepository : ICreatedPackagesRepository if (definition.Id == default) { + Sql query = new Sql(_umbracoDatabase!.SqlContext) + .SelectCount() + .From() + .Where(x => x.Name == definition.Name); + var exists = _umbracoDatabase.ExecuteScalar(query); + + if (exists > 0) + { + return false; + } + // Create dto from definition var dto = new CreatedPackageSchemaDto { @@ -173,6 +191,11 @@ public class CreatedPackageSchemaRepository : ICreatedPackagesRepository definition.Id = dto.Id; } + if (definition.PackageId == default) + { + definition.PackageId = Guid.NewGuid(); + } + // Save snapshot locally, we do this to the updated packagePath ExportPackage(definition); @@ -749,4 +772,18 @@ public class CreatedPackageSchemaRepository : ICreatedPackagesRepository mediaTypes.Add(mediaType); } } + + private PackageDefinition? CreatePackageDefinitionFromSchema(CreatedPackageSchemaDto packageSchema) + { + var packageDefinition = _xmlParser.ToPackageDefinition(XElement.Parse(packageSchema.Value)); + + if (packageDefinition is not null) + { + packageDefinition.Id = packageSchema.Id; + packageDefinition.Name = packageSchema.Name; + packageDefinition.PackageId = packageSchema.PackageId; + } + + return packageDefinition; + } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TrackedReferencesRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TrackedReferencesRepository.cs index c7966daec1..0a6ec43e0d 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TrackedReferencesRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TrackedReferencesRepository.cs @@ -92,27 +92,39 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement } var innerUnionSqlChild = _scopeAccessor.AmbientScope.Database.SqlContext.Sql().Select( - "[cr].childId as id", "[cr].parentId as otherId", "[rt].[alias]", "[rt].[name]", + "[cn].uniqueId as key", "[pn].uniqueId as otherKey, [cr].childId as id", "[cr].parentId as otherId", "[rt].[alias]", "[rt].[name]", "[rt].[isDependency]", "[rt].[dual]") .From("cr") .InnerJoin("rt") - .On((cr, rt) => rt.Dual == false && rt.Id == cr.RelationType, "cr", "rt"); + .On((cr, rt) => rt.Dual == false && rt.Id == cr.RelationType, "cr", "rt") + .InnerJoin("cn") + .On((cr, cn) => cr.ChildId == cn.NodeId, "cr", "cn") + .InnerJoin("pn") + .On((cr, pn) => cr.ParentId == pn.NodeId, "cr", "pn"); var innerUnionSqlDualParent = _scopeAccessor.AmbientScope.Database.SqlContext.Sql().Select( - "[dpr].parentId as id", "[dpr].childId as otherId", "[dprt].[alias]", "[dprt].[name]", + "[pn].uniqueId as key", "[cn].uniqueId as otherKey, [dpr].parentId as id", "[dpr].childId as otherId", "[dprt].[alias]", "[dprt].[name]", "[dprt].[isDependency]", "[dprt].[dual]") .From("dpr") .InnerJoin("dprt") .On( - (dpr, dprt) => dprt.Dual == true && dprt.Id == dpr.RelationType, "dpr", "dprt"); + (dpr, dprt) => dprt.Dual == true && dprt.Id == dpr.RelationType, "dpr", "dprt") + .InnerJoin("cn") + .On((dpr, cn) => dpr.ChildId == cn.NodeId, "dpr", "cn") + .InnerJoin("pn") + .On((dpr, pn) => dpr.ParentId == pn.NodeId, "dpr", "pn"); var innerUnionSql3 = _scopeAccessor.AmbientScope.Database.SqlContext.Sql().Select( - "[dcr].childId as id", "[dcr].parentId as otherId", "[dcrt].[alias]", "[dcrt].[name]", + "[cn].uniqueId as key", "[pn].uniqueId as otherKey, [dcr].childId as id", "[dcr].parentId as otherId", "[dcrt].[alias]", "[dcrt].[name]", "[dcrt].[isDependency]", "[dcrt].[dual]") .From("dcr") .InnerJoin("dcrt") .On( - (dcr, dcrt) => dcrt.Dual == true && dcrt.Id == dcr.RelationType, "dcr", "dcrt"); + (dcr, dcrt) => dcrt.Dual == true && dcrt.Id == dcr.RelationType, "dcr", "dcrt") + .InnerJoin("cn") + .On((dcr, cn) => dcr.ChildId == cn.NodeId, "dcr", "cn") + .InnerJoin("pn") + .On((dcr, pn) => dcr.ParentId == pn.NodeId, "dcr", "pn"); var innerUnionSql = innerUnionSqlChild.Union(innerUnionSqlDualParent).Union(innerUnionSql3); @@ -255,6 +267,69 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement return pagedResult?.Items.Select(MapDtoToEntity) ?? Enumerable.Empty(); } + public IEnumerable GetPagedRelationsForItem( + Guid key, + long skip, + long take, + bool filterMustBeIsDependency, + out long totalRecords) + { + Sql innerUnionSql = GetInnerUnionSql(); + var sql = _scopeAccessor.AmbientScope?.Database.SqlContext.Sql().SelectDistinct( + "[x].[otherId] as nodeId", + "[n].[uniqueId] as nodeKey", + "[n].[text] as nodeName", + "[n].[nodeObjectType] as nodeObjectType", + "[d].[published] as nodePublished", + "[ct].[icon] as contentTypeIcon", + "[ct].[alias] as contentTypeAlias", + "[ctn].[text] as contentTypeName", + "[x].[alias] as relationTypeAlias", + "[x].[name] as relationTypeName", + "[x].[isDependency] as relationTypeIsDependency", + "[x].[dual] as relationTypeIsBidirectional") + .From("n") + .InnerJoinNested(innerUnionSql, "x") + .On((n, x) => n.NodeId == x.OtherId, "n", "x") + .LeftJoin("c") + .On( + (left, right) => left.NodeId == right.NodeId, + aliasLeft: "n", + aliasRight: "c") + .LeftJoin("ct") + .On( + (left, right) => left.ContentTypeId == right.NodeId, + aliasLeft: "c", + aliasRight: "ct") + .LeftJoin("ctn") + .On( + (left, right) => left.NodeId == right.NodeId, + aliasLeft: "ct", + aliasRight: "ctn") + .LeftJoin("d") + .On( + (left, right) => left.NodeId == right.NodeId, + aliasLeft: "n", + aliasRight: "d") + .Where(x => x.Key == key, "x"); + + if (filterMustBeIsDependency) + { + sql = sql?.Where(rt => rt.IsDependency, "x"); + } + + // Ordering is required for paging + sql = sql?.OrderBy(x => x.Alias, "x"); + + RelationItemDto[] pagedResult = + _scopeAccessor.AmbientScope?.Database.SkipTake(skip, take, sql).ToArray() ?? + Array.Empty(); + totalRecords = _scopeAccessor.AmbientScope?.Database.Count(sql!) ?? 0; + + return _umbracoMapper.MapEnumerable(pagedResult); + } + + [Obsolete("Use overload that takes key instead of id. This will be removed in Umbraco 15.")] public IEnumerable GetPagedRelationsForItem( int id, long skip, @@ -299,11 +374,130 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement RelationItemDto[] pagedResult = _scopeAccessor.AmbientScope?.Database.SkipTake(skip, take, sql).ToArray() ?? Array.Empty(); - totalRecords = pagedResult.Length; + totalRecords = _scopeAccessor.AmbientScope?.Database.Count(sql!) ?? 0; return _umbracoMapper.MapEnumerable(pagedResult); } + public IEnumerable GetPagedItemsWithRelations( + ISet keys, + long skip, + long take, + bool filterMustBeIsDependency, + out long totalRecords) + { + Sql innerUnionSql = GetInnerUnionSql(); + Sql? sql = _scopeAccessor.AmbientScope?.Database.SqlContext.Sql().SelectDistinct( + "[x].[id] as nodeId", + "[n].[uniqueId] as nodeKey", + "[n].[text] as nodeName", + "[n].[nodeObjectType] as nodeObjectType", + "[ct].[icon] as contentTypeIcon", + "[ct].[alias] as contentTypeAlias", + "[ctn].[text] as contentTypeName", + "[x].[alias] as relationTypeAlias", + "[x].[name] as relationTypeName", + "[x].[isDependency] as relationTypeIsDependency", + "[x].[dual] as relationTypeIsBidirectional") + .From("n") + .InnerJoinNested(innerUnionSql, "x") + .On((n, x) => n.NodeId == x.Id, "n", "x") + .LeftJoin("c") + .On((left, right) => left.NodeId == right.NodeId, aliasLeft: "n", aliasRight: "c") + .LeftJoin("ct") + .On((left, right) => left.ContentTypeId == right.NodeId, aliasLeft: "c", + aliasRight: "ct") + .LeftJoin("ctn") + .On((left, right) => left.NodeId == right.NodeId, aliasLeft: "ct", + aliasRight: "ctn"); + if (keys.Any()) + { + sql = sql?.Where(x => keys.Contains(x.UniqueId), "n"); + } + + if (filterMustBeIsDependency) + { + sql = sql?.Where(rt => rt.IsDependency, "x"); + } + + // Ordering is required for paging + sql = sql?.OrderBy(x => x.Alias, "x"); + + RelationItemDto[] pagedResult = + _scopeAccessor.AmbientScope?.Database.SkipTake(skip, take, sql).ToArray() ?? + Array.Empty(); + + totalRecords = _scopeAccessor.AmbientScope?.Database.Count(sql!) ?? 0; + + return _umbracoMapper.MapEnumerable(pagedResult); + } + + public IEnumerable GetPagedDescendantsInReferences(Guid parentKey, long skip, long take, bool filterMustBeIsDependency, + out long totalRecords) + { + var syntax = _scopeAccessor.AmbientScope?.Database.SqlContext.SqlSyntax; + + // Gets the path of the parent with ",%" added + var subsubQuery = _scopeAccessor.AmbientScope?.Database.SqlContext.Sql() + .Select(syntax?.GetConcat("[node].[path]", "',%'")) + .From("node") + .Where(x => x.UniqueId == parentKey, "node"); + + // Gets the descendants of the parent node + Sql? subQuery = _scopeAccessor.AmbientScope?.Database.SqlContext.Sql() + .Select(x => x.NodeId) + .From() + .WhereLike(x => x.Path, subsubQuery); + + Sql innerUnionSql = GetInnerUnionSql(); + var sql = _scopeAccessor.AmbientScope?.Database.SqlContext.Sql().SelectDistinct( + "[x].[id] as nodeId", + "[n].[uniqueId] as nodeKey", + "[n].[text] as nodeName", + "[n].[nodeObjectType] as nodeObjectType", + "[d].[published] as nodePublished", + "[ct].[icon] as contentTypeIcon", + "[ct].[alias] as contentTypeAlias", + "[ctn].[text] as contentTypeName", + "[x].[alias] as relationTypeAlias", + "[x].[name] as relationTypeName", + "[x].[isDependency] as relationTypeIsDependency", + "[x].[dual] as relationTypeIsBidirectional") + .From("n") + .InnerJoinNested(innerUnionSql, "x") + .On((n, x) => n.NodeId == x.Id, "n", "x") + .LeftJoin("c").On((left, right) => left.NodeId == right.NodeId, + aliasLeft: "n", aliasRight: "c") + .LeftJoin("ct") + .On((left, right) => left.ContentTypeId == right.NodeId, aliasLeft: "c", + aliasRight: "ct") + .LeftJoin("ctn") + .On((left, right) => left.NodeId == right.NodeId, aliasLeft: "ct", + aliasRight: "ctn") + .LeftJoin("d") + .On( + (left, right) => left.NodeId == right.NodeId, + aliasLeft: "n", + aliasRight: "d"); + sql = sql?.WhereIn((System.Linq.Expressions.Expression>)(x => x.NodeId), subQuery, + "n"); + + if (filterMustBeIsDependency) + { + sql = sql?.Where(rt => rt.IsDependency, "x"); + } + + // Ordering is required for paging + sql = sql?.OrderBy(x => x.Alias, "x"); + + List? pagedResult = _scopeAccessor.AmbientScope?.Database.SkipTake(skip, take, sql); + totalRecords = _scopeAccessor.AmbientScope?.Database.Count(sql!) ?? 0; + + return _umbracoMapper.MapEnumerable(pagedResult ?? + new List()); + } + + [Obsolete("Use overload that takes keys instead of ids. This will be removed in Umbraco 15.")] public IEnumerable GetPagedItemsWithRelations( int[] ids, long skip, @@ -351,7 +545,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement RelationItemDto[] pagedResult = _scopeAccessor.AmbientScope?.Database.SkipTake(skip, take, sql).ToArray() ?? Array.Empty(); - totalRecords = pagedResult.Length; + totalRecords = _scopeAccessor.AmbientScope?.Database.Count(sql!) ?? 0; return _umbracoMapper.MapEnumerable(pagedResult); } @@ -414,7 +608,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement List? pagedResult = _scopeAccessor.AmbientScope?.Database.SkipTake(skip, take, sql); - totalRecords = pagedResult?.Count ?? 0; + totalRecords = _scopeAccessor.AmbientScope?.Database.Count(sql!) ?? 0; return _umbracoMapper.MapEnumerable(pagedResult ?? new List()); @@ -426,6 +620,10 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement [Column("otherId")] public int OtherId { get; set; } + [Column("key")] public Guid Key { get; set; } + + [Column("otherKey")] public Guid OtherKey { get; set; } + [Column("alias")] public string? Alias { get; set; } [Column("name")] public string? Name { get; set; } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs index fbd1913a58..b2e2ca5847 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs @@ -154,6 +154,49 @@ internal class UserRepository : EntityRepositoryBase, IUserRepositor public IUser? GetByUsername(string username, bool includeSecurityData) => GetWith(sql => sql.Where(x => x.Login == username), includeSecurityData); + public IUser? GetForUpgradeByUsername(string username) => GetUpgradeUserWith(sql => sql.Where(x => x.Login == username)); + + public IUser? GetForUpgradeByEmail(string email) => GetUpgradeUserWith(sql => sql.Where(x => x.Email == email)); + + public IUser? GetForUpgrade(int id) => GetUpgradeUserWith(sql => sql.Where(x => x.Id == id)); + + private IUser? GetUpgradeUserWith(Action> with) + { + if (_runtimeState.Level != RuntimeLevel.Upgrade) + { + return null; + } + + // We'll only return a user if we're in upgrade mode. + Sql sql = SqlContext.Sql() + .Select( + dto => dto.Id, + dto => dto.UserName, + dto => dto.Email, + dto => dto.Login, + dto => dto.Password, + dto => dto.PasswordConfig, + dto => dto.SecurityStampToken, + dto => dto.UserLanguage, + dto => dto.LastLockoutDate, + dto => dto.Disabled, + dto => dto.NoConsole) + .From(); + + with(sql); + + UserDto? userDto = Database.Fetch(sql).FirstOrDefault(); + + if (userDto is null) + { + return null; + } + + PerformGetReferencedDtos(new List { userDto }); + + return UserFactory.BuildEntity(_globalSettings, userDto); + } + /// /// Returns a user by id /// @@ -265,7 +308,8 @@ SELECT 4 AS [Key], COUNT(id) AS [Value] FROM umbracoUser WHERE userDisabled = 0 if (DateTime.UtcNow - found.LastValidatedUtc > _globalSettings.TimeOut) { //timeout detected, update the record - Logger.LogDebug("ClearLoginSession for sessionId {sessionId}", sessionId);ClearLoginSession(sessionId); + Logger.LogDebug("ClearLoginSession for sessionId {sessionId}", sessionId); + ClearLoginSession(sessionId); return false; } diff --git a/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseExtensions.cs b/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseExtensions.cs index 78bcc34f2b..29de5c0506 100644 --- a/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseExtensions.cs +++ b/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseExtensions.cs @@ -73,4 +73,11 @@ internal static class UmbracoDatabaseExtensions /// public static bool IsDatabaseEmpty(this IUmbracoDatabase database) => database.SqlContext.SqlSyntax.GetTablesInSchema(database).Any() == false; + + public static long Count(this IUmbracoDatabase database, Sql sql) + { + var query = new Sql().Select("COUNT(*)").From().Append("(").Append(sql).Append(")"); + + return database.ExecuteScalar(query); + } } diff --git a/src/Umbraco.Infrastructure/Security/BackOfficeIdentityUser.cs b/src/Umbraco.Infrastructure/Security/BackOfficeIdentityUser.cs index b617ce5a05..d91555ceeb 100644 --- a/src/Umbraco.Infrastructure/Security/BackOfficeIdentityUser.cs +++ b/src/Umbraco.Infrastructure/Security/BackOfficeIdentityUser.cs @@ -103,7 +103,17 @@ public class BackOfficeIdentityUser : UmbracoIdentityUser set => BeingDirty.SetPropertyValueAndDetectChanges(value, ref _culture!, nameof(Culture)); } - public Guid Key => UserIdToInt(Id).ToGuid(); + private Guid _key; + + public Guid Key + { + get => _key; + set + { + _key = value; + HasIdentity = true; + } + } /// /// Used to construct a new instance without an identity diff --git a/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs b/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs index a4ac523681..73b118f71f 100644 --- a/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs +++ b/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs @@ -87,14 +87,16 @@ public class BackOfficeUserStore : UmbracoUserStore - public Task ValidateSessionIdAsync(string? userId, string? sessionId) + public async Task ValidateSessionIdAsync(string? userId, string? sessionId) { if (Guid.TryParse(sessionId, out Guid guidSessionId)) { - return Task.FromResult(_userService.ValidateLoginSession(UserIdToInt(userId), guidSessionId)); + // We need to resolve the id from the key here... + var id = await ResolveEntityIdFromIdentityId(userId); + return _userService.ValidateLoginSession(id, guidSessionId); } - return Task.FromResult(false); + return false; } /// @@ -123,16 +125,16 @@ public class BackOfficeUserStore : UmbracoUserStore(); - var emptyPasswordValue = Constants.Security.EmptyPasswordPrefix + + // the password must be 'something' it could be empty if authenticating + // with an external provider so we'll just generate one and prefix it, the + // prefix will help us determine if the password hasn't actually been specified yet. + // this will hash the guid with a salt so should be nicely random + var aspHasher = new PasswordHasher(); + var emptyPasswordValue = Constants.Security.EmptyPasswordPrefix + aspHasher.HashPassword(user, Guid.NewGuid().ToString("N")); var userEntity = new User(_globalSettings, user.Name, user.Email, user.UserName, emptyPasswordValue) @@ -252,14 +254,13 @@ public class BackOfficeUserStore : UmbracoUserStore(user)))!; } + private IUser? FindUserFromString(string userId) + { + // We could use ResolveEntityIdFromIdentityId here, but that would require multiple DB calls, so let's not. + if (TryConvertIdentityIdToInt(userId, out var id)) + { + return _userService.GetUserById(id); + } + + // We couldn't directly convert the ID to an int, this is because the user logged in with external login. + // So we need to look up the user by key. + if (Guid.TryParse(userId, out Guid key)) + { + return _userService.GetAsync(key).GetAwaiter().GetResult(); + } + + throw new InvalidOperationException($"Unable to resolve user with ID {userId}"); + } + + protected override async Task ResolveEntityIdFromIdentityId(string? identityId) + { + if (TryConvertIdentityIdToInt(identityId, out var result)) + { + return result; + } + + // We couldn't directly convert the ID to an int, this is because the user logged in with external login. + // So we need to look up the user by key, and then get the ID. + if (Guid.TryParse(identityId, out Guid key)) + { + IUser? user = await _userService.GetAsync(key); + if (user is not null) + { + return user.Id; + } + } + + throw new InvalidOperationException($"Unable to resolve a user id from {identityId}"); + } + /// public override Task FindByEmailAsync( string email, @@ -529,11 +569,10 @@ public class BackOfficeUserStore : UmbracoUserStore?>(() => _externalLoginService.GetExternalLogins(userId))); + new Lazy?>(() => _externalLoginService.GetExternalLogins(user.Key))); user.SetTokensCallback( - new Lazy?>(() => _externalLoginService.GetExternalLoginTokens(userId))); + new Lazy?>(() => _externalLoginService.GetExternalLoginTokens(user.Key))); } return user; diff --git a/src/Umbraco.Infrastructure/Security/IdentityMapDefinition.cs b/src/Umbraco.Infrastructure/Security/IdentityMapDefinition.cs index 9c3b85af53..ce127a347a 100644 --- a/src/Umbraco.Infrastructure/Security/IdentityMapDefinition.cs +++ b/src/Umbraco.Infrastructure/Security/IdentityMapDefinition.cs @@ -1,6 +1,7 @@ // Copyright (c) Umbraco. // See LICENSE for more details. +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Configuration.Models; @@ -8,6 +9,7 @@ using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Extensions; namespace Umbraco.Cms.Core.Security; @@ -18,17 +20,35 @@ public class IdentityMapDefinition : IMapDefinition private readonly IEntityService _entityService; private readonly GlobalSettings _globalSettings; private readonly ILocalizedTextService _textService; + private readonly ITwoFactorLoginService _twoFactorLoginService; public IdentityMapDefinition( ILocalizedTextService textService, IEntityService entityService, IOptions globalSettings, - AppCaches appCaches) + AppCaches appCaches, + ITwoFactorLoginService twoFactorLoginService) { _textService = textService; _entityService = entityService; _globalSettings = globalSettings.Value; _appCaches = appCaches; + _twoFactorLoginService = twoFactorLoginService; + } + + [Obsolete("Use constructor that also takes an ITwoFactorLoginService. Scheduled for removal in V12")] + public IdentityMapDefinition( + ILocalizedTextService textService, + IEntityService entityService, + IOptions globalSettings, + AppCaches appCaches) + : this( + textService, + entityService, + globalSettings, + appCaches, + StaticServiceProvider.Instance.GetRequiredService()) + { } public void DefineMaps(IUmbracoMapper mapper) @@ -69,6 +89,7 @@ public class IdentityMapDefinition : IMapDefinition private void Map(IUser source, BackOfficeIdentityUser target) { // NOTE: Groups/Roles are set in the BackOfficeIdentityUser ctor + target.Key = source.Key; target.CalculatedMediaStartNodeIds = source.CalculateMediaStartNodeIds(_entityService, _appCaches); target.CalculatedContentStartNodeIds = source.CalculateContentStartNodeIds(_entityService, _appCaches); target.Email = source.Email; @@ -90,7 +111,7 @@ public class IdentityMapDefinition : IMapDefinition target.LockoutEnd = source.IsLockedOut ? DateTime.MaxValue.ToUniversalTime() : (DateTime?)null; } - // Umbraco.Code.MapAll -Id -LockoutEnabled -PhoneNumber -PhoneNumberConfirmed -TwoFactorEnabled -ConcurrencyStamp -NormalizedEmail -NormalizedUserName -Roles + // Umbraco.Code.MapAll -Id -LockoutEnabled -PhoneNumber -PhoneNumberConfirmed -ConcurrencyStamp -NormalizedEmail -NormalizedUserName -Roles private void Map(IMember source, MemberIdentityUser target) { target.Email = source.Email; @@ -112,6 +133,7 @@ public class IdentityMapDefinition : IMapDefinition target.CreatedDateUtc = source.CreateDate.ToUniversalTime(); target.Key = source.Key; target.MemberTypeAlias = source.ContentTypeAlias; + target.TwoFactorEnabled = _twoFactorLoginService.IsTwoFactorEnabledAsync(source.Key).GetAwaiter().GetResult(); // NB: same comments re AutoMapper as per BackOfficeUser } diff --git a/src/Umbraco.Infrastructure/Security/MemberUserStore.cs b/src/Umbraco.Infrastructure/Security/MemberUserStore.cs index 934cefb0b8..6e1931997a 100644 --- a/src/Umbraco.Infrastructure/Security/MemberUserStore.cs +++ b/src/Umbraco.Infrastructure/Security/MemberUserStore.cs @@ -243,7 +243,7 @@ public class MemberUserStore : UmbracoUserStore(user)))!; } + protected override Task ResolveEntityIdFromIdentityId(string? identityId) + { + if (TryConvertIdentityIdToInt(identityId, out var id)) + { + return Task.FromResult(id); + } + + if (Guid.TryParse(identityId, out Guid key)) + { + IMember? member = _memberService.GetByKey(key); + if (member is not null) + { + return Task.FromResult(member.Id); + } + } + + throw new InvalidOperationException($"Unable to resolve user with ID {identityId}"); + } + /// public override Task AddLoginAsync(MemberIdentityUser user, UserLoginInfo login, CancellationToken cancellationToken = default) diff --git a/src/Umbraco.Infrastructure/Security/UmbracoUserStore.cs b/src/Umbraco.Infrastructure/Security/UmbracoUserStore.cs index 35a8f2eea9..18acc14b1d 100644 --- a/src/Umbraco.Infrastructure/Security/UmbracoUserStore.cs +++ b/src/Umbraco.Infrastructure/Security/UmbracoUserStore.cs @@ -2,6 +2,7 @@ using System.ComponentModel; using System.Globalization; using System.Security.Claims; using Microsoft.AspNetCore.Identity; +using Umbraco.Cms.Infrastructure.Extensions; using Umbraco.Extensions; namespace Umbraco.Cms.Core.Security; @@ -31,6 +32,7 @@ public abstract class UmbracoUserStore [EditorBrowsable(EditorBrowsableState.Never)] public override Task AddClaimsAsync(TUser user, IEnumerable claims, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + [Obsolete("Use TryConvertIdentityIdToInt instead. Scheduled for removal in V15.")] protected static int UserIdToInt(string? userId) { if (int.TryParse(userId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result)) @@ -47,6 +49,34 @@ public abstract class UmbracoUserStore throw new InvalidOperationException($"Unable to convert user ID ({userId})to int using InvariantCulture"); } + protected abstract Task ResolveEntityIdFromIdentityId(string? identityId); + + protected static bool TryConvertIdentityIdToInt(string? userId, out int intId) + { + // The userId can in this case be one of three things + // 1. An int - this means that the user logged in normally, this is fine, we parse it and return it. + // 2. A fake Guid - this means that the user logged in using an external login provider, but we haven't migrated the users to have a key yet, so we need to convert it to an int. + // 3. A Guid - this means that the user logged in using an external login provider, so we have to resolve the user by key. + + if (int.TryParse(userId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result)) + { + intId = result; + return true; + } + + if (Guid.TryParse(userId, out Guid key)) + { + if (key.IsFakeGuid()) + { + intId = key.ToInt(); + return true; + } + } + + intId = default; + return false; + } + protected static string UserIdToString(int userId) => string.Intern(userId.ToString(CultureInfo.InvariantCulture)); /// diff --git a/src/Umbraco.Infrastructure/Services/Implement/PackagingService.cs b/src/Umbraco.Infrastructure/Services/Implement/PackagingService.cs index a0330d75fd..be3fe9d629 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/PackagingService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/PackagingService.cs @@ -1,11 +1,17 @@ using System.Xml.Linq; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Hosting; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Manifest; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Packaging; using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Packaging; +using Umbraco.Cms.Core.Services.OperationStatus; using Umbraco.Extensions; +using Umbraco.New.Cms.Core.Models; using File = System.IO.File; namespace Umbraco.Cms.Core.Services.Implement; @@ -23,6 +29,7 @@ public class PackagingService : IPackagingService private readonly IManifestParser _manifestParser; private readonly IPackageInstallation _packageInstallation; private readonly PackageMigrationPlanCollection _packageMigrationPlans; + private readonly IHostEnvironment _hostEnvironment; public PackagingService( IAuditService auditService, @@ -31,7 +38,8 @@ public class PackagingService : IPackagingService IEventAggregator eventAggregator, IManifestParser manifestParser, IKeyValueService keyValueService, - PackageMigrationPlanCollection packageMigrationPlans) + PackageMigrationPlanCollection packageMigrationPlans, + IHostEnvironment hostEnvironment) { _auditService = auditService; _createdPackages = createdPackages; @@ -40,6 +48,28 @@ public class PackagingService : IPackagingService _manifestParser = manifestParser; _keyValueService = keyValueService; _packageMigrationPlans = packageMigrationPlans; + _hostEnvironment = hostEnvironment; + } + + [Obsolete("Use constructor that also takes an IHostEnvironment instead. Scheduled for removal in V15")] + public PackagingService( + IAuditService auditService, + ICreatedPackagesRepository createdPackages, + IPackageInstallation packageInstallation, + IEventAggregator eventAggregator, + IManifestParser manifestParser, + IKeyValueService keyValueService, + PackageMigrationPlanCollection packageMigrationPlans) + : this( + auditService, + createdPackages, + packageInstallation, + eventAggregator, + manifestParser, + keyValueService, + packageMigrationPlans, + StaticServiceProvider.Instance.GetRequiredService()) + { } #region Installation @@ -91,24 +121,70 @@ public class PackagingService : IPackagingService #region Created/Installed Package Repositories + [Obsolete("Use DeleteCreatedPackageAsync instead. Scheduled for removal in Umbraco 15.")] public void DeleteCreatedPackage(int id, int userId = Constants.Security.SuperUserId) { PackageDefinition? package = GetCreatedPackageById(id); + Guid key = package?.PackageId ?? Guid.Empty; + + DeleteCreatedPackageAsync(key, userId).GetAwaiter().GetResult(); + } + + /// + public async Task> DeleteCreatedPackageAsync(Guid key, int userId = Constants.Security.SuperUserId) + { + PackageDefinition? package = await GetCreatedPackageByKeyAsync(key); if (package == null) { - return; + return Attempt.FailWithStatus(PackageOperationStatus.NotFound, null); } - _auditService.Add(AuditType.PackagerUninstall, userId, -1, "Package", $"Created package '{package.Name}' deleted. Package id: {package.Id}"); - _createdPackages.Delete(id); + _auditService.Add(AuditType.Delete, userId, -1, "Package", $"Created package '{package.Name}' deleted. Package key: {key}"); + _createdPackages.Delete(package.Id); + + return Attempt.SucceedWithStatus(PackageOperationStatus.Success, package); } public IEnumerable GetAllCreatedPackages() => _createdPackages.GetAll(); public PackageDefinition? GetCreatedPackageById(int id) => _createdPackages.GetById(id); + /// + public Task GetCreatedPackageByKeyAsync(Guid key) => Task.FromResult(_createdPackages.GetByKey(key)); + + [Obsolete("Use CreateCreatedPackageAsync or UpdateCreatedPackageAsync instead. Scheduled for removal in Umbraco 15.")] public bool SaveCreatedPackage(PackageDefinition definition) => _createdPackages.SavePackage(definition); + /// + public async Task> CreateCreatedPackageAsync(PackageDefinition package, int userId) + { + if (_createdPackages.SavePackage(package) == false) + { + if (string.IsNullOrEmpty(package.Name)) + { + return Attempt.FailWithStatus(PackageOperationStatus.InvalidName, package); + } + + return Attempt.FailWithStatus(PackageOperationStatus.DuplicateItemName, package); + } + + _auditService.Add(AuditType.New, userId, -1, "Package", $"Created package '{package.Name}' created. Package key: {package.PackageId}"); + return await Task.FromResult(Attempt.SucceedWithStatus(PackageOperationStatus.Success, package)); + + } + + /// + public async Task> UpdateCreatedPackageAsync(PackageDefinition package, int userId) + { + if (_createdPackages.SavePackage(package) == false) + { + return Attempt.FailWithStatus(PackageOperationStatus.NotFound, package); + } + + _auditService.Add(AuditType.New, userId, -1, "Package", $"Created package '{package.Name}' updated. Package key: {package.PackageId}"); + return await Task.FromResult(Attempt.SucceedWithStatus(PackageOperationStatus.Success, package)); + } + public string ExportCreatedPackage(PackageDefinition definition) => _createdPackages.ExportPackage(definition); public InstalledPackage? GetInstalledPackageByName(string packageName) @@ -116,36 +192,11 @@ public class PackagingService : IPackagingService public IEnumerable GetAllInstalledPackages() { - IReadOnlyDictionary? keyValues = - _keyValueService.FindByKeyPrefix(Constants.Conventions.Migrations.KeyValuePrefix); - - var installedPackages = new Dictionary(); - - // Collect the package from the package migration plans - foreach (PackageMigrationPlan plan in _packageMigrationPlans) - { - if (!installedPackages.TryGetValue(plan.PackageName, out InstalledPackage? installedPackage)) - { - installedPackage = new InstalledPackage { PackageName = plan.PackageName }; - installedPackages.Add(plan.PackageName, installedPackage); - } - - var currentPlans = installedPackage.PackageMigrationPlans.ToList(); - if (keyValues is null || keyValues.TryGetValue( - Constants.Conventions.Migrations.KeyValuePrefix + plan.Name, - out var currentState) is false) - { - currentState = null; - } - - currentPlans.Add(new InstalledPackageMigrationPlans - { - CurrentMigrationId = currentState, - FinalMigrationId = plan.FinalState, - }); - - installedPackage.PackageMigrationPlans = currentPlans; - } + // Collect the packages from the package migration plans + var installedPackages = GetInstalledPackagesFromMigrationPlansAsync(0, int.MaxValue) + .GetAwaiter() + .GetResult() + .Items.ToDictionary(package => package.PackageName!, package => package); // PackageName cannot be null here // Collect and merge the packages from the manifests foreach (PackageManifest package in _manifestParser.GetManifests()) @@ -157,7 +208,8 @@ public class PackagingService : IPackagingService if (!installedPackages.TryGetValue(package.PackageName, out InstalledPackage? installedPackage)) { - installedPackage = new InstalledPackage { + installedPackage = new InstalledPackage + { PackageName = package.PackageName, Version = string.IsNullOrEmpty(package.Version) ? "Unknown" : package.Version, }; @@ -173,4 +225,55 @@ public class PackagingService : IPackagingService } #endregion + + /// + public async Task> GetInstalledPackagesFromMigrationPlansAsync(int skip, int take) + { + IReadOnlyDictionary? keyValues = + _keyValueService.FindByKeyPrefix(Constants.Conventions.Migrations.KeyValuePrefix); + + InstalledPackage[] installedPackages = _packageMigrationPlans + .GroupBy(plan => plan.PackageName) + .Select(group => + { + var package = new InstalledPackage + { + PackageName = group.Key, + }; + + var currentState = keyValues? + .GetValueOrDefault(Constants.Conventions.Migrations.KeyValuePrefix + package.PackageName); + + package.PackageMigrationPlans = group + .Select(plan => new InstalledPackageMigrationPlans + { + CurrentMigrationId = currentState, + FinalMigrationId = plan.FinalState, + }); + + return package; + }).ToArray(); + + return await Task.FromResult(new PagedModel + { + Total = installedPackages.Count(), + Items = installedPackages.Skip(skip).Take(take), + }); + } + + /// + public Stream? GetPackageFileStream(PackageDefinition definition) + { + // Removing the ContentRootPath from the package path as a sub path is required for GetFileInfo() + var subPath = definition.PackagePath.Replace(_hostEnvironment.ContentRootPath, string.Empty); + + IFileInfo packageFile = _hostEnvironment.ContentRootFileProvider.GetFileInfo(subPath); + + if (packageFile.Exists == false) + { + return null; + } + + return packageFile.CreateReadStream(); + } } diff --git a/src/Umbraco.New.Cms.Infrastructure/Persistence/Mappers/RelationModelMapDefinition.cs b/src/Umbraco.New.Cms.Infrastructure/Persistence/Mappers/RelationModelMapDefinition.cs index 44dfc3bd4a..fb2833f9b6 100644 --- a/src/Umbraco.New.Cms.Infrastructure/Persistence/Mappers/RelationModelMapDefinition.cs +++ b/src/Umbraco.New.Cms.Infrastructure/Persistence/Mappers/RelationModelMapDefinition.cs @@ -22,5 +22,7 @@ public class RelationModelMapDefinition : IMapDefinition target.ContentTypeAlias = source.ChildContentTypeAlias; target.ContentTypeIcon = source.ChildContentTypeIcon; target.ContentTypeName = source.ChildContentTypeName; + + target.NodePublished = source.ChildNodePublished; } } diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs index e44a7449aa..a1c62d0641 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs @@ -31,6 +31,7 @@ using Umbraco.Cms.Core.Logging; using Umbraco.Cms.Core.Macros; using Umbraco.Cms.Core.Net; using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Telemetry; @@ -55,6 +56,7 @@ using Umbraco.Cms.Web.Common.Middleware; using Umbraco.Cms.Web.Common.ModelBinders; using Umbraco.Cms.Web.Common.Mvc; using Umbraco.Cms.Web.Common.Profiler; +using Umbraco.Cms.Web.Common.Repositories; using Umbraco.Cms.Web.Common.RuntimeMinification; using Umbraco.Cms.Web.Common.Security; using Umbraco.Cms.Web.Common.Templates; @@ -203,6 +205,8 @@ public static partial class UmbracoBuilderExtensions builder.Services.AddMiniProfiler(); builder.Services.ConfigureOptions(); + builder.Services.AddSingleton(); + builder.AddNotificationHandler(); return builder; } diff --git a/src/Umbraco.Web.Common/Profiler/WebProfiler.cs b/src/Umbraco.Web.Common/Profiler/WebProfiler.cs index 50a0de19a9..5b5e207e1c 100644 --- a/src/Umbraco.Web.Common/Profiler/WebProfiler.cs +++ b/src/Umbraco.Web.Common/Profiler/WebProfiler.cs @@ -1,5 +1,6 @@ using System.Net; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Extensions; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; @@ -8,6 +9,8 @@ using StackExchange.Profiling; using StackExchange.Profiling.Internal; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Logging; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.OperationStatus; using Umbraco.Cms.Core.Web; using Umbraco.Extensions; @@ -38,7 +41,10 @@ public class WebProfiler : IProfiler public void Start() { - MiniProfiler.StartNew(_httpContextAccessor.HttpContext?.Request.Path); + var name = $"{_httpContextAccessor.HttpContext?.Request.Method} {_httpContextAccessor.HttpContext?.Request.GetDisplayUrl()}"; + + MiniProfiler.StartNew(name); + MiniProfilerContext.Value = MiniProfiler.Current; } @@ -124,20 +130,16 @@ public class WebProfiler : IProfiler return false; } + IWebProfilerService? webProfilerService = _httpContextAccessor.HttpContext?.RequestServices?.GetService(); - if (bool.TryParse(request.Query["umbDebug"], out var umbDebug)) + if (webProfilerService is not null) { - return umbDebug; - } + Attempt shouldProfile = webProfilerService.GetStatus().GetAwaiter().GetResult(); - if (bool.TryParse(request.Headers["X-UMB-DEBUG"], out var xUmbDebug)) - { - return xUmbDebug; - } - - if (bool.TryParse(request.Cookies["UMB-DEBUG"], out var cUmbDebug)) - { - return cUmbDebug; + if (shouldProfile.Success) + { + return shouldProfile.Result; + } } return false; diff --git a/src/Umbraco.Web.Common/Repositories/WebProfilerRepository.cs b/src/Umbraco.Web.Common/Repositories/WebProfilerRepository.cs new file mode 100644 index 0000000000..52eaaf04ca --- /dev/null +++ b/src/Umbraco.Web.Common/Repositories/WebProfilerRepository.cs @@ -0,0 +1,48 @@ +using Microsoft.AspNetCore.Http; +using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Web.Common.Repositories; + +internal class WebProfilerRepository : IWebProfilerRepository +{ + private const string CookieName = "UMB-DEBUG"; + private const string HeaderName = "X-UMB-DEBUG"; + private const string QueryName = "umbDebug"; + + private readonly IHttpContextAccessor _httpContextAccessor; + + public WebProfilerRepository(IHttpContextAccessor httpContextAccessor) + { + _httpContextAccessor = httpContextAccessor; + } + + public void SetStatus(int userId, bool status) + { + if (status) + { + _httpContextAccessor.GetRequiredHttpContext().Response.Cookies.Append(CookieName, string.Empty, new CookieOptions { Expires = DateTime.Now.AddYears(1) }); + } + else + { + _httpContextAccessor.GetRequiredHttpContext().Response.Cookies.Delete(CookieName); + } + } + + public bool GetStatus(int userId) + { + + var request = _httpContextAccessor.GetRequiredHttpContext().Request; + if (bool.TryParse(request.Query[QueryName], out var umbDebug)) + { + return umbDebug; + } + + if (bool.TryParse(request.Headers[HeaderName], out var xUmbDebug)) + { + return xUmbDebug; + } + + return request.Cookies.ContainsKey(CookieName); + } +} diff --git a/src/Umbraco.Web.Common/UmbracoContext/UmbracoContext.cs b/src/Umbraco.Web.Common/UmbracoContext/UmbracoContext.cs index 878824c383..f8c41879ac 100644 --- a/src/Umbraco.Web.Common/UmbracoContext/UmbracoContext.cs +++ b/src/Umbraco.Web.Common/UmbracoContext/UmbracoContext.cs @@ -4,6 +4,8 @@ using Umbraco.Cms.Core; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Routing; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.OperationStatus; using Umbraco.Cms.Core.Web; using Umbraco.Extensions; @@ -17,6 +19,7 @@ public class UmbracoContext : DisposableObjectSlim, IUmbracoContext private readonly ICookieManager _cookieManager; private readonly IHostingEnvironment _hostingEnvironment; private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IWebProfilerService _webProfilerService; private readonly Lazy _publishedSnapshot; private readonly UmbracoRequestPaths _umbracoRequestPaths; private readonly UriUtility _uriUtility; @@ -36,7 +39,8 @@ public class UmbracoContext : DisposableObjectSlim, IUmbracoContext IHostingEnvironment hostingEnvironment, UriUtility uriUtility, ICookieManager cookieManager, - IHttpContextAccessor httpContextAccessor) + IHttpContextAccessor httpContextAccessor, + IWebProfilerService webProfilerService) { if (publishedSnapshotService == null) { @@ -47,6 +51,7 @@ public class UmbracoContext : DisposableObjectSlim, IUmbracoContext _hostingEnvironment = hostingEnvironment; _cookieManager = cookieManager; _httpContextAccessor = httpContextAccessor; + _webProfilerService = webProfilerService; ObjectCreated = DateTime.Now; UmbracoRequestId = Guid.NewGuid(); _umbracoRequestPaths = umbracoRequestPaths; @@ -116,11 +121,30 @@ _cleanedUmbracoUrl ??= _uriUtility.UriToUmbraco(OriginalRequestUrl); public IPublishedRequest? PublishedRequest { get; set; } /// - public bool IsDebug => // NOTE: the request can be null during app startup! - _hostingEnvironment.IsDebugMode - && (string.IsNullOrEmpty(_httpContextAccessor.HttpContext?.GetRequestValue("umbdebugshowtrace")) == false - || string.IsNullOrEmpty(_httpContextAccessor.HttpContext?.GetRequestValue("umbdebug")) == false - || string.IsNullOrEmpty(_cookieManager.GetCookieValue("UMB-DEBUG")) == false); + public bool IsDebug + { + get + { + if (_hostingEnvironment.IsDebugMode is false) + { + return false; + } + + if(string.IsNullOrEmpty(_httpContextAccessor.HttpContext?.GetRequestValue("umbdebugshowtrace")) is false) + { + return true; + } + + Attempt webProfilerStatusAttempt = _webProfilerService.GetStatus().GetAwaiter().GetResult(); + + if (webProfilerStatusAttempt.Success) + { + return webProfilerStatusAttempt.Result; + } + + return true; + } + } /// public bool InPreviewMode diff --git a/src/Umbraco.Web.Common/UmbracoContext/UmbracoContextFactory.cs b/src/Umbraco.Web.Common/UmbracoContext/UmbracoContextFactory.cs index 726f96cf31..2333cf2230 100644 --- a/src/Umbraco.Web.Common/UmbracoContext/UmbracoContextFactory.cs +++ b/src/Umbraco.Web.Common/UmbracoContext/UmbracoContextFactory.cs @@ -1,8 +1,11 @@ using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Routing; +using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Web; namespace Umbraco.Cms.Web.Common.UmbracoContext; @@ -15,11 +18,34 @@ public class UmbracoContextFactory : IUmbracoContextFactory private readonly ICookieManager _cookieManager; private readonly IHostingEnvironment _hostingEnvironment; private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IWebProfilerService _webProfilerService; private readonly IPublishedSnapshotService _publishedSnapshotService; private readonly IUmbracoContextAccessor _umbracoContextAccessor; private readonly UmbracoRequestPaths _umbracoRequestPaths; private readonly UriUtility _uriUtility; + + [Obsolete("Use non-obsolete ctor. This will be removed in Umbraco 15.")] + public UmbracoContextFactory( + IUmbracoContextAccessor umbracoContextAccessor, + IPublishedSnapshotService publishedSnapshotService, + UmbracoRequestPaths umbracoRequestPaths, + IHostingEnvironment hostingEnvironment, + UriUtility uriUtility, + ICookieManager cookieManager, + IHttpContextAccessor httpContextAccessor) + :this( + umbracoContextAccessor, + publishedSnapshotService, + umbracoRequestPaths, + hostingEnvironment, + uriUtility, + cookieManager, + httpContextAccessor, + StaticServiceProvider.Instance.GetRequiredService()) + { + + } /// /// Initializes a new instance of the class. /// @@ -30,7 +56,8 @@ public class UmbracoContextFactory : IUmbracoContextFactory IHostingEnvironment hostingEnvironment, UriUtility uriUtility, ICookieManager cookieManager, - IHttpContextAccessor httpContextAccessor) + IHttpContextAccessor httpContextAccessor, + IWebProfilerService webProfilerService) { _umbracoContextAccessor = umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor)); @@ -41,6 +68,7 @@ public class UmbracoContextFactory : IUmbracoContextFactory _uriUtility = uriUtility ?? throw new ArgumentNullException(nameof(uriUtility)); _cookieManager = cookieManager ?? throw new ArgumentNullException(nameof(cookieManager)); _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor)); + _webProfilerService = webProfilerService; } /// @@ -63,5 +91,6 @@ public class UmbracoContextFactory : IUmbracoContextFactory _hostingEnvironment, _uriUtility, _cookieManager, - _httpContextAccessor); + _httpContextAccessor, + _webProfilerService); } diff --git a/tests/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs b/tests/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs index dfe7b9377b..abc642e645 100644 --- a/tests/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs +++ b/tests/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs @@ -18,6 +18,7 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Web; using Umbraco.Cms.Api.Management; using Umbraco.Cms.Api.Management.Controllers.Install; +using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Persistence.Sqlite; using Umbraco.Cms.Persistence.SqlServer; using Umbraco.Cms.Tests.Common.Testing; @@ -77,10 +78,14 @@ namespace Umbraco.Cms.Tests.Integration.TestServerTest // Executes after the standard ConfigureServices method builder.ConfigureTestServices(services => - + { + services.AddSingleton(); + // Add a test auth scheme with a test auth handler to authn and assign the user services.AddAuthentication(TestAuthHandler.TestAuthenticationScheme) - .AddScheme(TestAuthHandler.TestAuthenticationScheme, options => { })); + .AddScheme( + TestAuthHandler.TestAuthenticationScheme, options => { }); + }); }); Client = Factory.CreateClient(new WebApplicationFactoryClientOptions diff --git a/tests/Umbraco.Tests.Integration/Testing/TestWebProfilerRepository.cs b/tests/Umbraco.Tests.Integration/Testing/TestWebProfilerRepository.cs new file mode 100644 index 0000000000..3d5dbb2e2b --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Testing/TestWebProfilerRepository.cs @@ -0,0 +1,12 @@ +using Umbraco.Cms.Core.Persistence.Repositories; + +namespace Umbraco.Cms.Tests.Integration.Testing; + +public class TestWebProfilerRepository : IWebProfilerRepository +{ + private bool _status = false; + + public void SetStatus(int userId, bool status) => _status = status; + + public bool GetStatus(int userId) => _status; +} diff --git a/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs b/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs index c83b13e0fe..c44c908fe8 100644 --- a/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs +++ b/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs @@ -8,6 +8,7 @@ using NUnit.Framework; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.IO; +using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Core.Web; @@ -19,6 +20,7 @@ using Umbraco.Cms.Persistence.SqlServer; using Umbraco.Cms.Tests.Common.Builders; using Umbraco.Cms.Tests.Integration.DependencyInjection; using Umbraco.Cms.Tests.Integration.Extensions; +using Umbraco.Cms.Tests.Integration.TestServerTest; using Constants = Umbraco.Cms.Core.Constants; namespace Umbraco.Cms.Tests.Integration.Testing; @@ -129,7 +131,9 @@ public abstract class UmbracoIntegrationTest : UmbracoIntegrationTestBase // We register this service because we need it for IRuntimeState, if we don't this breaks 900 tests services.AddSingleton(); + services.AddSingleton(); + services.AddLogger(webHostEnvironment, Configuration); // Add it! diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/NotificationsRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/NotificationsRepositoryTest.cs index 05b9946425..adafdec845 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/NotificationsRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/NotificationsRepositoryTest.cs @@ -147,6 +147,7 @@ public class NotificationsRepositoryTest : UmbracoIntegrationTest { var userDto = new UserDto { + Key = Guid.NewGuid(), Email = "test" + i, Login = "test" + i, Password = "test", @@ -209,6 +210,7 @@ public class NotificationsRepositoryTest : UmbracoIntegrationTest { var userDto = new UserDto { + Key = Guid.NewGuid(), Email = "test" + i, Login = "test" + i, Password = "test", diff --git a/tests/Umbraco.Tests.UnitTests/TestHelpers/Objects/TestUmbracoContextFactory.cs b/tests/Umbraco.Tests.UnitTests/TestHelpers/Objects/TestUmbracoContextFactory.cs index 2099d1d537..9e663fa9bc 100644 --- a/tests/Umbraco.Tests.UnitTests/TestHelpers/Objects/TestUmbracoContextFactory.cs +++ b/tests/Umbraco.Tests.UnitTests/TestHelpers/Objects/TestUmbracoContextFactory.cs @@ -7,6 +7,7 @@ using Moq; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Routing; +using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Web; using Umbraco.Cms.Tests.Common; using Umbraco.Cms.Web.Common.AspNetCore; @@ -68,7 +69,8 @@ public class TestUmbracoContextFactory hostingEnvironment, new UriUtility(hostingEnvironment), new AspNetCoreCookieManager(httpContextAccessor), - httpContextAccessor); + httpContextAccessor, + Mock.Of()); return umbracoContextFactory; } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberManagerTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberManagerTests.cs index 0026240e8b..f745c06ceb 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberManagerTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberManagerTests.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Logging; @@ -46,7 +42,8 @@ public class MemberManagerTests Mock.Of(), Mock.Of(), new TestOptionsSnapshot(new GlobalSettings()), - AppCaches.Disabled), + AppCaches.Disabled, + Mock.Of()) }; _fakeMemberStore = new MemberUserStore( diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberUserStoreTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberUserStoreTests.cs index 9e2a769e74..4e59fac4a6 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberUserStoreTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberUserStoreTests.cs @@ -235,14 +235,16 @@ public class MemberUserStoreTests public async Task GivenIDeleteUser_AndTheUserIsDeletedCorrectly_ThenIShouldGetASuccessResultAsync() { // arrange + var memberKey = new Guid("4B003A55-1DE9-4DEB-95A0-352FFC693D8F"); var sut = CreateSut(); - var fakeUser = new MemberIdentityUser(777); + var fakeUser = new MemberIdentityUser(777) { Key = memberKey }; var fakeCancellationToken = CancellationToken.None; IMemberType fakeMemberType = new MemberType(new MockShortStringHelper(), 77); IMember mockMember = new Member(fakeMemberType) { Id = 777, + Key = memberKey, Name = "fakeName", Email = "fakeemail@umbraco.com", Username = "fakeUsername", @@ -250,6 +252,7 @@ public class MemberUserStoreTests }; _mockMemberService.Setup(x => x.GetById(mockMember.Id)).Returns(mockMember); + _mockMemberService.Setup(x => x.GetByKey(mockMember.Key)).Returns(mockMember); _mockMemberService.Setup(x => x.Delete(mockMember)); // act @@ -258,7 +261,7 @@ public class MemberUserStoreTests // assert Assert.IsTrue(identityResult.Succeeded); Assert.IsTrue(!identityResult.Errors.Any()); - _mockMemberService.Verify(x => x.GetById(mockMember.Id)); + _mockMemberService.Verify(x => x.GetByKey(mockMember.Key)); _mockMemberService.Verify(x => x.Delete(mockMember)); _mockMemberService.VerifyNoOtherCalls(); } diff --git a/tools/Umbraco.JsonSchema/Umbraco.JsonSchema.csproj b/tools/Umbraco.JsonSchema/Umbraco.JsonSchema.csproj index 32da8f798a..732eccf01f 100644 --- a/tools/Umbraco.JsonSchema/Umbraco.JsonSchema.csproj +++ b/tools/Umbraco.JsonSchema/Umbraco.JsonSchema.csproj @@ -12,5 +12,7 @@ + +