Merge branch 'v13/dev' into v13/feature/extension-model

# Conflicts:
#	src/Umbraco.Cms.Api.Management/Controllers/Package/PackageControllerBase.cs
#	src/Umbraco.Cms.Api.Management/DependencyInjection/PackageBuilderExtensions.cs
#	src/Umbraco.Cms.Api.Management/ManagementApiComposer.cs
#	src/Umbraco.Core/CompatibilitySuppressions.xml
#	src/Umbraco.Infrastructure/CompatibilitySuppressions.xml
#	src/Umbraco.Infrastructure/Serialization/ContextualJsonSerializer.cs
#	src/Umbraco.Web.BackOffice/CompatibilitySuppressions.xml
This commit is contained in:
kjac
2023-02-23 15:28:44 +01:00
282 changed files with 9387 additions and 1597 deletions

6
.gitignore vendored
View File

@@ -68,7 +68,11 @@ preserve.belle
/build/docs.zip
/build/ui-docs.zip
/build/csharp-docs.zip
/src/Umbraco.Cms.StaticAssets/wwwroot/umbraco/
/src/Umbraco.Cms.StaticAssets/wwwroot/umbraco/backoffice
/src/Umbraco.Cms.StaticAssets/wwwroot/umbraco/assets
/src/Umbraco.Cms.StaticAssets/wwwroot/umbraco/js
/src/Umbraco.Cms.StaticAssets/wwwroot/umbraco/lib
/src/Umbraco.Cms.StaticAssets/wwwroot/umbraco/views
# Environment specific data
/src/Umbraco.Web.UI.Client/[Bb]uild/

View File

@@ -39,6 +39,7 @@ stages:
## Build
###############################################
- stage: Build
variables:
npm_config_cache: $(Pipeline.Workspace)/.npm_client
jobs:
@@ -47,6 +48,8 @@ stages:
pool:
vmImage: 'ubuntu-latest'
steps:
- checkout: self
submodules: true
- task: NodeTool@0
displayName: Use Node.js $(nodeVersion)
inputs:
@@ -110,7 +113,7 @@ stages:
}
}
foreach($csproj in Get-ChildItem Path "src/" -Recurse -Filter *.csproj)
foreach($csproj in Get-ChildItem -Recurse -Filter *.csproj)
{
dotnet pack $csproj --configuration $(buildConfiguration) --no-build --output $(Build.ArtifactStagingDirectory)/nupkg
}

View File

@@ -7,7 +7,6 @@ public class ProblemDetailsBuilder
{
private string? _title;
private string? _detail;
private int _status = StatusCodes.Status400BadRequest;
private string? _type;
public ProblemDetailsBuilder WithTitle(string title)
@@ -22,12 +21,6 @@ public class ProblemDetailsBuilder
return this;
}
public ProblemDetailsBuilder WithStatus(int status)
{
_status = status;
return this;
}
public ProblemDetailsBuilder WithType(string type)
{
_type = type;
@@ -39,7 +32,6 @@ public class ProblemDetailsBuilder
{
Title = _title,
Detail = _detail,
Status = _status,
Type = _type ?? "Error",
};
}

View File

@@ -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
{
}

View File

@@ -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<AuditLogResponseModel>), StatusCodes.Status200OK)]
public async Task<IActionResult> ByKey(Guid key, Direction orderDirection = Direction.Descending, DateTime? sinceDate = null, int skip = 0, int take = 100)
{
PagedModel<IAuditItem> result = await _auditService.GetItemsByKeyAsync(key, skip, take, orderDirection, sinceDate);
IEnumerable<AuditLogResponseModel> mapped = _auditLogViewModelFactory.CreateAuditLogViewModel(result.Items);
var viewModel = new PagedViewModel<AuditLogResponseModel>
{
Total = result.Total,
Items = mapped,
};
return Ok(viewModel);
}
}

View File

@@ -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<AuditLogResponseModel>), StatusCodes.Status200OK)]
public async Task<IActionResult> ByType(AuditType logType, DateTime? sinceDate = null, int skip = 0, int take = 100)
{
IAuditItem[] result = _auditService.GetLogs(logType, sinceDate).ToArray();
IEnumerable<AuditLogResponseModel> mapped = _auditLogViewModelFactory.CreateAuditLogViewModel(result.Skip(skip).Take(take));
var viewModel = new PagedViewModel<AuditLogResponseModel>
{
Total = result.Length,
Items = mapped,
};
return await Task.FromResult(Ok(viewModel));
}
}

View File

@@ -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<AuditLogWithUsernameResponseModel>), StatusCodes.Status200OK)]
public async Task<IActionResult> 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<IAuditItem> result = await _auditService.GetPagedItemsByUserAsync(
user.Key,
skip,
take,
orderDirection,
null,
sinceDate);
IEnumerable<AuditLogWithUsernameResponseModel> mapped = _auditLogViewModelFactory.CreateAuditLogWithUsernameViewModels(result.Items.Skip(skip).Take(take));
var viewModel = new PagedViewModel<AuditLogWithUsernameResponseModel>
{
Total = result.Total,
Items = mapped,
};
return Ok(viewModel);
}
}

View File

@@ -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;

View File

@@ -0,0 +1,37 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.ViewModels.Document;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Api.Management.Controllers.Document;
public class ByKeyDocumentController : DocumentControllerBase
{
private readonly IContentService _contentService;
private readonly IDocumentViewModelFactory _documentViewModelFactory;
public ByKeyDocumentController(IContentService contentService, IDocumentViewModelFactory documentViewModelFactory)
{
_contentService = contentService;
_documentViewModelFactory = documentViewModelFactory;
}
[HttpGet("{key:guid}")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(DocumentViewModel), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> ByKey(Guid key)
{
// FIXME: create and use an async get method here.
IContent? content = _contentService.GetById(key);
if (content == null)
{
return NotFound();
}
DocumentViewModel model = await _documentViewModelFactory.CreateViewModelAsync(content);
return Ok(model);
}
}

View File

@@ -0,0 +1,13 @@
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Routing;
using Umbraco.Cms.Core;
namespace Umbraco.Cms.Api.Management.Controllers.Document;
[ApiVersion("1.0")]
[ApiController]
[VersionedApiBackOfficeRoute(Constants.UdiEntityType.Document)]
[ApiExplorerSettings(GroupName = nameof(Constants.UdiEntityType.Document))]
public abstract class DocumentControllerBase : ManagementApiControllerBase
{
}

View File

@@ -0,0 +1,37 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.DocumentType;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Api.Management.Controllers.DocumentType;
public class ByKeyDocumentTypeController : DocumentTypeControllerBase
{
private readonly IContentTypeService _contentTypeService;
private readonly IUmbracoMapper _umbracoMapper;
public ByKeyDocumentTypeController(IContentTypeService contentTypeService, IUmbracoMapper umbracoMapper)
{
_contentTypeService = contentTypeService;
_umbracoMapper = umbracoMapper;
}
[HttpGet("{key:guid}")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(DocumentTypeViewModel), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> ByKey(Guid key)
{
// FIXME: create and use an async get method here.
IContentType? contentType = _contentTypeService.Get(key);
if (contentType == null)
{
return NotFound();
}
DocumentTypeViewModel model = _umbracoMapper.Map<DocumentTypeViewModel>(contentType)!;
return await Task.FromResult(Ok(model));
}
}

View File

@@ -0,0 +1,13 @@
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Routing;
using Umbraco.Cms.Core;
namespace Umbraco.Cms.Api.Management.Controllers.DocumentType;
[ApiVersion("1.0")]
[ApiController]
[VersionedApiBackOfficeRoute(Constants.UdiEntityType.DocumentType)]
[ApiExplorerSettings(GroupName = "Document Type")]
public abstract class DocumentTypeControllerBase : ManagementApiControllerBase
{
}

View File

@@ -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<IActionResult> LogLevelCounts(DateTime? startDate = null, DateTime? endDate = null)
{
Attempt<LogLevelCounts?, LogViewerOperationStatus> logLevelCountsAttempt =

View File

@@ -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;
}
/// <summary>
/// Gets a paginated list of the migration status of each installed package.
/// </summary>
/// <param name="skip">The amount of items to skip.</param>
/// <param name="take">The amount of items to take.</param>
/// <returns>The paged result of the installed packages migration status.</returns>
[HttpGet("migration-status")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedViewModel<PackageMigrationStatusViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedViewModel<PackageMigrationStatusViewModel>>> AllMigrationStatuses(int skip = 0, int take = 100)
{
PagedModel<InstalledPackage> migrationPlans = await _packagingService.GetInstalledPackagesFromMigrationPlansAsync(skip, take);
IEnumerable<PackageMigrationStatusViewModel> viewModels = _umbracoMapper.MapEnumerable<InstalledPackage, PackageMigrationStatusViewModel>(migrationPlans.Items);
return Ok(new PagedViewModel<PackageMigrationStatusViewModel>()
{
Total = migrationPlans.Total,
Items = viewModels,
});
}
}

View File

@@ -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;
}
/// <summary>
/// Gets a paginated list of all created packages.
/// </summary>
/// <param name="skip">The amount of items to skip.</param>
/// <param name="take">The amount of items to take.</param>
/// <returns>The paged result of the created packages.</returns>
[HttpGet]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedViewModel<PackageDefinitionViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedViewModel<PackageDefinitionViewModel>>> All(int skip = 0, int take = 100)
{
IEnumerable<PackageDefinition> createdPackages = _packagingService
.GetAllCreatedPackages()
.WhereNotNull()
.Skip(skip)
.Take(take);
return await Task.FromResult(Ok(_umbracoMapper.Map<PagedViewModel<PackageDefinitionViewModel>>(createdPackages)));
}
}

View File

@@ -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;
}
/// <summary>
/// Gets a package by key.
/// </summary>
/// <param name="key">The key of the package.</param>
/// <returns>The package or not found result.</returns>
[HttpGet("{key:guid}")]
[MapToApiVersion("1.0")]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(typeof(PackageDefinitionViewModel), StatusCodes.Status200OK)]
public async Task<ActionResult<PackageDefinitionViewModel>> ByKey(Guid key)
{
PackageDefinition? package = await _packagingService.GetCreatedPackageByKeyAsync(key);
if (package is null)
{
return NotFound();
}
return Ok(_umbracoMapper.Map<PackageDefinitionViewModel>(package));
}
}

View File

@@ -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;
}
/// <summary>
/// Creates a package.
/// </summary>
/// <param name="packageCreateModel">The model containing the data for a new package.</param>
/// <returns>The created package.</returns>
[HttpPost]
[MapToApiVersion("1.0")]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status201Created)]
public async Task<IActionResult> Create(PackageCreateModel packageCreateModel)
{
PackageDefinition packageDefinition = _packageDefinitionFactory.CreatePackageDefinition(packageCreateModel);
Attempt<PackageDefinition, PackageOperationStatus> result = await _packagingService.CreateCreatedPackageAsync(packageDefinition, CurrentUserId(_backOfficeSecurityAccessor));
return result.Success
? CreatedAtAction<ByKeyCreatedPackageController>(controller => nameof(controller.ByKey), packageDefinition.PackageId)
: PackageOperationStatusResult(result.Status);
}
}

View File

@@ -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
{
}

View File

@@ -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;
}
/// <summary>
/// Deletes a package with a given key.
/// </summary>
/// <param name="key">The key of the package.</param>
/// <returns>The result of the deletion.</returns>
[HttpDelete("{key:guid}")]
[MapToApiVersion("1.0")]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<IActionResult> Delete(Guid key)
{
Attempt<PackageDefinition?, PackageOperationStatus> result =
await _packagingService.DeleteCreatedPackageAsync(key, CurrentUserId(_backOfficeSecurityAccessor));
return result.Success
? Ok()
: PackageOperationStatusResult(result.Status);
}
}

View File

@@ -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;
/// <summary>
/// Downloads a package XML or ZIP file.
/// </summary>
/// <param name="key">The key of the package.</param>
/// <returns>The XML or ZIP file of the package or not found result.</returns>
[HttpGet("{key:guid}/download")]
[MapToApiVersion("1.0")]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)]
public async Task<IActionResult> 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;
}
}

View File

@@ -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;
}
/// <summary>
/// Updates a package.
/// </summary>
/// <param name="key">The key of the package.</param>
/// <param name="packageUpdateModel">The model containing the data for updating a package.</param>
/// <returns>The created package.</returns>
[HttpPut("{key:guid}")]
[MapToApiVersion("1.0")]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<IActionResult> 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<PackageDefinition, PackageOperationStatus> result = await _packagingService.UpdateCreatedPackageAsync(packageDefinition, CurrentUserId(_backOfficeSecurityAccessor));
return result.Success
? Ok()
: PackageOperationStatusResult(result.Status);
}
}

View File

@@ -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.Package;
@@ -9,4 +12,29 @@ namespace Umbraco.Cms.Api.Management.Controllers.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")
};
}

View File

@@ -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;
/// <summary>
/// Runs all migration plans for a package with a given name if any are pending.
/// </summary>
/// <param name="name">The name of the package.</param>
/// <returns>The result of running the package migrations.</returns>
[HttpPost("{name}/run-migration")]
[MapToApiVersion("1.0")]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status409Conflict)]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<IActionResult> RunMigrations(string name)
{
Attempt<bool, PackageMigrationOperationStatus> result = await _packageMigrationRunner.RunPendingPackageMigrations(name);
return result.Success
? Ok()
: PackageMigrationOperationStatusResult(result.Status);
}
}

View File

@@ -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<IActionResult> Status()
{
var result = await _webProfilerService.GetStatus();
return result.Success
? Ok(new ProfilingStatusViewModel(result.Result))
: WebProfilerOperationStatusResult(result.Status);
}
}

View File

@@ -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")
};
}

View File

@@ -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<ActionResult<ProfilingStatusViewModel>> Status()
=> await Task.FromResult(Ok(new ProfilingStatusViewModel(_hosting.IsDebugMode)));
}

View File

@@ -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<IActionResult> Status(ProfilingStatusViewModel model)
{
var result = await _webProfilerService.SetStatus(model.Enabled);
return result.Success
? Ok()
: WebProfilerOperationStatusResult(result.Status);
}
}

View File

@@ -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.
/// </remarks>
[HttpGet("{id:int}")]
[HttpGet("{key:guid}")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedViewModel<RelationItemViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedViewModel<RelationItemViewModel>>> Get(
int id,
long skip,
long take,
bool? filterMustBeIsDependency)
Guid key,
long skip = 0,
long take = 20,
bool filterMustBeIsDependency = false)
{
PagedModel<RelationItemModel> relationItems = _trackedReferencesService.GetPagedRelationsForItem(id, skip, take, filterMustBeIsDependency ?? false);
PagedModel<RelationItemModel> relationItems = await _trackedReferencesService.GetPagedRelationsForItemAsync(key, skip, take, filterMustBeIsDependency);
var pagedViewModel = new PagedViewModel<RelationItemViewModel>
{

View File

@@ -28,12 +28,12 @@ public class DescendantsTrackedReferenceController : TrackedReferenceControllerB
/// kind of relation.
/// This is basically finding the descending items which are children in relations.
/// </remarks>
[HttpGet("descendants/{parentId:int}")]
[HttpGet("descendants/{parentKey:guid}")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedViewModel<RelationItemViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedViewModel<RelationItemViewModel>>> Descendants(int parentId, long skip, long take, bool? filterMustBeIsDependency)
public async Task<ActionResult<PagedViewModel<RelationItemViewModel>>> Descendants(Guid parentKey, long skip, long take, bool filterMustBeIsDependency = true)
{
PagedModel<RelationItemModel> relationItems = _trackedReferencesSkipTakeService.GetPagedDescendantsInReferences(parentId, skip, take, filterMustBeIsDependency ?? true);
PagedModel<RelationItemModel> relationItems = await _trackedReferencesSkipTakeService.GetPagedDescendantsInReferencesAsync(parentKey, skip, take, filterMustBeIsDependency);
var pagedViewModel = new PagedViewModel<RelationItemViewModel>
{
Total = relationItems.Total,

View File

@@ -21,7 +21,7 @@ public class ItemsTrackedReferenceController : TrackedReferenceControllerBase
}
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// 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<RelationItemViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedViewModel<RelationItemViewModel>>> GetPagedReferencedItems([FromQuery]int[] ids, long skip, long take, bool? filterMustBeIsDependency)
public async Task<ActionResult<PagedViewModel<RelationItemViewModel>>> GetPagedReferencedItems([FromQuery(Name="key")]SortedSet<Guid> keys, long skip = 0, long take = 20, bool filterMustBeIsDependency = true)
{
PagedModel<RelationItemModel> relationItems = _trackedReferencesSkipTakeService.GetPagedItemsWithRelations(ids, skip, take, filterMustBeIsDependency ?? true);
PagedModel<RelationItemModel> relationItems = await _trackedReferencesSkipTakeService.GetPagedItemsWithRelationsAsync(keys, skip, take, filterMustBeIsDependency);
var pagedViewModel = new PagedViewModel<RelationItemViewModel>
{
Total = relationItems.Total,

View File

@@ -0,0 +1,39 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.ViewModels.UserGroups;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Api.Management.Controllers.UserGroups;
public class ByKeyUserGroupController : UserGroupsControllerBase
{
private readonly IUserGroupService _userGroupService;
private readonly IUserGroupViewModelFactory _userGroupViewModelFactory;
public ByKeyUserGroupController(
IUserGroupService userGroupService,
IUserGroupViewModelFactory userGroupViewModelFactory)
{
_userGroupService = userGroupService;
_userGroupViewModelFactory = userGroupViewModelFactory;
}
[HttpGet("{key:guid}")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(UserGroupViewModel), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<UserGroupViewModel>> ByKey(Guid key)
{
IUserGroup? userGroup = await _userGroupService.GetAsync(key);
if (userGroup is null)
{
return NotFound();
}
return await _userGroupViewModelFactory.CreateAsync(userGroup);
}
}

View File

@@ -0,0 +1,55 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.ViewModels.UserGroups;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
namespace Umbraco.Cms.Api.Management.Controllers.UserGroups;
public class CreateUserGroupController : UserGroupsControllerBase
{
private readonly IUserGroupService _userGroupService;
private readonly IUserGroupViewModelFactory _userGroupViewModelFactory;
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
public CreateUserGroupController(
IUserGroupService userGroupService,
IUserGroupViewModelFactory userGroupViewModelFactory,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
{
_userGroupService = userGroupService;
_userGroupViewModelFactory = userGroupViewModelFactory;
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
}
[HttpPost]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status201Created)]
public async Task<IActionResult> Create(UserGroupSaveModel userGroupSaveModel)
{
// FIXME: Comment this in when auth is in place and we can get a currently logged in user.
// IUser? currentUser = _backOfficeSecurityAccessor.BackOfficeSecurity?.CurrentUser;
// if (currentUser is null)
// {
// return UserGroupOperationStatusResult(UserGroupOperationStatus.MissingUser);
// }
Attempt<IUserGroup, UserGroupOperationStatus> userGroupCreationAttempt = await _userGroupViewModelFactory.CreateAsync(userGroupSaveModel);
if (userGroupCreationAttempt.Success is false)
{
return UserGroupOperationStatusResult(userGroupCreationAttempt.Status);
}
IUserGroup group = userGroupCreationAttempt.Result;
Attempt<IUserGroup, UserGroupOperationStatus> result = await _userGroupService.CreateAsync(group, /*currentUser.Id*/ -1);
return result.Success
? CreatedAtAction<ByKeyUserGroupController>(controller => nameof(controller.ByKey), group.Key)
: UserGroupOperationStatusResult(result.Status);
}
}

View File

@@ -0,0 +1,30 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
namespace Umbraco.Cms.Api.Management.Controllers.UserGroups;
public class DeleteUserGroupController : UserGroupsControllerBase
{
private readonly IUserGroupService _userGroupService;
public DeleteUserGroupController(IUserGroupService userGroupService)
{
_userGroupService = userGroupService;
}
[HttpDelete("{key:guid}")]
[MapToApiVersion("1.0")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> Delete(Guid key)
{
Attempt<UserGroupOperationStatus> result = await _userGroupService.DeleteAsync(key);
return result.Success
? Ok()
: UserGroupOperationStatusResult(result.Result);
}
}

View File

@@ -0,0 +1,40 @@
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.UserGroups;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Services;
using Umbraco.New.Cms.Core.Models;
namespace Umbraco.Cms.Api.Management.Controllers.UserGroups;
public class GetAllUserGroupController : UserGroupsControllerBase
{
private readonly IUserGroupService _userGroupService;
private readonly IUserGroupViewModelFactory _userViewModelFactory;
public GetAllUserGroupController(
IUserGroupService userGroupService,
IUserGroupViewModelFactory userViewModelFactory)
{
_userGroupService = userGroupService;
_userViewModelFactory = userViewModelFactory;
}
[HttpGet]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedViewModel<UserGroupViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedViewModel<UserGroupViewModel>>> GetAll(int skip = 0, int take = 100)
{
// FIXME: In the old controller this endpoint had a switch "onlyCurrentUserGroup"
// If this was enabled we'd only return the groups the current user was in
// and even if it was set to false we'd still remove the admin group.
// We still need to have this functionality, however, it does not belong here.
// Instead we should implement this functionality on the CurrentUserController
PagedModel<IUserGroup> userGroups = await _userGroupService.GetAllAsync(skip, take);
var viewModels = (await _userViewModelFactory.CreateMultipleAsync(userGroups.Items)).ToList();
return new PagedViewModel<UserGroupViewModel> { Total = userGroups.Total, Items = viewModels };
}
}

View File

@@ -0,0 +1,51 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.ViewModels.UserGroups;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
namespace Umbraco.Cms.Api.Management.Controllers.UserGroups;
public class UpdateUserGroupController : UserGroupsControllerBase
{
private readonly IUserGroupService _userGroupService;
private readonly IUserGroupViewModelFactory _userGroupViewModelFactory;
public UpdateUserGroupController(
IUserGroupService userGroupService,
IUserGroupViewModelFactory userGroupViewModelFactory)
{
_userGroupService = userGroupService;
_userGroupViewModelFactory = userGroupViewModelFactory;
}
[HttpPut("{key:guid}")]
[MapToApiVersion("1.0")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> Update(Guid key, UserGroupUpdateModel dataTypeUpdateModel)
{
IUserGroup? existingUserGroup = await _userGroupService.GetAsync(key);
if (existingUserGroup is null)
{
return UserGroupOperationStatusResult(UserGroupOperationStatus.NotFound);
}
Attempt<IUserGroup, UserGroupOperationStatus> userGroupUpdateAttempt = await _userGroupViewModelFactory.UpdateAsync(existingUserGroup, dataTypeUpdateModel);
if (userGroupUpdateAttempt.Success is false)
{
return UserGroupOperationStatusResult(userGroupUpdateAttempt.Status);
}
IUserGroup userGroup = userGroupUpdateAttempt.Result;
Attempt<IUserGroup, UserGroupOperationStatus> result = await _userGroupService.UpdateAsync(userGroup, -1);
return result.Success
? Ok()
: UserGroupOperationStatusResult(result.Status);
}
}

View File

@@ -0,0 +1,83 @@
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.UserGroups;
// TODO: This needs to be an authorized controller.
[ApiController]
[VersionedApiBackOfficeRoute("user-groups")]
[ApiExplorerSettings(GroupName = "User Groups")]
[ApiVersion("1.0")]
public class UserGroupsControllerBase : ManagementApiControllerBase
{
protected IActionResult UserGroupOperationStatusResult(UserGroupOperationStatus status) =>
status switch
{
UserGroupOperationStatus.NotFound => NotFound("The user group could not be found"),
UserGroupOperationStatus.AlreadyExists => Conflict(new ProblemDetailsBuilder()
.WithTitle("User group already exists")
.WithDetail("The user group exists already.")
.Build()),
UserGroupOperationStatus.DuplicateAlias => Conflict(new ProblemDetailsBuilder()
.WithTitle("Duplicate alias")
.WithDetail("A user group already exists with the attempted alias.")
.Build()),
UserGroupOperationStatus.MissingUser => Unauthorized(new ProblemDetailsBuilder()
.WithTitle("Missing user")
.WithDetail("A performing user was not found when attempting to create the user group.")
.Build()),
UserGroupOperationStatus.IsSystemUserGroup => BadRequest(new ProblemDetailsBuilder()
.WithTitle("System user group")
.WithDetail("The operation is not allowed on a system user group.")
.Build()),
UserGroupOperationStatus.UnauthorizedMissingUserSection => Unauthorized(new ProblemDetailsBuilder()
.WithTitle("Unauthorized")
.WithDetail("The performing user does not have access to the required section")
.Build()),
UserGroupOperationStatus.UnauthorizedMissingSections => Unauthorized(new ProblemDetailsBuilder()
.WithTitle("Unauthorized section")
.WithDetail("The specified allowed section contained a section the performing user doesn't have access to.")
.Build()),
UserGroupOperationStatus.UnauthorizedStartNodes => Unauthorized(new ProblemDetailsBuilder()
.WithTitle("Unauthorized start node")
.WithDetail("The specified start nodes contained a start node the performing user doesn't have access to.")
.Build()),
UserGroupOperationStatus.UnauthorizedMissingUserGroup => Unauthorized(new ProblemDetailsBuilder()
.WithTitle("User not in user group")
.WithDetail("The current user is not in the user group")
.Build()),
UserGroupOperationStatus.CancelledByNotification => BadRequest(new ProblemDetailsBuilder()
.WithTitle("Cancelled by notification")
.WithDetail("A notification handler prevented the language operation.")
.Build()),
UserGroupOperationStatus.DocumentStartNodeKeyNotFound => NotFound(new ProblemDetailsBuilder()
.WithTitle("Document start node key not found")
.WithDetail("The assigned document start node does not exists.")
.Build()),
UserGroupOperationStatus.MediaStartNodeKeyNotFound => NotFound(new ProblemDetailsBuilder()
.WithTitle("Media start node key not found")
.WithDetail("The assigned media start node does not exists.")
.Build()),
UserGroupOperationStatus.LanguageNotFound => NotFound(new ProblemDetailsBuilder()
.WithTitle("Language not found")
.WithDetail("The specified language cannot be found.")
.Build()),
UserGroupOperationStatus.NameTooLong => BadRequest(new ProblemDetailsBuilder()
.WithTitle("Name too long")
.WithDetail("User Group name is too long.")
.Build()),
UserGroupOperationStatus.AliasTooLong => BadRequest(new ProblemDetailsBuilder()
.WithTitle("Alias too long")
.WithDetail("The user group alias is too long.")
.Build()),
UserGroupOperationStatus.MissingName => BadRequest(new ProblemDetailsBuilder()
.WithTitle("Missing user group name.")
.WithDetail("The user group name is required, and cannot be an empty string.")
.Build()),
_ => StatusCode(StatusCodes.Status500InternalServerError, "Unknown user group operation status."),
};
}

View File

@@ -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<IAuditLogViewModelFactory, AuditLogViewModelFactory>();
return builder;
}
}

View File

@@ -0,0 +1,20 @@
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.Mapping.Document;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Mapping;
namespace Umbraco.Cms.Api.Management.DependencyInjection;
internal static class DocumentBuilderExtensions
{
internal static IUmbracoBuilder AddDocuments(this IUmbracoBuilder builder)
{
builder.Services.AddTransient<IDocumentViewModelFactory, DocumentViewModelFactory>();
builder.Services.AddTransient<IContentUrlFactory, ContentUrlFactory>();
builder.WithCollectionBuilder<MapDefinitionCollectionBuilder>().Add<DocumentMapDefinition>();
return builder;
}
}

View File

@@ -0,0 +1,15 @@
using Umbraco.Cms.Api.Management.Mapping.DocumentType;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Mapping;
namespace Umbraco.Cms.Api.Management.DependencyInjection;
internal static class DocumentTypeBuilderExtensions
{
internal static IUmbracoBuilder AddDocumentTypes(this IUmbracoBuilder builder)
{
builder.WithCollectionBuilder<MapDefinitionCollectionBuilder>().Add<DocumentTypeMapDefinition>();
return builder;
}
}

View File

@@ -1,4 +1,6 @@
using Umbraco.Cms.Api.Management.Mapping.Package;
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;
@@ -8,7 +10,9 @@ internal static class PackageBuilderExtensions
{
internal static IUmbracoBuilder AddPackages(this IUmbracoBuilder builder)
{
builder.WithCollectionBuilder<MapDefinitionCollectionBuilder>().Add<PackageManifestViewModelMapDefinition>();
builder.Services.AddTransient<IPackageDefinitionFactory, PackageDefinitionFactory>();
builder.WithCollectionBuilder<MapDefinitionCollectionBuilder>().Add<PackageViewModelMapDefinition>();
return builder;
}

View File

@@ -0,0 +1,14 @@
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Core.DependencyInjection;
namespace Umbraco.Cms.Api.Management.DependencyInjection;
internal static class UserGroupsBuilderExtensions
{
internal static IUmbracoBuilder AddUserGroups(this IUmbracoBuilder builder)
{
builder.Services.AddTransient<IUserGroupViewModelFactory, UserGroupViewModelFactory>();
return builder;
}
}

View File

@@ -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<AuditLogResponseModel> CreateAuditLogViewModel(IEnumerable<IAuditItem> auditItems) => auditItems.Select(CreateAuditLogViewModel);
public IEnumerable<AuditLogWithUsernameResponseModel> CreateAuditLogWithUsernameViewModels(IEnumerable<IAuditItem> 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;
}
}

View File

@@ -0,0 +1,66 @@
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Api.Management.ViewModels.Content;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.Routing;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Web;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Management.Factories;
public class ContentUrlFactory : IContentUrlFactory
{
private readonly IPublishedRouter _publishedRouter;
private readonly IUmbracoContextAccessor _umbracoContextAccessor;
private readonly ILanguageService _languageService;
private readonly ILocalizedTextService _localizedTextService;
private readonly IContentService _contentService;
private readonly IVariationContextAccessor _variationContextAccessor;
private readonly ILoggerFactory _loggerFactory;
private readonly UriUtility _uriUtility;
private readonly IPublishedUrlProvider _publishedUrlProvider;
public ContentUrlFactory(
IPublishedRouter publishedRouter,
IUmbracoContextAccessor umbracoContextAccessor,
ILanguageService languageService,
ILocalizedTextService localizedTextService,
IContentService contentService,
IVariationContextAccessor variationContextAccessor,
ILoggerFactory loggerFactory,
UriUtility uriUtility,
IPublishedUrlProvider publishedUrlProvider)
{
_publishedRouter = publishedRouter;
_umbracoContextAccessor = umbracoContextAccessor;
_languageService = languageService;
_localizedTextService = localizedTextService;
_contentService = contentService;
_variationContextAccessor = variationContextAccessor;
_loggerFactory = loggerFactory;
_uriUtility = uriUtility;
_publishedUrlProvider = publishedUrlProvider;
}
public async Task<IEnumerable<ContentUrlInfo>> GetUrlsAsync(IContent content)
{
IUmbracoContext umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext();
UrlInfo[] urlInfos = (await content.GetContentUrlsAsync(
_publishedRouter,
umbracoContext,
_languageService,
_localizedTextService,
_contentService,
_variationContextAccessor,
_loggerFactory.CreateLogger<IContent>(),
_uriUtility,
_publishedUrlProvider)).ToArray();
return urlInfos
.Where(urlInfo => urlInfo.IsUrl)
.Select(urlInfo => new ContentUrlInfo { Culture = urlInfo.Culture, Url = urlInfo.Text })
.ToArray();
}
}

View File

@@ -0,0 +1,36 @@
using Umbraco.Cms.Api.Management.ViewModels.Document;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Api.Management.Factories;
public class DocumentViewModelFactory : IDocumentViewModelFactory
{
private readonly IUmbracoMapper _umbracoMapper;
private readonly IContentUrlFactory _contentUrlFactory;
private readonly IFileService _fileService;
public DocumentViewModelFactory(
IUmbracoMapper umbracoMapper,
IContentUrlFactory contentUrlFactory,
IFileService fileService)
{
_umbracoMapper = umbracoMapper;
_contentUrlFactory = contentUrlFactory;
_fileService = fileService;
}
public async Task<DocumentViewModel> CreateViewModelAsync(IContent content)
{
DocumentViewModel viewModel = _umbracoMapper.Map<DocumentViewModel>(content)!;
viewModel.Urls = await _contentUrlFactory.GetUrlsAsync(content);
viewModel.TemplateKey = content.TemplateId.HasValue
? _fileService.GetTemplate(content.TemplateId.Value)?.Key
: null;
return viewModel;
}
}

View File

@@ -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<AuditLogResponseModel> CreateAuditLogViewModel(IEnumerable<IAuditItem> auditItems);
IEnumerable<AuditLogWithUsernameResponseModel> CreateAuditLogWithUsernameViewModels(IEnumerable<IAuditItem> auditItems);
}

View File

@@ -0,0 +1,9 @@
using Umbraco.Cms.Api.Management.ViewModels.Content;
using Umbraco.Cms.Core.Models;
namespace Umbraco.Cms.Api.Management.Factories;
public interface IContentUrlFactory
{
Task<IEnumerable<ContentUrlInfo>> GetUrlsAsync(IContent content);
}

View File

@@ -0,0 +1,9 @@
using Umbraco.Cms.Api.Management.ViewModels.Document;
using Umbraco.Cms.Core.Models;
namespace Umbraco.Cms.Api.Management.Factories;
public interface IDocumentViewModelFactory
{
Task<DocumentViewModel> CreateViewModelAsync(IContent content);
}

View File

@@ -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);
}

View File

@@ -0,0 +1,41 @@
using Umbraco.Cms.Api.Management.ViewModels.UserGroups;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Services.OperationStatus;
namespace Umbraco.Cms.Api.Management.Factories;
/// <summary>
/// A factory for creating <see cref="UserGroupViewModel"/>
/// </summary>
public interface IUserGroupViewModelFactory
{
/// <summary>
/// Creates a <see cref="UserGroupViewModel"/> based on a <see cref="UserGroup"/>
/// </summary>
/// <param name="userGroup"></param>
/// <returns></returns>
Task<UserGroupViewModel> CreateAsync(IUserGroup userGroup);
/// <summary>
/// Creates multiple <see cref="UserGroupViewModel"/> base on multiple <see cref="UserGroup"/>
/// </summary>
/// <param name="userGroups"></param>
/// <returns></returns>
Task<IEnumerable<UserGroupViewModel>> CreateMultipleAsync(IEnumerable<IUserGroup> userGroups);
/// <summary>
/// Creates an <see cref="IUserGroup"/> based on a <see cref="UserGroupSaveModel"/>
/// </summary>
/// <param name="saveModel"></param>
/// <returns>An attempt indicating if the operation was a success as well as a more detailed <see cref="UserGroupOperationStatus"/>.</returns>
Task<Attempt<IUserGroup, UserGroupOperationStatus>> CreateAsync(UserGroupSaveModel saveModel);
/// <summary>
/// Converts the values of an update model to fit with the existing backoffice implementations, and maps it to an existing user group.
/// </summary>
/// <param name="current">Existing user group to map to.</param>
/// <param name="update">Update model containing the new values.</param>
/// <returns>An attempt indicating if the operation was a success as well as a more detailed <see cref="UserGroupOperationStatus"/>.</returns>
Task<Attempt<IUserGroup, UserGroupOperationStatus>> UpdateAsync(IUserGroup current, UserGroupUpdateModel update);
}

View File

@@ -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<PackageDefinition>(packageCreateModel)!;
// Temp Id, PackageId and PackagePath for the newly created package
packageDefinition.Id = 0;
packageDefinition.PackageId = Guid.Empty;
packageDefinition.PackagePath = string.Empty;
return packageDefinition;
}
}

View File

@@ -0,0 +1,226 @@
using Umbraco.Cms.Api.Management.Mapping;
using Umbraco.Cms.Api.Management.ViewModels.UserGroups;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Cms.Core.Strings;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Management.Factories;
/// <inheritdoc />
public class UserGroupViewModelFactory : IUserGroupViewModelFactory
{
private readonly IEntityService _entityService;
private readonly IShortStringHelper _shortStringHelper;
private readonly ILanguageService _languageService;
public UserGroupViewModelFactory(
IEntityService entityService,
IShortStringHelper shortStringHelper,
ILanguageService languageService)
{
_entityService = entityService;
_shortStringHelper = shortStringHelper;
_languageService = languageService;
}
/// <inheritdoc />
public async Task<UserGroupViewModel> CreateAsync(IUserGroup userGroup)
{
Guid? contentStartNodeKey = GetKeyFromId(userGroup.StartContentId, UmbracoObjectTypes.Document);
Guid? mediaStartNodeKey = GetKeyFromId(userGroup.StartMediaId, UmbracoObjectTypes.Media);
Attempt<IEnumerable<string>, UserGroupOperationStatus> languageIsoCodesMappingAttempt = await MapLanguageIdsToIsoCodeAsync(userGroup.AllowedLanguages);
// We've gotten this data from the database, so the mapping should not fail
if (languageIsoCodesMappingAttempt.Success is false)
{
throw new InvalidOperationException($"Unknown language ID in User Group: {userGroup.Name}");
}
return new UserGroupViewModel
{
Name = userGroup.Name ?? string.Empty,
Key = userGroup.Key,
DocumentStartNodeKey = contentStartNodeKey,
MediaStartNodeKey = mediaStartNodeKey,
Icon = userGroup.Icon,
Languages = languageIsoCodesMappingAttempt.Result,
HasAccessToAllLanguages = userGroup.HasAccessToAllLanguages,
Permissions = userGroup.PermissionNames,
Sections = userGroup.AllowedSections.Select(SectionMapper.GetName),
};
}
/// <inheritdoc />
public async Task<IEnumerable<UserGroupViewModel>> CreateMultipleAsync(IEnumerable<IUserGroup> userGroups)
{
var userGroupViewModels = new List<UserGroupViewModel>();
foreach (IUserGroup userGroup in userGroups)
{
userGroupViewModels.Add(await CreateAsync(userGroup));
}
return userGroupViewModels;
}
/// <inheritdoc />
public async Task<Attempt<IUserGroup, UserGroupOperationStatus>> CreateAsync(UserGroupSaveModel saveModel)
{
var cleanedName = saveModel.Name.CleanForXss('[', ']', '(', ')', ':');
var group = new UserGroup(_shortStringHelper)
{
Name = cleanedName,
Alias = cleanedName,
Icon = saveModel.Icon,
HasAccessToAllLanguages = saveModel.HasAccessToAllLanguages,
PermissionNames = saveModel.Permissions,
};
Attempt<UserGroupOperationStatus> assignmentAttempt = AssignStartNodesToUserGroup(saveModel, group);
if (assignmentAttempt.Success is false)
{
return Attempt.FailWithStatus<IUserGroup, UserGroupOperationStatus>(assignmentAttempt.Result, group);
}
foreach (var section in saveModel.Sections)
{
group.AddAllowedSection(SectionMapper.GetAlias(section));
}
Attempt<IEnumerable<int>, UserGroupOperationStatus> languageIsoCodeMappingAttempt = await MapLanguageIsoCodesToIdsAsync(saveModel.Languages);
if (languageIsoCodeMappingAttempt.Success is false)
{
return Attempt.FailWithStatus<IUserGroup, UserGroupOperationStatus>(languageIsoCodeMappingAttempt.Status, group);
}
foreach (var languageId in languageIsoCodeMappingAttempt.Result)
{
group.AddAllowedLanguage(languageId);
}
return Attempt.SucceedWithStatus<IUserGroup, UserGroupOperationStatus>(UserGroupOperationStatus.Success, group);
}
/// <inheritdoc />
public async Task<Attempt<IUserGroup, UserGroupOperationStatus>> UpdateAsync(IUserGroup current, UserGroupUpdateModel update)
{
Attempt<UserGroupOperationStatus> assignmentAttempt = AssignStartNodesToUserGroup(update, current);
if (assignmentAttempt.Success is false)
{
return Attempt.FailWithStatus(assignmentAttempt.Result, current);
}
current.ClearAllowedLanguages();
Attempt<IEnumerable<int>, UserGroupOperationStatus> languageIdsMappingAttempt = await MapLanguageIsoCodesToIdsAsync(update.Languages);
if (languageIdsMappingAttempt.Success is false)
{
return Attempt.FailWithStatus(languageIdsMappingAttempt.Status, current);
}
foreach (var languageId in languageIdsMappingAttempt.Result)
{
current.AddAllowedLanguage(languageId);
}
current.ClearAllowedSections();
foreach (var sectionName in update.Sections)
{
current.AddAllowedSection(SectionMapper.GetAlias(sectionName));
}
current.Name = update.Name.CleanForXss('[', ']', '(', ')', ':');
current.Icon = update.Icon;
current.HasAccessToAllLanguages = update.HasAccessToAllLanguages;
current.PermissionNames = update.Permissions;
return Attempt.SucceedWithStatus(UserGroupOperationStatus.Success, current);
}
private async Task<Attempt<IEnumerable<string>, UserGroupOperationStatus>> MapLanguageIdsToIsoCodeAsync(IEnumerable<int> ids)
{
IEnumerable<ILanguage> languages = await _languageService.GetAllAsync();
string[] isoCodes = languages
.Where(x => ids.Contains(x.Id))
.Select(x => x.IsoCode)
.ToArray();
return isoCodes.Length == ids.Count()
? Attempt.SucceedWithStatus<IEnumerable<string>, UserGroupOperationStatus>(UserGroupOperationStatus.Success, isoCodes)
: Attempt.FailWithStatus<IEnumerable<string>, UserGroupOperationStatus>(UserGroupOperationStatus.LanguageNotFound, isoCodes);
}
private async Task<Attempt<IEnumerable<int>, UserGroupOperationStatus>> MapLanguageIsoCodesToIdsAsync(IEnumerable<string> isoCodes)
{
IEnumerable<ILanguage> languages = await _languageService.GetAllAsync();
int[] languageIds = languages
.Where(x => isoCodes.Contains(x.IsoCode))
.Select(x => x.Id)
.ToArray();
return languageIds.Length == isoCodes.Count()
? Attempt.SucceedWithStatus<IEnumerable<int>, UserGroupOperationStatus>(UserGroupOperationStatus.Success, languageIds)
: Attempt.FailWithStatus<IEnumerable<int>, UserGroupOperationStatus>(UserGroupOperationStatus.LanguageNotFound, languageIds);
}
private Attempt<UserGroupOperationStatus> AssignStartNodesToUserGroup(UserGroupBase source, IUserGroup target)
{
if (source.DocumentStartNodeKey is not null)
{
var contentId = GetIdFromKey(source.DocumentStartNodeKey.Value, UmbracoObjectTypes.Document);
if (contentId is null)
{
return Attempt.Fail(UserGroupOperationStatus.DocumentStartNodeKeyNotFound);
}
target.StartContentId = contentId;
}
if (source.MediaStartNodeKey is not null)
{
var mediaId = GetIdFromKey(source.MediaStartNodeKey.Value, UmbracoObjectTypes.Media);
if (mediaId is null)
{
return Attempt.Fail(UserGroupOperationStatus.MediaStartNodeKeyNotFound);
}
target.StartMediaId = mediaId;
}
return Attempt.Succeed(UserGroupOperationStatus.Success);
}
private Guid? GetKeyFromId(int? id, UmbracoObjectTypes objectType)
{
if (id is null)
{
return null;
}
Attempt<Guid> attempt = _entityService.GetKey(id.Value, objectType);
if (attempt.Success is false)
{
return null;
}
return attempt.Result;
}
private int? GetIdFromKey(Guid key, UmbracoObjectTypes objectType)
{
Attempt<int> attempt = _entityService.GetId(key, objectType);
if (attempt.Success is false)
{
return null;
}
return attempt.Result;
}
}

View File

@@ -25,6 +25,9 @@ public class ManagementApiComposer : IComposer
.AddUpgrader()
.AddSearchManagement()
.AddTrees()
.AddAuditLogs()
.AddDocuments()
.AddDocumentTypes()
.AddLanguages()
.AddDictionary()
.AddFileUpload()
@@ -35,6 +38,7 @@ public class ManagementApiComposer : IComposer
.AddDataTypes()
.AddTemplates()
.AddLogViewer()
.AddUserGroups()
.AddPackages()
.AddBackOfficeAuthentication()
.AddApiVersioning()

View File

@@ -0,0 +1,71 @@
using Umbraco.Cms.Api.Management.ViewModels.Content;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Management.Mapping.Content;
public abstract class ContentMapDefinition<TContent, TValueViewModel, TVariantViewModel>
where TContent : IContentBase
where TValueViewModel : ValueViewModelBase, new()
where TVariantViewModel : VariantViewModelBase, new()
{
private readonly PropertyEditorCollection _propertyEditorCollection;
protected ContentMapDefinition(PropertyEditorCollection propertyEditorCollection) => _propertyEditorCollection = propertyEditorCollection;
protected delegate void ValueViewModelMapping(IDataEditor propertyEditor, TValueViewModel variantViewModel);
protected delegate void VariantViewModelMapping(string? culture, string? segment, TVariantViewModel variantViewModel);
protected IEnumerable<TValueViewModel> MapValueViewModels(TContent source, ValueViewModelMapping? additionalPropertyMapping = null) =>
source
.Properties
.SelectMany(property => property
.Values
.Select(propertyValue =>
{
IDataEditor? propertyEditor = _propertyEditorCollection[property.PropertyType.PropertyEditorAlias];
if (propertyEditor == null)
{
return null;
}
var variantViewModel = new TValueViewModel
{
Culture = propertyValue.Culture,
Segment = propertyValue.Segment,
Alias = property.Alias,
Value = propertyEditor.GetValueEditor().ToEditor(property, propertyValue.Culture, propertyValue.Segment)
};
additionalPropertyMapping?.Invoke(propertyEditor, variantViewModel);
return variantViewModel;
}))
.WhereNotNull()
.ToArray();
protected IEnumerable<TVariantViewModel> MapVariantViewModels(TContent source, VariantViewModelMapping? additionalVariantMapping = null)
{
IPropertyValue[] propertyValues = source.Properties.SelectMany(propertyCollection => propertyCollection.Values).ToArray();
var cultures = source.AvailableCultures.DefaultIfEmpty(null).ToArray();
var segments = propertyValues.Select(property => property.Segment).Distinct().DefaultIfEmpty(null).ToArray();
return cultures
.SelectMany(culture => segments.Select(segment =>
{
var variantViewModel = new TVariantViewModel
{
Culture = culture,
Segment = segment,
Name = source.GetCultureName(culture) ?? string.Empty,
CreateDate = source.CreateDate, // apparently there is no culture specific creation date
UpdateDate = culture == null
? source.UpdateDate
: source.GetUpdateDate(culture) ?? source.UpdateDate,
};
additionalVariantMapping?.Invoke(culture, segment, variantViewModel);
return variantViewModel;
}))
.ToArray();
}
}

View File

@@ -0,0 +1,78 @@
using Umbraco.Cms.Api.Management.ViewModels.ContentType;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Management.Mapping.ContentType;
public abstract class ContentTypeMapDefinition<TContentType, TPropertyTypeViewModel, TPropertyTypeContainerViewModel>
where TContentType : IContentTypeBase
where TPropertyTypeViewModel : PropertyTypeViewModelBase, new()
where TPropertyTypeContainerViewModel : PropertyTypeContainerViewModelBase, new()
{
protected IEnumerable<TPropertyTypeViewModel> MapPropertyTypes(TContentType source)
{
// create a mapping table between properties and their associated groups
var groupKeysByPropertyKeys = source
.PropertyGroups
.SelectMany(propertyGroup => (propertyGroup.PropertyTypes?.ToArray() ?? Array.Empty<PropertyType>())
.Select(propertyType => new { GroupKey = propertyGroup.Key, PropertyTypeKey = propertyType.Key }))
.ToDictionary(map => map.PropertyTypeKey, map => map.GroupKey);
return source.PropertyTypes.Select(propertyType =>
new TPropertyTypeViewModel
{
Key = propertyType.Key,
ContainerKey = groupKeysByPropertyKeys.ContainsKey(propertyType.Key)
? groupKeysByPropertyKeys[propertyType.Key]
: null,
Name = propertyType.Name,
Alias = propertyType.Alias,
Description = propertyType.Description,
DataTypeKey = propertyType.DataTypeKey,
VariesByCulture = propertyType.VariesByCulture(),
VariesBySegment = propertyType.VariesBySegment(),
Validation = new PropertyTypeValidation
{
Mandatory = propertyType.Mandatory,
MandatoryMessage = propertyType.MandatoryMessage,
RegEx = propertyType.ValidationRegExp,
RegExMessage = propertyType.ValidationRegExpMessage
},
Appearance = new PropertyTypeAppearance
{
LabelOnTop = propertyType.LabelOnTop
}
})
.ToArray();
}
protected IEnumerable<TPropertyTypeContainerViewModel> MapPropertyTypeContainers(TContentType source)
{
// create a mapping table between property group aliases and keys
var groupKeysByGroupAliases = source
.PropertyGroups
.ToDictionary(propertyGroup => propertyGroup.Alias, propertyGroup => propertyGroup.Key);
Guid? ParentGroupKey(PropertyGroup group)
{
var path = group.Alias.Split(Constants.CharArrays.ForwardSlash);
return path.Length == 1 || groupKeysByGroupAliases.TryGetValue(path.First(), out Guid parentGroupKey) == false
? null
: parentGroupKey;
}
return source
.PropertyGroups
.Select(propertyGroup =>
new TPropertyTypeContainerViewModel
{
Key = propertyGroup.Key,
ParentKey = ParentGroupKey(propertyGroup),
Type = propertyGroup.Type.ToString(),
SortOrder = propertyGroup.SortOrder,
Name = propertyGroup.Name,
})
.ToArray();
}
}

View File

@@ -0,0 +1,57 @@
using Umbraco.Cms.Api.Management.Mapping.Content;
using Umbraco.Cms.Api.Management.ViewModels.Content;
using Umbraco.Cms.Api.Management.ViewModels.Document;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.PropertyEditors;
namespace Umbraco.Cms.Api.Management.Mapping.Document;
public class DocumentMapDefinition : ContentMapDefinition<IContent, DocumentValueViewModel, DocumentVariantViewModel>, IMapDefinition
{
public DocumentMapDefinition(PropertyEditorCollection propertyEditorCollection)
: base(propertyEditorCollection)
{
}
public void DefineMaps(IUmbracoMapper mapper)
=> mapper.Define<IContent, DocumentViewModel>((_, _) => new DocumentViewModel(), Map);
// Umbraco.Code.MapAll -Urls -TemplateKey
private void Map(IContent source, DocumentViewModel target, MapperContext context)
{
target.Key = source.Key;
target.ContentTypeKey = source.ContentType.Key;
target.Values = MapValueViewModels(source);
target.Variants = MapVariantViewModels(
source,
(culture, _, documentVariantViewModel) =>
{
documentVariantViewModel.State = GetSavedState(source, culture);
documentVariantViewModel.PublishDate = culture == null
? source.PublishDate
: source.GetPublishDate(culture);
});
}
private ContentState GetSavedState(IContent content, string? culture)
{
if (content.Id <= 0 || (culture != null && content.IsCultureAvailable(culture) == false))
{
return ContentState.NotCreated;
}
var isDraft = content.PublishedState == PublishedState.Unpublished ||
(culture != null && content.IsCulturePublished(culture) == false);
if (isDraft)
{
return ContentState.Draft;
}
var isEdited = culture != null
? content.IsCultureEdited(culture)
: content.Edited;
return isEdited ? ContentState.PublishedPendingChanges : ContentState.Published;
}
}

View File

@@ -0,0 +1,62 @@
using Umbraco.Cms.Api.Management.Mapping.ContentType;
using Umbraco.Cms.Api.Management.ViewModels.ContentType;
using Umbraco.Cms.Api.Management.ViewModels.DocumentType;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Extensions;
using ContentTypeSort = Umbraco.Cms.Api.Management.ViewModels.ContentType.ContentTypeSort;
namespace Umbraco.Cms.Api.Management.Mapping.DocumentType;
public class DocumentTypeMapDefinition : ContentTypeMapDefinition<IContentType, DocumentTypePropertyTypeViewModel, DocumentTypePropertyTypeContainerViewModel>, IMapDefinition
{
public void DefineMaps(IUmbracoMapper mapper)
=> mapper.Define<IContentType, DocumentTypeViewModel>((_, _) => new DocumentTypeViewModel(), Map);
// Umbraco.Code.MapAll
private void Map(IContentType source, DocumentTypeViewModel target, MapperContext context)
{
target.Key = source.Key;
target.Alias = source.Alias;
target.Name = source.Name ?? string.Empty;
target.Description = source.Description;
target.Icon = source.Icon ?? string.Empty;
target.AllowedAsRoot = source.AllowedAsRoot;
target.VariesByCulture = source.VariesByCulture();
target.VariesBySegment = source.VariesBySegment();
target.IsElement = source.IsElement;
target.Containers = MapPropertyTypeContainers(source);
target.Properties = MapPropertyTypes(source);
if (source.AllowedContentTypes != null)
{
target.AllowedContentTypes = source.AllowedContentTypes.Select(contentTypeSort
=> new ContentTypeSort { Key = contentTypeSort.Key, SortOrder = contentTypeSort.SortOrder });
}
if (source.AllowedTemplates != null)
{
target.AllowedTemplateKeys = source.AllowedTemplates.Select(template => template.Key);
}
target.DefaultTemplateKey = source.DefaultTemplate?.Key;
if (source.HistoryCleanup != null)
{
target.Cleanup = new ContentTypeCleanup
{
PreventCleanup = source.HistoryCleanup.PreventCleanup,
KeepAllVersionsNewerThanDays = source.HistoryCleanup.KeepAllVersionsNewerThanDays,
KeepLatestVersionPerDayForDays = source.HistoryCleanup.KeepLatestVersionPerDayForDays
};
}
target.Compositions = source.ContentTypeComposition.Select(contentType => new ContentTypeComposition
{
Key = contentType.Key,
CompositionType = contentType.Id == source.ParentId
? ContentTypeCompositionType.Inheritance
: ContentTypeCompositionType.Composition
}).ToArray();
}
}

View File

@@ -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<PackageModelBase, PackageDefinition>((_, _) => new PackageDefinition(), Map);
mapper.Define<PackageDefinition, PackageDefinitionViewModel>(
(_, _) => new PackageDefinitionViewModel
{
Name = string.Empty,
PackagePath = string.Empty
},
Map);
mapper.Define<InstalledPackage, PackageMigrationStatusViewModel>((_, _) => new PackageMigrationStatusViewModel { PackageName = string.Empty }, Map);
mapper.Define<IEnumerable<PackageDefinition>, PagedViewModel<PackageDefinitionViewModel>>((_, _) => new PagedViewModel<PackageDefinitionViewModel>(), 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<PackageDefinition> source, PagedViewModel<PackageDefinitionViewModel> target, MapperContext context)
{
PackageDefinition[] definitions = source.ToArray();
target.Items = context.MapEnumerable<PackageDefinition, PackageDefinitionViewModel>(definitions);
target.Total = definitions.Length;
}
}

View File

@@ -0,0 +1,54 @@
namespace Umbraco.Cms.Api.Management.Mapping;
/// <summary>
/// Maps from the old section aliases to the new section names.
/// This is static since it's expected to be removed, so might as well make the clean up work as easy as possible.
/// FIXME: This is a temporary thing until permissions is fleshed out and section is either migrated to some form of permission
/// </summary>
public static class SectionMapper
{
private static readonly List<SectionMapping> _sectionMappings = new()
{
new SectionMapping { Alias = "content", Name = "Umb.Section.Content" },
new SectionMapping { Alias = "media", Name = "Umb.Section.Media" },
new SectionMapping { Alias = "member", Name = "Umb.Section.Members" },
new SectionMapping { Alias = "settings", Name = "Umb.Section.Settings" },
new SectionMapping { Alias = "packages", Name = "Umb.Section.Packages" },
new SectionMapping { Alias = "translation", Name = "Umb.Section.Translation" },
new SectionMapping { Alias = "users", Name = "Umb.Section.Users" },
new SectionMapping { Alias = "forms", Name = "Umb.Section.Forms" },
};
public static string GetName(string alias)
{
SectionMapping? mapping = _sectionMappings.FirstOrDefault(x => x.Alias == alias);
if (mapping is not null)
{
return mapping.Name;
}
// If we can't find it we just fall back to the alias
return alias;
}
public static string GetAlias(string name)
{
SectionMapping? mapping = _sectionMappings.FirstOrDefault(x => x.Name == name);
if (mapping is not null)
{
return mapping.Alias;
}
// If we can't find it we just fall back to the name
return name;
}
private class SectionMapping
{
public required string Alias { get; init; }
public required string Name { get; init; }
}
}

View File

@@ -23,6 +23,7 @@ public class TrackedReferenceViewModelsMapDefinition : IMapDefinition
target.RelationTypeIsBidirectional = source.RelationTypeIsBidirectional;
target.RelationTypeIsDependency = source.RelationTypeIsDependency;
target.RelationTypeName = source.RelationTypeName;
target.NodePublished = source.NodePublished;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -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; }
}

View File

@@ -0,0 +1,8 @@
using Umbraco.Cms.Core.Models;
namespace Umbraco.Cms.Api.Management.ViewModels.AuditLogs;
/// <inheritdoc />
public class AuditLogResponseModel : AuditLogBaseModel
{
}

View File

@@ -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; }
}

View File

@@ -0,0 +1,27 @@
namespace Umbraco.Cms.Api.Management.ViewModels.Content;
/// <summary>
/// The saved state of a content item
/// </summary>
public enum ContentState
{
/// <summary>
/// The item isn't created yet
/// </summary>
NotCreated = 1,
/// <summary>
/// The item is saved but isn't published
/// </summary>
Draft = 2,
/// <summary>
/// The item is published and there are no pending changes
/// </summary>
Published = 3,
/// <summary>
/// The item is published and there are pending changes
/// </summary>
PublishedPendingChanges = 4,
}

View File

@@ -0,0 +1,8 @@
namespace Umbraco.Cms.Api.Management.ViewModels.Content;
public class ContentUrlInfo
{
public required string? Culture { get; init; }
public required string Url { get; init; }
}

View File

@@ -0,0 +1,14 @@
namespace Umbraco.Cms.Api.Management.ViewModels.Content;
public abstract class ContentViewModelBase<TValueViewModelBase, TVariantViewModel>
where TValueViewModelBase : ValueViewModelBase
where TVariantViewModel : VariantViewModelBase
{
public Guid Key { get; set; }
public Guid ContentTypeKey { get; set; }
public IEnumerable<TValueViewModelBase> Values { get; set; } = Array.Empty<TValueViewModelBase>();
public IEnumerable<TVariantViewModel> Variants { get; set; } = Array.Empty<TVariantViewModel>();
}

View File

@@ -0,0 +1,12 @@
namespace Umbraco.Cms.Api.Management.ViewModels.Content;
public abstract class ValueViewModelBase
{
public string? Culture { get; set; }
public string? Segment { get; set; }
public string Alias { get; set; } = string.Empty;
public object? Value { get; set; }
}

View File

@@ -0,0 +1,14 @@
namespace Umbraco.Cms.Api.Management.ViewModels.Content;
public abstract class VariantViewModelBase
{
public string? Culture { get; set; }
public string? Segment { get; set; }
public string Name { get; set; } = string.Empty;
public DateTime CreateDate { get; set; }
public DateTime UpdateDate { get; set; }
}

View File

@@ -0,0 +1,10 @@
namespace Umbraco.Cms.Api.Management.ViewModels.ContentType;
public class ContentTypeCleanup
{
public bool PreventCleanup { get; init; }
public int? KeepAllVersionsNewerThanDays { get; init; }
public int? KeepLatestVersionPerDayForDays { get; init; }
}

View File

@@ -0,0 +1,8 @@
namespace Umbraco.Cms.Api.Management.ViewModels.ContentType;
public class ContentTypeComposition
{
public required Guid Key { get; init; }
public required ContentTypeCompositionType CompositionType { get; init; }
}

View File

@@ -0,0 +1,7 @@
namespace Umbraco.Cms.Api.Management.ViewModels.ContentType;
public enum ContentTypeCompositionType
{
Composition,
Inheritance
}

View File

@@ -0,0 +1,8 @@
namespace Umbraco.Cms.Api.Management.ViewModels.ContentType;
public class ContentTypeSort
{
public required Guid Key { get; init; }
public required int SortOrder { get; init; }
}

View File

@@ -0,0 +1,34 @@
namespace Umbraco.Cms.Api.Management.ViewModels.ContentType;
public abstract class ContentTypeViewModelBase<TPropertyType, TPropertyTypeContainer>
where TPropertyType : PropertyTypeViewModelBase
where TPropertyTypeContainer : PropertyTypeContainerViewModelBase
{
public Guid Key { get; set; }
public string Alias { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public string? Description { get; set; }
public string Icon { get; set; } = string.Empty;
public bool AllowedAsRoot { get; set; }
public bool VariesByCulture { get; set; }
public bool VariesBySegment { get; set; }
public bool IsElement { get; set; }
public IEnumerable<TPropertyType> Properties { get; set; } = Array.Empty<TPropertyType>();
public IEnumerable<TPropertyTypeContainer> Containers { get; set; } = Array.Empty<TPropertyTypeContainer>();
public IEnumerable<ContentTypeSort> AllowedContentTypes { get; set; } = Array.Empty<ContentTypeSort>();
public IEnumerable<ContentTypeComposition> Compositions { get; set; } = Array.Empty<ContentTypeComposition>();
public ContentTypeCleanup Cleanup { get; set; } = new();
}

View File

@@ -0,0 +1,6 @@
namespace Umbraco.Cms.Api.Management.ViewModels.ContentType;
public class PropertyTypeAppearance
{
public bool LabelOnTop { get; set; }
}

View File

@@ -0,0 +1,15 @@
namespace Umbraco.Cms.Api.Management.ViewModels.ContentType;
public abstract class PropertyTypeContainerViewModelBase
{
public Guid Key { get; set; }
public Guid? ParentKey { get; set; }
public string? Name { get; set; }
// NOTE: This needs to be a string because it can be anything in the future (= not necessarily limited to "tab" or "group")
public string Type { get; set; } = string.Empty;
public int SortOrder { get; set; }
}

View File

@@ -0,0 +1,12 @@
namespace Umbraco.Cms.Api.Management.ViewModels.ContentType;
public class PropertyTypeValidation
{
public bool Mandatory { get; set; }
public string? MandatoryMessage { get; set; }
public string? RegEx { get; set; }
public string? RegExMessage { get; set; }
}

View File

@@ -0,0 +1,24 @@
namespace Umbraco.Cms.Api.Management.ViewModels.ContentType;
public abstract class PropertyTypeViewModelBase
{
public Guid Key { get; set; }
public Guid? ContainerKey { get; set; }
public string Alias { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public string? Description { get; set; }
public Guid DataTypeKey { get; set; }
public bool VariesByCulture { get; set; }
public bool VariesBySegment { get; set; }
public PropertyTypeValidation Validation { get; set; } = new();
public PropertyTypeAppearance Appearance { get; set; } = new();
}

View File

@@ -1,6 +1,6 @@
namespace Umbraco.Cms.Api.Management.ViewModels.DataType;
public class DataTypeViewModel : DataTypeModelBase
public class DataTypeViewModel : DataTypeModelBase, INamedEntityViewModel
{
public Guid Key { get; set; }

View File

@@ -1,6 +1,6 @@
namespace Umbraco.Cms.Api.Management.ViewModels.Dictionary;
public class DictionaryItemViewModel : DictionaryItemModelBase
public class DictionaryItemViewModel : DictionaryItemModelBase, INamedEntityViewModel
{
public Guid Key { get; set; }
}

View File

@@ -0,0 +1,7 @@
using Umbraco.Cms.Api.Management.ViewModels.Content;
namespace Umbraco.Cms.Api.Management.ViewModels.Document;
public class DocumentValueViewModel : ValueViewModelBase
{
}

View File

@@ -0,0 +1,10 @@
using Umbraco.Cms.Api.Management.ViewModels.Content;
namespace Umbraco.Cms.Api.Management.ViewModels.Document;
public class DocumentVariantViewModel : VariantViewModelBase
{
public ContentState State { get; set; }
public DateTime? PublishDate { get; set; }
}

View File

@@ -0,0 +1,10 @@
using Umbraco.Cms.Api.Management.ViewModels.Content;
namespace Umbraco.Cms.Api.Management.ViewModels.Document;
public class DocumentViewModel : ContentViewModelBase<DocumentValueViewModel, DocumentVariantViewModel>
{
public IEnumerable<ContentUrlInfo> Urls { get; set; } = Array.Empty<ContentUrlInfo>();
public Guid? TemplateKey { get; set; }
}

View File

@@ -0,0 +1,7 @@
using Umbraco.Cms.Api.Management.ViewModels.ContentType;
namespace Umbraco.Cms.Api.Management.ViewModels.DocumentType;
public class DocumentTypePropertyTypeContainerViewModel : PropertyTypeContainerViewModelBase
{
}

View File

@@ -0,0 +1,7 @@
using Umbraco.Cms.Api.Management.ViewModels.ContentType;
namespace Umbraco.Cms.Api.Management.ViewModels.DocumentType;
public class DocumentTypePropertyTypeViewModel : PropertyTypeViewModelBase
{
}

View File

@@ -0,0 +1,10 @@
using Umbraco.Cms.Api.Management.ViewModels.ContentType;
namespace Umbraco.Cms.Api.Management.ViewModels.DocumentType;
public class DocumentTypeViewModel : ContentTypeViewModelBase<DocumentTypePropertyTypeViewModel, DocumentTypePropertyTypeContainerViewModel>
{
public IEnumerable<Guid> AllowedTemplateKeys { get; set; } = Array.Empty<Guid>();
public Guid? DefaultTemplateKey { get; set; }
}

View File

@@ -1,6 +1,6 @@
namespace Umbraco.Cms.Api.Management.ViewModels.Folder;
public class FolderViewModel : FolderModelBase
public class FolderViewModel : FolderModelBase, INamedEntityViewModel
{
public Guid Key { get; set; }

View File

@@ -0,0 +1,8 @@
namespace Umbraco.Cms.Api.Management.ViewModels;
public interface INamedEntityViewModel
{
Guid Key { get; }
string Name { get;}
}

View File

@@ -0,0 +1,5 @@
namespace Umbraco.Cms.Api.Management.ViewModels.Package;
public class PackageCreateModel : PackageModelBase
{
}

View File

@@ -0,0 +1,17 @@
using System.ComponentModel;
namespace Umbraco.Cms.Api.Management.ViewModels.Package;
public class PackageDefinitionViewModel : PackageModelBase
{
/// <summary>
/// Gets or sets the key.
/// </summary>
public Guid Key { get; set; }
/// <summary>
/// Gets or sets the full path to the package's XML file.
/// </summary>
[ReadOnly(true)]
public required string PackagePath { get; set; }
}

View File

@@ -0,0 +1,14 @@
namespace Umbraco.Cms.Api.Management.ViewModels.Package;
public class PackageMigrationStatusViewModel
{
/// <summary>
/// Gets or sets the name of the package.
/// </summary>
public required string PackageName { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the package has any pending migrations to run.
/// </summary>
public bool HasPendingMigrations { get; set; }
}

View File

@@ -0,0 +1,74 @@
namespace Umbraco.Cms.Api.Management.ViewModels.Package;
public class PackageModelBase
{
/// <summary>
/// Gets or sets the name.
/// </summary>
public required string Name { get; set; }
/// <summary>
/// Gets or sets the id of the selected content node.
/// </summary>
public string? ContentNodeId { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to load all child nodes of the selected content node.
/// </summary>
public bool ContentLoadChildNodes { get; set; }
/// <summary>
/// Gets or sets the list of media keys for the selected media items.
/// </summary>
public IList<Guid> MediaKeys { get; set; } = new List<Guid>();
/// <summary>
/// Gets or sets a value indicating whether to load all child nodes of the selected media items.
/// </summary>
public bool MediaLoadChildNodes { get; set; }
/// <summary>
/// Gets or sets the list of ids for the selected document types.
/// </summary>
public IList<string> DocumentTypes { get; set; } = new List<string>();
/// <summary>
/// Gets or sets the list of ids for the selected media types.
/// </summary>
public IList<string> MediaTypes { get; set; } = new List<string>();
/// <summary>
/// Gets or sets the list of ids for the selected data types.
/// </summary>
public IList<string> DataTypes { get; set; } = new List<string>();
/// <summary>
/// Gets or sets the list of ids for the selected templates.
/// </summary>
public IList<string> Templates { get; set; } = new List<string>();
/// <summary>
/// Gets or sets the list of relative paths for the selected partial views.
/// </summary>
public IList<string> PartialViews { get; set; } = new List<string>();
/// <summary>
/// Gets or sets the list of names for the selected stylesheets.
/// </summary>
public IList<string> Stylesheets { get; set; } = new List<string>();
/// <summary>
/// Gets or sets the list of names for the selected scripts.
/// </summary>
public IList<string> Scripts { get; set; } = new List<string>();
/// <summary>
/// Gets or sets the list of ids for the selected languages.
/// </summary>
public IList<string> Languages { get; set; } = new List<string>();
/// <summary>
/// Gets or sets the list of ids for the selected dictionary items.
/// </summary>
public IList<string> DictionaryItems { get; set; } = new List<string>();
}

View File

@@ -0,0 +1,12 @@
using System.ComponentModel;
namespace Umbraco.Cms.Api.Management.ViewModels.Package;
public class PackageUpdateModel : PackageModelBase
{
/// <summary>
/// Gets or sets the full path to the package's XML file.
/// </summary>
[ReadOnly(true)]
public required string PackagePath { get; set; }
}

View File

@@ -1,6 +1,6 @@
namespace Umbraco.Cms.Api.Management.ViewModels.RecycleBin;
public class RecycleBinItemViewModel
public class RecycleBinItemViewModel : INamedEntityViewModel
{
public Guid Key { get; set; }

View File

@@ -1,6 +1,6 @@
namespace Umbraco.Cms.Api.Management.ViewModels.Template;
public class TemplateViewModel : TemplateModelBase
public class TemplateViewModel : TemplateModelBase, INamedEntityViewModel
{
public Guid Key { get; set; }
}

View File

@@ -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; }
}

View File

@@ -1,6 +1,6 @@
namespace Umbraco.Cms.Api.Management.ViewModels.Tree;
public class EntityTreeItemViewModel : TreeItemViewModel
public class EntityTreeItemViewModel : TreeItemViewModel, INamedEntityViewModel
{
public Guid Key { get; set; }

View File

@@ -0,0 +1,58 @@
namespace Umbraco.Cms.Api.Management.ViewModels.UserGroups;
/// <summary>
/// <para>
/// Base class for front-end representation of a User Group.
/// </para>
/// <para>
/// Contains all the properties shared between Save, Update, Representation, etc...
/// </para>
/// </summary>
public class UserGroupBase
{
/// <summary>
/// The name of the user groups
/// </summary>
public required string Name { get; init; }
/// <summary>
/// The Icon for the user group
/// </summary>
public string? Icon { get; init; }
/// <summary>
/// The sections that the user group has access to
/// </summary>
public required IEnumerable<string> Sections { get; init; }
/// <summary>
/// The languages that the user group has access to
/// </summary>
public required IEnumerable<string> Languages { get; init; }
/// <summary>
/// Flag indicating if the user group gives access to all languages, regardless of <see cref="UserGroupBase.Languages"/>.
/// </summary>
public required bool HasAccessToAllLanguages { get; init; }
/// <summary>
/// The key of the document that should act as root node for the user group
/// <remarks>
/// This can be overwritten by a different user group if a user is a member of multiple groups
/// </remarks>
/// </summary>
public Guid? DocumentStartNodeKey { get; init; }
/// <summary>
/// The Key of the media that should act as root node for the user group
/// <remarks>
/// This can be overwritten by a different user group if a user is a member of multiple groups
/// </remarks>
/// </summary>
public Guid? MediaStartNodeKey { get; init; }
/// <summary>
/// Ad-hoc list of permissions provided, and maintained by the front-end. The server has no concept of what these mean.
/// </summary>
public required ISet<string> Permissions { get; init; }
}

View File

@@ -0,0 +1,6 @@
namespace Umbraco.Cms.Api.Management.ViewModels.UserGroups;
public class UserGroupSaveModel : UserGroupBase
{
}

View File

@@ -0,0 +1,6 @@
namespace Umbraco.Cms.Api.Management.ViewModels.UserGroups;
public class UserGroupUpdateModel : UserGroupBase
{
}

View File

@@ -0,0 +1,10 @@
namespace Umbraco.Cms.Api.Management.ViewModels.UserGroups;
public class UserGroupViewModel : UserGroupBase, INamedEntityViewModel
{
/// <summary>
/// The key identifier for the user group.
/// </summary>
public required Guid Key { get; init; }
}

Some files were not shown because too many files have changed in this diff Show More