diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index ae89c0a6bc..86d160aef3 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -2,29 +2,38 @@ name: "Code scanning - action" on: push: - branches: ['*/dev','*/contrib'] + branches: + - '*/dev' + - '*/contrib' pull_request: # The branches below must be a subset of the branches above - branches: ['*/dev','*/contrib'] + branches: + - '*/dev' + - '*/contrib' permissions: contents: read +env: + dotnetVersion: 7.x + dotnetIncludePreviewVersions: true + solution: umbraco.sln + buildConfiguration: SkipTests + DOTNET_NOLOGO: true + DOTNET_GENERATE_ASPNET_CERTIFICATE: false + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true + DOTNET_CLI_TELEMETRY_OPTOUT: true + jobs: CodeQL-Build: - permissions: - actions: read # for github/codeql-action/init to get workflow details - contents: read # for actions/checkout to fetch code - security-events: write # for github/codeql-action/analyze to upload SARIF results + actions: read # for github/codeql-action/init to get workflow details + contents: read # for actions/checkout to fetch code + security-events: write # for github/codeql-action/analyze to upload SARIF results runs-on: ubuntu-latest - permissions: - actions: read - contents: read - security-events: write steps: - - name: Checkout repository + - name: Checkout uses: actions/checkout@v3 with: fetch-depth: 0 @@ -35,14 +44,17 @@ jobs: with: config-file: ./.github/config/codeql-config.yml - - name: Setup dotnet + - name: Use .NET ${{ env.dotnetVersion }} uses: actions/setup-dotnet@v2 with: - dotnet-version: '7.x' - include-prerelease: true + dotnet-version: ${{ env.dotnetVersion }} + include-prerelease: ${{ env.dotnetIncludePreviewVersions }} - - name: dotnet build - run: dotnet build umbraco.sln -c SkipTests + - name: Run dotnet restore + run: dotnet restore ${{ env.solution }} + + - name: Run dotnet build + run: dotnet build ${{ env.solution }} --configuration ${{ env.buildConfiguration }} --no-restore -p:ContinuousIntegrationBuild=true - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v2 diff --git a/Directory.Build.props b/Directory.Build.props index eb1c44d65d..d9914970a8 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,7 @@ + net7.0 Umbraco HQ Umbraco Copyright © Umbraco $([System.DateTime]::Today.ToString('yyyy')) @@ -45,7 +46,7 @@ - + $(MSBuildThisFileDirectory) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index 3f0ed57cb1..489377f5db 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -25,6 +25,7 @@ parameters: variables: nodeVersion: 16.17.0 dotnetVersion: 7.x + dotnetIncludePreviewVersions: true solution: umbraco.sln buildConfiguration: Release UMBRACO__CMS__GLOBAL__ID: 00000000-0000-0000-0000-000000000042 @@ -72,7 +73,7 @@ stages: inputs: version: $(dotnetVersion) performMultiLevelLookup: true - includePreviewVersions: true + includePreviewVersions: $(dotnetIncludePreviewVersions) - task: DotNetCoreCLI@2 displayName: Run dotnet restore inputs: @@ -110,7 +111,7 @@ stages: } } - dotnet pack $(solution) --configuration $(buildConfiguration) --no-build --output $(Build.ArtifactStagingDirectory)/nupkg + dotnet pack $(solution) --configuration $(buildConfiguration) --no-build -p:BuildProjectReferences=false --output $(Build.ArtifactStagingDirectory)/nupkg - script: | sha="$(Build.SourceVersion)" sha=${sha:0:7} @@ -133,7 +134,7 @@ stages: displayName: Prepare API Documentation dependsOn: Build variables: - umbracoMajorVersion: $[ stageDependencies.Build.A.outputs['determineMajorVersion.majorVersion'] ] + umbracoMajorVersion: $[ stageDependencies.Build.A.outputs['determineMajorVersion.majorVersion'] ] jobs: # C# API Reference - job: @@ -246,7 +247,7 @@ stages: inputs: version: $(dotnetVersion) performMultiLevelLookup: true - includePreviewVersions: true + includePreviewVersions: $(dotnetIncludePreviewVersions) - task: DotNetCoreCLI@2 displayName: Run dotnet test inputs: @@ -283,7 +284,7 @@ stages: inputs: version: $(dotnetVersion) performMultiLevelLookup: true - includePreviewVersions: true + includePreviewVersions: $(dotnetIncludePreviewVersions) - task: DotNetCoreCLI@2 displayName: Run dotnet test inputs: @@ -293,7 +294,7 @@ stages: testRunTitle: Integration Tests SQLite - $(Agent.OS) env: Tests__Database__DatabaseType: 'Sqlite' - Umbraco__Cms__global__MainDomLock: 'FileSystemMainDomLock' + Umbraco__CMS__Global__MainDomLock: 'FileSystemMainDomLock' # Integration Tests (SQL Server) - job: @@ -336,12 +337,11 @@ stages: env: Tests__Database__DatabaseType: $(testDb) Tests__Database__SQLServerMasterConnectionString: $(connectionString) - Umbraco__Cms__global__MainDomLock: 'SqlMainDomLock' + Umbraco__CMS__Global__MainDomLock: 'SqlMainDomLock' - stage: E2E variables: npm_config_cache: $(Pipeline.Workspace)/.npm_e2e - CYPRESS_CACHE_FOLDER: $(Pipeline.Workspace)/cypress_binaries displayName: E2E Tests dependsOn: Build jobs: @@ -352,9 +352,9 @@ stages: - name: Umbraco__CMS__Unattended__InstallUnattended # Windows only value: true - name: Umbraco__CMS__Unattended__UnattendedUserName # Windows only - value: Cypress Test + value: Playwright Test - name: Umbraco__CMS__Unattended__UnattendedUserEmail # Windows only - value: cypress@umbraco.com + value: playwright@umbraco.com - name: Umbraco__CMS__Unattended__UnattendedUserPassword # Windows only value: UmbracoAcceptance123! - name: Umbraco__CMS__Global__InstallMissingDatabase # Windows only @@ -362,11 +362,11 @@ stages: - name: UmbracoDatabaseServer # Windows only value: (LocalDB)\MSSQLLocalDB - name: UmbracoDatabaseName # Windows only - value: Cypress + value: Playwright - name: ConnectionStrings__umbracoDbDSN # Windows only value: Server=$(UmbracoDatabaseServer);Database=$(UmbracoDatabaseName);Integrated Security=true; - - name: CYPRESS_BASE_URL - value: http://localhost:8080 + - name: PLAYWRIGHT_BASE_URL + value: https://localhost:8443 strategy: matrix: Linux: @@ -375,6 +375,7 @@ stages: dockerImageName: umbraco-linux Windows: vmImage: 'windows-latest' + DOTNET_GENERATE_ASPNET_CERTIFICATE: true # Automatically generate HTTPS development certificate on Windows pool: vmImage: $(vmImage) steps: @@ -395,19 +396,17 @@ stages: "npm_e2e" | "$(Agent.OS)" "npm_e2e" path: $(npm_config_cache) - - task: Cache@2 - displayName: Cache cypress binaries - inputs: - key: '"cypress_binaries" | "$(Agent.OS)" | $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/package-lock.json' - path: $(CYPRESS_CACHE_FOLDER) - task: PowerShell@2 - displayName: Generate Cypress.env.json + displayName: Generate .env inputs: targetType: inline - script: > - @{ username = "$(Umbraco__CMS__Unattended__UnattendedUserEmail)"; password = "$(Umbraco__CMS__Unattended__UnattendedUserPassword)" } | ConvertTo-Json | Set-Content -Path "tests/Umbraco.Tests.AcceptanceTest/cypress.env.json"# + workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/ + script: | + New-Item -Path "." -Name ".env" -ItemType "file" -Value "UMBRACO_USER_LOGIN=$(Umbraco__CMS__Unattended__UnattendedUserEmail) + UMBRACO_USER_PASSWORD=$(Umbraco__CMS__Unattended__UnattendedUserPassword) + URL=$(PLAYWRIGHT_BASE_URL)" - script: npm ci --no-fund --no-audit --prefer-offline - workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/ + workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/ displayName: Run npm ci - powershell: sqllocaldb start mssqllocaldb displayName: Start localdb (Windows only) @@ -420,7 +419,7 @@ stages: inputs: version: $(dotnetVersion) performMultiLevelLookup: true - includePreviewVersions: true + includePreviewVersions: $(dotnetIncludePreviewVersions) # Linux containers smooth - task: PowerShell@2 condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux')) @@ -433,10 +432,13 @@ stages: docker build -t $(dockerImageName):$sha -f $(dockerfile) . mkdir -p $(Build.ArtifactStagingDirectory)/docker-images docker save -o $(Build.ArtifactStagingDirectory)/docker-images/$(dockerImageName).$sha.tar $(dockerImageName):$sha - docker run --name $(dockerImageName) -dp 8080:5000 -e UMBRACO__CMS__GLOBAL__ID=$(UMBRACO__CMS__GLOBAL__ID) $(dockerImageName):$sha + + # Manually generate HTTPS development certificate on Linux + dotnet dev-certs https -ep ${HOME}/.aspnet/https/aspnetapp.pfx -p UmbracoAcceptance123! + dotnet dev-certs https --trust + + docker run --name $(dockerImageName) -dp 8080:5000 -dp 8443:5001 -e UMBRACO__CMS__GLOBAL__ID=$(UMBRACO__CMS__GLOBAL__ID) -e ASPNETCORE_Kestrel__Certificates__Default__Password="UmbracoAcceptance123!" -e ASPNETCORE_Kestrel__Certificates__Default__Path=/https/aspnetapp.pfx -v ${HOME}/.aspnet/https:/https/ $(dockerImageName):$sha docker ps - # Windows containers take forever. - # --no-launch-profile stops ASPNETCORE_ENVIRONMENT=Development which breaks the users.ts tests (smtp config = invite user button) # Urls matching docker setup. - task: PowerShell@2 condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT')) @@ -446,10 +448,10 @@ stages: targetType: inline script: | dotnet new --install ./nupkg/Umbraco.Templates.*.nupkg - dotnet new umbraco --name Cypress -o . --no-restore + dotnet new umbraco --name Playwright --no-restore --output . dotnet restore --configfile ./nuget.config dotnet build --configuration $(buildConfiguration) --no-restore - Start-Process -FilePath "dotnet" -ArgumentList "run --configuration $(buildConfiguration) --no-build --no-launch-profile --urls $(CYPRESS_BASE_URL)" + Start-Process -FilePath "dotnet" -ArgumentList "run --configuration $(buildConfiguration) --no-build --no-launch-profile --urls $(PLAYWRIGHT_BASE_URL)" - task: PowerShell@2 displayName: Wait for app inputs: @@ -457,31 +459,33 @@ stages: workingDirectory: tests/Umbraco.Tests.AcceptanceTest script: | npm i -g wait-on - wait-on -v --interval 1000 --timeout 120000 $(CYPRESS_BASE_URL) + wait-on -v --interval 1000 --timeout 120000 $(PLAYWRIGHT_BASE_URL) - task: PowerShell@2 - displayName: Run Cypress (Desktop) + displayName: Install Playwright + inputs: + targetType: inline + workingDirectory: tests/Umbraco.Tests.AcceptanceTest + script: npx playwright install + - task: PowerShell@2 + displayName: Run Playwright (Desktop) continueOnError: true inputs: targetType: inline workingDirectory: tests/Umbraco.Tests.AcceptanceTest - script: 'npm run test -- --reporter junit --reporter-options "mochaFile=results/test-output-D-[hash].xml,toConsole=true" --config="viewportHeight=1600,viewportWidth=2560,screenshotsFolder=cypress/artifacts/desktop/screenshots,videosFolder=cypress/artifacts/desktop/videos,videoUploadOnPasses=false"' - - task: PublishTestResults@2 - displayName: Publish test results - condition: always() - inputs: - testResultsFormat: 'JUnit' - testResultsFiles: 'tests/Umbraco.Tests.AcceptanceTest/results/test-output-D-*.xml' - mergeTestResults: true - testRunTitle: "e2e - $(Agent.OS)" + script: 'npm run test --ignore-certificate-errors' + - bash: | + if [ -f $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/results/ ]; then + echo "##vso[task.setVariable variable=myfileexists]true" + fi - task: CopyFiles@2 displayName: Prepare artifacts - condition: always() + condition: eq(variables.myfileexists, 'true') inputs: - sourceFolder: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/cypress/artifacts - targetFolder: $(Build.ArtifactStagingDirectory)/cypresss + sourceFolder: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/results/ + targetFolder: $(Build.ArtifactStagingDirectory)/playwright - task: PublishPipelineArtifact@1 displayName: "Publish test artifacts" - condition: always() + condition: eq(variables.myfileexists, 'true') inputs: targetPath: $(Build.ArtifactStagingDirectory) artifact: 'E2E artifacts - $(Agent.OS) - Attempt #$(System.JobAttempt)' @@ -506,8 +510,8 @@ stages: inputs: artifact: nupkg path: $(Build.ArtifactStagingDirectory)/nupkg - - task: DotNetCoreCLI@2 - displayName: dotnet restore + - task: NuGetCommand@2 + displayName: NuGet push inputs: command: restore projects: $(solution) @@ -533,8 +537,8 @@ stages: inputs: artifact: nupkg path: $(Build.ArtifactStagingDirectory)/nupkg - - task: DotNetCoreCLI@2 - displayName: dotnet restore + - task: NuGetCommand@2 + displayName: NuGet push inputs: command: restore projects: $(solution) diff --git a/src/JsonSchema/JsonSchema.csproj b/src/JsonSchema/JsonSchema.csproj index 0769f11d94..5f255e75d1 100644 --- a/src/JsonSchema/JsonSchema.csproj +++ b/src/JsonSchema/JsonSchema.csproj @@ -1,6 +1,5 @@ - net7.0 Exe false false @@ -13,7 +12,7 @@ - - + + diff --git a/src/JsonSchema/UmbracoJsonSchemaGenerator.cs b/src/JsonSchema/UmbracoJsonSchemaGenerator.cs index 4422436d8d..8cb5bafcbf 100644 --- a/src/JsonSchema/UmbracoJsonSchemaGenerator.cs +++ b/src/JsonSchema/UmbracoJsonSchemaGenerator.cs @@ -1,11 +1,10 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System.Net.Http; -using System.Threading.Tasks; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using NJsonSchema.Generation; +using Umbraco.Cms.Core.Configuration.Models; namespace JsonSchema { @@ -50,6 +49,9 @@ namespace JsonSchema { NJsonSchema.JsonSchema schema = _innerGenerator.Generate(typeof(AppSettings)); + // TODO: when the "UmbracoPath" setter is removed from "GlobalSettings" (scheduled for V12), remove this line as well + schema.Definitions["UmbracoCmsCoreConfigurationModelsGlobalSettings"]?.Properties?.Remove(nameof(GlobalSettings.UmbracoPath)); + return JsonConvert.DeserializeObject(schema.ToJson())!; } } diff --git a/src/Umbraco.Cms.Imaging.ImageSharp/Umbraco.Cms.Imaging.ImageSharp.csproj b/src/Umbraco.Cms.Imaging.ImageSharp/Umbraco.Cms.Imaging.ImageSharp.csproj index ef6ef5f095..0eb87fbda7 100644 --- a/src/Umbraco.Cms.Imaging.ImageSharp/Umbraco.Cms.Imaging.ImageSharp.csproj +++ b/src/Umbraco.Cms.Imaging.ImageSharp/Umbraco.Cms.Imaging.ImageSharp.csproj @@ -2,7 +2,6 @@ Umbraco CMS - Imaging - ImageSharp Adds imaging support using ImageSharp/ImageSharp.Web to Umbraco CMS. - net7.0 false diff --git a/src/Umbraco.Cms.ManagementApi/Builders/ProblemDetailsBuilder.cs b/src/Umbraco.Cms.ManagementApi/Builders/ProblemDetailsBuilder.cs new file mode 100644 index 0000000000..66a88ae669 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Builders/ProblemDetailsBuilder.cs @@ -0,0 +1,45 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace Umbraco.Cms.ManagementApi.Builders; + +public class ProblemDetailsBuilder +{ + private string? _title; + private string? _detail; + private int _status = StatusCodes.Status400BadRequest; + private string? _type; + + public ProblemDetailsBuilder WithTitle(string title) + { + _title = title; + return this; + } + + public ProblemDetailsBuilder WithDetail(string detail) + { + _detail = detail; + return this; + } + + public ProblemDetailsBuilder WithStatus(int status) + { + _status = status; + return this; + } + + public ProblemDetailsBuilder WithType(string type) + { + _type = type; + return this; + } + + public ProblemDetails Build() => + new() + { + Title = _title, + Detail = _detail, + Status = _status, + Type = _type ?? "Error", + }; +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Analytics/AllAnalyticsController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Analytics/AllAnalyticsController.cs new file mode 100644 index 0000000000..780d6f90d3 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Analytics/AllAnalyticsController.cs @@ -0,0 +1,22 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.ManagementApi.ViewModels.Pagination; + +namespace Umbraco.Cms.ManagementApi.Controllers.Analytics; + +public class AllAnalyticsController : AnalyticsControllerBase +{ + [HttpGet("all")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task> GetAll(int skip, int take) + { + TelemetryLevel[] levels = Enum.GetValues(); + return await Task.FromResult(new PagedViewModel + { + Total = levels.Length, + Items = levels.Skip(skip).Take(take), + }); + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Analytics/AnalyticsControllerBase.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Analytics/AnalyticsControllerBase.cs new file mode 100644 index 0000000000..f0ed4bae2c --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Analytics/AnalyticsControllerBase.cs @@ -0,0 +1,13 @@ +using Microsoft.AspNetCore.Mvc; +using NSwag.Annotations; +using Umbraco.New.Cms.Web.Common.Routing; + +namespace Umbraco.Cms.ManagementApi.Controllers.Analytics; + +[ApiController] +[BackOfficeRoute("api/v{version:apiVersion}/analytics")] +[OpenApiTag("Analytics")] +[ApiVersion("1.0")] +public abstract class AnalyticsControllerBase : ManagementApiControllerBase +{ +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Analytics/GetAnalyticsController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Analytics/GetAnalyticsController.cs new file mode 100644 index 0000000000..1c45e41e71 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Analytics/GetAnalyticsController.cs @@ -0,0 +1,18 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.ViewModels.Analytics; + +namespace Umbraco.Cms.ManagementApi.Controllers.Analytics; + +public class GetAnalyticsController : AnalyticsControllerBase +{ + private readonly IMetricsConsentService _metricsConsentService; + + public GetAnalyticsController(IMetricsConsentService metricsConsentService) => _metricsConsentService = metricsConsentService; + + [HttpGet] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(AnalyticsLevelViewModel), StatusCodes.Status200OK)] + public async Task Get() => await Task.FromResult(new AnalyticsLevelViewModel { AnalyticsLevel = _metricsConsentService.GetConsentLevel() }); +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Analytics/SetAnalyticsController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Analytics/SetAnalyticsController.cs new file mode 100644 index 0000000000..cfdd1e8b4f --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Analytics/SetAnalyticsController.cs @@ -0,0 +1,36 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.ViewModels.Analytics; +using Umbraco.Cms.ManagementApi.ViewModels.Server; + +namespace Umbraco.Cms.ManagementApi.Controllers.Analytics; + +public class SetAnalyticsController : AnalyticsControllerBase +{ + private readonly IMetricsConsentService _metricsConsentService; + + public SetAnalyticsController(IMetricsConsentService metricsConsentService) => _metricsConsentService = metricsConsentService; + + [HttpPost] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task SetConsentLevel(AnalyticsLevelViewModel analyticsLevelViewModel) + { + if (!Enum.IsDefined(analyticsLevelViewModel.AnalyticsLevel)) + { + var invalidModelProblem = new ProblemDetails + { + Title = "Invalid AnalyticsLevel value", + Detail = "The provided value for AnalyticsLevel is not valid", + Status = StatusCodes.Status400BadRequest, + Type = "Error", + }; + return BadRequest(invalidModelProblem); + } + + _metricsConsentService.SetConsentLevel(analyticsLevelViewModel.AnalyticsLevel); + return await Task.FromResult(Ok()); + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Culture/AllCultureController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Culture/AllCultureController.cs new file mode 100644 index 0000000000..713898c640 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Culture/AllCultureController.cs @@ -0,0 +1,33 @@ +using System.Globalization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.ManagementApi.ViewModels.Culture; +using Umbraco.Cms.ManagementApi.ViewModels.Pagination; + +namespace Umbraco.Cms.ManagementApi.Controllers.Culture; + +public class AllCultureController : CultureControllerBase +{ + private readonly IUmbracoMapper _umbracoMapper; + + public AllCultureController(IUmbracoMapper umbracoMapper) => _umbracoMapper = umbracoMapper; + + /// + /// Returns all cultures available for creating languages. + /// + /// + [HttpGet] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task> GetAll(int skip, int take) + { + IEnumerable list = CultureInfo.GetCultures(CultureTypes.AllCultures) + .DistinctBy(x => x.Name) + .OrderBy(x => x.EnglishName) + .Skip(skip) + .Take(take); + + return await Task.FromResult(_umbracoMapper.Map>(list)!); + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Culture/CultureControllerBase.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Culture/CultureControllerBase.cs new file mode 100644 index 0000000000..26cdb1bf5b --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Culture/CultureControllerBase.cs @@ -0,0 +1,13 @@ +using Microsoft.AspNetCore.Mvc; +using NSwag.Annotations; +using Umbraco.New.Cms.Web.Common.Routing; + +namespace Umbraco.Cms.ManagementApi.Controllers.Culture; + +[ApiController] +[BackOfficeRoute("api/v{version:apiVersion}/culture")] +[OpenApiTag("Culture")] +[ApiVersion("1.0")] +public abstract class CultureControllerBase : ManagementApiControllerBase +{ +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/DataType/Tree/ChildrenDataTypeTreeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/DataType/Tree/ChildrenDataTypeTreeController.cs new file mode 100644 index 0000000000..7aee93d03c --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/DataType/Tree/ChildrenDataTypeTreeController.cs @@ -0,0 +1,24 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.ViewModels.Pagination; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; + +namespace Umbraco.Cms.ManagementApi.Controllers.DataType.Tree; + +public class ChildrenDataTypeTreeController : DataTypeTreeControllerBase +{ + public ChildrenDataTypeTreeController(IEntityService entityService, IDataTypeService dataTypeService) + : base(entityService, dataTypeService) + { + } + + [HttpGet("children")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task>> Children(Guid parentKey, int skip = 0, int take = 100, bool foldersOnly = false) + { + RenderFoldersOnly(foldersOnly); + return await GetChildren(parentKey, skip, take); + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/DataType/Tree/DataTypeTreeControllerBase.cs b/src/Umbraco.Cms.ManagementApi/Controllers/DataType/Tree/DataTypeTreeControllerBase.cs new file mode 100644 index 0000000000..4bdae5a2fa --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/DataType/Tree/DataTypeTreeControllerBase.cs @@ -0,0 +1,46 @@ +using Microsoft.AspNetCore.Mvc; +using NSwag.Annotations; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.Controllers.Tree; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; +using Umbraco.New.Cms.Web.Common.Routing; + +namespace Umbraco.Cms.ManagementApi.Controllers.DataType.Tree; + +[ApiVersion("1.0")] +[ApiController] +[VersionedApiBackOfficeRoute($"{Constants.UdiEntityType.DataType}/tree")] +[OpenApiTag(nameof(Constants.UdiEntityType.DataType))] +public class DataTypeTreeControllerBase : FolderTreeControllerBase +{ + private readonly IDataTypeService _dataTypeService; + + public DataTypeTreeControllerBase(IEntityService entityService, IDataTypeService dataTypeService) + : base(entityService) => + _dataTypeService = dataTypeService; + + protected override UmbracoObjectTypes ItemObjectType => UmbracoObjectTypes.DataType; + + protected override UmbracoObjectTypes FolderObjectType => UmbracoObjectTypes.DataTypeContainer; + + protected override FolderTreeItemViewModel[] MapTreeItemViewModels(Guid? parentKey, IEntitySlim[] entities) + { + var dataTypes = _dataTypeService + .GetAll(entities.Select(entity => entity.Id).ToArray()) + .ToDictionary(contentType => contentType.Id); + + return entities.Select(entity => + { + FolderTreeItemViewModel viewModel = MapTreeItemViewModel(parentKey, entity); + if (dataTypes.TryGetValue(entity.Id, out IDataType? dataType)) + { + viewModel.Icon = dataType.Editor?.Icon ?? viewModel.Icon; + } + + return viewModel; + }).ToArray(); + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/DataType/Tree/ItemsDataTypeTreeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/DataType/Tree/ItemsDataTypeTreeController.cs new file mode 100644 index 0000000000..82eabcb6f4 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/DataType/Tree/ItemsDataTypeTreeController.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; + +namespace Umbraco.Cms.ManagementApi.Controllers.DataType.Tree; + +public class ItemsDataTypeTreeController : DataTypeTreeControllerBase +{ + public ItemsDataTypeTreeController(IEntityService entityService, IDataTypeService dataTypeService) + : base(entityService, dataTypeService) + { + } + + [HttpGet("items")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + public async Task>> Items([FromQuery(Name = "key")] Guid[] keys) + => await GetItems(keys); +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/DataType/Tree/RootDataTypeTreeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/DataType/Tree/RootDataTypeTreeController.cs new file mode 100644 index 0000000000..adae969985 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/DataType/Tree/RootDataTypeTreeController.cs @@ -0,0 +1,24 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.ViewModels.Pagination; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; + +namespace Umbraco.Cms.ManagementApi.Controllers.DataType.Tree; + +public class RootDataTypeTreeController : DataTypeTreeControllerBase +{ + public RootDataTypeTreeController(IEntityService entityService, IDataTypeService dataTypeService) + : base(entityService, dataTypeService) + { + } + + [HttpGet("root")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task>> Root(int skip = 0, int take = 100, bool foldersOnly = false) + { + RenderFoldersOnly(foldersOnly); + return await GetRoot(skip, take); + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Dictionary/AllDictionaryController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Dictionary/AllDictionaryController.cs new file mode 100644 index 0000000000..5de8f6c441 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Dictionary/AllDictionaryController.cs @@ -0,0 +1,69 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.ViewModels.Dictionary; +using Umbraco.Cms.ManagementApi.ViewModels.Pagination; + +namespace Umbraco.Cms.ManagementApi.Controllers.Dictionary; + +public class AllDictionaryController : DictionaryControllerBase +{ + private readonly ILocalizationService _localizationService; + private readonly IUmbracoMapper _umbracoMapper; + + public AllDictionaryController(ILocalizationService localizationService, IUmbracoMapper umbracoMapper) + { + _localizationService = localizationService; + _umbracoMapper = umbracoMapper; + } + + + /// + /// Retrieves a list with all dictionary items + /// + /// + /// The . + /// + [HttpGet] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task> All(int skip, int take) + { + IDictionaryItem[] items = _localizationService.GetDictionaryItemDescendants(null).ToArray(); + var list = new List(items.Length); + + // Build the proper tree structure, as we can have nested dictionary items + BuildTree(list, items); + + var model = new PagedViewModel + { + Total = list.Count, + Items = list.Skip(skip).Take(take), + }; + return await Task.FromResult(model); + } + + // recursive method to build a tree structure from the flat structure returned above + private void BuildTree(List list, IDictionaryItem[] items, int level = 0, Guid? parentId = null) + { + IDictionaryItem[] children = items.Where(t => t.ParentId == parentId).ToArray(); + if (children.Any() == false) + { + return; + } + + foreach (IDictionaryItem child in children.OrderBy(item => item.ItemKey)) + { + DictionaryOverviewViewModel? display = _umbracoMapper.Map(child); + if (display is not null) + { + display.Level = level; + list.Add(display); + } + + BuildTree(list, items, level + 1, child.Key); + } + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Dictionary/ByKeyDictionaryController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Dictionary/ByKeyDictionaryController.cs new file mode 100644 index 0000000000..c5372aee9c --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Dictionary/ByKeyDictionaryController.cs @@ -0,0 +1,47 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.ViewModels.Dictionary; +using Umbraco.New.Cms.Core.Factories; + +namespace Umbraco.Cms.ManagementApi.Controllers.Dictionary; + +public class ByIdDictionaryController : DictionaryControllerBase +{ + private readonly ILocalizationService _localizationService; + private readonly IDictionaryFactory _dictionaryFactory; + + public ByIdDictionaryController( + ILocalizationService localizationService, + IDictionaryFactory dictionaryFactory) + { + _localizationService = localizationService; + _dictionaryFactory = dictionaryFactory; + } + + /// + /// Gets a dictionary item by guid + /// + /// + /// The id. + /// + /// + /// The . Returns a not found response when dictionary item does not exist + /// + [HttpGet("{key:guid}")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(DictionaryViewModel), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(NotFoundResult), StatusCodes.Status404NotFound)] + public async Task> ByKey(Guid key) + { + IDictionaryItem? dictionary = _localizationService.GetDictionaryItemById(key); + if (dictionary == null) + { + return NotFound(); + } + + return await Task.FromResult(_dictionaryFactory.CreateDictionaryViewModel(dictionary)); + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Dictionary/CreateDictionaryController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Dictionary/CreateDictionaryController.cs new file mode 100644 index 0000000000..694f1af903 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Dictionary/CreateDictionaryController.cs @@ -0,0 +1,90 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.ViewModels.Dictionary; +using Umbraco.Extensions; + +namespace Umbraco.Cms.ManagementApi.Controllers.Dictionary; + +public class CreateDictionaryController : DictionaryControllerBase +{ + private readonly ILocalizationService _localizationService; + private readonly ILocalizedTextService _localizedTextService; + private readonly GlobalSettings _globalSettings; + private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor; + private readonly ILogger _logger; + + public CreateDictionaryController( + ILocalizationService localizationService, + ILocalizedTextService localizedTextService, + IOptionsSnapshot globalSettings, + IBackOfficeSecurityAccessor backofficeSecurityAccessor, + ILogger logger) + { + _localizationService = localizationService; + _localizedTextService = localizedTextService; + _globalSettings = globalSettings.Value; + _backofficeSecurityAccessor = backofficeSecurityAccessor; + _logger = logger; + } + + /// + /// Creates a new dictionary item + /// + /// The viewmodel to pass to the action + /// + /// The . + /// + [HttpPost("create")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(CreatedResult), StatusCodes.Status201Created)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + public async Task> Create(DictionaryItemViewModel dictionaryViewModel) + { + if (string.IsNullOrEmpty(dictionaryViewModel.Key.ToString())) + { + return ValidationProblem("Key can not be empty."); // TODO: translate + } + + if (_localizationService.DictionaryItemExists(dictionaryViewModel.Key.ToString())) + { + var message = _localizedTextService.Localize( + "dictionaryItem", + "changeKeyError", + _backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.GetUserCulture(_localizedTextService, _globalSettings), + new Dictionary + { + { "0", dictionaryViewModel.Key.ToString() }, + }); + return await Task.FromResult(ValidationProblem(message)); + } + + try + { + Guid? parentGuid = null; + + if (dictionaryViewModel.ParentId.HasValue) + { + parentGuid = dictionaryViewModel.ParentId; + } + + IDictionaryItem item = _localizationService.CreateDictionaryItemWithIdentity( + dictionaryViewModel.Key.ToString(), + parentGuid, + string.Empty); + + + return await Task.FromResult(Created($"api/v1.0/dictionary/{item.Key}", item.Key)); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error creating dictionary with {Name} under {ParentId}", dictionaryViewModel.Key, dictionaryViewModel.ParentId); + return await Task.FromResult(ValidationProblem("Error creating dictionary item")); + } + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Dictionary/DeleteDictionaryController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Dictionary/DeleteDictionaryController.cs new file mode 100644 index 0000000000..30cf41ed6f --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Dictionary/DeleteDictionaryController.cs @@ -0,0 +1,51 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.ManagementApi.Controllers.Dictionary; + +public class DeleteDictionaryController : DictionaryControllerBase +{ + private readonly ILocalizationService _localizationService; + private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; + + public DeleteDictionaryController(ILocalizationService localizationService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor) + { + _localizationService = localizationService; + _backOfficeSecurityAccessor = backOfficeSecurityAccessor; + } + /// + /// Deletes a data type with a given ID + /// + /// The key of the dictionary item to delete + /// + /// + /// + [HttpDelete("{key}")] + [MapToApiVersion("1.0")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(typeof(NotFoundResult), StatusCodes.Status404NotFound)] + public async Task Delete(Guid key) + { + IDictionaryItem? foundDictionary = _localizationService.GetDictionaryItemByKey(key.ToString()); + + if (foundDictionary == null) + { + return await Task.FromResult(NotFound()); + } + + IEnumerable foundDictionaryDescendants = + _localizationService.GetDictionaryItemDescendants(foundDictionary.Key); + + foreach (IDictionaryItem dictionaryItem in foundDictionaryDescendants) + { + _localizationService.Delete(dictionaryItem, _backOfficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.Id ?? -1); + } + + _localizationService.Delete(foundDictionary, _backOfficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.Id ?? -1); + + return await Task.FromResult(Ok()); + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Dictionary/DictionaryControllerBase.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Dictionary/DictionaryControllerBase.cs new file mode 100644 index 0000000000..015d0a2ffe --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Dictionary/DictionaryControllerBase.cs @@ -0,0 +1,14 @@ +using Microsoft.AspNetCore.Mvc; +using NSwag.Annotations; +using Umbraco.New.Cms.Web.Common.Routing; + +namespace Umbraco.Cms.ManagementApi.Controllers.Dictionary; + +[ApiController] +[BackOfficeRoute("api/v{version:apiVersion}/dictionary")] +[OpenApiTag("Dictionary")] +[ApiVersion("1.0")] +// TODO: Add authentication +public abstract class DictionaryControllerBase : ManagementApiControllerBase +{ +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Dictionary/ExportDictionaryController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Dictionary/ExportDictionaryController.cs new file mode 100644 index 0000000000..f4944220d5 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Dictionary/ExportDictionaryController.cs @@ -0,0 +1,44 @@ +using System.Net.Mime; +using System.Text; +using System.Xml.Linq; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Extensions; + +namespace Umbraco.Cms.ManagementApi.Controllers.Dictionary; + +public class ExportDictionaryController : DictionaryControllerBase +{ + private readonly ILocalizationService _localizationService; + private readonly IEntityXmlSerializer _entityXmlSerializer; + + public ExportDictionaryController(ILocalizationService localizationService, IEntityXmlSerializer entityXmlSerializer) + { + _localizationService = localizationService; + _entityXmlSerializer = entityXmlSerializer; + } + + [HttpGet("export/{key:guid}")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(NotFoundObjectResult), StatusCodes.Status404NotFound)] + public async Task ExportDictionary(Guid key, bool includeChildren = false) + { + IDictionaryItem? dictionaryItem = _localizationService.GetDictionaryItemById(key); + if (dictionaryItem is null) + { + return await Task.FromResult(NotFound("No dictionary item found with id ")); + } + + XElement xml = _entityXmlSerializer.Serialize(dictionaryItem, includeChildren); + + var fileName = $"{dictionaryItem.ItemKey}.udt"; + + // Set custom header so umbRequestHelper.downloadFile can save the correct filename + HttpContext.Response.Headers.Add("x-filename", fileName); + + return await Task.FromResult(File(Encoding.UTF8.GetBytes(xml.ToDataString()), MediaTypeNames.Application.Octet, fileName)); + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Dictionary/ImportDictionaryController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Dictionary/ImportDictionaryController.cs new file mode 100644 index 0000000000..cbf9a0b9c7 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Dictionary/ImportDictionaryController.cs @@ -0,0 +1,54 @@ +using System.Net.Mime; +using System.Text; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.Services; +using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment; + +namespace Umbraco.Cms.ManagementApi.Controllers.Dictionary; + +public class ImportDictionaryController : DictionaryControllerBase +{ + private readonly IHostingEnvironment _hostingEnvironment; + private readonly IDictionaryService _dictionaryService; + private readonly IWebHostEnvironment _webHostEnvironment; + private readonly ILoadDictionaryItemService _loadDictionaryItemService; + + public ImportDictionaryController( + IHostingEnvironment hostingEnvironment, + IDictionaryService dictionaryService, + IWebHostEnvironment webHostEnvironment, + ILoadDictionaryItemService loadDictionaryItemService) + { + _hostingEnvironment = hostingEnvironment; + _dictionaryService = dictionaryService; + _webHostEnvironment = webHostEnvironment; + _loadDictionaryItemService = loadDictionaryItemService; + } + + [HttpPost("import")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(ContentResult), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(NotFoundResult), StatusCodes.Status404NotFound)] + public async Task ImportDictionary(string file, int? parentId) + { + if (string.IsNullOrWhiteSpace(file)) + { + return NotFound(); + } + + var filePath = Path.Combine(_hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Data), file); + if (_webHostEnvironment.ContentRootFileProvider.GetFileInfo(filePath) is null) + { + return await Task.FromResult(NotFound()); + } + + IDictionaryItem dictionaryItem = _loadDictionaryItemService.Load(filePath, parentId); + + return await Task.FromResult(Content(_dictionaryService.CalculatePath(dictionaryItem.ParentId, dictionaryItem.Id), MediaTypeNames.Text.Plain, Encoding.UTF8)); + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Dictionary/UpdateDictionaryController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Dictionary/UpdateDictionaryController.cs new file mode 100644 index 0000000000..be2e50460f --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Dictionary/UpdateDictionaryController.cs @@ -0,0 +1,75 @@ +using System.Net.Mime; +using System.Text; +using System.Text.Json; +using Json.Patch; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.Serialization; +using Umbraco.Cms.ManagementApi.Services; +using Umbraco.Cms.ManagementApi.ViewModels.Dictionary; +using Umbraco.Cms.ManagementApi.ViewModels.JsonPatch; +using Umbraco.New.Cms.Core.Factories; + +namespace Umbraco.Cms.ManagementApi.Controllers.Dictionary; + +public class UpdateDictionaryController : DictionaryControllerBase +{ + private readonly ILocalizationService _localizationService; + private readonly IUmbracoMapper _umbracoMapper; + private readonly IDictionaryService _dictionaryService; + private readonly IDictionaryFactory _dictionaryFactory; + private readonly IJsonPatchService _jsonPatchService; + private readonly ISystemTextJsonSerializer _systemTextJsonSerializer; + + public UpdateDictionaryController( + ILocalizationService localizationService, + IUmbracoMapper umbracoMapper, + IDictionaryService dictionaryService, + IDictionaryFactory dictionaryFactory, + IJsonPatchService jsonPatchService, + ISystemTextJsonSerializer systemTextJsonSerializer) + { + _localizationService = localizationService; + _umbracoMapper = umbracoMapper; + _dictionaryService = dictionaryService; + _dictionaryFactory = dictionaryFactory; + _jsonPatchService = jsonPatchService; + _systemTextJsonSerializer = systemTextJsonSerializer; + } + + [HttpPatch("{id:Guid}")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(ContentResult), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(NotFoundResult), StatusCodes.Status404NotFound)] + public async Task Update(Guid id, JsonPatchViewModel[] updateViewModel) + { + IDictionaryItem? dictionaryItem = _localizationService.GetDictionaryItemById(id); + + if (dictionaryItem is null) + { + return NotFound(); + } + + DictionaryViewModel dictionaryToPatch = _umbracoMapper.Map(dictionaryItem)!; + + PatchResult? result = _jsonPatchService.Patch(updateViewModel, dictionaryToPatch); + + if (result?.Result is null) + { + throw new JsonException("Could not patch the JsonPatchViewModel"); + } + + DictionaryViewModel? updatedDictionaryItem = _systemTextJsonSerializer.Deserialize(result.Result.ToJsonString()); + if (updatedDictionaryItem is null) + { + throw new JsonException("Could not serialize from PatchResult to DictionaryViewModel"); + } + + IDictionaryItem dictionaryToSave = _dictionaryFactory.CreateDictionaryItem(updatedDictionaryItem!); + _localizationService.Save(dictionaryToSave); + return await Task.FromResult(Content(_dictionaryService.CalculatePath(dictionaryToSave.ParentId, dictionaryToSave.Id), MediaTypeNames.Text.Plain, Encoding.UTF8)); + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Dictionary/UploadDictionaryController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Dictionary/UploadDictionaryController.cs new file mode 100644 index 0000000000..f14af830f5 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Dictionary/UploadDictionaryController.cs @@ -0,0 +1,51 @@ +using System.Xml; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.Models; +using Umbraco.Cms.ManagementApi.Services; +using Umbraco.Cms.ManagementApi.ViewModels.Dictionary; +using Umbraco.Extensions; +using Umbraco.New.Cms.Core.Factories; + +namespace Umbraco.Cms.ManagementApi.Controllers.Dictionary; + +public class UploadDictionaryController : DictionaryControllerBase +{ + private readonly ILocalizedTextService _localizedTextService; + private readonly IUploadFileService _uploadFileService; + private readonly IDictionaryFactory _dictionaryFactory; + + public UploadDictionaryController(ILocalizedTextService localizedTextService, IUploadFileService uploadFileService, IDictionaryFactory dictionaryFactory) + { + _localizedTextService = localizedTextService; + _uploadFileService = uploadFileService; + _dictionaryFactory = dictionaryFactory; + } + + [HttpPost("upload")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(DictionaryImportViewModel), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + public async Task> Upload(IFormFile file) + { + FormFileUploadResult formFileUploadResult = _uploadFileService.TryLoad(file); + if (formFileUploadResult.CouldLoad is false || formFileUploadResult.XmlDocument is null) + { + return await Task.FromResult(ValidationProblem( + _localizedTextService.Localize("media", "failedFileUpload"), + formFileUploadResult.ErrorMessage)); + } + + DictionaryImportViewModel model = _dictionaryFactory.CreateDictionaryImportViewModel(formFileUploadResult); + + if (!model.DictionaryItems.Any()) + { + return ValidationProblem( + _localizedTextService.Localize("media", "failedFileUpload"), + _localizedTextService.Localize("dictionary", "noItemsInFile")); + } + + return await Task.FromResult(model); + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/DictionaryItem/Tree/ChildrenDictionaryItemTreeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/DictionaryItem/Tree/ChildrenDictionaryItemTreeController.cs new file mode 100644 index 0000000000..0969b808c9 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/DictionaryItem/Tree/ChildrenDictionaryItemTreeController.cs @@ -0,0 +1,39 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.Services.Paging; +using Umbraco.Cms.ManagementApi.ViewModels.Pagination; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; + +namespace Umbraco.Cms.ManagementApi.Controllers.DictionaryItem.Tree; + +public class ChildrenDictionaryItemTreeController : DictionaryItemTreeControllerBase +{ + public ChildrenDictionaryItemTreeController(IEntityService entityService, ILocalizationService localizationService) + : base(entityService, localizationService) + { + } + + [HttpGet("children")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task>> Children(Guid parentKey, int skip = 0, int take = 100) + { + if (PaginationService.ConvertSkipTakeToPaging(skip, take, out var pageNumber, out var pageSize, out ProblemDetails? error) == false) + { + return BadRequest(error); + } + + IDictionaryItem[] dictionaryItems = PaginatedDictionaryItems( + pageNumber, + pageSize, + LocalizationService.GetDictionaryItemChildren(parentKey), + out var totalItems); + + EntityTreeItemViewModel[] viewModels = MapTreeItemViewModels(null, dictionaryItems); + + PagedViewModel result = PagedViewModel(viewModels, totalItems); + return await Task.FromResult(Ok(result)); + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/DictionaryItem/Tree/DictionaryItemTreeControllerBase.cs b/src/Umbraco.Cms.ManagementApi/Controllers/DictionaryItem/Tree/DictionaryItemTreeControllerBase.cs new file mode 100644 index 0000000000..0d6a513e2d --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/DictionaryItem/Tree/DictionaryItemTreeControllerBase.cs @@ -0,0 +1,53 @@ +using Microsoft.AspNetCore.Mvc; +using NSwag.Annotations; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.Controllers.Tree; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; +using Umbraco.New.Cms.Web.Common.Routing; + +namespace Umbraco.Cms.ManagementApi.Controllers.DictionaryItem.Tree; + +[ApiVersion("1.0")] +[ApiController] +[VersionedApiBackOfficeRoute($"{Constants.UdiEntityType.DictionaryItem}/tree")] +[OpenApiTag(nameof(Constants.UdiEntityType.DictionaryItem))] +// NOTE: at the moment dictionary items aren't supported by EntityService, so we have little use of the +// tree controller base. We'll keep it though, in the hope that we can mend EntityService. +public class DictionaryItemTreeControllerBase : EntityTreeControllerBase +{ + public DictionaryItemTreeControllerBase(IEntityService entityService, ILocalizationService localizationService) + : base(entityService) => + LocalizationService = localizationService; + + // dictionary items do not currently have a known UmbracoObjectType, so we'll settle with Unknown for now + protected override UmbracoObjectTypes ItemObjectType => UmbracoObjectTypes.Unknown; + + protected ILocalizationService LocalizationService { get; } + + protected EntityTreeItemViewModel[] MapTreeItemViewModels(Guid? parentKey, IDictionaryItem[] dictionaryItems) + => dictionaryItems.Select(dictionaryItem => new EntityTreeItemViewModel + { + Icon = Constants.Icons.RelationType, + Name = dictionaryItem.ItemKey, + Key = dictionaryItem.Key, + Type = Constants.UdiEntityType.DictionaryItem, + HasChildren = false, + IsContainer = LocalizationService.GetDictionaryItemChildren(dictionaryItem.Key).Any(), + ParentKey = parentKey + }).ToArray(); + + // localization service does not (yet) allow pagination of dictionary items, we have to do it in memory for now + protected IDictionaryItem[] PaginatedDictionaryItems(long pageNumber, int pageSize, IEnumerable allDictionaryItems, out long totalItems) + { + IDictionaryItem[] allDictionaryItemsAsArray = allDictionaryItems.ToArray(); + + totalItems = allDictionaryItemsAsArray.Length; + return allDictionaryItemsAsArray + .OrderBy(item => item.ItemKey) + .Skip((int)pageNumber * pageSize) + .Take(pageSize) + .ToArray(); + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/DictionaryItem/Tree/ItemsDictionaryItemTreeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/DictionaryItem/Tree/ItemsDictionaryItemTreeController.cs new file mode 100644 index 0000000000..69d2fda33d --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/DictionaryItem/Tree/ItemsDictionaryItemTreeController.cs @@ -0,0 +1,27 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; + +namespace Umbraco.Cms.ManagementApi.Controllers.DictionaryItem.Tree; + +public class ItemsDictionaryItemTreeController : DictionaryItemTreeControllerBase +{ + public ItemsDictionaryItemTreeController(IEntityService entityService, ILocalizationService localizationService) + : base(entityService, localizationService) + { + } + + [HttpGet("items")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + public async Task>> Items([FromQuery(Name = "key")] Guid[] keys) + { + IDictionaryItem[] dictionaryItems = LocalizationService.GetDictionaryItemsByIds(keys).ToArray(); + + EntityTreeItemViewModel[] viewModels = MapTreeItemViewModels(null, dictionaryItems); + + return await Task.FromResult(Ok(viewModels)); + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/DictionaryItem/Tree/RootDictionaryItemTreeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/DictionaryItem/Tree/RootDictionaryItemTreeController.cs new file mode 100644 index 0000000000..18abf7a728 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/DictionaryItem/Tree/RootDictionaryItemTreeController.cs @@ -0,0 +1,39 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.Services.Paging; +using Umbraco.Cms.ManagementApi.ViewModels.Pagination; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; + +namespace Umbraco.Cms.ManagementApi.Controllers.DictionaryItem.Tree; + +public class RootDictionaryItemTreeController : DictionaryItemTreeControllerBase +{ + public RootDictionaryItemTreeController(IEntityService entityService, ILocalizationService localizationService) + : base(entityService, localizationService) + { + } + + [HttpGet("root")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task>> Root(int skip = 0, int take = 100) + { + if (PaginationService.ConvertSkipTakeToPaging(skip, take, out var pageNumber, out var pageSize, out ProblemDetails? error) == false) + { + return BadRequest(error); + } + + IDictionaryItem[] dictionaryItems = PaginatedDictionaryItems( + pageNumber, + pageSize, + LocalizationService.GetRootDictionaryItems(), + out var totalItems); + + EntityTreeItemViewModel[] viewModels = MapTreeItemViewModels(null, dictionaryItems); + + PagedViewModel result = PagedViewModel(viewModels, totalItems); + return await Task.FromResult(Ok(result)); + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Document/RecycleBin/ChildrenDocumentRecycleBinController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Document/RecycleBin/ChildrenDocumentRecycleBinController.cs new file mode 100644 index 0000000000..3127382588 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Document/RecycleBin/ChildrenDocumentRecycleBinController.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.ViewModels.Pagination; +using Umbraco.Cms.ManagementApi.ViewModels.RecycleBin; + +namespace Umbraco.Cms.ManagementApi.Controllers.Document.RecycleBin; + +public class ChildrenDocumentRecycleBinController : DocumentRecycleBinControllerBase +{ + public ChildrenDocumentRecycleBinController(IEntityService entityService) + : base(entityService) + { + } + + [HttpGet("children")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task>> Children(Guid parentKey, int skip = 0, int take = 100) + => await GetChildren(parentKey, skip, take); +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Document/RecycleBin/DocumentRecycleBinControllerBase.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Document/RecycleBin/DocumentRecycleBinControllerBase.cs new file mode 100644 index 0000000000..747b6b3296 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Document/RecycleBin/DocumentRecycleBinControllerBase.cs @@ -0,0 +1,43 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using NSwag.Annotations; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.Controllers.RecycleBin; +using Umbraco.Cms.ManagementApi.Filters; +using Umbraco.Cms.ManagementApi.ViewModels.RecycleBin; +using Umbraco.New.Cms.Web.Common.Routing; + +namespace Umbraco.Cms.ManagementApi.Controllers.Document.RecycleBin; + +[ApiVersion("1.0")] +[ApiController] +[VersionedApiBackOfficeRoute($"{Constants.UdiEntityType.Document}/recycle-bin")] +[RequireDocumentTreeRootAccess] +[ProducesResponseType(StatusCodes.Status401Unauthorized)] +[OpenApiTag(nameof(Constants.UdiEntityType.Document))] +public class DocumentRecycleBinControllerBase : RecycleBinControllerBase +{ + public DocumentRecycleBinControllerBase(IEntityService entityService) + : base(entityService) + { + } + + protected override UmbracoObjectTypes ItemObjectType => UmbracoObjectTypes.Document; + + protected override int RecycleBinRootId => Constants.System.RecycleBinContent; + + protected override RecycleBinItemViewModel MapRecycleBinViewModel(Guid? parentKey, IEntitySlim entity) + { + RecycleBinItemViewModel viewModel = base.MapRecycleBinViewModel(parentKey, entity); + + if (entity is IDocumentEntitySlim documentEntitySlim) + { + viewModel.Icon = documentEntitySlim.ContentTypeIcon ?? viewModel.Icon; + } + + return viewModel; + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Document/RecycleBin/RootDocumentRecycleBinController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Document/RecycleBin/RootDocumentRecycleBinController.cs new file mode 100644 index 0000000000..21b79eb745 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Document/RecycleBin/RootDocumentRecycleBinController.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.ViewModels.Pagination; +using Umbraco.Cms.ManagementApi.ViewModels.RecycleBin; + +namespace Umbraco.Cms.ManagementApi.Controllers.Document.RecycleBin; + +public class RootDocumentRecycleBinController : DocumentRecycleBinControllerBase +{ + public RootDocumentRecycleBinController(IEntityService entityService) + : base(entityService) + { + } + + [HttpGet("root")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task>> Root(int skip = 0, int take = 100) + => await GetRoot(skip, take); +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Document/Tree/ChildrenDocumentTreeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Document/Tree/ChildrenDocumentTreeController.cs new file mode 100644 index 0000000000..1a72e9a41f --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Document/Tree/ChildrenDocumentTreeController.cs @@ -0,0 +1,34 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.Services.Entities; +using Umbraco.Cms.ManagementApi.ViewModels.Pagination; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; + +namespace Umbraco.Cms.ManagementApi.Controllers.Document.Tree; + +public class ChildrenDocumentTreeController : DocumentTreeControllerBase +{ + public ChildrenDocumentTreeController( + IEntityService entityService, + IUserStartNodeEntitiesService userStartNodeEntitiesService, + IDataTypeService dataTypeService, + IPublicAccessService publicAccessService, + AppCaches appCaches, + IBackOfficeSecurityAccessor backofficeSecurityAccessor) + : base(entityService, userStartNodeEntitiesService, dataTypeService, publicAccessService, appCaches, backofficeSecurityAccessor) + { + } + + [HttpGet("children")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task>> Children(Guid parentKey, int skip = 0, int take = 100, Guid? dataTypeKey = null, string? culture = null) + { + IgnoreUserStartNodesForDataType(dataTypeKey); + RenderForClientCulture(culture); + return await GetChildren(parentKey, skip, take); + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Document/Tree/DocumentTreeControllerBase.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Document/Tree/DocumentTreeControllerBase.cs new file mode 100644 index 0000000000..a20237537d --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Document/Tree/DocumentTreeControllerBase.cs @@ -0,0 +1,94 @@ +using Microsoft.AspNetCore.Mvc; +using NSwag.Annotations; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.Controllers.Tree; +using Umbraco.Cms.ManagementApi.Services.Entities; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; +using Umbraco.Extensions; +using Umbraco.New.Cms.Web.Common.Routing; + +namespace Umbraco.Cms.ManagementApi.Controllers.Document.Tree; + +[ApiVersion("1.0")] +[ApiController] +[VersionedApiBackOfficeRoute($"{Constants.UdiEntityType.Document}/tree")] +[OpenApiTag(nameof(Constants.UdiEntityType.Document))] +public abstract class DocumentTreeControllerBase : UserStartNodeTreeControllerBase +{ + private readonly IPublicAccessService _publicAccessService; + private readonly AppCaches _appCaches; + private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor; + private string? _culture; + + protected DocumentTreeControllerBase( + IEntityService entityService, + IUserStartNodeEntitiesService userStartNodeEntitiesService, + IDataTypeService dataTypeService, + IPublicAccessService publicAccessService, + AppCaches appCaches, + IBackOfficeSecurityAccessor backofficeSecurityAccessor) + : base(entityService, userStartNodeEntitiesService, dataTypeService) + { + _publicAccessService = publicAccessService; + _appCaches = appCaches; + _backofficeSecurityAccessor = backofficeSecurityAccessor; + } + + protected override UmbracoObjectTypes ItemObjectType => UmbracoObjectTypes.Document; + + protected override Ordering ItemOrdering => Ordering.By(nameof(Infrastructure.Persistence.Dtos.NodeDto.SortOrder)); + + protected void RenderForClientCulture(string? culture) => _culture = culture; + + protected override DocumentTreeItemViewModel MapTreeItemViewModel(Guid? parentKey, IEntitySlim entity) + { + DocumentTreeItemViewModel viewModel = base.MapTreeItemViewModel(parentKey, entity); + + if (entity is IDocumentEntitySlim documentEntitySlim) + { + viewModel.IsPublished = documentEntitySlim.Published; + viewModel.IsEdited = documentEntitySlim.Edited; + viewModel.Icon = documentEntitySlim.ContentTypeIcon ?? viewModel.Icon; + viewModel.IsProtected = _publicAccessService.IsProtected(entity.Path); + + if (_culture != null && documentEntitySlim.Variations.VariesByCulture()) + { + viewModel.Name = documentEntitySlim.CultureNames.TryGetValue(_culture, out var cultureName) + ? cultureName + : $"({viewModel.Name})"; + + viewModel.IsPublished = documentEntitySlim.PublishedCultures.Contains(_culture); + viewModel.IsEdited = documentEntitySlim.EditedCultures.Contains(_culture); + } + + viewModel.IsEdited &= viewModel.IsPublished; + } + + return viewModel; + } + + // TODO: delete these (faking start node setup for unlimited editor) + protected override int[] GetUserStartNodeIds() => new[] { -1 }; + + protected override string[] GetUserStartNodePaths() => Array.Empty(); + + // TODO: use these implementations instead of the dummy ones above once we have backoffice auth in place + // protected override int[] GetUserStartNodeIds() + // => _backofficeSecurityAccessor + // .BackOfficeSecurity? + // .CurrentUser? + // .CalculateContentStartNodeIds(EntityService, _appCaches) + // ?? Array.Empty(); + // + // protected override string[] GetUserStartNodePaths() + // => _backofficeSecurityAccessor + // .BackOfficeSecurity? + // .CurrentUser? + // .GetContentStartNodePaths(EntityService, _appCaches) + // ?? Array.Empty(); +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Document/Tree/ItemsDocumentTreeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Document/Tree/ItemsDocumentTreeController.cs new file mode 100644 index 0000000000..a18dfea069 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Document/Tree/ItemsDocumentTreeController.cs @@ -0,0 +1,33 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.Services.Entities; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; + +namespace Umbraco.Cms.ManagementApi.Controllers.Document.Tree; + +public class ItemsDocumentTreeController : DocumentTreeControllerBase +{ + public ItemsDocumentTreeController( + IEntityService entityService, + IUserStartNodeEntitiesService userStartNodeEntitiesService, + IDataTypeService dataTypeService, + IPublicAccessService publicAccessService, + AppCaches appCaches, + IBackOfficeSecurityAccessor backofficeSecurityAccessor) + : base(entityService, userStartNodeEntitiesService, dataTypeService, publicAccessService, appCaches, backofficeSecurityAccessor) + { + } + + [HttpGet("items")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + public async Task>> Items([FromQuery(Name = "key")] Guid[] keys, Guid? dataTypeKey = null, string? culture = null) + { + IgnoreUserStartNodesForDataType(dataTypeKey); + RenderForClientCulture(culture); + return await GetItems(keys); + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Document/Tree/RootDocumentTreeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Document/Tree/RootDocumentTreeController.cs new file mode 100644 index 0000000000..1091292162 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Document/Tree/RootDocumentTreeController.cs @@ -0,0 +1,34 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.Services.Entities; +using Umbraco.Cms.ManagementApi.ViewModels.Pagination; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; + +namespace Umbraco.Cms.ManagementApi.Controllers.Document.Tree; + +public class RootDocumentTreeController : DocumentTreeControllerBase +{ + public RootDocumentTreeController( + IEntityService entityService, + IUserStartNodeEntitiesService userStartNodeEntitiesService, + IDataTypeService dataTypeService, + IPublicAccessService publicAccessService, + AppCaches appCaches, + IBackOfficeSecurityAccessor backofficeSecurityAccessor) + : base(entityService, userStartNodeEntitiesService, dataTypeService, publicAccessService, appCaches, backofficeSecurityAccessor) + { + } + + [HttpGet("root")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task>> Root(int skip = 0, int take = 100, Guid? dataTypeKey = null, string? culture = null) + { + IgnoreUserStartNodesForDataType(dataTypeKey); + RenderForClientCulture(culture); + return await GetRoot(skip, take); + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/DocumentBlueprint/Tree/DocumentBlueprintTreeControllerBase.cs b/src/Umbraco.Cms.ManagementApi/Controllers/DocumentBlueprint/Tree/DocumentBlueprintTreeControllerBase.cs new file mode 100644 index 0000000000..c6247da3a9 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/DocumentBlueprint/Tree/DocumentBlueprintTreeControllerBase.cs @@ -0,0 +1,56 @@ +using Microsoft.AspNetCore.Mvc; +using NSwag.Annotations; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.Controllers.Tree; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; +using Umbraco.New.Cms.Web.Common.Routing; + +namespace Umbraco.Cms.ManagementApi.Controllers.DocumentBlueprint.Tree; + +[ApiVersion("1.0")] +[ApiController] +[VersionedApiBackOfficeRoute($"{Constants.UdiEntityType.DocumentBlueprint}/tree")] +[OpenApiTag(nameof(Constants.UdiEntityType.DocumentBlueprint))] +public class DocumentBlueprintTreeControllerBase : EntityTreeControllerBase +{ + private readonly IContentTypeService _contentTypeService; + + public DocumentBlueprintTreeControllerBase(IEntityService entityService, IContentTypeService contentTypeService) + : base(entityService) => + _contentTypeService = contentTypeService; + + protected override UmbracoObjectTypes ItemObjectType => UmbracoObjectTypes.DocumentBlueprint; + + protected override DocumentBlueprintTreeItemViewModel[] MapTreeItemViewModels(Guid? parentKey, IEntitySlim[] entities) + { + var contentTypeAliases = entities + .OfType() + .Select(entity => entity.ContentTypeAlias) + .ToArray(); + + var contentTypeIds = _contentTypeService.GetAllContentTypeIds(contentTypeAliases).ToArray(); + var contentTypeByAlias = _contentTypeService + .GetAll(contentTypeIds) + .ToDictionary(contentType => contentType.Alias); + + return entities.Select(entity => + { + DocumentBlueprintTreeItemViewModel viewModel = base.MapTreeItemViewModel(parentKey, entity); + viewModel.Icon = Constants.Icons.Blueprint; + viewModel.HasChildren = false; + + if (entity is IDocumentEntitySlim documentEntitySlim + && contentTypeByAlias.TryGetValue(documentEntitySlim.ContentTypeAlias, out IContentType? contentType)) + { + viewModel.DocumentTypeKey = contentType.Key; + viewModel.DocumentTypeAlias = contentType.Alias; + viewModel.DocumentTypeName = contentType.Name; + } + + return viewModel; + }).ToArray(); + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/DocumentBlueprint/Tree/ItemsDocumentBlueprintTreeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/DocumentBlueprint/Tree/ItemsDocumentBlueprintTreeController.cs new file mode 100644 index 0000000000..6b7edb6fab --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/DocumentBlueprint/Tree/ItemsDocumentBlueprintTreeController.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.ViewModels.Pagination; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; + +namespace Umbraco.Cms.ManagementApi.Controllers.DocumentBlueprint.Tree; + +public class ItemsDocumentBlueprintTreeController : DocumentBlueprintTreeControllerBase +{ + public ItemsDocumentBlueprintTreeController(IEntityService entityService, IContentTypeService contentTypeService) + : base(entityService, contentTypeService) + { + } + + [HttpGet("items")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + public async Task>> Items([FromQuery(Name = "key")] Guid[] keys) + => await GetItems(keys); +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/DocumentBlueprint/Tree/RootDocumentBlueprintTreeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/DocumentBlueprint/Tree/RootDocumentBlueprintTreeController.cs new file mode 100644 index 0000000000..b9dd33d3bf --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/DocumentBlueprint/Tree/RootDocumentBlueprintTreeController.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.ViewModels.Pagination; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; + +namespace Umbraco.Cms.ManagementApi.Controllers.DocumentBlueprint.Tree; + +public class RootDocumentBlueprintTreeController : DocumentBlueprintTreeControllerBase +{ + public RootDocumentBlueprintTreeController(IEntityService entityService, IContentTypeService contentTypeService) + : base(entityService, contentTypeService) + { + } + + [HttpGet("root")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task>> Root(int skip = 0, int take = 100) + => await GetRoot(skip, take); +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/DocumentType/Tree/ChildrenDocumentTypeTreeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/DocumentType/Tree/ChildrenDocumentTypeTreeController.cs new file mode 100644 index 0000000000..b6deb6e3f6 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/DocumentType/Tree/ChildrenDocumentTypeTreeController.cs @@ -0,0 +1,24 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.ViewModels.Pagination; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; + +namespace Umbraco.Cms.ManagementApi.Controllers.DocumentType.Tree; + +public class ChildrenDocumentTypeTreeController : DocumentTypeTreeControllerBase +{ + public ChildrenDocumentTypeTreeController(IEntityService entityService, IContentTypeService contentTypeService) + : base(entityService, contentTypeService) + { + } + + [HttpGet("children")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task>> Children(Guid parentKey, int skip = 0, int take = 100, bool foldersOnly = false) + { + RenderFoldersOnly(foldersOnly); + return await GetChildren(parentKey, skip, take); + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/DocumentType/Tree/DocumentTypeTreeControllerBase.cs b/src/Umbraco.Cms.ManagementApi/Controllers/DocumentType/Tree/DocumentTypeTreeControllerBase.cs new file mode 100644 index 0000000000..cc4c224ef5 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/DocumentType/Tree/DocumentTypeTreeControllerBase.cs @@ -0,0 +1,47 @@ +using Microsoft.AspNetCore.Mvc; +using NSwag.Annotations; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.Controllers.Tree; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; +using Umbraco.New.Cms.Web.Common.Routing; + +namespace Umbraco.Cms.ManagementApi.Controllers.DocumentType.Tree; + +[ApiVersion("1.0")] +[ApiController] +[VersionedApiBackOfficeRoute($"{Constants.UdiEntityType.DocumentType}/tree")] +[OpenApiTag(nameof(Constants.UdiEntityType.DocumentType))] +public class DocumentTypeTreeControllerBase : FolderTreeControllerBase +{ + private readonly IContentTypeService _contentTypeService; + + public DocumentTypeTreeControllerBase(IEntityService entityService, IContentTypeService contentTypeService) + : base(entityService) => + _contentTypeService = contentTypeService; + + protected override UmbracoObjectTypes ItemObjectType => UmbracoObjectTypes.DocumentType; + + protected override UmbracoObjectTypes FolderObjectType => UmbracoObjectTypes.DocumentTypeContainer; + + protected override DocumentTypeTreeItemViewModel[] MapTreeItemViewModels(Guid? parentKey, IEntitySlim[] entities) + { + var contentTypes = _contentTypeService + .GetAll(entities.Select(entity => entity.Id).ToArray()) + .ToDictionary(contentType => contentType.Id); + + return entities.Select(entity => + { + DocumentTypeTreeItemViewModel viewModel = MapTreeItemViewModel(parentKey, entity); + if (contentTypes.TryGetValue(entity.Id, out IContentType? contentType)) + { + viewModel.Icon = contentType.Icon ?? viewModel.Icon; + viewModel.IsElement = contentType.IsElement; + } + + return viewModel; + }).ToArray(); + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/DocumentType/Tree/ItemsDocumentTypeTreeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/DocumentType/Tree/ItemsDocumentTypeTreeController.cs new file mode 100644 index 0000000000..e19bf249c6 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/DocumentType/Tree/ItemsDocumentTypeTreeController.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; + +namespace Umbraco.Cms.ManagementApi.Controllers.DocumentType.Tree; + +public class ItemsDocumentTypeTreeController : DocumentTypeTreeControllerBase +{ + public ItemsDocumentTypeTreeController(IEntityService entityService, IContentTypeService contentTypeService) + : base(entityService, contentTypeService) + { + } + + [HttpGet("items")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + public async Task>> Items([FromQuery(Name = "key")] Guid[] keys) + => await GetItems(keys); +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/DocumentType/Tree/RootDocumentTypeTreeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/DocumentType/Tree/RootDocumentTypeTreeController.cs new file mode 100644 index 0000000000..9ffbfb6bf1 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/DocumentType/Tree/RootDocumentTypeTreeController.cs @@ -0,0 +1,24 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.ViewModels.Pagination; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; + +namespace Umbraco.Cms.ManagementApi.Controllers.DocumentType.Tree; + +public class RootDocumentTypeTreeController : DocumentTypeTreeControllerBase +{ + public RootDocumentTypeTreeController(IEntityService entityService, IContentTypeService contentTypeService) + : base(entityService, contentTypeService) + { + } + + [HttpGet("root")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task>> Root(int skip = 0, int take = 100, bool foldersOnly = false) + { + RenderFoldersOnly(foldersOnly); + return await GetRoot(skip, take); + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/ExamineManagement/ExamineManagementControllerBase.cs b/src/Umbraco.Cms.ManagementApi/Controllers/ExamineManagement/ExamineManagementControllerBase.cs new file mode 100644 index 0000000000..6431e35403 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/ExamineManagement/ExamineManagementControllerBase.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Mvc; +using NSwag.Annotations; +using Umbraco.New.Cms.Web.Common.Routing; + +namespace Umbraco.Cms.ManagementApi.Controllers.ExamineManagement; + +[ApiController] +[BackOfficeRoute("api/v{version:apiVersion}/examineManagement")] +[OpenApiTag("ExamineManagement")] +public class ExamineManagementControllerBase : ManagementApiControllerBase +{ +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/ExamineManagement/IndexExamineManagementController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/ExamineManagement/IndexExamineManagementController.cs new file mode 100644 index 0000000000..13fb27b318 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/ExamineManagement/IndexExamineManagementController.cs @@ -0,0 +1,54 @@ +using Examine; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.ManagementApi.Factories; +using Umbraco.Cms.ManagementApi.ViewModels.ExamineManagement; + +namespace Umbraco.Cms.ManagementApi.Controllers.ExamineManagement; + +[ApiVersion("1.0")] +public class IndexExamineManagementController : ExamineManagementControllerBase +{ + private readonly IExamineIndexViewModelFactory _examineIndexViewModelFactory; + private readonly IExamineManager _examineManager; + + public IndexExamineManagementController( + IExamineIndexViewModelFactory examineIndexViewModelFactory, + IExamineManager examineManager) + { + _examineIndexViewModelFactory = examineIndexViewModelFactory; + _examineManager = examineManager; + } + + /// + /// Check if the index has been rebuilt + /// + /// + /// + /// + /// This is kind of rudimentary since there's no way we can know that the index has rebuilt, we + /// have a listener for the index op complete so we'll just check if that key is no longer there in the runtime cache + /// + [HttpGet("index")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ExamineIndexViewModel), StatusCodes.Status200OK)] + public async Task> Index(string indexName) + { + if (_examineManager.TryGetIndex(indexName, out IIndex? index)) + { + return await Task.FromResult(_examineIndexViewModelFactory.Create(index!)); + } + + var invalidModelProblem = new ProblemDetails + { + Title = "Index Not Found", + Detail = $"No index found with name = {indexName}", + Status = StatusCodes.Status400BadRequest, + Type = "Error", + }; + + return await Task.FromResult(BadRequest(invalidModelProblem)); + + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/ExamineManagement/IndexesExamineManagementController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/ExamineManagement/IndexesExamineManagementController.cs new file mode 100644 index 0000000000..d94ce3759b --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/ExamineManagement/IndexesExamineManagementController.cs @@ -0,0 +1,42 @@ +using Examine; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Infrastructure.Examine; +using Umbraco.Cms.ManagementApi.Factories; +using Umbraco.Cms.ManagementApi.ViewModels.ExamineManagement; +using Umbraco.Cms.ManagementApi.ViewModels.Pagination; +using Umbraco.Extensions; + +namespace Umbraco.Cms.ManagementApi.Controllers.ExamineManagement; + +[ApiVersion("1.0")] +public class IndexesExamineManagementController : ExamineManagementControllerBase +{ + private readonly IExamineManager _examineManager; + private readonly IExamineIndexViewModelFactory _examineIndexViewModelFactory; + + public IndexesExamineManagementController( + IExamineManager examineManager, + IExamineIndexViewModelFactory examineIndexViewModelFactory) + { + _examineManager = examineManager; + _examineIndexViewModelFactory = examineIndexViewModelFactory; + } + + /// + /// Get the details for indexers + /// + /// + [HttpGet("indexes")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task> Indexes(int skip, int take) + { + ExamineIndexViewModel[] indexes = _examineManager.Indexes + .Select(_examineIndexViewModelFactory.Create) + .OrderBy(examineIndexModel => examineIndexModel.Name?.TrimEnd("Indexer")).ToArray(); + + var viewModel = new PagedViewModel { Items = indexes.Skip(skip).Take(take), Total = indexes.Length }; + return viewModel; + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/ExamineManagement/RebuildExamineManagementController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/ExamineManagement/RebuildExamineManagementController.cs new file mode 100644 index 0000000000..586994e908 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/ExamineManagement/RebuildExamineManagementController.cs @@ -0,0 +1,82 @@ +using Examine; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Umbraco.Cms.Infrastructure.Examine; +using Umbraco.New.Cms.Infrastructure.Services; + +namespace Umbraco.Cms.ManagementApi.Controllers.ExamineManagement; + +[ApiVersion("1.0")] +public class RebuildExamineManagementController : ExamineManagementControllerBase +{ + private readonly ILogger _logger; + private readonly IIndexingRebuilderService _indexingRebuilderService; + private readonly IExamineManager _examineManager; + + public RebuildExamineManagementController( + ILogger logger, + IIndexingRebuilderService indexingRebuilderService, + IExamineManager examineManager) + { + _logger = logger; + _indexingRebuilderService = indexingRebuilderService; + _examineManager = examineManager; + } + + /// + /// Rebuilds the index + /// + /// + /// + [HttpPost("rebuild")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(OkResult), StatusCodes.Status200OK)] + public async Task Rebuild(string indexName) + { + if (!_examineManager.TryGetIndex(indexName, out var index)) + { + var invalidModelProblem = new ProblemDetails + { + Title = "Index Not Found", + Detail = $"No index found with name = {indexName}", + Status = StatusCodes.Status400BadRequest, + Type = "Error", + }; + + return await Task.FromResult(BadRequest(invalidModelProblem)); + } + + if (!_indexingRebuilderService.CanRebuild(index.Name)) + { + var invalidModelProblem = new ProblemDetails + { + Title = "Could not validate the populator", + Detail = + $"The index {index?.Name} could not be rebuilt because we could not validate its associated {typeof(IIndexPopulator)}", + Status = StatusCodes.Status400BadRequest, + Type = "Error", + }; + + return await Task.FromResult(BadRequest(invalidModelProblem)); + } + + _logger.LogInformation("Rebuilding index '{IndexName}'", indexName); + + if (_indexingRebuilderService.TryRebuild(index, indexName)) + { + return await Task.FromResult(Ok()); + } + + var problemDetails = new ProblemDetails + { + Title = "Index could not be rebuilt", + Detail = $"The index {index.Name} could not be rebuild. Check the log for details on this error.", + Status = StatusCodes.Status400BadRequest, + Type = "Error", + }; + + return await Task.FromResult(Conflict(problemDetails)); + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/ExamineManagement/SearchExamineManagementController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/ExamineManagement/SearchExamineManagementController.cs new file mode 100644 index 0000000000..3d5426ca0c --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/ExamineManagement/SearchExamineManagementController.cs @@ -0,0 +1,80 @@ +using Examine; +using Examine.Search; +using Lucene.Net.QueryParsers.Classic; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.ManagementApi.Services; +using Umbraco.Cms.ManagementApi.ViewModels.ExamineManagement; +using Umbraco.Cms.ManagementApi.ViewModels.Pagination; +using Umbraco.Extensions; + +namespace Umbraco.Cms.ManagementApi.Controllers.ExamineManagement; + +[ApiVersion("1.0")] +public class SearchExamineManagementController : ExamineManagementControllerBase +{ + private readonly IExamineManagerService _examineManagerService; + + public SearchExamineManagementController(IExamineManagerService examineManagerService) => _examineManagerService = examineManagerService; + + [HttpGet("search")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel>), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + public async Task>> GetSearchResults(string searcherName, string? query, int skip, int take) + { + query = query?.Trim(); + + if (query.IsNullOrWhiteSpace()) + { + return new PagedViewModel(); + } + + if (!_examineManagerService.TryFindSearcher(searcherName, out ISearcher searcher)) + { + var invalidModelProblem = new ProblemDetails + { + Title = "Could not find a valid searcher", + Detail = "The provided searcher name did not match any of our registered searchers", + Status = StatusCodes.Status404NotFound, + Type = "Error", + }; + + return NotFound(invalidModelProblem); + } + + ISearchResults results; + + // NativeQuery will work for a single word/phrase too (but depends on the implementation) the lucene one will work. + try + { + results = searcher + .CreateQuery() + .NativeQuery(query) + .Execute(QueryOptions.SkipTake(skip, take)); + } + catch (ParseException) + { + var invalidModelProblem = new ProblemDetails + { + Title = "Could not parse the query", + Detail = "Parser could not parse the query. Please double check if the query is valid. Sometimes this can also happen if your query starts with a wildcard (*)", + Status = StatusCodes.Status404NotFound, + Type = "Error", + }; + + return BadRequest(invalidModelProblem); + } + + return await Task.FromResult(new PagedViewModel + { + Total = results.TotalItemCount, + Items = results.Select(x => new SearchResultViewModel + { + Id = x.Id, + Score = x.Score, + Fields = x.AllValues.OrderBy(y => y.Key).Select(y => new FieldViewModel { Name = y.Key, Values = y.Value }), + }), + }); + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/ExamineManagement/SearchersExamineManagementController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/ExamineManagement/SearchersExamineManagementController.cs new file mode 100644 index 0000000000..cd1ed5a912 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/ExamineManagement/SearchersExamineManagementController.cs @@ -0,0 +1,39 @@ +using Examine; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Infrastructure.Examine; +using Umbraco.Cms.ManagementApi.ViewModels.ExamineManagement; +using Umbraco.Cms.ManagementApi.ViewModels.Pagination; +using Umbraco.Extensions; + +namespace Umbraco.Cms.ManagementApi.Controllers.ExamineManagement; + +[ApiVersion("1.0")] +public class SearchersExamineManagementController : ExamineManagementControllerBase +{ + private readonly IExamineManager _examineManager; + + public SearchersExamineManagementController(IExamineManager examineManager) => _examineManager = examineManager; + + /// + /// Get the details for searchers + /// + /// + [HttpGet("searchers")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task>> Searchers(int skip, int take) + { + var searchers = new List( + _examineManager.RegisteredSearchers.Select(searcher => new SearcherViewModel { Name = searcher.Name }) + .OrderBy(x => + x.Name.TrimEnd("Searcher"))); // order by name , but strip the "Searcher" from the end if it exists + var viewModel = new PagedViewModel + { + Items = searchers.Skip(skip).Take(take), + Total = searchers.Count, + }; + + return await Task.FromResult(Ok(viewModel)); + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Help/GetHelpController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Help/GetHelpController.cs new file mode 100644 index 0000000000..648b4e2bcc --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Help/GetHelpController.cs @@ -0,0 +1,80 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Serialization; +using Umbraco.Cms.ManagementApi.Builders; +using Umbraco.Cms.ManagementApi.Factories; +using Umbraco.Cms.ManagementApi.ViewModels.Help; +using Umbraco.Cms.ManagementApi.ViewModels.Pagination; + +namespace Umbraco.Cms.ManagementApi.Controllers.Help; + +public class GetHelpController : HelpControllerBase +{ + private readonly ILogger _logger; + private readonly IJsonSerializer _jsonSerializer; + private HelpPageSettings _helpPageSettings; + + public GetHelpController( + IOptionsMonitor helpPageSettings, + ILogger logger, + IJsonSerializer jsonSerializer) + { + _logger = logger; + _jsonSerializer = jsonSerializer; + _helpPageSettings = helpPageSettings.CurrentValue; + helpPageSettings.OnChange(UpdateHelpPageSettings); + } + + private void UpdateHelpPageSettings(HelpPageSettings settings) => _helpPageSettings = settings; + + [HttpGet] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task Get(string section, string? tree, int skip, int take, string? baseUrl = "https://our.umbraco.com") + { + if (IsAllowedUrl(baseUrl) is false) + { + _logger.LogError($"The following URL is not listed in the allowlist for HelpPage in HelpPageSettings: {baseUrl}"); + + ProblemDetails invalidModelProblem = + new ProblemDetailsBuilder() + .WithTitle("Invalid database configuration") + .WithDetail("The provided database configuration is invalid") + .Build(); + + return BadRequest(invalidModelProblem); + } + + var url = string.Format(baseUrl + "/Umbraco/Documentation/Lessons/GetContextHelpDocs?sectionAlias={0}&treeAlias={1}", section, tree); + + try + { + var httpClient = new HttpClient(); + + // fetch dashboard json and parse to JObject + var json = await httpClient.GetStringAsync(url); + List? result = _jsonSerializer.Deserialize>(json); + if (result != null) + { + return Ok(new PagedViewModel + { + Total = result.Count, + Items = result.Skip(skip).Take(take), + }); + } + } + catch (HttpRequestException rex) + { + _logger.LogInformation($"Check your network connection, exception: {rex.Message}"); + } + + return Ok(PagedViewModel.Empty()); + } + + private bool IsAllowedUrl(string? url) => + _helpPageSettings.HelpPageUrlAllowList is null || _helpPageSettings.HelpPageUrlAllowList.Contains(url); +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Help/HelpControllerBase.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Help/HelpControllerBase.cs new file mode 100644 index 0000000000..07d227a23e --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Help/HelpControllerBase.cs @@ -0,0 +1,13 @@ +using Microsoft.AspNetCore.Mvc; +using NSwag.Annotations; +using Umbraco.New.Cms.Web.Common.Routing; + +namespace Umbraco.Cms.ManagementApi.Controllers.Help; + +[ApiController] +[BackOfficeRoute("api/v{version:apiVersion}/help")] +[OpenApiTag("Help")] +[ApiVersion("1.0")] +public abstract class HelpControllerBase : ManagementApiControllerBase +{ +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Install/InstallControllerBase.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Install/InstallControllerBase.cs index 275a5cd7b7..359e62b4b8 100644 --- a/src/Umbraco.Cms.ManagementApi/Controllers/Install/InstallControllerBase.cs +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Install/InstallControllerBase.cs @@ -7,9 +7,9 @@ using Umbraco.New.Cms.Web.Common.Routing; namespace Umbraco.Cms.ManagementApi.Controllers.Install; [ApiController] -[BackOfficeRoute("api/v{version:apiVersion}/install")] +[VersionedApiBackOfficeRoute("install")] [OpenApiTag("Install")] [RequireRuntimeLevel(RuntimeLevel.Install)] -public abstract class InstallControllerBase : Controller +public abstract class InstallControllerBase : ManagementApiControllerBase { } diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Install/ValidateDatabaseInstallController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Install/ValidateDatabaseInstallController.cs index ae5e26c2c1..2866487ea1 100644 --- a/src/Umbraco.Cms.ManagementApi/Controllers/Install/ValidateDatabaseInstallController.cs +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Install/ValidateDatabaseInstallController.cs @@ -27,16 +27,13 @@ public class ValidateDatabaseInstallController : InstallControllerBase [ProducesResponseType(StatusCodes.Status200OK)] public async Task ValidateDatabase(DatabaseInstallViewModel viewModel) { - // TODO: Async - We need to figure out what we want to do with async endpoints that doesn't do anything async - // We want these to be async for future use (Ideally we'll have more async things), - // But we need to figure out how we want to handle it in the meantime? use Task.FromResult or? DatabaseModel databaseModel = _mapper.Map(viewModel)!; var success = _databaseBuilder.ConfigureDatabaseConnection(databaseModel, true); if (success) { - return Ok(); + return await Task.FromResult(Ok()); } var invalidModelProblem = new ProblemDetails @@ -47,6 +44,6 @@ public class ValidateDatabaseInstallController : InstallControllerBase Type = "Error", }; - return BadRequest(invalidModelProblem); + return await Task.FromResult(BadRequest(invalidModelProblem)); } } diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Language/AllLanguageController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Language/AllLanguageController.cs new file mode 100644 index 0000000000..8927984c61 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Language/AllLanguageController.cs @@ -0,0 +1,36 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.ViewModels.Language; +using Umbraco.Cms.ManagementApi.ViewModels.Pagination; +using Umbraco.New.Cms.Core.Models; + +namespace Umbraco.Cms.ManagementApi.Controllers.Language; + +public class AllLanguageController : LanguageControllerBase +{ + private readonly ILocalizationService _localizationService; + private readonly IUmbracoMapper _umbracoMapper; + + public AllLanguageController(ILocalizationService localizationService, IUmbracoMapper umbracoMapper) + { + _localizationService = localizationService; + _umbracoMapper = umbracoMapper; + } + + /// 1 + /// Returns all currently configured languages. + /// + /// + [HttpGet] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task?> GetAll(int skip, int take) + { + PagedModel allLanguages = _localizationService.GetAllLanguagesPaged(skip, take); + + return await Task.FromResult(_umbracoMapper.Map, PagedViewModel>(allLanguages)!); + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Language/ByIdLanguageController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Language/ByIdLanguageController.cs new file mode 100644 index 0000000000..7567ccfc8a --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Language/ByIdLanguageController.cs @@ -0,0 +1,35 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.ViewModels.Language; + +namespace Umbraco.Cms.ManagementApi.Controllers.Language; + +public class ByIdLanguageController : LanguageControllerBase +{ + private readonly ILocalizationService _localizationService; + private readonly IUmbracoMapper _umbracoMapper; + + public ByIdLanguageController(ILocalizationService localizationService, IUmbracoMapper umbracoMapper) + { + _localizationService = localizationService; + _umbracoMapper = umbracoMapper; + } + + [HttpGet("{id:int}")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(NotFoundResult), StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> ById(int id) + { + ILanguage? lang = _localizationService.GetLanguageById(id); + if (lang is null) + { + return NotFound(); + } + + return await Task.FromResult(_umbracoMapper.Map(lang)); + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Language/CreateLanguageController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Language/CreateLanguageController.cs new file mode 100644 index 0000000000..2e56cb1d19 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Language/CreateLanguageController.cs @@ -0,0 +1,61 @@ +using System.Globalization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.ViewModels.Language; +using Umbraco.New.Cms.Core.Services.Installer; + +namespace Umbraco.Cms.ManagementApi.Controllers.Language; + +public class CreateLanguageController : LanguageControllerBase +{ + private readonly ILanguageService _languageService; + private readonly IUmbracoMapper _umbracoMapper; + private readonly ILocalizationService _localizationService; + + public CreateLanguageController(ILanguageService languageService, IUmbracoMapper umbracoMapper, ILocalizationService localizationService) + { + _languageService = languageService; + _umbracoMapper = umbracoMapper; + _localizationService = localizationService; + } + + /// + /// Creates or saves a language + /// + [HttpPost("create")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status201Created)] + // TODO: This needs to be an authorized endpoint. + public async Task Create(LanguageViewModel language) + { + if (_languageService.LanguageAlreadyExists(language.Id, language.IsoCode)) + { + // Someone is trying to create a language that already exist + ModelState.AddModelError("IsoCode", "The language " + language.IsoCode + " already exists"); + return ValidationProblem(ModelState); + } + + // Creating a new lang... + CultureInfo culture; + try + { + culture = CultureInfo.GetCultureInfo(language.IsoCode); + } + catch (CultureNotFoundException) + { + ModelState.AddModelError("IsoCode", "No Culture found with name " + language.IsoCode); + return ValidationProblem(ModelState); + } + + language.Name ??= culture.EnglishName; + + ILanguage newLang = _umbracoMapper.Map(language)!; + + _localizationService.Save(newLang); + return await Task.FromResult(Created($"api/v1.0/language/{newLang.Id}", null)); + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Language/DeleteLanguageController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Language/DeleteLanguageController.cs new file mode 100644 index 0000000000..f3886e3089 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Language/DeleteLanguageController.cs @@ -0,0 +1,50 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.Builders; + +namespace Umbraco.Cms.ManagementApi.Controllers.Language; + +public class DeleteLanguageController : LanguageControllerBase +{ + private readonly ILocalizationService _localizationService; + + public DeleteLanguageController(ILocalizationService localizationService) => _localizationService = localizationService; + + /// + /// Deletes a language with a given ID + /// + [HttpDelete("{id:int}")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status200OK)] + // TODO: This needs to be an authorized endpoint. + public async Task Delete(int id) + { + ILanguage? language = _localizationService.GetLanguageById(id); + if (language == null) + { + return await Task.FromResult(NotFound()); + } + + // the service would not let us do it, but test here nevertheless + if (language.IsDefault) + { + ProblemDetails invalidModelProblem = + new ProblemDetailsBuilder() + .WithTitle("Cannot delete default language") + .WithDetail($"Language '{language.IsoCode}' is currently set to 'default' and can not be deleted.") + .Build(); + + return BadRequest(invalidModelProblem); + } + + // service is happy deleting a language that's fallback for another language, + // will just remove it - so no need to check here + _localizationService.Delete(language); + + return await Task.FromResult(Ok()); + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Language/LanguageControllerBase.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Language/LanguageControllerBase.cs new file mode 100644 index 0000000000..05495bd699 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Language/LanguageControllerBase.cs @@ -0,0 +1,13 @@ +using Microsoft.AspNetCore.Mvc; +using NSwag.Annotations; +using Umbraco.New.Cms.Web.Common.Routing; + +namespace Umbraco.Cms.ManagementApi.Controllers.Language; + +[ApiController] +[BackOfficeRoute("api/v{version:apiVersion}/language")] +[OpenApiTag("Language")] +[ApiVersion("1.0")] +public abstract class LanguageControllerBase : ManagementApiControllerBase +{ +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Language/UpdateLanguageController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Language/UpdateLanguageController.cs new file mode 100644 index 0000000000..d36e8cb687 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Language/UpdateLanguageController.cs @@ -0,0 +1,66 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.ViewModels.Language; +using Umbraco.New.Cms.Core.Services.Installer; + +namespace Umbraco.Cms.ManagementApi.Controllers.Language; + +public class UpdateLanguageController : LanguageControllerBase +{ + private readonly ILanguageService _languageService; + private readonly IUmbracoMapper _umbracoMapper; + private readonly ILocalizationService _localizationService; + + public UpdateLanguageController(ILanguageService languageService, IUmbracoMapper umbracoMapper, ILocalizationService localizationService) + { + _languageService = languageService; + _umbracoMapper = umbracoMapper; + _localizationService = localizationService; + } + + /// + /// Updates a language + /// + [HttpPut("update")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(NotFoundResult), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status200OK)] + // TODO: This needs to be an authorized endpoint. + public async Task Update(LanguageViewModel language) + { + ILanguage? existingById = language.Id != default ? _localizationService.GetLanguageById(language.Id) : null; + if (existingById is null) + { + return await Task.FromResult(NotFound()); + } + + // note that the service will prevent the default language from being "un-defaulted" + // but does not hurt to test here - though the UI should prevent it too + if (existingById.IsDefault && !language.IsDefault) + { + ModelState.AddModelError("IsDefault", "Cannot un-default the default language."); + return await Task.FromResult(ValidationProblem(ModelState)); + } + + existingById = _umbracoMapper.Map(language, existingById); + + if (!_languageService.CanUseLanguagesFallbackLanguage(existingById)) + { + ModelState.AddModelError("FallbackLanguage", "The selected fall back language does not exist."); + return await Task.FromResult(ValidationProblem(ModelState)); + } + + if (!_languageService.CanGetProperFallbackLanguage(existingById)) + { + ModelState.AddModelError("FallbackLanguage", $"The selected fall back language {_localizationService.GetLanguageById(existingById.FallbackLanguageId!.Value)} would create a circular path."); + return await Task.FromResult(ValidationProblem(ModelState)); + } + + _localizationService.Save(existingById); + return await Task.FromResult(Ok()); + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/ManagementApiControllerBase.cs b/src/Umbraco.Cms.ManagementApi/Controllers/ManagementApiControllerBase.cs new file mode 100644 index 0000000000..c6e78654b6 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/ManagementApiControllerBase.cs @@ -0,0 +1,10 @@ +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.ManagementApi.Filters; + +namespace Umbraco.Cms.ManagementApi.Controllers; + +[ManagementApiJsonConfiguration] +public class ManagementApiControllerBase : Controller +{ + +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Media/RecycleBin/ChildrenMediaRecycleBinController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Media/RecycleBin/ChildrenMediaRecycleBinController.cs new file mode 100644 index 0000000000..48ef9b4227 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Media/RecycleBin/ChildrenMediaRecycleBinController.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.ViewModels.Pagination; +using Umbraco.Cms.ManagementApi.ViewModels.RecycleBin; + +namespace Umbraco.Cms.ManagementApi.Controllers.Media.RecycleBin; + +public class ChildrenMediaRecycleBinController : MediaRecycleBinControllerBase +{ + public ChildrenMediaRecycleBinController(IEntityService entityService) + : base(entityService) + { + } + + [HttpGet("children")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task>> Children(Guid parentKey, int skip = 0, int take = 100) + => await GetChildren(parentKey, skip, take); +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Media/RecycleBin/MediaRecycleBinControllerBase.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Media/RecycleBin/MediaRecycleBinControllerBase.cs new file mode 100644 index 0000000000..157e1099de --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Media/RecycleBin/MediaRecycleBinControllerBase.cs @@ -0,0 +1,43 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using NSwag.Annotations; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.Controllers.RecycleBin; +using Umbraco.Cms.ManagementApi.Filters; +using Umbraco.Cms.ManagementApi.ViewModels.RecycleBin; +using Umbraco.New.Cms.Web.Common.Routing; + +namespace Umbraco.Cms.ManagementApi.Controllers.Media.RecycleBin; + +[ApiVersion("1.0")] +[ApiController] +[VersionedApiBackOfficeRoute($"{Constants.UdiEntityType.Media}/recycle-bin")] +[RequireMediaTreeRootAccess] +[ProducesResponseType(StatusCodes.Status401Unauthorized)] +[OpenApiTag(nameof(Constants.UdiEntityType.Media))] +public class MediaRecycleBinControllerBase : RecycleBinControllerBase +{ + public MediaRecycleBinControllerBase(IEntityService entityService) + : base(entityService) + { + } + + protected override UmbracoObjectTypes ItemObjectType => UmbracoObjectTypes.Media; + + protected override int RecycleBinRootId => Constants.System.RecycleBinMedia; + + protected override RecycleBinItemViewModel MapRecycleBinViewModel(Guid? parentKey, IEntitySlim entity) + { + RecycleBinItemViewModel viewModel = base.MapRecycleBinViewModel(parentKey, entity); + + if (entity is IMediaEntitySlim mediaEntitySlim) + { + viewModel.Icon = mediaEntitySlim.ContentTypeIcon ?? viewModel.Icon; + } + + return viewModel; + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Media/RecycleBin/RootMediaRecycleBinController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Media/RecycleBin/RootMediaRecycleBinController.cs new file mode 100644 index 0000000000..9ae1330d58 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Media/RecycleBin/RootMediaRecycleBinController.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.ViewModels.Pagination; +using Umbraco.Cms.ManagementApi.ViewModels.RecycleBin; + +namespace Umbraco.Cms.ManagementApi.Controllers.Media.RecycleBin; + +public class RootMediaRecycleBinController : MediaRecycleBinControllerBase +{ + public RootMediaRecycleBinController(IEntityService entityService) + : base(entityService) + { + } + + [HttpGet("root")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task>> Root(int skip = 0, int take = 100) + => await GetRoot(skip, take); +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Media/Tree/ChildrenMediaTreeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Media/Tree/ChildrenMediaTreeController.cs new file mode 100644 index 0000000000..18d7b924af --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Media/Tree/ChildrenMediaTreeController.cs @@ -0,0 +1,32 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.Services.Entities; +using Umbraco.Cms.ManagementApi.ViewModels.Pagination; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; + +namespace Umbraco.Cms.ManagementApi.Controllers.Media.Tree; + +public class ChildrenMediaTreeController : MediaTreeControllerBase +{ + public ChildrenMediaTreeController( + IEntityService entityService, + IUserStartNodeEntitiesService userStartNodeEntitiesService, + IDataTypeService dataTypeService, + AppCaches appCaches, + IBackOfficeSecurityAccessor backofficeSecurityAccessor) + : base(entityService, userStartNodeEntitiesService, dataTypeService, appCaches, backofficeSecurityAccessor) + { + } + + [HttpGet("children")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task>> Children(Guid parentKey, int skip = 0, int take = 100, Guid? dataTypeKey = null) + { + IgnoreUserStartNodesForDataType(dataTypeKey); + return await GetChildren(parentKey, skip, take); + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Media/Tree/ItemsMediaTreeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Media/Tree/ItemsMediaTreeController.cs new file mode 100644 index 0000000000..2ebf1a559f --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Media/Tree/ItemsMediaTreeController.cs @@ -0,0 +1,31 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.Services.Entities; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; + +namespace Umbraco.Cms.ManagementApi.Controllers.Media.Tree; + +public class ItemsMediaTreeController : MediaTreeControllerBase +{ + public ItemsMediaTreeController( + IEntityService entityService, + IUserStartNodeEntitiesService userStartNodeEntitiesService, + IDataTypeService dataTypeService, + AppCaches appCaches, + IBackOfficeSecurityAccessor backofficeSecurityAccessor) + : base(entityService, userStartNodeEntitiesService, dataTypeService, appCaches, backofficeSecurityAccessor) + { + } + + [HttpGet("items")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + public async Task>> Items([FromQuery(Name = "key")] Guid[] keys, Guid? dataTypeKey = null) + { + IgnoreUserStartNodesForDataType(dataTypeKey); + return await GetItems(keys); + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Media/Tree/MediaTreeControllerBase.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Media/Tree/MediaTreeControllerBase.cs new file mode 100644 index 0000000000..c03f05c71d --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Media/Tree/MediaTreeControllerBase.cs @@ -0,0 +1,72 @@ +using Microsoft.AspNetCore.Mvc; +using NSwag.Annotations; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.Controllers.Tree; +using Umbraco.Cms.ManagementApi.Services.Entities; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; +using Umbraco.New.Cms.Web.Common.Routing; + +namespace Umbraco.Cms.ManagementApi.Controllers.Media.Tree; + +[ApiVersion("1.0")] +[ApiController] +[VersionedApiBackOfficeRoute($"{Constants.UdiEntityType.Media}/tree")] +[OpenApiTag(nameof(Constants.UdiEntityType.Media))] +public class MediaTreeControllerBase : UserStartNodeTreeControllerBase +{ + private readonly AppCaches _appCaches; + private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor; + + public MediaTreeControllerBase( + IEntityService entityService, + IUserStartNodeEntitiesService userStartNodeEntitiesService, + IDataTypeService dataTypeService, + AppCaches appCaches, + IBackOfficeSecurityAccessor backofficeSecurityAccessor) + : base(entityService, userStartNodeEntitiesService, dataTypeService) + { + _appCaches = appCaches; + _backofficeSecurityAccessor = backofficeSecurityAccessor; + } + + protected override UmbracoObjectTypes ItemObjectType => UmbracoObjectTypes.Media; + + protected override Ordering ItemOrdering => Ordering.By(nameof(Infrastructure.Persistence.Dtos.NodeDto.SortOrder)); + + protected override ContentTreeItemViewModel MapTreeItemViewModel(Guid? parentKey, IEntitySlim entity) + { + ContentTreeItemViewModel viewModel = base.MapTreeItemViewModel(parentKey, entity); + + if (entity is IMediaEntitySlim mediaEntitySlim) + { + viewModel.Icon = mediaEntitySlim.ContentTypeIcon ?? viewModel.Icon; + } + + return viewModel; + } + + // TODO: delete these (faking start node setup for unlimited editor) + protected override int[] GetUserStartNodeIds() => new[] { -1 }; + + protected override string[] GetUserStartNodePaths() => Array.Empty(); + + // TODO: use these implementations instead of the dummy ones above once we have backoffice auth in place + // protected override int[] GetUserStartNodeIds() + // => _backofficeSecurityAccessor + // .BackOfficeSecurity? + // .CurrentUser? + // .CalculateMediaStartNodeIds(EntityService, _appCaches) + // ?? Array.Empty(); + // + // protected override string[] GetUserStartNodePaths() + // => _backofficeSecurityAccessor + // .BackOfficeSecurity? + // .CurrentUser? + // .GetMediaStartNodePaths(EntityService, _appCaches) + // ?? Array.Empty(); +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Media/Tree/RootMediaTreeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Media/Tree/RootMediaTreeController.cs new file mode 100644 index 0000000000..59723a6ffd --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Media/Tree/RootMediaTreeController.cs @@ -0,0 +1,32 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.Services.Entities; +using Umbraco.Cms.ManagementApi.ViewModels.Pagination; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; + +namespace Umbraco.Cms.ManagementApi.Controllers.Media.Tree; + +public class RootMediaTreeController : MediaTreeControllerBase +{ + public RootMediaTreeController( + IEntityService entityService, + IUserStartNodeEntitiesService userStartNodeEntitiesService, + IDataTypeService dataTypeService, + AppCaches appCaches, + IBackOfficeSecurityAccessor backofficeSecurityAccessor) + : base(entityService, userStartNodeEntitiesService, dataTypeService, appCaches, backofficeSecurityAccessor) + { + } + + [HttpGet("root")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task>> Root(int skip = 0, int take = 100, Guid? dataTypeKey = null) + { + IgnoreUserStartNodesForDataType(dataTypeKey); + return await GetRoot(skip, take); + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/MediaType/Tree/ChildrenMediaTypeTreeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/MediaType/Tree/ChildrenMediaTypeTreeController.cs new file mode 100644 index 0000000000..ee416938d3 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/MediaType/Tree/ChildrenMediaTypeTreeController.cs @@ -0,0 +1,24 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.ViewModels.Pagination; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; + +namespace Umbraco.Cms.ManagementApi.Controllers.MediaType.Tree; + +public class ChildrenMediaTypeTreeController : MediaTypeTreeControllerBase +{ + public ChildrenMediaTypeTreeController(IEntityService entityService, IMediaTypeService mediaTypeService) + : base(entityService, mediaTypeService) + { + } + + [HttpGet("children")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task>> Children(Guid parentKey, int skip = 0, int take = 100, bool foldersOnly = false) + { + RenderFoldersOnly(foldersOnly); + return await GetChildren(parentKey, skip, take); + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/MediaType/Tree/ItemsMediaTypeTreeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/MediaType/Tree/ItemsMediaTypeTreeController.cs new file mode 100644 index 0000000000..363751fee7 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/MediaType/Tree/ItemsMediaTypeTreeController.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; + +namespace Umbraco.Cms.ManagementApi.Controllers.MediaType.Tree; + +public class ItemsMediaTypeTreeController : MediaTypeTreeControllerBase +{ + public ItemsMediaTypeTreeController(IEntityService entityService, IMediaTypeService mediaTypeService) + : base(entityService, mediaTypeService) + { + } + + [HttpGet("items")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + public async Task>> Items([FromQuery(Name = "key")] Guid[] keys) + => await GetItems(keys); +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/MediaType/Tree/MediaTypeTreeControllerBase.cs b/src/Umbraco.Cms.ManagementApi/Controllers/MediaType/Tree/MediaTypeTreeControllerBase.cs new file mode 100644 index 0000000000..5b06c46439 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/MediaType/Tree/MediaTypeTreeControllerBase.cs @@ -0,0 +1,46 @@ +using Microsoft.AspNetCore.Mvc; +using NSwag.Annotations; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.Controllers.Tree; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; +using Umbraco.New.Cms.Web.Common.Routing; + +namespace Umbraco.Cms.ManagementApi.Controllers.MediaType.Tree; + +[ApiVersion("1.0")] +[ApiController] +[VersionedApiBackOfficeRoute($"{Constants.UdiEntityType.MediaType}/tree")] +[OpenApiTag(nameof(Constants.UdiEntityType.MediaType))] +public class MediaTypeTreeControllerBase : FolderTreeControllerBase +{ + private readonly IMediaTypeService _mediaTypeService; + + public MediaTypeTreeControllerBase(IEntityService entityService, IMediaTypeService mediaTypeService) + : base(entityService) => + _mediaTypeService = mediaTypeService; + + protected override UmbracoObjectTypes ItemObjectType => UmbracoObjectTypes.MediaType; + + protected override UmbracoObjectTypes FolderObjectType => UmbracoObjectTypes.MediaTypeContainer; + + protected override FolderTreeItemViewModel[] MapTreeItemViewModels(Guid? parentKey, IEntitySlim[] entities) + { + var mediaTypes = _mediaTypeService + .GetAll(entities.Select(entity => entity.Id).ToArray()) + .ToDictionary(contentType => contentType.Id); + + return entities.Select(entity => + { + FolderTreeItemViewModel viewModel = MapTreeItemViewModel(parentKey, entity); + if (mediaTypes.TryGetValue(entity.Id, out IMediaType? mediaType)) + { + viewModel.Icon = mediaType.Icon ?? viewModel.Icon; + } + + return viewModel; + }).ToArray(); + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/MediaType/Tree/RootMediaTypeTreeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/MediaType/Tree/RootMediaTypeTreeController.cs new file mode 100644 index 0000000000..902fc1fcc5 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/MediaType/Tree/RootMediaTypeTreeController.cs @@ -0,0 +1,24 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.ViewModels.Pagination; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; + +namespace Umbraco.Cms.ManagementApi.Controllers.MediaType.Tree; + +public class RootMediaTypeTreeController : MediaTypeTreeControllerBase +{ + public RootMediaTypeTreeController(IEntityService entityService, IMediaTypeService mediaTypeService) + : base(entityService, mediaTypeService) + { + } + + [HttpGet("root")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task>> Root(int skip = 0, int take = 100, bool foldersOnly = false) + { + RenderFoldersOnly(foldersOnly); + return await GetRoot(skip, take); + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/MemberGroup/Tree/ItemsMemberGroupTreeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/MemberGroup/Tree/ItemsMemberGroupTreeController.cs new file mode 100644 index 0000000000..94db46be4e --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/MemberGroup/Tree/ItemsMemberGroupTreeController.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; + +namespace Umbraco.Cms.ManagementApi.Controllers.MemberGroup.Tree; + +public class ItemsMemberGroupTreeController : MemberGroupTreeControllerBase +{ + public ItemsMemberGroupTreeController(IEntityService entityService) + : base(entityService) + { + } + + [HttpGet("items")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + public async Task>> Items([FromQuery(Name = "key")] Guid[] keys) + => await GetItems(keys); +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/MemberGroup/Tree/MemberGroupTreeControllerBase.cs b/src/Umbraco.Cms.ManagementApi/Controllers/MemberGroup/Tree/MemberGroupTreeControllerBase.cs new file mode 100644 index 0000000000..b3ee033a25 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/MemberGroup/Tree/MemberGroupTreeControllerBase.cs @@ -0,0 +1,32 @@ +using Microsoft.AspNetCore.Mvc; +using NSwag.Annotations; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.Controllers.Tree; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; +using Umbraco.New.Cms.Web.Common.Routing; + +namespace Umbraco.Cms.ManagementApi.Controllers.MemberGroup.Tree; + +[ApiVersion("1.0")] +[ApiController] +[VersionedApiBackOfficeRoute($"{Constants.UdiEntityType.MemberGroup}/tree")] +[OpenApiTag(nameof(Constants.UdiEntityType.MemberGroup))] +public class MemberGroupTreeControllerBase : EntityTreeControllerBase +{ + public MemberGroupTreeControllerBase(IEntityService entityService) + : base(entityService) + { + } + + protected override UmbracoObjectTypes ItemObjectType => UmbracoObjectTypes.MemberGroup; + + protected override EntityTreeItemViewModel MapTreeItemViewModel(Guid? parentKey, IEntitySlim entity) + { + EntityTreeItemViewModel viewModel = base.MapTreeItemViewModel(parentKey, entity); + viewModel.Icon = Constants.Icons.MemberGroup; + return viewModel; + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/MemberGroup/Tree/RootMemberGroupTreeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/MemberGroup/Tree/RootMemberGroupTreeController.cs new file mode 100644 index 0000000000..4b9e7a35ab --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/MemberGroup/Tree/RootMemberGroupTreeController.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.ViewModels.Pagination; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; + +namespace Umbraco.Cms.ManagementApi.Controllers.MemberGroup.Tree; + +public class RootMemberGroupTreeController : MemberGroupTreeControllerBase +{ + public RootMemberGroupTreeController(IEntityService entityService) + : base(entityService) + { + } + + [HttpGet("root")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task>> Root(int skip = 0, int take = 100) + => await GetRoot(skip, take); +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/MemberType/Tree/ItemsMemberTypeTreeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/MemberType/Tree/ItemsMemberTypeTreeController.cs new file mode 100644 index 0000000000..249cdd5d67 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/MemberType/Tree/ItemsMemberTypeTreeController.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; + +namespace Umbraco.Cms.ManagementApi.Controllers.MemberType.Tree; + +public class ItemsMemberTypeTreeController : MemberTypeTreeControllerBase +{ + public ItemsMemberTypeTreeController(IEntityService entityService) + : base(entityService) + { + } + + [HttpGet("items")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + public async Task>> Items([FromQuery(Name = "key")] Guid[] keys) + => await GetItems(keys); +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/MemberType/Tree/MemberTypeTreeControllerBase.cs b/src/Umbraco.Cms.ManagementApi/Controllers/MemberType/Tree/MemberTypeTreeControllerBase.cs new file mode 100644 index 0000000000..88183dfd58 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/MemberType/Tree/MemberTypeTreeControllerBase.cs @@ -0,0 +1,32 @@ +using Microsoft.AspNetCore.Mvc; +using NSwag.Annotations; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.Controllers.Tree; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; +using Umbraco.New.Cms.Web.Common.Routing; + +namespace Umbraco.Cms.ManagementApi.Controllers.MemberType.Tree; + +[ApiVersion("1.0")] +[ApiController] +[VersionedApiBackOfficeRoute($"{Constants.UdiEntityType.MemberType}/tree")] +[OpenApiTag(nameof(Constants.UdiEntityType.MemberType))] +public class MemberTypeTreeControllerBase : EntityTreeControllerBase +{ + public MemberTypeTreeControllerBase(IEntityService entityService) + : base(entityService) + { + } + + protected override UmbracoObjectTypes ItemObjectType => UmbracoObjectTypes.MemberType; + + protected override EntityTreeItemViewModel MapTreeItemViewModel(Guid? parentKey, IEntitySlim entity) + { + EntityTreeItemViewModel viewModel = base.MapTreeItemViewModel(parentKey, entity); + viewModel.Icon = Constants.Icons.User; + return viewModel; + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/MemberType/Tree/RootMemberTypeTreeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/MemberType/Tree/RootMemberTypeTreeController.cs new file mode 100644 index 0000000000..92e3dc1f7c --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/MemberType/Tree/RootMemberTypeTreeController.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.ViewModels.Pagination; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; + +namespace Umbraco.Cms.ManagementApi.Controllers.MemberType.Tree; + +public class RootMemberTypeTreeController : MemberTypeTreeControllerBase +{ + public RootMemberTypeTreeController(IEntityService entityService) + : base(entityService) + { + } + + [HttpGet("root")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task>> Root(int skip = 0, int take = 100) + => await GetRoot(skip, take); +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/PartialView/Tree/ChildrenPartialViewTreeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/PartialView/Tree/ChildrenPartialViewTreeController.cs new file mode 100644 index 0000000000..91e9354d2c --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/PartialView/Tree/ChildrenPartialViewTreeController.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.IO; +using Umbraco.Cms.ManagementApi.ViewModels.Pagination; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; + +namespace Umbraco.Cms.ManagementApi.Controllers.PartialView.Tree; + +public class ChildrenPartialViewTreeController : PartialViewTreeControllerBase +{ + public ChildrenPartialViewTreeController(FileSystems fileSystems) + : base(fileSystems) + { + } + + [HttpGet("children")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task>> Children(string path, int skip = 0, int take = 100) + => await GetChildren(path, skip, take); +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/PartialView/Tree/ItemsPartialViewTreeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/PartialView/Tree/ItemsPartialViewTreeController.cs new file mode 100644 index 0000000000..d6107a844a --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/PartialView/Tree/ItemsPartialViewTreeController.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.IO; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; + +namespace Umbraco.Cms.ManagementApi.Controllers.PartialView.Tree; + +public class ItemsPartialViewTreeController : PartialViewTreeControllerBase +{ + public ItemsPartialViewTreeController(FileSystems fileSystems) + : base(fileSystems) + { + } + + [HttpGet("items")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + public async Task>> Items([FromQuery(Name = "path")] string[] paths) + => await GetItems(paths); +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/PartialView/Tree/PartialViewTreeControllerBase.cs b/src/Umbraco.Cms.ManagementApi/Controllers/PartialView/Tree/PartialViewTreeControllerBase.cs new file mode 100644 index 0000000000..95ad0eb6cf --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/PartialView/Tree/PartialViewTreeControllerBase.cs @@ -0,0 +1,25 @@ +using Microsoft.AspNetCore.Mvc; +using NSwag.Annotations; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.IO; +using Umbraco.Cms.ManagementApi.Controllers.Tree; +using Umbraco.New.Cms.Web.Common.Routing; + +namespace Umbraco.Cms.ManagementApi.Controllers.PartialView.Tree; + +[ApiVersion("1.0")] +[ApiController] +[VersionedApiBackOfficeRoute($"{Constants.UdiEntityType.PartialView}/tree")] +[OpenApiTag(nameof(Constants.UdiEntityType.PartialView))] +public class PartialViewTreeControllerBase : FileSystemTreeControllerBase +{ + public PartialViewTreeControllerBase(FileSystems fileSystems) + => FileSystem = fileSystems.PartialViewsFileSystem ?? + throw new ArgumentException("Missing partial views file system", nameof(fileSystems)); + + protected override IFileSystem FileSystem { get; } + + protected override string FileIcon(string path) => Constants.Icons.PartialView; + + protected override string ItemType(string path) => Constants.UdiEntityType.PartialView; +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/PartialView/Tree/RootPartialViewTreeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/PartialView/Tree/RootPartialViewTreeController.cs new file mode 100644 index 0000000000..536ff007a1 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/PartialView/Tree/RootPartialViewTreeController.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.IO; +using Umbraco.Cms.ManagementApi.ViewModels.Pagination; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; + +namespace Umbraco.Cms.ManagementApi.Controllers.PartialView.Tree; + +public class RootPartialViewTreeController : PartialViewTreeControllerBase +{ + public RootPartialViewTreeController(FileSystems fileSystems) + : base(fileSystems) + { + } + + [HttpGet("root")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task>> Root(int skip = 0, int take = 100) + => await GetRoot(skip, take); +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Profiling/ProfilingControllerBase.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Profiling/ProfilingControllerBase.cs new file mode 100644 index 0000000000..ebcae8fcf4 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Profiling/ProfilingControllerBase.cs @@ -0,0 +1,13 @@ +using Microsoft.AspNetCore.Mvc; +using NSwag.Annotations; +using Umbraco.New.Cms.Web.Common.Routing; + +namespace Umbraco.Cms.ManagementApi.Controllers.Profiling; + +[ApiVersion("1.0")] +[ApiController] +[BackOfficeRoute("api/v{version:apiVersion}/profiling")] +[OpenApiTag("Profiling")] +public class ProfilingControllerBase : ManagementApiControllerBase +{ +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Profiling/StatusProfilingController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Profiling/StatusProfilingController.cs new file mode 100644 index 0000000000..e3c2c3bfd2 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Profiling/StatusProfilingController.cs @@ -0,0 +1,19 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Hosting; +using Umbraco.Cms.ManagementApi.ViewModels.Profiling; + +namespace Umbraco.Cms.ManagementApi.Controllers.Profiling; + +public class StatusProfilingController : ProfilingControllerBase +{ + private readonly IHostingEnvironment _hosting; + + public StatusProfilingController(IHostingEnvironment hosting) => _hosting = hosting; + + [HttpGet("status")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(ProfilingStatusViewModel), StatusCodes.Status200OK)] + public async Task> Status() + => await Task.FromResult(Ok(new ProfilingStatusViewModel(_hosting.IsDebugMode))); +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/PublishedCache/CollectPublishedCacheController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/PublishedCache/CollectPublishedCacheController.cs new file mode 100644 index 0000000000..35bedf36f0 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/PublishedCache/CollectPublishedCacheController.cs @@ -0,0 +1,23 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.PublishedCache; + +namespace Umbraco.Cms.ManagementApi.Controllers.PublishedCache; + +public class CollectPublishedCacheController : PublishedCacheControllerBase +{ + private readonly IPublishedSnapshotService _publishedSnapshotService; + + public CollectPublishedCacheController(IPublishedSnapshotService publishedSnapshotService) + => _publishedSnapshotService = publishedSnapshotService; + + [HttpPost("collect")] + [MapToApiVersion("1.0")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task Collect() + { + GC.Collect(); + await _publishedSnapshotService.CollectAsync(); + return Ok(); + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/PublishedCache/PublishedCacheControllerBase.cs b/src/Umbraco.Cms.ManagementApi/Controllers/PublishedCache/PublishedCacheControllerBase.cs new file mode 100644 index 0000000000..206266e872 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/PublishedCache/PublishedCacheControllerBase.cs @@ -0,0 +1,13 @@ +using Microsoft.AspNetCore.Mvc; +using NSwag.Annotations; +using Umbraco.New.Cms.Web.Common.Routing; + +namespace Umbraco.Cms.ManagementApi.Controllers.PublishedCache; + +[ApiVersion("1.0")] +[ApiController] +[BackOfficeRoute("api/v{version:apiVersion}/published-cache")] +[OpenApiTag("PublishedCache")] +public class PublishedCacheControllerBase : ManagementApiControllerBase +{ +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/PublishedCache/RebuildPublishedCacheController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/PublishedCache/RebuildPublishedCacheController.cs new file mode 100644 index 0000000000..85a6602a52 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/PublishedCache/RebuildPublishedCacheController.cs @@ -0,0 +1,22 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.PublishedCache; + +namespace Umbraco.Cms.ManagementApi.Controllers.PublishedCache; + +public class RebuildPublishedCacheController : PublishedCacheControllerBase +{ + private readonly IPublishedSnapshotService _publishedSnapshotService; + + public RebuildPublishedCacheController(IPublishedSnapshotService publishedSnapshotService) + => _publishedSnapshotService = publishedSnapshotService; + + [HttpPost("rebuild")] + [MapToApiVersion("1.0")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task Collect() + { + _publishedSnapshotService.Rebuild(); + return await Task.FromResult(Ok()); + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/PublishedCache/ReloadPublishedCacheController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/PublishedCache/ReloadPublishedCacheController.cs new file mode 100644 index 0000000000..f4237ef210 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/PublishedCache/ReloadPublishedCacheController.cs @@ -0,0 +1,23 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Cache; +using Umbraco.Extensions; + +namespace Umbraco.Cms.ManagementApi.Controllers.PublishedCache; + +public class ReloadPublishedCacheController : PublishedCacheControllerBase +{ + private readonly DistributedCache _distributedCache; + + public ReloadPublishedCacheController(DistributedCache distributedCache) => _distributedCache = distributedCache; + + [HttpPost("reload")] + [MapToApiVersion("1.0")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task Reload() + { + _distributedCache.RefreshAllPublishedSnapshot(); + return await Task.FromResult(Ok()); + } +} + diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/PublishedCache/StatusPublishedCacheController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/PublishedCache/StatusPublishedCacheController.cs new file mode 100644 index 0000000000..26047b1291 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/PublishedCache/StatusPublishedCacheController.cs @@ -0,0 +1,19 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.PublishedCache; + +namespace Umbraco.Cms.ManagementApi.Controllers.PublishedCache; + +public class StatusPublishedCacheController : PublishedCacheControllerBase +{ + private readonly IPublishedSnapshotStatus _publishedSnapshotStatus; + + public StatusPublishedCacheController(IPublishedSnapshotStatus publishedSnapshotStatus) + => _publishedSnapshotStatus = publishedSnapshotStatus; + + [HttpGet("status")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(string), StatusCodes.Status200OK)] + public async Task> Status() + => await Task.FromResult(Ok(_publishedSnapshotStatus.GetStatus())); +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/RecycleBin/RecycleBinControllerBase.cs b/src/Umbraco.Cms.ManagementApi/Controllers/RecycleBin/RecycleBinControllerBase.cs new file mode 100644 index 0000000000..041ba9e9af --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/RecycleBin/RecycleBinControllerBase.cs @@ -0,0 +1,111 @@ +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.Services.Paging; +using Umbraco.Cms.ManagementApi.ViewModels.Pagination; +using Umbraco.Cms.ManagementApi.ViewModels.RecycleBin; + +namespace Umbraco.Cms.ManagementApi.Controllers.RecycleBin; + +public abstract class RecycleBinControllerBase : Controller + where TItem : RecycleBinItemViewModel, new() +{ + private readonly IEntityService _entityService; + private readonly string _itemUdiType; + + protected RecycleBinControllerBase(IEntityService entityService) + { + _entityService = entityService; + // ReSharper disable once VirtualMemberCallInConstructor + _itemUdiType = ItemObjectType.GetUdiType(); + } + + protected abstract UmbracoObjectTypes ItemObjectType { get; } + + protected abstract int RecycleBinRootId { get; } + + protected async Task>> GetRoot(int skip, int take) + { + if (PaginationService.ConvertSkipTakeToPaging(skip, take, out var pageNumber, out var pageSize, out ProblemDetails? error) == false) + { + return BadRequest(error); + } + + IEntitySlim[] rootEntities = GetPagedRootEntities(pageNumber, pageSize, out var totalItems); + + TItem[] treeItemViewModels = MapRecycleBinViewModels(null, rootEntities); + + PagedViewModel result = PagedViewModel(treeItemViewModels, totalItems); + return await Task.FromResult(Ok(result)); + } + + protected async Task>> GetChildren(Guid parentKey, int skip, int take) + { + if (PaginationService.ConvertSkipTakeToPaging(skip, take, out var pageNumber, out var pageSize, out ProblemDetails? error) == false) + { + return BadRequest(error); + } + + IEntitySlim[] children = GetPagedChildEntities(parentKey, pageNumber, pageSize, out var totalItems); + + TItem[] treeItemViewModels = MapRecycleBinViewModels(parentKey, children); + + PagedViewModel result = PagedViewModel(treeItemViewModels, totalItems); + + return await Task.FromResult(Ok(result)); + } + + protected virtual TItem MapRecycleBinViewModel(Guid? parentKey, IEntitySlim entity) + { + if (entity == null) + { + throw new ArgumentNullException(nameof(entity)); + } + + var viewModel = new TItem + { + Icon = _itemUdiType, + Name = entity.Name!, + Key = entity.Key, + Type = _itemUdiType, + HasChildren = entity.HasChildren, + IsContainer = entity.IsContainer, + ParentKey = parentKey + }; + + return viewModel; + } + + private IEntitySlim[] GetPagedRootEntities(long pageNumber, int pageSize, out long totalItems) + { + IEntitySlim[] rootEntities = _entityService + .GetPagedTrashedChildren(RecycleBinRootId, ItemObjectType, pageNumber, pageSize, out totalItems) + .ToArray(); + + return rootEntities; + } + + private IEntitySlim[] GetPagedChildEntities(Guid parentKey, long pageNumber, int pageSize, out long totalItems) + { + IEntitySlim? parent = _entityService.Get(parentKey, ItemObjectType); + if (parent == null || parent.Trashed == false) + { + // not much else we can do here but return nothing + totalItems = 0; + return Array.Empty(); + } + + IEntitySlim[] children = _entityService + .GetPagedTrashedChildren(parent.Id, ItemObjectType, pageNumber, pageSize, out totalItems) + .ToArray(); + + return children; + } + + private TItem[] MapRecycleBinViewModels(Guid? parentKey, IEntitySlim[] entities) + => entities.Select(entity => MapRecycleBinViewModel(parentKey, entity)).ToArray(); + + private PagedViewModel PagedViewModel(IEnumerable treeItemViewModels, long totalItems) + => new() { Total = totalItems, Items = treeItemViewModels }; +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Relation/ByChildRelationController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Relation/ByChildRelationController.cs new file mode 100644 index 0000000000..e78b070d56 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Relation/ByChildRelationController.cs @@ -0,0 +1,52 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.Factories; +using Umbraco.Cms.ManagementApi.ViewModels.Pagination; +using Umbraco.Cms.ManagementApi.ViewModels.Relation; +using Umbraco.Extensions; + +namespace Umbraco.Cms.ManagementApi.Controllers.Relation; + +public class ByChildRelationController : RelationControllerBase +{ + private readonly IRelationService _relationService; + private readonly IRelationViewModelFactory _relationViewModelFactory; + + public ByChildRelationController( + IRelationService relationService, + IRelationViewModelFactory relationViewModelFactory) + { + _relationService = relationService; + _relationViewModelFactory = relationViewModelFactory; + } + + [HttpGet("childRelations/{childId:int}")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task> ByChild(int childId, int skip, int take, string? relationTypeAlias = "") + { + IRelation[] relations = _relationService.GetByChildId(childId).ToArray(); + RelationViewModel[] result = Array.Empty(); + + if (relations.Any()) + { + if (string.IsNullOrWhiteSpace(relationTypeAlias) == false) + { + result = _relationViewModelFactory.CreateMultiple(relations.Where(x => + x.RelationType.Alias.InvariantEquals(relationTypeAlias))).ToArray(); + } + else + { + result = _relationViewModelFactory.CreateMultiple(relations).ToArray(); + } + } + + return await Task.FromResult(new PagedViewModel + { + Total = result.Length, + Items = result.Skip(skip).Take(take), + }); + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Relation/ByIdRelationController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Relation/ByIdRelationController.cs new file mode 100644 index 0000000000..a825aa4b16 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Relation/ByIdRelationController.cs @@ -0,0 +1,35 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.Factories; +using Umbraco.Cms.ManagementApi.ViewModels.Relation; + +namespace Umbraco.Cms.ManagementApi.Controllers.Relation; + +public class ByIdRelationController : RelationControllerBase +{ + private readonly IRelationService _relationService; + private readonly IRelationViewModelFactory _relationViewModelFactory; + + public ByIdRelationController(IRelationService relationService, IRelationViewModelFactory relationViewModelFactory) + { + _relationService = relationService; + _relationViewModelFactory = relationViewModelFactory; + } + + [HttpGet("{id:int}")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(RelationViewModel), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(NotFoundResult), StatusCodes.Status404NotFound)] + public async Task ById(int id) + { + IRelation? relation = _relationService.GetById(id); + if (relation is null) + { + return NotFound(); + } + + return await Task.FromResult(Ok(_relationViewModelFactory.Create(relation))); + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Relation/RelationControllerBase.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Relation/RelationControllerBase.cs new file mode 100644 index 0000000000..1c400c6ef1 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Relation/RelationControllerBase.cs @@ -0,0 +1,15 @@ +using Microsoft.AspNetCore.Mvc; +using NSwag.Annotations; +using Umbraco.New.Cms.Web.Common.Routing; + +namespace Umbraco.Cms.ManagementApi.Controllers.Relation; + +[ApiController] +[BackOfficeRoute("api/v{version:apiVersion}/relation")] +[OpenApiTag("Relation")] +[ApiVersion("1.0")] +// TODO: Implement Authentication +public abstract class RelationControllerBase : ManagementApiControllerBase +{ + +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/RelationType/Tree/ItemsRelationTypeTreeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/RelationType/Tree/ItemsRelationTypeTreeController.cs new file mode 100644 index 0000000000..7e2054a594 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/RelationType/Tree/ItemsRelationTypeTreeController.cs @@ -0,0 +1,32 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; + +namespace Umbraco.Cms.ManagementApi.Controllers.RelationType.Tree; + +public class ItemsRelationTypeTreeController : RelationTypeTreeControllerBase +{ + private readonly IRelationService _relationService; + + public ItemsRelationTypeTreeController(IEntityService entityService, IRelationService relationService) + : base(entityService) => + _relationService = relationService; + + [HttpGet("items")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + public async Task>> Items([FromQuery(Name = "key")] Guid[] keys) + { + // relation service does not allow fetching a collection of relation types by their ids; instead it relies + // heavily on caching, which means this is as fast as it gets - even if it looks less than performant + IRelationType[] relationTypes = _relationService + .GetAllRelationTypes() + .Where(relationType => keys.Contains(relationType.Key)).ToArray(); + + EntityTreeItemViewModel[] viewModels = MapTreeItemViewModels(null, relationTypes); + + return await Task.FromResult(Ok(viewModels)); + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/RelationType/Tree/RelationTypeTreeControllerBase.cs b/src/Umbraco.Cms.ManagementApi/Controllers/RelationType/Tree/RelationTypeTreeControllerBase.cs new file mode 100644 index 0000000000..c90c124686 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/RelationType/Tree/RelationTypeTreeControllerBase.cs @@ -0,0 +1,38 @@ +using Microsoft.AspNetCore.Mvc; +using NSwag.Annotations; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.Controllers.Tree; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; +using Umbraco.New.Cms.Web.Common.Routing; + +namespace Umbraco.Cms.ManagementApi.Controllers.RelationType.Tree; + +[ApiVersion("1.0")] +[ApiController] +[VersionedApiBackOfficeRoute($"{Constants.UdiEntityType.RelationType}/tree")] +[OpenApiTag(nameof(Constants.UdiEntityType.RelationType))] +// NOTE: at the moment relation types aren't supported by EntityService, so we have little use of the +// tree controller base. We'll keep it though, in the hope that we can mend EntityService. +public class RelationTypeTreeControllerBase : EntityTreeControllerBase +{ + public RelationTypeTreeControllerBase(IEntityService entityService) + : base(entityService) + { + } + + protected override UmbracoObjectTypes ItemObjectType => UmbracoObjectTypes.RelationType; + + protected EntityTreeItemViewModel[] MapTreeItemViewModels(Guid? parentKey, IRelationType[] relationTypes) + => relationTypes.Select(relationType => new EntityTreeItemViewModel + { + Icon = Constants.Icons.RelationType, + Name = relationType.Name!, + Key = relationType.Key, + Type = Constants.UdiEntityType.RelationType, + HasChildren = false, + IsContainer = false, + ParentKey = parentKey + }).ToArray(); +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/RelationType/Tree/RootRelationTypeTreeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/RelationType/Tree/RootRelationTypeTreeController.cs new file mode 100644 index 0000000000..2c49282147 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/RelationType/Tree/RootRelationTypeTreeController.cs @@ -0,0 +1,44 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.Services.Paging; +using Umbraco.Cms.ManagementApi.ViewModels.Pagination; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; + +namespace Umbraco.Cms.ManagementApi.Controllers.RelationType.Tree; + +public class RootRelationTypeTreeController : RelationTypeTreeControllerBase +{ + private readonly IRelationService _relationService; + + public RootRelationTypeTreeController(IEntityService entityService, IRelationService relationService) + : base(entityService) => + _relationService = relationService; + + [HttpGet("root")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task>> Root(int skip = 0, int take = 100) + { + if (PaginationService.ConvertSkipTakeToPaging(skip, take, out var pageNumber, out var pageSize, out ProblemDetails? error) == false) + { + return BadRequest(error); + } + + // pagination is not supported (yet) by relation service, so we do it in memory for now + // - chances are we won't have many relation types, so it won't be an actual issue + IRelationType[] allRelationTypes = _relationService.GetAllRelationTypes().ToArray(); + + EntityTreeItemViewModel[] viewModels = MapTreeItemViewModels( + null, + allRelationTypes + .OrderBy(relationType => relationType.Name) + .Skip((int)(pageNumber * pageSize)) + .Take(pageSize) + .ToArray()); + + PagedViewModel result = PagedViewModel(viewModels, allRelationTypes.Length); + return await Task.FromResult(Ok(result)); + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Script/Tree/ChildrenScriptTreeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Script/Tree/ChildrenScriptTreeController.cs new file mode 100644 index 0000000000..d84958ba80 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Script/Tree/ChildrenScriptTreeController.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.IO; +using Umbraco.Cms.ManagementApi.ViewModels.Pagination; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; + +namespace Umbraco.Cms.ManagementApi.Controllers.Script.Tree; + +public class ChildrenScriptTreeController : ScriptTreeControllerBase +{ + public ChildrenScriptTreeController(FileSystems fileSystems) + : base(fileSystems) + { + } + + [HttpGet("children")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task>> Children(string path, int skip = 0, int take = 100) + => await GetChildren(path, skip, take); +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Script/Tree/ItemsScriptTreeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Script/Tree/ItemsScriptTreeController.cs new file mode 100644 index 0000000000..99cd6d990e --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Script/Tree/ItemsScriptTreeController.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.IO; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; + +namespace Umbraco.Cms.ManagementApi.Controllers.Script.Tree; + +public class ItemsScriptTreeController : ScriptTreeControllerBase +{ + public ItemsScriptTreeController(FileSystems fileSystems) + : base(fileSystems) + { + } + + [HttpGet("items")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + public async Task>> Items([FromQuery(Name = "path")] string[] paths) + => await GetItems(paths); +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Script/Tree/RootScriptTreeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Script/Tree/RootScriptTreeController.cs new file mode 100644 index 0000000000..775a42f248 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Script/Tree/RootScriptTreeController.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.IO; +using Umbraco.Cms.ManagementApi.ViewModels.Pagination; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; + +namespace Umbraco.Cms.ManagementApi.Controllers.Script.Tree; + +public class RootScriptTreeController : ScriptTreeControllerBase +{ + public RootScriptTreeController(FileSystems fileSystems) + : base(fileSystems) + { + } + + [HttpGet("root")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task>> Root(int skip = 0, int take = 100) + => await GetRoot(skip, take); +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Script/Tree/ScriptTreeControllerBase.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Script/Tree/ScriptTreeControllerBase.cs new file mode 100644 index 0000000000..4e204da4ee --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Script/Tree/ScriptTreeControllerBase.cs @@ -0,0 +1,25 @@ +using Microsoft.AspNetCore.Mvc; +using NSwag.Annotations; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.IO; +using Umbraco.Cms.ManagementApi.Controllers.Tree; +using Umbraco.New.Cms.Web.Common.Routing; + +namespace Umbraco.Cms.ManagementApi.Controllers.Script.Tree; + +[ApiVersion("1.0")] +[ApiController] +[VersionedApiBackOfficeRoute($"{Constants.UdiEntityType.Script}/tree")] +[OpenApiTag(nameof(Constants.UdiEntityType.Script))] +public class ScriptTreeControllerBase : FileSystemTreeControllerBase +{ + public ScriptTreeControllerBase(FileSystems fileSystems) + => FileSystem = fileSystems.ScriptsFileSystem ?? + throw new ArgumentException("Missing scripts file system", nameof(fileSystems)); + + protected override IFileSystem FileSystem { get; } + + protected override string FileIcon(string path) => Constants.Icons.Script; + + protected override string ItemType(string path) => Constants.UdiEntityType.Script; +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Server/ServerControllerBase.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Server/ServerControllerBase.cs index cdb4921ba3..9dd6b3a192 100644 --- a/src/Umbraco.Cms.ManagementApi/Controllers/Server/ServerControllerBase.cs +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Server/ServerControllerBase.cs @@ -5,9 +5,9 @@ using Umbraco.New.Cms.Web.Common.Routing; namespace Umbraco.Cms.ManagementApi.Controllers.Server; [ApiController] -[BackOfficeRoute("api/v{version:apiVersion}/server")] +[VersionedApiBackOfficeRoute("server")] [OpenApiTag("Server")] -public abstract class ServerControllerBase : Controller +public abstract class ServerControllerBase : ManagementApiControllerBase { } diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Server/StatusServerController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Server/StatusServerController.cs index 875e685c27..5625eeff35 100644 --- a/src/Umbraco.Cms.ManagementApi/Controllers/Server/StatusServerController.cs +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Server/StatusServerController.cs @@ -17,5 +17,5 @@ public class StatusServerController : ServerControllerBase [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] [ProducesResponseType(typeof(ServerStatusViewModel), StatusCodes.Status200OK)] public async Task> Get() => - new ServerStatusViewModel { ServerStatus = _runtimeState.Level }; + await Task.FromResult(new ServerStatusViewModel { ServerStatus = _runtimeState.Level }); } diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Server/VersionServerController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Server/VersionServerController.cs index fbd4f271e7..903c850ddb 100644 --- a/src/Umbraco.Cms.ManagementApi/Controllers/Server/VersionServerController.cs +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Server/VersionServerController.cs @@ -18,5 +18,8 @@ public class VersionServerController : ServerControllerBase [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] [ProducesResponseType(typeof(VersionViewModel), StatusCodes.Status200OK)] public async Task> Get() => - new VersionViewModel { Version = _umbracoVersion.SemanticVersion.ToSemanticStringWithoutBuild() }; + await Task.FromResult(new VersionViewModel + { + Version = _umbracoVersion.SemanticVersion.ToSemanticStringWithoutBuild() + }); } diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/StaticFile/Tree/ChildrenStaticFileTreeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/StaticFile/Tree/ChildrenStaticFileTreeController.cs new file mode 100644 index 0000000000..71a659f336 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/StaticFile/Tree/ChildrenStaticFileTreeController.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.IO; +using Umbraco.Cms.ManagementApi.ViewModels.Pagination; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; + +namespace Umbraco.Cms.ManagementApi.Controllers.StaticFile.Tree; + +public class ChildrenStaticFileTreeController : StaticFileTreeControllerBase +{ + public ChildrenStaticFileTreeController(IPhysicalFileSystem physicalFileSystem) + : base(physicalFileSystem) + { + } + + [HttpGet("children")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task>> Children(string path, int skip = 0, int take = 100) + => await GetChildren(path, skip, take); +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/StaticFile/Tree/ItemsStaticFileTreeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/StaticFile/Tree/ItemsStaticFileTreeController.cs new file mode 100644 index 0000000000..205f92d94f --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/StaticFile/Tree/ItemsStaticFileTreeController.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.IO; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; + +namespace Umbraco.Cms.ManagementApi.Controllers.StaticFile.Tree; + +public class ItemsStaticFileTreeController : StaticFileTreeControllerBase +{ + public ItemsStaticFileTreeController(IPhysicalFileSystem physicalFileSystem) + : base(physicalFileSystem) + { + } + + [HttpGet("items")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + public async Task>> Items([FromQuery(Name = "path")] string[] paths) + => await GetItems(paths); +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/StaticFile/Tree/RootStaticFileTreeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/StaticFile/Tree/RootStaticFileTreeController.cs new file mode 100644 index 0000000000..925f7f1ecf --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/StaticFile/Tree/RootStaticFileTreeController.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.IO; +using Umbraco.Cms.ManagementApi.ViewModels.Pagination; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; + +namespace Umbraco.Cms.ManagementApi.Controllers.StaticFile.Tree; + +public class RootStaticFileTreeController : StaticFileTreeControllerBase +{ + public RootStaticFileTreeController(IPhysicalFileSystem physicalFileSystem) + : base(physicalFileSystem) + { + } + + [HttpGet("root")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task>> Root(int skip = 0, int take = 100) + => await GetRoot(skip, take); +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/StaticFile/Tree/StaticFileTreeControllerBase.cs b/src/Umbraco.Cms.ManagementApi/Controllers/StaticFile/Tree/StaticFileTreeControllerBase.cs new file mode 100644 index 0000000000..ec50a54495 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/StaticFile/Tree/StaticFileTreeControllerBase.cs @@ -0,0 +1,42 @@ +using Microsoft.AspNetCore.Mvc; +using NSwag.Annotations; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.IO; +using Umbraco.Cms.ManagementApi.Controllers.Tree; +using Umbraco.New.Cms.Web.Common.Routing; + +namespace Umbraco.Cms.ManagementApi.Controllers.StaticFile.Tree; + +[ApiVersion("1.0")] +[ApiController] +[VersionedApiBackOfficeRoute("static-file/tree")] +[OpenApiTag("StaticFile")] +public class StaticFileTreeControllerBase : FileSystemTreeControllerBase +{ + private static readonly string[] _allowedRootFolders = { "App_Plugins", "wwwroot" }; + + public StaticFileTreeControllerBase(IPhysicalFileSystem physicalFileSystem) + => FileSystem = physicalFileSystem; + + protected override IFileSystem FileSystem { get; } + + protected override string FileIcon(string path) => Constants.Icons.DefaultIcon; + + protected override string ItemType(string path) => "static-file"; + + protected override string[] GetDirectories(string path) => + IsTreeRootPath(path) + ? _allowedRootFolders + : IsAllowedPath(path) + ? base.GetDirectories(path) + : Array.Empty(); + + protected override string[] GetFiles(string path) + => IsTreeRootPath(path) || IsAllowedPath(path) == false + ? Array.Empty() + : base.GetFiles(path); + + private bool IsTreeRootPath(string path) => string.IsNullOrWhiteSpace(path); + + private bool IsAllowedPath(string path) => _allowedRootFolders.Contains(path) || _allowedRootFolders.Any(folder => path.StartsWith($"{folder}/")); +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Stylesheet/Tree/ChildrenStylesheetTreeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Stylesheet/Tree/ChildrenStylesheetTreeController.cs new file mode 100644 index 0000000000..abd50401d5 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Stylesheet/Tree/ChildrenStylesheetTreeController.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.IO; +using Umbraco.Cms.ManagementApi.ViewModels.Pagination; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; + +namespace Umbraco.Cms.ManagementApi.Controllers.Stylesheet.Tree; + +public class ChildrenStylesheetTreeController : StylesheetTreeControllerBase +{ + public ChildrenStylesheetTreeController(FileSystems fileSystems) + : base(fileSystems) + { + } + + [HttpGet("children")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task>> Children(string path, int skip = 0, int take = 100) + => await GetChildren(path, skip, take); +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Stylesheet/Tree/ItemsStylesheetTreeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Stylesheet/Tree/ItemsStylesheetTreeController.cs new file mode 100644 index 0000000000..de2e779ba1 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Stylesheet/Tree/ItemsStylesheetTreeController.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.IO; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; + +namespace Umbraco.Cms.ManagementApi.Controllers.Stylesheet.Tree; + +public class ItemsStylesheetTreeController : StylesheetTreeControllerBase +{ + public ItemsStylesheetTreeController(FileSystems fileSystems) + : base(fileSystems) + { + } + + [HttpGet("items")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + public async Task>> Items([FromQuery(Name = "path")] string[] paths) + => await GetItems(paths); +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Stylesheet/Tree/RootStylesheetTreeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Stylesheet/Tree/RootStylesheetTreeController.cs new file mode 100644 index 0000000000..ca5138befc --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Stylesheet/Tree/RootStylesheetTreeController.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.IO; +using Umbraco.Cms.ManagementApi.ViewModels.Pagination; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; + +namespace Umbraco.Cms.ManagementApi.Controllers.Stylesheet.Tree; + +public class RootStylesheetTreeController : StylesheetTreeControllerBase +{ + public RootStylesheetTreeController(FileSystems fileSystems) + : base(fileSystems) + { + } + + [HttpGet("root")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task>> Root(int skip = 0, int take = 100) + => await GetRoot(skip, take); +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Stylesheet/Tree/StylesheetTreeControllerBase.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Stylesheet/Tree/StylesheetTreeControllerBase.cs new file mode 100644 index 0000000000..b529752293 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Stylesheet/Tree/StylesheetTreeControllerBase.cs @@ -0,0 +1,25 @@ +using Microsoft.AspNetCore.Mvc; +using NSwag.Annotations; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.IO; +using Umbraco.Cms.ManagementApi.Controllers.Tree; +using Umbraco.New.Cms.Web.Common.Routing; + +namespace Umbraco.Cms.ManagementApi.Controllers.Stylesheet.Tree; + +[ApiVersion("1.0")] +[ApiController] +[VersionedApiBackOfficeRoute($"{Constants.UdiEntityType.Stylesheet}/tree")] +[OpenApiTag(nameof(Constants.UdiEntityType.Stylesheet))] +public class StylesheetTreeControllerBase : FileSystemTreeControllerBase +{ + public StylesheetTreeControllerBase(FileSystems fileSystems) + => FileSystem = fileSystems.StylesheetsFileSystem ?? + throw new ArgumentException("Missing stylesheets file system", nameof(fileSystems)); + + protected override IFileSystem FileSystem { get; } + + protected override string FileIcon(string path) => Constants.Icons.Stylesheet; + + protected override string ItemType(string path) => Constants.UdiEntityType.Stylesheet; +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Template/Tree/ChildrenTemplateTreeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Template/Tree/ChildrenTemplateTreeController.cs new file mode 100644 index 0000000000..6ae058a7b0 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Template/Tree/ChildrenTemplateTreeController.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.ViewModels.Pagination; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; + +namespace Umbraco.Cms.ManagementApi.Controllers.Template.Tree; + +public class ChildrenTemplateTreeController : TemplateTreeControllerBase +{ + public ChildrenTemplateTreeController(IEntityService entityService) + : base(entityService) + { + } + + [HttpGet("children")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task>> Children(Guid parentKey, int skip = 0, int take = 100) + => await GetChildren(parentKey, skip, take); +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Template/Tree/ItemsTemplateTreeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Template/Tree/ItemsTemplateTreeController.cs new file mode 100644 index 0000000000..fb4a29c621 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Template/Tree/ItemsTemplateTreeController.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; + +namespace Umbraco.Cms.ManagementApi.Controllers.Template.Tree; + +public class ItemsTemplateTreeController : TemplateTreeControllerBase +{ + public ItemsTemplateTreeController(IEntityService entityService) + : base(entityService) + { + } + + [HttpGet("items")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + public async Task>> Items([FromQuery(Name = "key")] Guid[] keys) + => await GetItems(keys); +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Template/Tree/RootTemplateTreeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Template/Tree/RootTemplateTreeController.cs new file mode 100644 index 0000000000..af03a8b316 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Template/Tree/RootTemplateTreeController.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.ViewModels.Pagination; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; + +namespace Umbraco.Cms.ManagementApi.Controllers.Template.Tree; + +public class RootTemplateTreeController : TemplateTreeControllerBase +{ + public RootTemplateTreeController(IEntityService entityService) + : base(entityService) + { + } + + [HttpGet("root")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task>> Root(int skip = 0, int take = 100) + => await GetRoot(skip, take); +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Template/Tree/TemplateTreeControllerBase.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Template/Tree/TemplateTreeControllerBase.cs new file mode 100644 index 0000000000..be885f26be --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Template/Tree/TemplateTreeControllerBase.cs @@ -0,0 +1,32 @@ +using Microsoft.AspNetCore.Mvc; +using NSwag.Annotations; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.Controllers.Tree; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; +using Umbraco.New.Cms.Web.Common.Routing; + +namespace Umbraco.Cms.ManagementApi.Controllers.Template.Tree; + +[ApiVersion("1.0")] +[ApiController] +[VersionedApiBackOfficeRoute($"{Constants.UdiEntityType.Template}/tree")] +[OpenApiTag(nameof(Constants.UdiEntityType.Template))] +public class TemplateTreeControllerBase : EntityTreeControllerBase +{ + public TemplateTreeControllerBase(IEntityService entityService) + : base(entityService) + { + } + + protected override UmbracoObjectTypes ItemObjectType => UmbracoObjectTypes.Template; + + protected override EntityTreeItemViewModel MapTreeItemViewModel(Guid? parentKey, IEntitySlim entity) + { + EntityTreeItemViewModel viewModel = base.MapTreeItemViewModel(parentKey, entity); + viewModel.Icon = Constants.Icons.Template; + return viewModel; + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/TrackedReferences/DescendantsTrackedReferencesController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/TrackedReferences/DescendantsTrackedReferencesController.cs new file mode 100644 index 0000000000..ecac832a84 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/TrackedReferences/DescendantsTrackedReferencesController.cs @@ -0,0 +1,45 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.ViewModels.Pagination; +using Umbraco.Cms.ManagementApi.ViewModels.TrackedReferences; +using Umbraco.New.Cms.Core.Models; + +namespace Umbraco.Cms.ManagementApi.Controllers.TrackedReferences; + +public class DescendantsTrackedReferencesController : TrackedReferencesControllerBase +{ + private readonly ITrackedReferencesService _trackedReferencesSkipTakeService; + private readonly IUmbracoMapper _umbracoMapper; + + public DescendantsTrackedReferencesController(ITrackedReferencesService trackedReferencesSkipTakeService, IUmbracoMapper umbracoMapper) + { + _trackedReferencesSkipTakeService = trackedReferencesSkipTakeService; + _umbracoMapper = umbracoMapper; + } + + /// + /// Gets a page list of the child nodes of the current item used in any kind of relation. + /// + /// + /// Used when deleting and unpublishing a single item to check if this item has any descending items that are in any + /// kind of relation. + /// This is basically finding the descending items which are children in relations. + /// + [HttpGet("descendants/{parentId:int}")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task>> Descendants(int parentId, long skip, long take, bool? filterMustBeIsDependency) + { + PagedModel relationItems = _trackedReferencesSkipTakeService.GetPagedDescendantsInReferences(parentId, skip, take, filterMustBeIsDependency ?? true); + var pagedViewModel = new PagedViewModel + { + Total = relationItems.Total, + Items = _umbracoMapper.MapEnumerable(relationItems.Items), + }; + + return await Task.FromResult(pagedViewModel); + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/TrackedReferences/ForItemTrackedReferencesController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/TrackedReferences/ForItemTrackedReferencesController.cs new file mode 100644 index 0000000000..a415cf08bf --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/TrackedReferences/ForItemTrackedReferencesController.cs @@ -0,0 +1,49 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.ViewModels.Pagination; +using Umbraco.Cms.ManagementApi.ViewModels.TrackedReferences; +using Umbraco.New.Cms.Core.Models; + +namespace Umbraco.Cms.ManagementApi.Controllers.TrackedReferences; + +public class ForItemTrackedReferencesController : TrackedReferencesControllerBase +{ + private readonly ITrackedReferencesService _trackedReferencesService; + private readonly IUmbracoMapper _umbracoMapper; + + public ForItemTrackedReferencesController(ITrackedReferencesService trackedReferencesService, IUmbracoMapper umbracoMapper) + { + _trackedReferencesService = trackedReferencesService; + _umbracoMapper = umbracoMapper; + } + + /// + /// Gets a page list of tracked references for the current item, so you can see where an item is being used. + /// + /// + /// Used by info tabs on content, media etc. and for the delete and unpublish of single items. + /// This is basically finding parents of relations. + /// + [HttpGet("{id:int}")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task>> Get( + int id, + long skip, + long take, + bool? filterMustBeIsDependency) + { + PagedModel relationItems = _trackedReferencesService.GetPagedRelationsForItem(id, skip, take, filterMustBeIsDependency ?? false); + + var pagedViewModel = new PagedViewModel + { + Total = relationItems.Total, + Items = _umbracoMapper.MapEnumerable(relationItems.Items), + }; + + return await Task.FromResult(pagedViewModel); + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/TrackedReferences/MultipleTrackedReferencesController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/TrackedReferences/MultipleTrackedReferencesController.cs new file mode 100644 index 0000000000..041d208915 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/TrackedReferences/MultipleTrackedReferencesController.cs @@ -0,0 +1,44 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.ViewModels.Pagination; +using Umbraco.Cms.ManagementApi.ViewModels.TrackedReferences; +using Umbraco.New.Cms.Core.Models; + +namespace Umbraco.Cms.ManagementApi.Controllers.TrackedReferences; + +public class MultipleTrackedReferencesController : TrackedReferencesControllerBase +{ + private readonly ITrackedReferencesService _trackedReferencesSkipTakeService; + private readonly IUmbracoMapper _umbracoMapper; + + public MultipleTrackedReferencesController(ITrackedReferencesService trackedReferencesSkipTakeService, IUmbracoMapper umbracoMapper) + { + _trackedReferencesSkipTakeService = trackedReferencesSkipTakeService; + _umbracoMapper = umbracoMapper; + } + + /// + /// Gets a page list of the items used in any kind of relation from selected integer ids. + /// + /// + /// Used when bulk deleting content/media and bulk unpublishing content (delete and unpublish on List view). + /// This is basically finding children of relations. + /// + [HttpGet("multiple")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task>> GetPagedReferencedItems([FromQuery]int[] ids, long skip, long take, bool? filterMustBeIsDependency) + { + PagedModel relationItems = _trackedReferencesSkipTakeService.GetPagedItemsWithRelations(ids, skip, take, filterMustBeIsDependency ?? true); + var pagedViewModel = new PagedViewModel + { + Total = relationItems.Total, + Items = _umbracoMapper.MapEnumerable(relationItems.Items), + }; + + return await Task.FromResult(pagedViewModel); + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/TrackedReferences/TrackedReferencesControllerBase.cs b/src/Umbraco.Cms.ManagementApi/Controllers/TrackedReferences/TrackedReferencesControllerBase.cs new file mode 100644 index 0000000000..8ed6c23112 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/TrackedReferences/TrackedReferencesControllerBase.cs @@ -0,0 +1,13 @@ +using Microsoft.AspNetCore.Mvc; +using NSwag.Annotations; +using Umbraco.New.Cms.Web.Common.Routing; + +namespace Umbraco.Cms.ManagementApi.Controllers.TrackedReferences; + +[ApiController] +[BackOfficeRoute("api/v{version:apiVersion}/trackedReferences")] +[OpenApiTag("TrackedReferences")] +[ApiVersion("1.0")] +public abstract class TrackedReferencesControllerBase : ManagementApiControllerBase +{ +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Tree/EntityTreeControllerBase.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Tree/EntityTreeControllerBase.cs new file mode 100644 index 0000000000..3cd05e5eb1 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Tree/EntityTreeControllerBase.cs @@ -0,0 +1,132 @@ +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.Services.Paging; +using Umbraco.Cms.ManagementApi.ViewModels.Pagination; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; +using Umbraco.Extensions; + +namespace Umbraco.Cms.ManagementApi.Controllers.Tree; + +public abstract class EntityTreeControllerBase : ManagementApiControllerBase + where TItem : EntityTreeItemViewModel, new() +{ + private readonly string _itemUdiType; + + protected EntityTreeControllerBase(IEntityService entityService) + { + EntityService = entityService; + + // ReSharper disable once VirtualMemberCallInConstructor + _itemUdiType = ItemObjectType.GetUdiType(); + } + + protected IEntityService EntityService { get; } + + protected abstract UmbracoObjectTypes ItemObjectType { get; } + + protected virtual Ordering ItemOrdering => Ordering.By(nameof(Infrastructure.Persistence.Dtos.NodeDto.Text)); + + protected async Task>> GetRoot(int skip, int take) + { + if (PaginationService.ConvertSkipTakeToPaging(skip, take, out var pageNumber, out var pageSize, out ProblemDetails? error) == false) + { + return BadRequest(error); + } + + IEntitySlim[] rootEntities = GetPagedRootEntities(pageNumber, pageSize, out var totalItems); + + TItem[] treeItemViewModels = MapTreeItemViewModels(null, rootEntities); + + PagedViewModel result = PagedViewModel(treeItemViewModels, totalItems); + return await Task.FromResult(Ok(result)); + } + + protected async Task>> GetChildren(Guid parentKey, int skip, int take) + { + if (PaginationService.ConvertSkipTakeToPaging(skip, take, out var pageNumber, out var pageSize, out ProblemDetails? error) == false) + { + return BadRequest(error); + } + + IEntitySlim[] children = GetPagedChildEntities(parentKey, pageNumber, pageSize, out var totalItems); + + TItem[] treeItemViewModels = MapTreeItemViewModels(parentKey, children); + + PagedViewModel result = PagedViewModel(treeItemViewModels, totalItems); + return await Task.FromResult(Ok(result)); + } + + protected async Task>> GetItems(Guid[] keys) + { + if (keys.IsCollectionEmpty()) + { + return await Task.FromResult(Ok(PagedViewModel(Array.Empty(), 0))); + } + + IEntitySlim[] itemEntities = GetEntities(keys); + + TItem[] treeItemViewModels = MapTreeItemViewModels(null, itemEntities); + + return await Task.FromResult(Ok(treeItemViewModels)); + } + + protected virtual IEntitySlim[] GetPagedRootEntities(long pageNumber, int pageSize, out long totalItems) + => EntityService + .GetPagedChildren( + Constants.System.Root, + ItemObjectType, + pageNumber, + pageSize, + out totalItems, + ordering: ItemOrdering) + .ToArray(); + + protected virtual IEntitySlim[] GetPagedChildEntities(Guid parentKey, long pageNumber, int pageSize, out long totalItems) + { + // EntityService is only able to get paged children by parent ID, so we must first map parent key to parent ID + Attempt parentId = EntityService.GetId(parentKey, ItemObjectType); + if (parentId.Success == false) + { + // not much else we can do here but return nothing + totalItems = 0; + return Array.Empty(); + } + + IEntitySlim[] children = EntityService.GetPagedChildren( + parentId.Result, + ItemObjectType, + pageNumber, + pageSize, + out totalItems, + ordering: ItemOrdering) + .ToArray(); + return children; + } + + protected virtual IEntitySlim[] GetEntities(Guid[] keys) => EntityService.GetAll(ItemObjectType, keys).ToArray(); + + protected virtual TItem[] MapTreeItemViewModels(Guid? parentKey, IEntitySlim[] entities) + => entities.Select(entity => MapTreeItemViewModel(parentKey, entity)).ToArray(); + + protected virtual TItem MapTreeItemViewModel(Guid? parentKey, IEntitySlim entity) + { + var viewModel = new TItem + { + Icon = _itemUdiType, + Name = entity.Name!, + Key = entity.Key, + Type = _itemUdiType, + HasChildren = entity.HasChildren, + IsContainer = entity.IsContainer, + ParentKey = parentKey + }; + + return viewModel; + } + + protected PagedViewModel PagedViewModel(IEnumerable treeItemViewModels, long totalItems) + => new() { Total = totalItems, Items = treeItemViewModels }; +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Tree/FileSystemTreeControllerBase.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Tree/FileSystemTreeControllerBase.cs new file mode 100644 index 0000000000..76113312a7 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Tree/FileSystemTreeControllerBase.cs @@ -0,0 +1,110 @@ +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.IO; +using Umbraco.Cms.ManagementApi.Services.Paging; +using Umbraco.Cms.ManagementApi.ViewModels.Pagination; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; +using Umbraco.Extensions; + +namespace Umbraco.Cms.ManagementApi.Controllers.Tree; + +public abstract class FileSystemTreeControllerBase : ManagementApiControllerBase +{ + protected abstract IFileSystem FileSystem { get; } + + protected abstract string FileIcon(string path); + + protected abstract string ItemType(string path); + + protected async Task>> GetRoot(int skip, int take) + { + if (PaginationService.ConvertSkipTakeToPaging(skip, take, out var pageNumber, out var pageSize, out ProblemDetails? error) == false) + { + return BadRequest(error); + } + + FileSystemTreeItemViewModel[] viewModels = GetPathViewModels(string.Empty, pageNumber, pageSize, out var totalItems); + + PagedViewModel result = PagedViewModel(viewModels, totalItems); + return await Task.FromResult(Ok(result)); + } + + protected async Task>> GetChildren(string path, int skip, int take) + { + if (PaginationService.ConvertSkipTakeToPaging(skip, take, out var pageNumber, out var pageSize, out ProblemDetails? error) == false) + { + return BadRequest(error); + } + + FileSystemTreeItemViewModel[] viewModels = GetPathViewModels(path, pageNumber, pageSize, out var totalItems); + + PagedViewModel result = PagedViewModel(viewModels, totalItems); + return await Task.FromResult(Ok(result)); + } + + protected async Task>> GetItems(string[] paths) + { + FileSystemTreeItemViewModel[] viewModels = paths + .Where(FileSystem.FileExists) + .Select(path => + { + var fileName = GetFileName(path); + return fileName.IsNullOrWhiteSpace() + ? null + : MapViewModel(path, fileName, false); + }).WhereNotNull().ToArray(); + + return await Task.FromResult(Ok(viewModels)); + } + + protected virtual string[] GetDirectories(string path) => FileSystem + .GetDirectories(path) + .OrderBy(directory => directory) + .ToArray(); + + protected virtual string[] GetFiles(string path) => FileSystem + .GetFiles(path) + .OrderBy(file => file) + .ToArray(); + + protected virtual string GetFileName(string path) => FileSystem.GetFileName(path); + + protected virtual bool DirectoryHasChildren(string path) + => FileSystem.GetFiles(path).Any() || FileSystem.GetDirectories(path).Any(); + + private FileSystemTreeItemViewModel[] GetPathViewModels(string path, long pageNumber, int pageSize, out long totalItems) + { + var allItems = GetDirectories(path) + .Select(directory => new { Path = directory, IsFolder = true }) + .Union(GetFiles(path).Select(file => new { Path = file, IsFolder = false })) + .ToArray(); + + totalItems = allItems.Length; + + FileSystemTreeItemViewModel ViewModel(string itemPath, bool isFolder) + => MapViewModel( + itemPath, + isFolder ? Path.GetFileName(itemPath) : FileSystem.GetFileName(itemPath), + isFolder); + + return allItems + .Skip((int)(pageNumber * pageSize)) + .Take(pageSize) + .Select(item => ViewModel(item.Path, item.IsFolder)) + .ToArray(); + } + + private PagedViewModel PagedViewModel(IEnumerable viewModels, long totalItems) + => new() { Total = totalItems, Items = viewModels }; + + private FileSystemTreeItemViewModel MapViewModel(string path, string name, bool isFolder) + => new() + { + Path = path, + Name = name, + Icon = isFolder ? Constants.Icons.Folder : FileIcon(path), + HasChildren = isFolder && DirectoryHasChildren(path), + Type = ItemType(path), + IsFolder = isFolder + }; +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Tree/FolderTreeControllerBase.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Tree/FolderTreeControllerBase.cs new file mode 100644 index 0000000000..778dd1dd8e --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Tree/FolderTreeControllerBase.cs @@ -0,0 +1,88 @@ +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; + +namespace Umbraco.Cms.ManagementApi.Controllers.Tree; + +public abstract class FolderTreeControllerBase : EntityTreeControllerBase + where TItem : FolderTreeItemViewModel, new() +{ + private readonly Guid _folderObjectTypeId; + private bool _foldersOnly; + + protected FolderTreeControllerBase(IEntityService entityService) + : base(entityService) => + // ReSharper disable once VirtualMemberCallInConstructor + _folderObjectTypeId = FolderObjectType.GetGuid(); + + protected abstract UmbracoObjectTypes FolderObjectType { get; } + + protected void RenderFoldersOnly(bool foldersOnly) => _foldersOnly = foldersOnly; + + protected override IEntitySlim[] GetPagedRootEntities(long pageNumber, int pageSize, out long totalItems) + => GetEntities( + Constants.System.Root, + pageNumber, + pageSize, + out totalItems); + + protected override IEntitySlim[] GetPagedChildEntities(Guid parentKey, long pageNumber, int pageSize, out long totalItems) + { + // EntityService is only able to get paged children by parent ID, so we must first map parent key to parent ID + Attempt parentId = EntityService.GetId(parentKey, FolderObjectType); + if (parentId.Success == false) + { + parentId = EntityService.GetId(parentKey, ItemObjectType); + if (parentId.Success == false) + { + // not much else we can do here but return nothing + totalItems = 0; + return Array.Empty(); + } + } + + return GetEntities( + parentId.Result, + pageNumber, + pageSize, + out totalItems); + } + + protected override TItem MapTreeItemViewModel(Guid? parentKey, IEntitySlim entity) + { + TItem viewModel = base.MapTreeItemViewModel(parentKey, entity); + + if (entity.NodeObjectType == _folderObjectTypeId) + { + viewModel.IsFolder = true; + viewModel.Icon = Constants.Icons.Folder; + } + + return viewModel; + } + + private IEntitySlim[] GetEntities(int parentId, long pageNumber, int pageSize, out long totalItems) + { + totalItems = 0; + + // EntityService is not able to paginate children of multiple item types, so we will only paginate the + // item type entities and always return all folders as part of the the first result page + IEntitySlim[] folderEntities = pageNumber == 0 + ? EntityService.GetChildren(parentId, FolderObjectType).OrderBy(c => c.Name).ToArray() + : Array.Empty(); + IEntitySlim[] itemEntities = _foldersOnly + ? Array.Empty() + : EntityService.GetPagedChildren( + parentId, + ItemObjectType, + pageNumber, + pageSize, + out totalItems, + ordering: ItemOrdering) + .ToArray(); + + return folderEntities.Union(itemEntities).ToArray(); + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Tree/UserStartNodeTreeControllerBase.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Tree/UserStartNodeTreeControllerBase.cs new file mode 100644 index 0000000000..63cf90dc5c --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Tree/UserStartNodeTreeControllerBase.cs @@ -0,0 +1,117 @@ +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.Models.Entities; +using Umbraco.Cms.ManagementApi.Services.Entities; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; +using Umbraco.Extensions; + +namespace Umbraco.Cms.ManagementApi.Controllers.Tree; + +public abstract class UserStartNodeTreeControllerBase : EntityTreeControllerBase + where TItem : ContentTreeItemViewModel, new() +{ + private readonly IUserStartNodeEntitiesService _userStartNodeEntitiesService; + private readonly IDataTypeService _dataTypeService; + + private int[]? _userStartNodeIds; + private string[]? _userStartNodePaths; + private Dictionary _accessMap = new(); + private Guid? _dataTypeKey; + + protected UserStartNodeTreeControllerBase( + IEntityService entityService, + IUserStartNodeEntitiesService userStartNodeEntitiesService, + IDataTypeService dataTypeService) + : base(entityService) + { + _userStartNodeEntitiesService = userStartNodeEntitiesService; + _dataTypeService = dataTypeService; + } + + protected abstract int[] GetUserStartNodeIds(); + + protected abstract string[] GetUserStartNodePaths(); + + protected void IgnoreUserStartNodesForDataType(Guid? dataTypeKey) => _dataTypeKey = dataTypeKey; + + protected override IEntitySlim[] GetPagedRootEntities(long pageNumber, int pageSize, out long totalItems) + => UserHasRootAccess() || IgnoreUserStartNodes() + ? base.GetPagedRootEntities(pageNumber, pageSize, out totalItems) + : CalculateAccessMap(() => _userStartNodeEntitiesService.RootUserAccessEntities(ItemObjectType, UserStartNodeIds), out totalItems); + + protected override IEntitySlim[] GetPagedChildEntities(Guid parentKey, long pageNumber, int pageSize, out long totalItems) + { + IEntitySlim[] children = base.GetPagedChildEntities(parentKey, pageNumber, pageSize, out totalItems); + return UserHasRootAccess() || IgnoreUserStartNodes() + ? children + : CalculateAccessMap(() => _userStartNodeEntitiesService.ChildUserAccessEntities(children, UserStartNodePaths), out totalItems); + } + + protected override IEntitySlim[] GetEntities(Guid[] keys) + { + IEntitySlim[] entities = base.GetEntities(keys); + return UserHasRootAccess() || IgnoreUserStartNodes() + ? entities + : CalculateAccessMap(() => _userStartNodeEntitiesService.UserAccessEntities(entities, UserStartNodePaths), out _); + } + + protected override TItem[] MapTreeItemViewModels(Guid? parentKey, IEntitySlim[] entities) + { + if (UserHasRootAccess() || IgnoreUserStartNodes()) + { + return base.MapTreeItemViewModels(parentKey, entities); + } + + // for users with no root access, only add items for the entities contained within the calculated access map. + // the access map may contain entities that the user does not have direct access to, but need still to see, + // because it has descendants that the user *does* have access to. these entities are added as "no access" items. + TItem[] contentTreeItemViewModels = entities.Select(entity => + { + if (_accessMap.TryGetValue(entity.Key, out var hasAccess) == false) + { + // entity is not a part of the calculated access map + return null; + } + + // direct access => return a regular item + // no direct access => return a "no access" item + return hasAccess + ? MapTreeItemViewModel(parentKey, entity) + : MapTreeItemViewModelAsNoAccess(parentKey, entity); + }) + .WhereNotNull() + .ToArray(); + + return contentTreeItemViewModels; + } + + private int[] UserStartNodeIds => _userStartNodeIds ??= GetUserStartNodeIds(); + + private string[] UserStartNodePaths => _userStartNodePaths ??= GetUserStartNodePaths(); + + private bool UserHasRootAccess() => UserStartNodeIds.Contains(Constants.System.Root); + + private bool IgnoreUserStartNodes() + => _dataTypeKey.HasValue + && _dataTypeService.IsDataTypeIgnoringUserStartNodes(_dataTypeKey.Value); + + private IEntitySlim[] CalculateAccessMap(Func> getUserAccessEntities, out long totalItems) + { + UserAccessEntity[] userAccessEntities = getUserAccessEntities().ToArray(); + + _accessMap = userAccessEntities.ToDictionary(uae => uae.Entity.Key, uae => uae.HasAccess); + + IEntitySlim[] entities = userAccessEntities.Select(uae => uae.Entity).ToArray(); + totalItems = entities.Length; + + return entities; + } + + private TItem MapTreeItemViewModelAsNoAccess(Guid? parentKey, IEntitySlim entity) + { + TItem viewModel = MapTreeItemViewModel(parentKey, entity); + viewModel.NoAccess = true; + return viewModel; + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Upgrade/SettingsUpgradeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Upgrade/SettingsUpgradeController.cs index a7a92cddc7..d86aae2eae 100644 --- a/src/Umbraco.Cms.ManagementApi/Controllers/Upgrade/SettingsUpgradeController.cs +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Upgrade/SettingsUpgradeController.cs @@ -33,6 +33,6 @@ public class SettingsUpgradeController : UpgradeControllerBase UpgradeSettingsModel upgradeSettings = _upgradeSettingsFactory.GetUpgradeSettings(); UpgradeSettingsViewModel viewModel = _mapper.Map(upgradeSettings)!; - return viewModel; + return await Task.FromResult(viewModel); } } diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Upgrade/UpgradeControllerBase.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Upgrade/UpgradeControllerBase.cs index 2b489501ec..084515aba2 100644 --- a/src/Umbraco.Cms.ManagementApi/Controllers/Upgrade/UpgradeControllerBase.cs +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Upgrade/UpgradeControllerBase.cs @@ -10,9 +10,9 @@ namespace Umbraco.Cms.ManagementApi.Controllers.Upgrade; [ApiController] [RequireRuntimeLevel(RuntimeLevel.Upgrade)] -[BackOfficeRoute("api/v{version:apiVersion}/upgrade")] +[VersionedApiBackOfficeRoute("upgrade")] [OpenApiTag("Upgrade")] -public abstract class UpgradeControllerBase : Controller +public abstract class UpgradeControllerBase : ManagementApiControllerBase { } diff --git a/src/Umbraco.Cms.ManagementApi/DependencyInjection/ExamineManagementBuilderExtensions.cs b/src/Umbraco.Cms.ManagementApi/DependencyInjection/ExamineManagementBuilderExtensions.cs new file mode 100644 index 0000000000..c6578667e8 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/DependencyInjection/ExamineManagementBuilderExtensions.cs @@ -0,0 +1,24 @@ +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Infrastructure.Examine; +using Umbraco.Cms.ManagementApi.Factories; +using Umbraco.Cms.ManagementApi.Services; +using Umbraco.New.Cms.Infrastructure.Services; + +namespace Umbraco.Cms.ManagementApi.DependencyInjection; + +public static class ExamineManagementBuilderExtensions +{ + internal static IUmbracoBuilder AddExamineManagement(this IUmbracoBuilder builder) + { + // Add examine service + builder.Services.AddTransient(); + builder.Services.AddTransient(); + + // Add factories + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + return builder; + } +} diff --git a/src/Umbraco.Cms.ManagementApi/DependencyInjection/FactoryBuilderExtensions.cs b/src/Umbraco.Cms.ManagementApi/DependencyInjection/FactoryBuilderExtensions.cs new file mode 100644 index 0000000000..5df44f4c8a --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/DependencyInjection/FactoryBuilderExtensions.cs @@ -0,0 +1,17 @@ +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.ManagementApi.Factories; +using Umbraco.New.Cms.Core.Factories; + +namespace Umbraco.Cms.ManagementApi.DependencyInjection; + +public static class FactoryBuilderExtensions +{ + internal static IUmbracoBuilder AddFactories(this IUmbracoBuilder builder) + { + builder.Services.AddTransient(); + builder.Services.AddTransient(); + return builder; + } + +} diff --git a/src/Umbraco.Cms.ManagementApi/DependencyInjection/InstallerBuilderExtensions.cs b/src/Umbraco.Cms.ManagementApi/DependencyInjection/InstallerBuilderExtensions.cs index 385cd1ff51..2dd13b659b 100644 --- a/src/Umbraco.Cms.ManagementApi/DependencyInjection/InstallerBuilderExtensions.cs +++ b/src/Umbraco.Cms.ManagementApi/DependencyInjection/InstallerBuilderExtensions.cs @@ -1,7 +1,10 @@ using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.ManagementApi.Mapping.Dictionary; using Umbraco.Cms.ManagementApi.Mapping.Installer; +using Umbraco.Cms.ManagementApi.Services.Entities; +using Umbraco.Cms.ManagementApi.Services.Paging; using Umbraco.New.Cms.Core.Factories; using Umbraco.New.Cms.Core.Installer; using Umbraco.New.Cms.Core.Installer.Steps; @@ -18,9 +21,6 @@ public static class InstallerBuilderExtensions { IServiceCollection services = builder.Services; - builder.WithCollectionBuilder() - .Add(); - services.AddTransient(); services.AddTransient(); services.AddTransient(); @@ -76,4 +76,10 @@ public static class InstallerBuilderExtensions public static UpgradeStepCollectionBuilder UpgradeSteps(this IUmbracoBuilder builder) => builder.WithCollectionBuilder(); + + internal static IUmbracoBuilder AddTrees(this IUmbracoBuilder builder) + { + builder.Services.AddTransient(); + return builder; + } } diff --git a/src/Umbraco.Cms.ManagementApi/DependencyInjection/MappingBuilderExtensions.cs b/src/Umbraco.Cms.ManagementApi/DependencyInjection/MappingBuilderExtensions.cs new file mode 100644 index 0000000000..109d9a9f7c --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/DependencyInjection/MappingBuilderExtensions.cs @@ -0,0 +1,28 @@ +using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.ManagementApi.Mapping.Culture; +using Umbraco.Cms.ManagementApi.Mapping.Dictionary; +using Umbraco.Cms.ManagementApi.Mapping.Installer; +using Umbraco.Cms.ManagementApi.Mapping.Languages; +using Umbraco.Cms.ManagementApi.Mapping.Relation; +using Umbraco.Cms.ManagementApi.Mapping.TrackedReferences; +using Umbraco.New.Cms.Infrastructure.Persistence.Mappers; + +namespace Umbraco.Cms.ManagementApi.DependencyInjection; + +public static class MappingBuilderExtensions +{ + internal static IUmbracoBuilder AddMappers(this IUmbracoBuilder builder) + { + builder.WithCollectionBuilder() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add(); + + return builder; + } +} diff --git a/src/Umbraco.Cms.ManagementApi/DependencyInjection/ServicesBuilderExtensions.cs b/src/Umbraco.Cms.ManagementApi/DependencyInjection/ServicesBuilderExtensions.cs new file mode 100644 index 0000000000..cb739478c5 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/DependencyInjection/ServicesBuilderExtensions.cs @@ -0,0 +1,22 @@ +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.ManagementApi.Serialization; +using Umbraco.Cms.ManagementApi.Services; +using Umbraco.New.Cms.Core.Services.Installer; +using Umbraco.New.Cms.Core.Services.Languages; + +namespace Umbraco.Cms.ManagementApi.DependencyInjection; + +public static class ServicesBuilderExtensions +{ + internal static IUmbracoBuilder AddServices(this IUmbracoBuilder builder) + { + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + + return builder; + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Factories/DictionaryFactory.cs b/src/Umbraco.Cms.ManagementApi/Factories/DictionaryFactory.cs new file mode 100644 index 0000000000..eac69e29c7 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Factories/DictionaryFactory.cs @@ -0,0 +1,99 @@ +using System.Xml; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Mapping; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.Models; +using Umbraco.Cms.ManagementApi.ViewModels.Dictionary; +using Umbraco.New.Cms.Core.Factories; + +namespace Umbraco.Cms.ManagementApi.Factories; + +public class DictionaryFactory : IDictionaryFactory +{ + private readonly IUmbracoMapper _umbracoMapper; + private readonly ILocalizationService _localizationService; + private readonly IDictionaryService _dictionaryService; + private readonly CommonMapper _commonMapper; + + public DictionaryFactory( + IUmbracoMapper umbracoMapper, + ILocalizationService localizationService, + IDictionaryService dictionaryService, + CommonMapper commonMapper) + { + _umbracoMapper = umbracoMapper; + _localizationService = localizationService; + _dictionaryService = dictionaryService; + _commonMapper = commonMapper; + } + + public IDictionaryItem CreateDictionaryItem(DictionaryViewModel dictionaryViewModel) + { + IDictionaryItem mappedItem = _umbracoMapper.Map(dictionaryViewModel)!; + IDictionaryItem? dictionaryItem = _localizationService.GetDictionaryItemById(dictionaryViewModel.Key); + mappedItem.Id = dictionaryItem!.Id; + return mappedItem; + } + + public DictionaryViewModel CreateDictionaryViewModel(IDictionaryItem dictionaryItem) + { + DictionaryViewModel dictionaryViewModel = _umbracoMapper.Map(dictionaryItem)!; + + dictionaryViewModel.ContentApps = _commonMapper.GetContentAppsForEntity(dictionaryItem); + dictionaryViewModel.Path = _dictionaryService.CalculatePath(dictionaryItem.ParentId, dictionaryItem.Id); + + var translations = new List(); + // add all languages and the translations + foreach (ILanguage lang in _localizationService.GetAllLanguages()) + { + var langId = lang.Id; + IDictionaryTranslation? translation = dictionaryItem.Translations?.FirstOrDefault(x => x.LanguageId == langId); + + translations.Add(new DictionaryTranslationViewModel + { + IsoCode = lang.IsoCode, + DisplayName = lang.CultureName, + Translation = translation?.Value ?? string.Empty, + LanguageId = lang.Id, + Id = translation?.Id ?? 0, + Key = translation?.Key ?? Guid.Empty, + }); + } + + dictionaryViewModel.Translations = translations; + + return dictionaryViewModel; + } + + public DictionaryImportViewModel CreateDictionaryImportViewModel(FormFileUploadResult formFileUploadResult) + { + if (formFileUploadResult.CouldLoad is false || formFileUploadResult.XmlDocument is null) + { + throw new ArgumentNullException("The document of the FormFileUploadResult cannot be null"); + } + + var model = new DictionaryImportViewModel + { + TempFileName = formFileUploadResult.TemporaryPath, DictionaryItems = new List(), + }; + + var level = 1; + var currentParent = string.Empty; + foreach (XmlNode dictionaryItem in formFileUploadResult.XmlDocument.GetElementsByTagName("DictionaryItem")) + { + var name = dictionaryItem.Attributes?.GetNamedItem("Name")?.Value ?? string.Empty; + var parentKey = dictionaryItem?.ParentNode?.Attributes?.GetNamedItem("Key")?.Value ?? string.Empty; + + if (parentKey != currentParent || level == 1) + { + level += 1; + currentParent = parentKey; + } + + model.DictionaryItems.Add(new DictionaryItemsImportViewModel { Level = level, Name = name }); + } + + return model; + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Factories/ExamineIndexViewModelFactory.cs b/src/Umbraco.Cms.ManagementApi/Factories/ExamineIndexViewModelFactory.cs new file mode 100644 index 0000000000..a8d9bee0a1 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Factories/ExamineIndexViewModelFactory.cs @@ -0,0 +1,52 @@ +using Examine; +using Umbraco.Cms.Core; +using Umbraco.Cms.Infrastructure.Examine; +using Umbraco.Cms.ManagementApi.ViewModels.ExamineManagement; +using Umbraco.New.Cms.Infrastructure.Services; + +namespace Umbraco.Cms.ManagementApi.Factories; + +public class ExamineIndexViewModelFactory : IExamineIndexViewModelFactory +{ + private readonly IIndexDiagnosticsFactory _indexDiagnosticsFactory; + private readonly IIndexRebuilder _indexRebuilder; + private readonly IIndexingRebuilderService _indexingRebuilderService; + + public ExamineIndexViewModelFactory(IIndexDiagnosticsFactory indexDiagnosticsFactory, IIndexRebuilder indexRebuilder, IIndexingRebuilderService indexingRebuilderService) + { + _indexDiagnosticsFactory = indexDiagnosticsFactory; + _indexRebuilder = indexRebuilder; + _indexingRebuilderService = indexingRebuilderService; + } + + public ExamineIndexViewModel Create(IIndex index) + { + if (_indexingRebuilderService.IsRebuilding(index.Name)) + { + return new ExamineIndexViewModel + { + Name = index.Name, + HealthStatus = "Rebuilding", + SearcherName = index.Searcher.Name, + DocumentCount = 0, + FieldCount = 0, + }; + } + + IIndexDiagnostics indexDiag = _indexDiagnosticsFactory.Create(index); + + Attempt isHealthy = indexDiag.IsHealthy(); + + var indexerModel = new ExamineIndexViewModel + { + Name = index.Name, + HealthStatus = isHealthy.Success ? isHealthy.Result ?? "Healthy" : isHealthy.Result ?? "Unhealthy", + CanRebuild = _indexRebuilder.CanRebuild(index.Name), + SearcherName = index.Searcher.Name, + DocumentCount = indexDiag.GetDocumentCount(), + FieldCount = indexDiag.GetFieldNames().Count(), + }; + + return indexerModel; + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Factories/IDictionaryFactory.cs b/src/Umbraco.Cms.ManagementApi/Factories/IDictionaryFactory.cs new file mode 100644 index 0000000000..39a4c87ff3 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Factories/IDictionaryFactory.cs @@ -0,0 +1,13 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.ManagementApi.Models; +using Umbraco.Cms.ManagementApi.ViewModels.Dictionary; + +namespace Umbraco.New.Cms.Core.Factories; + +public interface IDictionaryFactory +{ + IDictionaryItem CreateDictionaryItem(DictionaryViewModel dictionaryViewModel); + DictionaryViewModel CreateDictionaryViewModel(IDictionaryItem dictionaryItem); + + DictionaryImportViewModel CreateDictionaryImportViewModel(FormFileUploadResult formFileUploadResult); +} diff --git a/src/Umbraco.Cms.ManagementApi/Factories/IExamineIndexViewModelFactory.cs b/src/Umbraco.Cms.ManagementApi/Factories/IExamineIndexViewModelFactory.cs new file mode 100644 index 0000000000..c992a5a14b --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Factories/IExamineIndexViewModelFactory.cs @@ -0,0 +1,9 @@ +using Examine; +using Umbraco.Cms.ManagementApi.ViewModels.ExamineManagement; + +namespace Umbraco.Cms.ManagementApi.Factories; + +public interface IExamineIndexViewModelFactory +{ + ExamineIndexViewModel Create(IIndex index); +} diff --git a/src/Umbraco.Cms.ManagementApi/Factories/IRelationViewModelFactory.cs b/src/Umbraco.Cms.ManagementApi/Factories/IRelationViewModelFactory.cs new file mode 100644 index 0000000000..a10c1e0256 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Factories/IRelationViewModelFactory.cs @@ -0,0 +1,11 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.ManagementApi.ViewModels.Relation; + +namespace Umbraco.Cms.ManagementApi.Factories; + +public interface IRelationViewModelFactory +{ + RelationViewModel Create(IRelation relation); + + IEnumerable CreateMultiple(IEnumerable relations); +} diff --git a/src/Umbraco.Cms.ManagementApi/Factories/RelationViewModelFactory.cs b/src/Umbraco.Cms.ManagementApi/Factories/RelationViewModelFactory.cs new file mode 100644 index 0000000000..fe156ae092 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Factories/RelationViewModelFactory.cs @@ -0,0 +1,35 @@ +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.ViewModels.Relation; + +namespace Umbraco.Cms.ManagementApi.Factories; + +public class RelationViewModelFactory : IRelationViewModelFactory +{ + private readonly IRelationService _relationService; + private readonly IUmbracoMapper _umbracoMapper; + + public RelationViewModelFactory(IRelationService relationService, IUmbracoMapper umbracoMapper) + { + _relationService = relationService; + _umbracoMapper = umbracoMapper; + } + + public RelationViewModel Create(IRelation relation) + { + RelationViewModel relationViewModel = _umbracoMapper.Map(relation)!; + Tuple? entities = _relationService.GetEntitiesFromRelation(relation); + + if (entities is not null) + { + relationViewModel.ParentName = entities.Item1.Name; + relationViewModel.ChildName = entities.Item2.Name; + } + + return relationViewModel; + } + + public IEnumerable CreateMultiple(IEnumerable relations) => relations.Select(Create); +} diff --git a/src/Umbraco.Cms.ManagementApi/Filters/ManagementApiJsonConfigurationAttribute.cs b/src/Umbraco.Cms.ManagementApi/Filters/ManagementApiJsonConfigurationAttribute.cs new file mode 100644 index 0000000000..d71bb9f5e2 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Filters/ManagementApiJsonConfigurationAttribute.cs @@ -0,0 +1,33 @@ +using System.Text.Json; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace Umbraco.Cms.ManagementApi.Filters; + +public class ManagementApiJsonConfigurationAttribute : TypeFilterAttribute +{ + public ManagementApiJsonConfigurationAttribute() : base(typeof(SystemTextJsonConfigurationFilter)) => + Order = 1; // Must be low, to be overridden by other custom formatters, but higher then all framework stuff. + + private class SystemTextJsonConfigurationFilter : IResultFilter + { + public void OnResultExecuted(ResultExecutedContext context) + { + } + + public void OnResultExecuting(ResultExecutingContext context) + { + if (context.Result is ObjectResult objectResult) + { + var serializerOptions = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + }; + objectResult.Formatters.Clear(); + objectResult.Formatters.Add(new ManagementApiJsonOutputFormatter(serializerOptions)); + } + } + } +} + + diff --git a/src/Umbraco.Cms.ManagementApi/Filters/ManagementApiJsonOutputFormatter.cs b/src/Umbraco.Cms.ManagementApi/Filters/ManagementApiJsonOutputFormatter.cs new file mode 100644 index 0000000000..ccc9dbddaf --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Filters/ManagementApiJsonOutputFormatter.cs @@ -0,0 +1,19 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.AspNetCore.Mvc.Formatters; + +namespace Umbraco.Cms.ManagementApi.Filters; + +public class ManagementApiJsonOutputFormatter : SystemTextJsonOutputFormatter +{ + public ManagementApiJsonOutputFormatter(JsonSerializerOptions jsonSerializerOptions) : base(RegisterJsonConverters(jsonSerializerOptions)) + { + } + + protected static JsonSerializerOptions RegisterJsonConverters(JsonSerializerOptions serializerOptions) + { + serializerOptions.Converters.Add(new JsonStringEnumConverter()); + + return serializerOptions; + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Filters/RequireDocumentTreeRootAccessAttribute.cs b/src/Umbraco.Cms.ManagementApi/Filters/RequireDocumentTreeRootAccessAttribute.cs new file mode 100644 index 0000000000..8426884716 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Filters/RequireDocumentTreeRootAccessAttribute.cs @@ -0,0 +1,19 @@ +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.ManagementApi.Filters; + +public class RequireDocumentTreeRootAccessAttribute : RequireTreeRootAccessAttribute +{ + protected override int[] GetUserStartNodeIds(IUser user, ActionExecutingContext context) + { + AppCaches appCaches = context.HttpContext.RequestServices.GetRequiredService(); + IEntityService entityService = context.HttpContext.RequestServices.GetRequiredService(); + + return user.CalculateContentStartNodeIds(entityService, appCaches) ?? Array.Empty(); + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Filters/RequireMediaTreeRootAccessAttribute.cs b/src/Umbraco.Cms.ManagementApi/Filters/RequireMediaTreeRootAccessAttribute.cs new file mode 100644 index 0000000000..42dd62c5f2 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Filters/RequireMediaTreeRootAccessAttribute.cs @@ -0,0 +1,19 @@ +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.ManagementApi.Filters; + +public class RequireMediaTreeRootAccessAttribute : RequireTreeRootAccessAttribute +{ + protected override int[] GetUserStartNodeIds(IUser user, ActionExecutingContext context) + { + AppCaches appCaches = context.HttpContext.RequestServices.GetRequiredService(); + IEntityService entityService = context.HttpContext.RequestServices.GetRequiredService(); + + return user.CalculateMediaStartNodeIds(entityService, appCaches) ?? Array.Empty(); + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Filters/RequireTreeRootAccessAttribute.cs b/src/Umbraco.Cms.ManagementApi/Filters/RequireTreeRootAccessAttribute.cs new file mode 100644 index 0000000000..60e98ed565 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Filters/RequireTreeRootAccessAttribute.cs @@ -0,0 +1,40 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Security; + +namespace Umbraco.Cms.ManagementApi.Filters; + +public abstract class RequireTreeRootAccessAttribute : ActionFilterAttribute +{ + public override void OnActionExecuting(ActionExecutingContext context) + { + IBackOfficeSecurityAccessor backOfficeSecurityAccessor = context.HttpContext.RequestServices.GetRequiredService(); + IUser? user = backOfficeSecurityAccessor.BackOfficeSecurity?.CurrentUser; + + var startNodeIds = user != null ? GetUserStartNodeIds(user, context) : Array.Empty(); + + // TODO: remove this once we have backoffice auth in place + startNodeIds = new[] { Constants.System.Root }; + + if (startNodeIds.Contains(Constants.System.Root)) + { + return; + } + + var problemDetails = new ProblemDetails + { + Title = "Unauthorized user", + Detail = "The current backoffice user should have access to the tree root", + Status = StatusCodes.Status401Unauthorized, + Type = "Error", + }; + + context.Result = new ObjectResult(problemDetails) { StatusCode = StatusCodes.Status401Unauthorized }; + } + + protected abstract int[] GetUserStartNodeIds(IUser user, ActionExecutingContext context); +} diff --git a/src/Umbraco.Cms.ManagementApi/ManagementApiComposer.cs b/src/Umbraco.Cms.ManagementApi/ManagementApiComposer.cs index 095b4e39e3..450f1494b5 100644 --- a/src/Umbraco.Cms.ManagementApi/ManagementApiComposer.cs +++ b/src/Umbraco.Cms.ManagementApi/ManagementApiComposer.cs @@ -1,4 +1,4 @@ -using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Diagnostics; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; @@ -35,7 +35,12 @@ public class ManagementApiComposer : IComposer builder .AddNewInstaller() - .AddUpgrader(); + .AddUpgrader() + .AddExamineManagement() + .AddTrees() + .AddFactories() + .AddServices() + .AddMappers(); services.AddApiVersioning(options => { @@ -52,6 +57,10 @@ public class ManagementApiComposer : IComposer options.Version = ApiAllName; options.DocumentName = ApiAllName; options.Description = "This shows all APIs available in this version of Umbraco - Including all the legacy apis that is available for backward compatibility"; + options.PostProcess = document => + { + document.Tags = document.Tags.OrderBy(tag => tag.Name).ToList(); + }; }); services.AddVersionedApiExplorer(options => @@ -113,6 +122,8 @@ public class ManagementApiComposer : IComposer config.SwaggerRoutes.Clear(); var swaggerPath = $"{officePath}/swagger/{ApiAllName}/swagger.json"; config.SwaggerRoutes.Add(new SwaggerUi3Route(ApiAllName, swaggerPath)); + config.OperationsSorter = "alpha"; + config.TagsSorter = "alpha"; }); } }, diff --git a/src/Umbraco.Cms.ManagementApi/Mapping/Culture/CultureViewModelMapDefinition.cs b/src/Umbraco.Cms.ManagementApi/Mapping/Culture/CultureViewModelMapDefinition.cs new file mode 100644 index 0000000000..d625365089 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Mapping/Culture/CultureViewModelMapDefinition.cs @@ -0,0 +1,32 @@ +using System.Globalization; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.ManagementApi.ViewModels.Culture; +using Umbraco.Cms.ManagementApi.ViewModels.Pagination; + +namespace Umbraco.Cms.ManagementApi.Mapping.Culture; + +/// +public class CultureViewModelMapDefinition : IMapDefinition +{ + /// + public void DefineMaps(IUmbracoMapper mapper) + { + mapper.Define, PagedViewModel>((source, context) => new PagedViewModel(), Map); + mapper.Define((source, context) => new CultureViewModel(), Map); + } + + // Umbraco.Code.MapAll + private static void Map(CultureInfo source, CultureViewModel target, MapperContext context) + { + target.Name = source.Name; + target.EnglishName = source.EnglishName; + } + + // Umbraco.Code.MapAll + private static void Map(IEnumerable source, PagedViewModel target, MapperContext context) + { + CultureInfo[] cultureInfos = source.ToArray(); + target.Items = context.MapEnumerable(cultureInfos); + target.Total = cultureInfos.Length; + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Mapping/Dictionary/DictionaryViewModelMapDefinition.cs b/src/Umbraco.Cms.ManagementApi/Mapping/Dictionary/DictionaryViewModelMapDefinition.cs new file mode 100644 index 0000000000..1f530eaafe --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Mapping/Dictionary/DictionaryViewModelMapDefinition.cs @@ -0,0 +1,71 @@ +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.ViewModels.Dictionary; + +namespace Umbraco.Cms.ManagementApi.Mapping.Dictionary; + +public class DictionaryViewModelMapDefinition : IMapDefinition +{ + private readonly ILocalizationService _localizationService; + + public DictionaryViewModelMapDefinition(ILocalizationService localizationService) => _localizationService = localizationService; + + public void DefineMaps(IUmbracoMapper mapper) + { + mapper.Define((source, context) => new DictionaryItem(string.Empty), Map); + mapper.Define((source, context) => new DictionaryViewModel(), Map); + mapper.Define((source, context) => new DictionaryTranslation(source.LanguageId, string.Empty), Map); + mapper.Define((source, context) => new DictionaryOverviewViewModel(), Map); + + } + + // Umbraco.Code.MapAll -Id -CreateDate -UpdateDate + private void Map(DictionaryViewModel source, IDictionaryItem target, MapperContext context) + { + target.ItemKey = source.Name!; + target.Key = source.Key; + target.ParentId = source.ParentId; + target.Translations = context.MapEnumerable(source.Translations); + target.DeleteDate = null; + } + + // Umbraco.Code.MapAll -CreateDate -DeleteDate -UpdateDate -Language + private void Map(DictionaryTranslationViewModel source, IDictionaryTranslation target, MapperContext context) + { + target.Value = source.Translation; + target.Id = source.Id; + target.Key = source.Key; + } + + // Umbraco.Code.MapAll -Icon -Trashed -Alias -NameIsDirty -ContentApps -Path -Translations + private void Map(IDictionaryItem source, DictionaryViewModel target, MapperContext context) + { + target.Key = source.Key; + target.Name = source.ItemKey; + target.ParentId = source.ParentId ?? null; + } + + // Umbraco.Code.MapAll -Level -Translations + private void Map(IDictionaryItem source, DictionaryOverviewViewModel target, MapperContext context) + { + target.Key = source.Key; + target.Name = source.ItemKey; + + // add all languages and the translations + foreach (ILanguage lang in _localizationService.GetAllLanguages()) + { + var langId = lang.Id; + IDictionaryTranslation? translation = source.Translations?.FirstOrDefault(x => x.LanguageId == langId); + + target.Translations.Add( + new DictionaryTranslationOverviewViewModel + { + DisplayName = lang.CultureName, + HasTranslation = translation != null && string.IsNullOrEmpty(translation.Value) == false, + }); + } + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Mapping/Languages/LanguageViewModelsMapDefinition.cs b/src/Umbraco.Cms.ManagementApi/Mapping/Languages/LanguageViewModelsMapDefinition.cs new file mode 100644 index 0000000000..9fafcc122b --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Mapping/Languages/LanguageViewModelsMapDefinition.cs @@ -0,0 +1,80 @@ +using NPoco.FluentMappings; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.ManagementApi.ViewModels.Language; +using Umbraco.Cms.ManagementApi.ViewModels.Pagination; +using Umbraco.New.Cms.Core.Models; + +namespace Umbraco.Cms.ManagementApi.Mapping.Languages; + +public class LanguageViewModelsMapDefinition : IMapDefinition +{ + public void DefineMaps(IUmbracoMapper mapper) + { + mapper.Define((source, context) => new Language(string.Empty, string.Empty), Map); + mapper.Define((source, context) => new LanguageViewModel(), Map); + mapper.Define, PagedViewModel>((source, context) => new PagedViewModel(), Map); + + } + + // Umbraco.Code.MapAll + private static void Map(ILanguage source, LanguageViewModel target, MapperContext context) + { + target.Id = source.Id; + target.IsoCode = source.IsoCode; + target.Name = source.CultureName; + target.IsDefault = source.IsDefault; + target.IsMandatory = source.IsMandatory; + target.FallbackLanguageId = source.FallbackLanguageId; + } + + + // Umbraco.Code.MapAll + private static void Map(LanguageViewModel source, ILanguage target, MapperContext context) + { + target.CreateDate = default; + if (!string.IsNullOrEmpty(source.Name)) + { + target.CultureName = source.Name; + } + + target.DeleteDate = null; + target.FallbackLanguageId = source.FallbackLanguageId; + target.Id = source.Id; + target.IsDefault = source.IsDefault; + target.IsMandatory = source.IsMandatory; + target.IsoCode = source.IsoCode; + target.Key = default; + target.UpdateDate = default; + } + + private static void Map(PagedModel source, PagedViewModel target, MapperContext context) + { + if (target is null) + { + throw new ArgumentNullException(nameof(target)); + } + + if (target is not PagedViewModel list) + { + throw new NotSupportedException($"{nameof(target)} must be a List."); + } + + List temp = context.MapEnumerable(source.Items); + + // Put the default language first in the list & then sort rest by a-z + LanguageViewModel? defaultLang = temp.SingleOrDefault(x => x.IsDefault); + + var languages = new List(); + + // insert default lang first, then remaining language a-z + if (defaultLang is not null) + { + languages.Add(defaultLang); + languages.AddRange(temp.Where(x => x != defaultLang).OrderBy(x => x.Name)); + } + + list.Items = languages; + list.Total = source.Total; + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Mapping/Relation/RelationViewModelsMapDefinition.cs b/src/Umbraco.Cms.ManagementApi/Mapping/Relation/RelationViewModelsMapDefinition.cs new file mode 100644 index 0000000000..19a6ba14a2 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Mapping/Relation/RelationViewModelsMapDefinition.cs @@ -0,0 +1,22 @@ +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.ManagementApi.ViewModels.Relation; + +namespace Umbraco.Cms.ManagementApi.Mapping.Relation; + +public class RelationViewModelsMapDefinition : IMapDefinition +{ + public void DefineMaps(IUmbracoMapper mapper) + { + mapper.Define((source, context) => new RelationViewModel(), Map); + } + + // Umbraco.Code.MapAll -ParentName -ChildName + private void Map(IRelation source, RelationViewModel target, MapperContext context) + { + target.ChildId = source.ChildId; + target.Comment = source.Comment; + target.CreateDate = source.CreateDate; + target.ParentId = source.ParentId; + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Mapping/TrackedReferences/TrackedReferenceViewModelsMapDefinition.cs b/src/Umbraco.Cms.ManagementApi/Mapping/TrackedReferences/TrackedReferenceViewModelsMapDefinition.cs new file mode 100644 index 0000000000..cf35a63a3a --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Mapping/TrackedReferences/TrackedReferenceViewModelsMapDefinition.cs @@ -0,0 +1,28 @@ +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.ManagementApi.ViewModels.TrackedReferences; + +namespace Umbraco.Cms.ManagementApi.Mapping.TrackedReferences; + +public class TrackedReferenceViewModelsMapDefinition : IMapDefinition +{ + public void DefineMaps(IUmbracoMapper mapper) + { + mapper.Define((source, context) => new RelationItemViewModel(), Map); + } + + // Umbraco.Code.MapAll + private void Map(RelationItemModel source, RelationItemViewModel target, MapperContext context) + { + target.ContentTypeAlias = source.ContentTypeAlias; + target.ContentTypeIcon = source.ContentTypeIcon; + target.ContentTypeName = source.ContentTypeName; + target.NodeKey = source.NodeKey; + target.NodeName = source.NodeName; + target.NodeType = source.NodeType; + target.RelationTypeIsBidirectional = source.RelationTypeIsBidirectional; + target.RelationTypeIsDependency = source.RelationTypeIsDependency; + target.RelationTypeName = source.RelationTypeName; + } + +} diff --git a/src/Umbraco.Cms.ManagementApi/Models/Entities/UserAccessEntity.cs b/src/Umbraco.Cms.ManagementApi/Models/Entities/UserAccessEntity.cs new file mode 100644 index 0000000000..7f7981e693 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Models/Entities/UserAccessEntity.cs @@ -0,0 +1,16 @@ +using Umbraco.Cms.Core.Models.Entities; + +namespace Umbraco.Cms.ManagementApi.Models.Entities; + +public class UserAccessEntity +{ + public UserAccessEntity(IEntitySlim entity, bool hasAccess) + { + Entity = entity; + HasAccess = hasAccess; + } + + public IEntitySlim Entity { get; } + + public bool HasAccess { get; } +} diff --git a/src/Umbraco.Cms.ManagementApi/Models/FormFileUploadResult.cs b/src/Umbraco.Cms.ManagementApi/Models/FormFileUploadResult.cs new file mode 100644 index 0000000000..c785d2b08c --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Models/FormFileUploadResult.cs @@ -0,0 +1,14 @@ +using System.Xml; + +namespace Umbraco.Cms.ManagementApi.Models; + +public class FormFileUploadResult +{ + public bool CouldLoad { get; set; } + + public XmlDocument? XmlDocument { get; set; } + + public string? ErrorMessage { get; set; } + + public string? TemporaryPath { get; set; } +} diff --git a/src/Umbraco.Cms.ManagementApi/OpenApi.json b/src/Umbraco.Cms.ManagementApi/OpenApi.json index 47bc9e0fbb..4fce2998b6 100644 --- a/src/Umbraco.Cms.ManagementApi/OpenApi.json +++ b/src/Umbraco.Cms.ManagementApi/OpenApi.json @@ -11,6 +11,1740 @@ } ], "paths": { + "/umbraco/api/v1/upgrade/authorize": { + "post": { + "tags": [ + "Upgrade" + ], + "operationId": "AuthorizeUpgrade_Authorize", + "responses": { + "200": { + "description": "" + }, + "428": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "500": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + } + }, + "/umbraco/api/v1/upgrade/settings": { + "get": { + "tags": [ + "Upgrade" + ], + "operationId": "SettingsUpgrade_Settings", + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpgradeSettingsViewModel" + } + } + } + }, + "428": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + } + }, + "/umbraco/api/v1/template/tree/children": { + "get": { + "tags": [ + "Template" + ], + "operationId": "ChildrenTemplateTree_Children", + "parameters": [ + { + "name": "parentKey", + "in": "query", + "schema": { + "type": "string", + "format": "guid" + }, + "x-position": 1 + }, + { + "name": "skip", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + }, + "x-position": 2 + }, + { + "name": "take", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 100 + }, + "x-position": 3 + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedViewModelOfEntityTreeItemViewModel" + } + } + } + } + } + } + }, + "/umbraco/api/v1/template/tree/items": { + "get": { + "tags": [ + "Template" + ], + "operationId": "ItemsTemplateTree_Items", + "parameters": [ + { + "name": "key", + "x-originalName": "keys", + "in": "query", + "style": "form", + "explode": true, + "schema": { + "type": "array", + "nullable": true, + "items": { + "type": "string", + "format": "guid" + } + }, + "x-position": 1 + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EntityTreeItemViewModel" + } + } + } + } + } + } + } + }, + "/umbraco/api/v1/template/tree/root": { + "get": { + "tags": [ + "Template" + ], + "operationId": "RootTemplateTree_Root", + "parameters": [ + { + "name": "skip", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + }, + "x-position": 1 + }, + { + "name": "take", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 100 + }, + "x-position": 2 + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedViewModelOfEntityTreeItemViewModel" + } + } + } + } + } + } + }, + "/umbraco/api/v1/trackedReferences/{id}": { + "get": { + "description": "Used by info tabs on content, media etc. and for the delete and unpublish of single items.\nThis is basically finding parents of relations.", + "operationId": "ForItemTrackedReferences_Get", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "format": "int32", + "type": "integer" + }, + "x-position": 1 + }, + { + "in": "query", + "name": "skip", + "schema": { + "format": "int64", + "type": "integer" + }, + "x-position": 2 + }, + { + "in": "query", + "name": "take", + "schema": { + "format": "int64", + "type": "integer" + }, + "x-position": 3 + }, + { + "in": "query", + "name": "filterMustBeIsDependency", + "schema": { + "nullable": true, + "type": "boolean" + }, + "x-position": 4 + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedViewModelOfRelationItemViewModel" + } + } + }, + "description": "" + } + }, + "summary": "Gets a page list of tracked references for the current item, so you can see where an item is being used.", + "tags": [ + "TrackedReferences" + ] + } + }, + "/umbraco/api/v1/trackedReferences/descendants/{parentId}": { + "get": { + "description": "Used when deleting and unpublishing a single item to check if this item has any descending items that are in any\nkind of relation.\nThis is basically finding the descending items which are children in relations.", + "operationId": "DescendantsTrackedReferences_Descendants", + "parameters": [ + { + "in": "path", + "name": "parentId", + "required": true, + "schema": { + "format": "int32", + "type": "integer" + }, + "x-position": 1 + }, + { + "in": "query", + "name": "skip", + "schema": { + "format": "int64", + "type": "integer" + }, + "x-position": 2 + }, + { + "in": "query", + "name": "take", + "schema": { + "format": "int64", + "type": "integer" + }, + "x-position": 3 + }, + { + "in": "query", + "name": "filterMustBeIsDependency", + "schema": { + "nullable": true, + "type": "boolean" + }, + "x-position": 4 + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedViewModelOfRelationItemViewModel" + } + } + }, + "description": "" + } + }, + "summary": "Gets a page list of the child nodes of the current item used in any kind of relation.", + "tags": [ + "TrackedReferences" + ] + } + }, + "/umbraco/api/v1/trackedReferences/multiple": { + "get": { + "description": "Used when bulk deleting content/media and bulk unpublishing content (delete and unpublish on List view).\nThis is basically finding children of relations.", + "operationId": "MultipleTrackedReferences_GetPagedReferencedItems", + "parameters": [ + { + "explode": true, + "in": "query", + "name": "ids", + "schema": { + "items": { + "format": "int32", + "type": "integer" + }, + "nullable": true, + "type": "array" + }, + "style": "form", + "x-position": 1 + }, + { + "in": "query", + "name": "skip", + "schema": { + "format": "int64", + "type": "integer" + }, + "x-position": 2 + }, + { + "in": "query", + "name": "take", + "schema": { + "format": "int64", + "type": "integer" + }, + "x-position": 3 + }, + { + "in": "query", + "name": "filterMustBeIsDependency", + "schema": { + "nullable": true, + "type": "boolean" + }, + "x-position": 4 + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedViewModelOfRelationItemViewModel" + } + } + }, + "description": "" + } + }, + "summary": "Gets a page list of the items used in any kind of relation from selected integer ids.", + "tags": [ + "TrackedReferences" + ] + } + }, + "/umbraco/api/v1/stylesheet/tree/children": { + "get": { + "tags": [ + "Stylesheet" + ], + "operationId": "ChildrenStylesheetTree_Children", + "parameters": [ + { + "name": "path", + "in": "query", + "schema": { + "type": "string", + "nullable": true + }, + "x-position": 1 + }, + { + "name": "skip", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + }, + "x-position": 2 + }, + { + "name": "take", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 100 + }, + "x-position": 3 + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedViewModelOfFileSystemTreeItemViewModel" + } + } + } + } + } + } + }, + "/umbraco/api/v1/stylesheet/tree/items": { + "get": { + "tags": [ + "Stylesheet" + ], + "operationId": "ItemsStylesheetTree_Items", + "parameters": [ + { + "name": "path", + "x-originalName": "paths", + "in": "query", + "style": "form", + "explode": true, + "schema": { + "type": "array", + "nullable": true, + "items": { + "type": "string" + } + }, + "x-position": 1 + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FileSystemTreeItemViewModel" + } + } + } + } + } + } + } + }, + "/umbraco/api/v1/stylesheet/tree/root": { + "get": { + "tags": [ + "Stylesheet" + ], + "operationId": "RootStylesheetTree_Root", + "parameters": [ + { + "name": "skip", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + }, + "x-position": 1 + }, + { + "name": "take", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 100 + }, + "x-position": 2 + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedViewModelOfFileSystemTreeItemViewModel" + } + } + } + } + } + } + }, + "/umbraco/api/v1/static-file/tree/children": { + "get": { + "tags": [ + "StaticFile" + ], + "operationId": "ChildrenStaticFileTree_Children", + "parameters": [ + { + "name": "path", + "in": "query", + "schema": { + "type": "string", + "nullable": true + }, + "x-position": 1 + }, + { + "name": "skip", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + }, + "x-position": 2 + }, + { + "name": "take", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 100 + }, + "x-position": 3 + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedViewModelOfFileSystemTreeItemViewModel" + } + } + } + } + } + } + }, + "/umbraco/api/v1/static-file/tree/items": { + "get": { + "tags": [ + "StaticFile" + ], + "operationId": "ItemsStaticFileTree_Items", + "parameters": [ + { + "name": "path", + "x-originalName": "paths", + "in": "query", + "style": "form", + "explode": true, + "schema": { + "type": "array", + "nullable": true, + "items": { + "type": "string" + } + }, + "x-position": 1 + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FileSystemTreeItemViewModel" + } + } + } + } + } + } + } + }, + "/umbraco/api/v1/static-file/tree/root": { + "get": { + "tags": [ + "StaticFile" + ], + "operationId": "RootStaticFileTree_Root", + "parameters": [ + { + "name": "skip", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + }, + "x-position": 1 + }, + { + "name": "take", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 100 + }, + "x-position": 2 + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedViewModelOfFileSystemTreeItemViewModel" + } + } + } + } + } + } + }, + "/umbraco/api/v1/server/status": { + "get": { + "tags": [ + "Server" + ], + "operationId": "StatusServer_Get", + "responses": { + "400": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServerStatusViewModel" + } + } + } + } + } + } + }, + "/umbraco/api/v1/server/version": { + "get": { + "tags": [ + "Server" + ], + "operationId": "VersionServer_Get", + "responses": { + "400": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VersionViewModel" + } + } + } + } + } + } + }, + "/umbraco/api/v1/script/tree/children": { + "get": { + "tags": [ + "Script" + ], + "operationId": "ChildrenScriptTree_Children", + "parameters": [ + { + "name": "path", + "in": "query", + "schema": { + "type": "string", + "nullable": true + }, + "x-position": 1 + }, + { + "name": "skip", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + }, + "x-position": 2 + }, + { + "name": "take", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 100 + }, + "x-position": 3 + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedViewModelOfFileSystemTreeItemViewModel" + } + } + } + } + } + } + }, + "/umbraco/api/v1/script/tree/items": { + "get": { + "tags": [ + "Script" + ], + "operationId": "ItemsScriptTree_Items", + "parameters": [ + { + "name": "path", + "x-originalName": "paths", + "in": "query", + "style": "form", + "explode": true, + "schema": { + "type": "array", + "nullable": true, + "items": { + "type": "string" + } + }, + "x-position": 1 + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FileSystemTreeItemViewModel" + } + } + } + } + } + } + } + }, + "/umbraco/api/v1/script/tree/root": { + "get": { + "tags": [ + "Script" + ], + "operationId": "RootScriptTree_Root", + "parameters": [ + { + "name": "skip", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + }, + "x-position": 1 + }, + { + "name": "take", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 100 + }, + "x-position": 2 + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedViewModelOfFileSystemTreeItemViewModel" + } + } + } + } + } + } + }, + "/umbraco/api/v1/relation-type/tree/items": { + "get": { + "tags": [ + "RelationType" + ], + "operationId": "ItemsRelationTypeTree_Items", + "parameters": [ + { + "name": "key", + "x-originalName": "keys", + "in": "query", + "style": "form", + "explode": true, + "schema": { + "type": "array", + "nullable": true, + "items": { + "type": "string", + "format": "guid" + } + }, + "x-position": 1 + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FolderTreeItemViewModel" + } + } + } + } + } + } + } + }, + "/umbraco/api/v1/relation-type/tree/root": { + "get": { + "tags": [ + "RelationType" + ], + "operationId": "RootRelationTypeTree_Root", + "parameters": [ + { + "name": "skip", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + }, + "x-position": 1 + }, + { + "name": "take", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 100 + }, + "x-position": 2 + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedViewModelOfEntityTreeItemViewModel" + } + } + } + } + } + } + }, + "/umbraco/api/v1/published-cache/collect": { + "post": { + "tags": [ + "PublishedCache" + ], + "operationId": "CollectPublishedCache_Collect", + "responses": { + "200": { + "description": "" + } + } + } + }, + "/umbraco/api/v1/published-cache/rebuild": { + "post": { + "tags": [ + "PublishedCache" + ], + "operationId": "RebuildPublishedCache_Collect", + "responses": { + "200": { + "description": "" + } + } + } + }, + "/umbraco/api/v1/published-cache/reload": { + "post": { + "tags": [ + "PublishedCache" + ], + "operationId": "ReloadPublishedCache_Reload", + "responses": { + "200": { + "description": "" + } + } + } + }, + "/umbraco/api/v1/published-cache/status": { + "get": { + "tags": [ + "PublishedCache" + ], + "operationId": "StatusPublishedCache_Status", + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/umbraco/api/v1/profiling/status": { + "get": { + "tags": [ + "Profiling" + ], + "operationId": "StatusProfiling_Status", + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProfilingStatusViewModel" + } + } + } + } + } + } + }, + "/umbraco/api/v1/partial-view/tree/children": { + "get": { + "tags": [ + "PartialView" + ], + "operationId": "ChildrenPartialViewTree_Children", + "parameters": [ + { + "name": "path", + "in": "query", + "schema": { + "type": "string", + "nullable": true + }, + "x-position": 1 + }, + { + "name": "skip", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + }, + "x-position": 2 + }, + { + "name": "take", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 100 + }, + "x-position": 3 + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedViewModelOfFileSystemTreeItemViewModel" + } + } + } + } + } + } + }, + "/umbraco/api/v1/partial-view/tree/items": { + "get": { + "tags": [ + "PartialView" + ], + "operationId": "ItemsPartialViewTree_Items", + "parameters": [ + { + "name": "path", + "x-originalName": "paths", + "in": "query", + "style": "form", + "explode": true, + "schema": { + "type": "array", + "nullable": true, + "items": { + "type": "string" + } + }, + "x-position": 1 + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FileSystemTreeItemViewModel" + } + } + } + } + } + } + } + }, + "/umbraco/api/v1/partial-view/tree/root": { + "get": { + "tags": [ + "PartialView" + ], + "operationId": "RootPartialViewTree_Root", + "parameters": [ + { + "name": "skip", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + }, + "x-position": 1 + }, + { + "name": "take", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 100 + }, + "x-position": 2 + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedViewModelOfFileSystemTreeItemViewModel" + } + } + } + } + } + } + }, + "/umbraco/api/v1/member-type/tree/items": { + "get": { + "tags": [ + "MemberType" + ], + "operationId": "ItemsMemberTypeTree_Items", + "parameters": [ + { + "name": "key", + "x-originalName": "keys", + "in": "query", + "style": "form", + "explode": true, + "schema": { + "type": "array", + "nullable": true, + "items": { + "type": "string", + "format": "guid" + } + }, + "x-position": 1 + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EntityTreeItemViewModel" + } + } + } + } + } + } + } + }, + "/umbraco/api/v1/member-type/tree/root": { + "get": { + "tags": [ + "MemberType" + ], + "operationId": "RootMemberTypeTree_Root", + "parameters": [ + { + "name": "skip", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + }, + "x-position": 1 + }, + { + "name": "take", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 100 + }, + "x-position": 2 + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedViewModelOfEntityTreeItemViewModel" + } + } + } + } + } + } + }, + "/umbraco/api/v1/member-group/tree/items": { + "get": { + "tags": [ + "MemberGroup" + ], + "operationId": "ItemsMemberGroupTree_Items", + "parameters": [ + { + "name": "key", + "x-originalName": "keys", + "in": "query", + "style": "form", + "explode": true, + "schema": { + "type": "array", + "nullable": true, + "items": { + "type": "string", + "format": "guid" + } + }, + "x-position": 1 + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EntityTreeItemViewModel" + } + } + } + } + } + } + } + }, + "/umbraco/api/v1/member-group/tree/root": { + "get": { + "tags": [ + "MemberGroup" + ], + "operationId": "RootMemberGroupTree_Root", + "parameters": [ + { + "name": "skip", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + }, + "x-position": 1 + }, + { + "name": "take", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 100 + }, + "x-position": 2 + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedViewModelOfEntityTreeItemViewModel" + } + } + } + } + } + } + }, + "/umbraco/api/v1/media/tree/children": { + "get": { + "tags": [ + "Media" + ], + "operationId": "ChildrenMediaTree_Children", + "parameters": [ + { + "name": "parentKey", + "in": "query", + "schema": { + "type": "string", + "format": "guid" + }, + "x-position": 1 + }, + { + "name": "skip", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + }, + "x-position": 2 + }, + { + "name": "take", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 100 + }, + "x-position": 3 + }, + { + "name": "dataTypeKey", + "in": "query", + "schema": { + "type": "string", + "format": "guid", + "nullable": true + }, + "x-position": 4 + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedViewModelOfContentTreeItemViewModel" + } + } + } + } + } + } + }, + "/umbraco/api/v1/media/tree/items": { + "get": { + "tags": [ + "Media" + ], + "operationId": "ItemsMediaTree_Items", + "parameters": [ + { + "name": "key", + "x-originalName": "keys", + "in": "query", + "style": "form", + "explode": true, + "schema": { + "type": "array", + "nullable": true, + "items": { + "type": "string", + "format": "guid" + } + }, + "x-position": 1 + }, + { + "name": "dataTypeKey", + "in": "query", + "schema": { + "type": "string", + "format": "guid", + "nullable": true + }, + "x-position": 2 + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ContentTreeItemViewModel" + } + } + } + } + } + } + } + }, + "/umbraco/api/v1/media/tree/root": { + "get": { + "tags": [ + "Media" + ], + "operationId": "RootMediaTree_Root", + "parameters": [ + { + "name": "skip", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + }, + "x-position": 1 + }, + { + "name": "take", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 100 + }, + "x-position": 2 + }, + { + "name": "dataTypeKey", + "in": "query", + "schema": { + "type": "string", + "format": "guid", + "nullable": true + }, + "x-position": 3 + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedViewModelOfContentTreeItemViewModel" + } + } + } + } + } + } + }, + "/umbraco/api/v1/media/recycle-bin/children": { + "get": { + "tags": [ + "Media" + ], + "operationId": "ChildrenMediaRecycleBin_Children", + "parameters": [ + { + "name": "parentKey", + "in": "query", + "schema": { + "type": "string", + "format": "guid" + }, + "x-position": 1 + }, + { + "name": "skip", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + }, + "x-position": 2 + }, + { + "name": "take", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 100 + }, + "x-position": 3 + } + ], + "responses": { + "401": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedViewModelOfRecycleBinItemViewModel" + } + } + } + } + } + } + }, + "/umbraco/api/v1/media/recycle-bin/root": { + "get": { + "tags": [ + "Media" + ], + "operationId": "RootMediaRecycleBin_Root", + "parameters": [ + { + "name": "skip", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + }, + "x-position": 1 + }, + { + "name": "take", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 100 + }, + "x-position": 2 + } + ], + "responses": { + "401": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedViewModelOfRecycleBinItemViewModel" + } + } + } + } + } + } + }, + "/umbraco/api/v1/media-type/tree/children": { + "get": { + "tags": [ + "MediaType" + ], + "operationId": "ChildrenMediaTypeTree_Children", + "parameters": [ + { + "name": "parentKey", + "in": "query", + "schema": { + "type": "string", + "format": "guid" + }, + "x-position": 1 + }, + { + "name": "skip", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + }, + "x-position": 2 + }, + { + "name": "take", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 100 + }, + "x-position": 3 + }, + { + "name": "foldersOnly", + "in": "query", + "schema": { + "type": "boolean", + "default": false + }, + "x-position": 4 + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedViewModelOfFolderTreeItemViewModel" + } + } + } + } + } + } + }, + "/umbraco/api/v1/media-type/tree/items": { + "get": { + "tags": [ + "MediaType" + ], + "operationId": "ItemsMediaTypeTree_Items", + "parameters": [ + { + "name": "key", + "x-originalName": "keys", + "in": "query", + "style": "form", + "explode": true, + "schema": { + "type": "array", + "nullable": true, + "items": { + "type": "string", + "format": "guid" + } + }, + "x-position": 1 + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FolderTreeItemViewModel" + } + } + } + } + } + } + } + }, + "/umbraco/api/v1/media-type/tree/root": { + "get": { + "tags": [ + "MediaType" + ], + "operationId": "RootMediaTypeTree_Root", + "parameters": [ + { + "name": "skip", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + }, + "x-position": 1 + }, + { + "name": "take", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 100 + }, + "x-position": 2 + }, + { + "name": "foldersOnly", + "in": "query", + "schema": { + "type": "boolean", + "default": false + }, + "x-position": 3 + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedViewModelOfFolderTreeItemViewModel" + } + } + } + } + } + } + }, "/umbraco/api/v1/install/settings": { "get": { "tags": [ @@ -131,62 +1865,250 @@ } } }, - "/umbraco/api/v1/upgrade/authorize": { - "post": { - "tags": [ - "Upgrade" + "/umbraco/api/v1/language": { + "get": { + "operationId": "AllLanguage_GetAll", + "parameters": [ + { + "in": "query", + "name": "skip", + "schema": { + "format": "int32", + "type": "integer" + }, + "x-position": 1 + }, + { + "in": "query", + "name": "take", + "schema": { + "format": "int32", + "type": "integer" + }, + "x-position": 2 + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedViewModelOfLanguageViewModel" + } + } + }, + "description": "" + } + }, + "summary": "1\n Returns all currently configured languages.", + "tags": [ + "Language" + ] + } + }, + "/umbraco/api/v1/language/{id}": { + "delete": { + "operationId": "DeleteLanguage_Delete", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "format": "int32", + "type": "integer" + }, + "x-position": 1 + } ], - "operationId": "AuthorizeUpgrade_Authorize", "responses": { "200": { "description": "" }, - "428": { - "description": "", + "400": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProblemDetails" } } + }, + "description": "" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + }, + "description": "" + } + }, + "summary": "Deletes a language with a given ID", + "tags": [ + "Language" + ] + }, + "get": { + "operationId": "ByIdLanguage_ById", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "format": "int32", + "type": "integer" + }, + "x-position": 1 + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LanguageViewModel" + } + } + }, + "description": "" + }, + "404": { + "content": { + "application/octet-stream": { + "schema": { + "format": "binary", + "type": "string" + } + } + }, + "description": "" + } + }, + "tags": [ + "Language" + ] + } + }, + "/umbraco/api/v1/language/create": { + "post": { + "operationId": "CreateLanguage_Create", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LanguageViewModel" + } } }, - "500": { - "description": "", + "required": true, + "x-name": "language", + "x-position": 1 + }, + "responses": { + "201": { + "description": "" + }, + "400": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProblemDetails" } } - } + }, + "description": "" } - } + }, + "summary": "Creates or saves a language", + "tags": [ + "Language" + ] } }, - "/umbraco/api/v1/upgrade/settings": { + "/umbraco/api/v1/language/update": { + "put": { + "operationId": "UpdateLanguage_Update", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LanguageViewModel" + } + } + }, + "required": true, + "x-name": "language", + "x-position": 1 + }, + "responses": { + "200": { + "description": "" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + }, + "description": "" + }, + "404": { + "content": { + "application/octet-stream": { + "schema": { + "format": "binary", + "type": "string" + } + } + }, + "description": "" + } + }, + "summary": "Updates a language", + "tags": [ + "Language" + ] + } + }, + "/umbraco/api/v1/examineManagement/indexes": { "get": { "tags": [ - "Upgrade" + "ExamineManagement" + ], + "operationId": "IndexesExamineManagement_Indexes", + "parameters": [ + { + "name": "skip", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + }, + "x-position": 1 + }, + { + "name": "take", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + }, + "x-position": 2 + } ], - "operationId": "SettingsUpgrade_Settings", "responses": { "200": { "description": "", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UpgradeSettingsViewModel" - } - } - } - }, - "428": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" + "$ref": "#/components/schemas/PagedViewModelOfExamineIndexViewModel" } } } @@ -194,12 +2116,23 @@ } } }, - "/umbraco/api/v1/server/status": { + "/umbraco/api/v1/examineManagement/index": { "get": { "tags": [ - "Server" + "ExamineManagement" + ], + "operationId": "IndexExamineManagement_Index", + "parameters": [ + { + "name": "indexName", + "in": "query", + "schema": { + "type": "string", + "nullable": true + }, + "x-position": 1 + } ], - "operationId": "StatusServer_Get", "responses": { "400": { "description": "", @@ -216,7 +2149,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ServerStatusViewModel" + "$ref": "#/components/schemas/ExamineIndexViewModel" } } } @@ -224,12 +2157,23 @@ } } }, - "/umbraco/api/v1/server/version": { - "get": { + "/umbraco/api/v1/examineManagement/rebuild": { + "post": { "tags": [ - "Server" + "ExamineManagement" + ], + "operationId": "RebuildExamineManagement_Rebuild", + "parameters": [ + { + "name": "indexName", + "in": "query", + "schema": { + "type": "string", + "nullable": true + }, + "x-position": 1 + } ], - "operationId": "VersionServer_Get", "responses": { "400": { "description": "", @@ -241,12 +2185,1536 @@ } } }, + "200": { + "description": "", + "content": { + "application/octet-stream": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + } + } + } + }, + "/umbraco/api/v1/examineManagement/searchers": { + "get": { + "tags": [ + "ExamineManagement" + ], + "operationId": "SearchersExamineManagement_Searchers", + "parameters": [ + { + "name": "skip", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + }, + "x-position": 1 + }, + { + "name": "take", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + }, + "x-position": 2 + } + ], + "responses": { "200": { "description": "", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/VersionViewModel" + "$ref": "#/components/schemas/PagedViewModelOfSearcherViewModel" + } + } + } + } + } + } + }, + "/umbraco/api/v1/examineManagement/search": { + "get": { + "tags": [ + "ExamineManagement" + ], + "operationId": "SearchExamineManagement_GetSearchResults", + "parameters": [ + { + "name": "searcherName", + "in": "query", + "schema": { + "type": "string", + "nullable": true + }, + "x-position": 1 + }, + { + "name": "query", + "in": "query", + "schema": { + "type": "string", + "nullable": true + }, + "x-position": 2 + }, + { + "name": "skip", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + }, + "x-position": 3 + }, + { + "name": "take", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + }, + "x-position": 4 + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedViewModelOfPagedViewModelOfSearchResultViewModel" + } + } + } + }, + "404": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + } + }, + "/umbraco/api/v1/help": { + "get": { + "operationId": "GetHelp_Get", + "parameters": [ + { + "in": "query", + "name": "section", + "schema": { + "nullable": true, + "type": "string" + }, + "x-position": 1 + }, + { + "in": "query", + "name": "tree", + "schema": { + "nullable": true, + "type": "string" + }, + "x-position": 2 + }, + { + "in": "query", + "name": "skip", + "schema": { + "format": "int32", + "type": "integer" + }, + "x-position": 3 + }, + { + "in": "query", + "name": "take", + "schema": { + "format": "int32", + "type": "integer" + }, + "x-position": 4 + }, + { + "in": "query", + "name": "baseUrl", + "schema": { + "default": "https://our.umbraco.com", + "nullable": true, + "type": "string" + }, + "x-position": 5 + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedViewModelOfHelpPageViewModel" + } + } + }, + "description": "" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + }, + "description": "" + } + }, + "tags": [ + "Help" + ] + } + }, + "/umbraco/api/v1/dictionary/{id}": { + "patch": { + "operationId": "UpdateDictionary_Update", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "format": "guid", + "type": "string" + }, + "x-position": 1 + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/JsonPatchViewModel" + }, + "type": "array" + } + } + }, + "required": true, + "x-name": "updateViewModel", + "x-position": 2 + }, + "responses": { + "200": { + "content": { + "application/octet-stream": { + "schema": { + "format": "binary", + "type": "string" + } + } + }, + "description": "" + }, + "404": { + "content": { + "application/octet-stream": { + "schema": { + "format": "binary", + "type": "string" + } + } + }, + "description": "" + } + }, + "tags": [ + "Dictionary" + ] + } + }, + "/umbraco/api/v1/dictionary/{key}": { + "delete": { + "operationId": "DeleteDictionary_Delete", + "parameters": [ + { + "description": "The key of the dictionary item to delete", + "in": "path", + "name": "key", + "required": true, + "schema": { + "format": "guid", + "type": "string" + }, + "x-position": 1 + } + ], + "responses": { + "200": { + "description": "HttpResponseMessage\n " + }, + "404": { + "content": { + "application/octet-stream": { + "schema": { + "format": "binary", + "type": "string" + } + } + }, + "description": "" + } + }, + "summary": "Deletes a data type with a given ID", + "tags": [ + "Dictionary" + ] + }, + "get": { + "operationId": "ByIdDictionary_ByKey", + "parameters": [ + { + "description": "The id.\n ", + "in": "path", + "name": "key", + "required": true, + "schema": { + "format": "guid", + "type": "string" + }, + "x-position": 1 + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DictionaryViewModel" + } + } + }, + "description": "The DictionaryDisplay. Returns a not found response when dictionary item does not exist\n " + }, + "404": { + "content": { + "application/octet-stream": { + "schema": { + "format": "binary", + "type": "string" + } + } + }, + "description": "" + } + }, + "summary": "Gets a dictionary item by guid", + "tags": [ + "Dictionary" + ] + } + }, + "/umbraco/api/v1/dictionary/create": { + "post": { + "operationId": "CreateDictionary_Create", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DictionaryItemViewModel" + } + } + }, + "description": "The viewmodel to pass to the action", + "required": true, + "x-name": "dictionaryViewModel", + "x-position": 1 + }, + "responses": { + "201": { + "content": { + "application/octet-stream": { + "schema": { + "format": "binary", + "type": "string" + } + } + }, + "description": "The HttpResponseMessage.\n " + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + }, + "description": "" + } + }, + "summary": "Creates a new dictionary item", + "tags": [ + "Dictionary" + ] + } + }, + "/umbraco/api/v1/dictionary/export/{key}": { + "get": { + "operationId": "ExportDictionary_ExportDictionary", + "parameters": [ + { + "in": "path", + "name": "key", + "required": true, + "schema": { + "format": "guid", + "type": "string" + }, + "x-position": 1 + }, + { + "in": "query", + "name": "includeChildren", + "schema": { + "default": false, + "type": "boolean" + }, + "x-position": 2 + } + ], + "responses": { + "200": { + "content": { + "application/octet-stream": { + "schema": { + "format": "binary", + "type": "string" + } + } + }, + "description": "" + }, + "404": { + "content": { + "application/octet-stream": { + "schema": { + "format": "binary", + "type": "string" + } + } + }, + "description": "" + } + }, + "tags": [ + "Dictionary" + ] + } + }, + "/umbraco/api/v1/dictionary/import": { + "post": { + "operationId": "ImportDictionary_ImportDictionary", + "parameters": [ + { + "in": "query", + "name": "file", + "schema": { + "nullable": true, + "type": "string" + }, + "x-position": 1 + }, + { + "in": "query", + "name": "parentId", + "schema": { + "format": "int32", + "nullable": true, + "type": "integer" + }, + "x-position": 2 + } + ], + "responses": { + "200": { + "content": { + "application/octet-stream": { + "schema": { + "format": "binary", + "type": "string" + } + } + }, + "description": "" + }, + "404": { + "content": { + "application/octet-stream": { + "schema": { + "format": "binary", + "type": "string" + } + } + }, + "description": "" + } + }, + "tags": [ + "Dictionary" + ] + } + }, + "/umbraco/api/v1/dictionary/upload": { + "post": { + "operationId": "UploadDictionary_Upload", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "properties": { + "file": { + "format": "binary", + "nullable": true, + "type": "string" + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DictionaryImportViewModel" + } + } + }, + "description": "" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + }, + "description": "" + } + }, + "tags": [ + "Dictionary" + ] + } + }, + "/umbraco/api/v1/document/tree/children": { + "get": { + "tags": [ + "Document" + ], + "operationId": "ChildrenDocumentTree_Children", + "parameters": [ + { + "name": "parentKey", + "in": "query", + "schema": { + "type": "string", + "format": "guid" + }, + "x-position": 1 + }, + { + "name": "skip", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + }, + "x-position": 2 + }, + { + "name": "take", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 100 + }, + "x-position": 3 + }, + { + "name": "dataTypeKey", + "in": "query", + "schema": { + "type": "string", + "format": "guid", + "nullable": true + }, + "x-position": 4 + }, + { + "name": "culture", + "in": "query", + "schema": { + "type": "string", + "nullable": true + }, + "x-position": 5 + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedViewModelOfDocumentTreeItemViewModel" + } + } + } + } + } + } + }, + "/umbraco/api/v1/document/tree/items": { + "get": { + "tags": [ + "Document" + ], + "operationId": "ItemsDocumentTree_Items", + "parameters": [ + { + "name": "key", + "x-originalName": "keys", + "in": "query", + "style": "form", + "explode": true, + "schema": { + "type": "array", + "nullable": true, + "items": { + "type": "string", + "format": "guid" + } + }, + "x-position": 1 + }, + { + "name": "dataTypeKey", + "in": "query", + "schema": { + "type": "string", + "format": "guid", + "nullable": true + }, + "x-position": 2 + }, + { + "name": "culture", + "in": "query", + "schema": { + "type": "string", + "nullable": true + }, + "x-position": 3 + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DocumentTreeItemViewModel" + } + } + } + } + } + } + } + }, + "/umbraco/api/v1/document/tree/root": { + "get": { + "tags": [ + "Document" + ], + "operationId": "RootDocumentTree_Root", + "parameters": [ + { + "name": "skip", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + }, + "x-position": 1 + }, + { + "name": "take", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 100 + }, + "x-position": 2 + }, + { + "name": "dataTypeKey", + "in": "query", + "schema": { + "type": "string", + "format": "guid", + "nullable": true + }, + "x-position": 3 + }, + { + "name": "culture", + "in": "query", + "schema": { + "type": "string", + "nullable": true + }, + "x-position": 4 + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedViewModelOfDocumentTreeItemViewModel" + } + } + } + } + } + } + }, + "/umbraco/api/v1/document/recycle-bin/children": { + "get": { + "tags": [ + "Document" + ], + "operationId": "ChildrenDocumentRecycleBin_Children", + "parameters": [ + { + "name": "parentKey", + "in": "query", + "schema": { + "type": "string", + "format": "guid" + }, + "x-position": 1 + }, + { + "name": "skip", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + }, + "x-position": 2 + }, + { + "name": "take", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 100 + }, + "x-position": 3 + } + ], + "responses": { + "401": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedViewModelOfRecycleBinItemViewModel" + } + } + } + } + } + } + }, + "/umbraco/api/v1/document/recycle-bin/root": { + "get": { + "tags": [ + "Document" + ], + "operationId": "RootDocumentRecycleBin_Root", + "parameters": [ + { + "name": "skip", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + }, + "x-position": 1 + }, + { + "name": "take", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 100 + }, + "x-position": 2 + } + ], + "responses": { + "401": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedViewModelOfRecycleBinItemViewModel" + } + } + } + } + } + } + }, + "/umbraco/api/v1/document-type/tree/children": { + "get": { + "tags": [ + "DocumentType" + ], + "operationId": "ChildrenDocumentTypeTree_Children", + "parameters": [ + { + "name": "parentKey", + "in": "query", + "schema": { + "type": "string", + "format": "guid" + }, + "x-position": 1 + }, + { + "name": "skip", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + }, + "x-position": 2 + }, + { + "name": "take", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 100 + }, + "x-position": 3 + }, + { + "name": "foldersOnly", + "in": "query", + "schema": { + "type": "boolean", + "default": false + }, + "x-position": 4 + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedViewModelOfDocumentTypeTreeItemViewModel" + } + } + } + } + } + } + }, + "/umbraco/api/v1/document-type/tree/items": { + "get": { + "tags": [ + "DocumentType" + ], + "operationId": "ItemsDocumentTypeTree_Items", + "parameters": [ + { + "name": "key", + "x-originalName": "keys", + "in": "query", + "style": "form", + "explode": true, + "schema": { + "type": "array", + "nullable": true, + "items": { + "type": "string", + "format": "guid" + } + }, + "x-position": 1 + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DocumentTypeTreeItemViewModel" + } + } + } + } + } + } + } + }, + "/umbraco/api/v1/document-type/tree/root": { + "get": { + "tags": [ + "DocumentType" + ], + "operationId": "RootDocumentTypeTree_Root", + "parameters": [ + { + "name": "skip", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + }, + "x-position": 1 + }, + { + "name": "take", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 100 + }, + "x-position": 2 + }, + { + "name": "foldersOnly", + "in": "query", + "schema": { + "type": "boolean", + "default": false + }, + "x-position": 3 + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedViewModelOfDocumentTypeTreeItemViewModel" + } + } + } + } + } + } + }, + "/umbraco/api/v1/document-blueprint/tree/items": { + "get": { + "tags": [ + "DocumentBlueprint" + ], + "operationId": "ItemsDocumentBlueprintTree_Items", + "parameters": [ + { + "name": "key", + "x-originalName": "keys", + "in": "query", + "style": "form", + "explode": true, + "schema": { + "type": "array", + "nullable": true, + "items": { + "type": "string", + "format": "guid" + } + }, + "x-position": 1 + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DocumentBlueprintTreeItemViewModel" + } + } + } + } + } + } + } + }, + "/umbraco/api/v1/document-blueprint/tree/root": { + "get": { + "tags": [ + "DocumentBlueprint" + ], + "operationId": "RootDocumentBlueprintTree_Root", + "parameters": [ + { + "name": "skip", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + }, + "x-position": 1 + }, + { + "name": "take", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 100 + }, + "x-position": 2 + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedViewModelOfDocumentBlueprintTreeItemViewModel" + } + } + } + } + } + } + }, + "/umbraco/api/v1/dictionary": { + "get": { + "operationId": "AllDictionary_All", + "parameters": [ + { + "in": "query", + "name": "skip", + "schema": { + "format": "int32", + "type": "integer" + }, + "x-position": 1 + }, + { + "in": "query", + "name": "take", + "schema": { + "format": "int32", + "type": "integer" + }, + "x-position": 2 + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedViewModelOfDictionaryOverviewViewModel" + } + } + }, + "description": "The IEnumerable`1.\n " + } + }, + "summary": "Retrieves a list with all dictionary items", + "tags": [ + "Dictionary" + ] + } + }, + "/umbraco/api/v1/dictionary-item/tree/children": { + "get": { + "tags": [ + "DictionaryItem" + ], + "operationId": "ChildrenDictionaryItemTree_Children", + "parameters": [ + { + "name": "parentKey", + "in": "query", + "schema": { + "type": "string", + "format": "guid" + }, + "x-position": 1 + }, + { + "name": "skip", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + }, + "x-position": 2 + }, + { + "name": "take", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 100 + }, + "x-position": 3 + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedViewModelOfEntityTreeItemViewModel" + } + } + } + } + } + } + }, + "/umbraco/api/v1/dictionary-item/tree/items": { + "get": { + "tags": [ + "DictionaryItem" + ], + "operationId": "ItemsDictionaryItemTree_Items", + "parameters": [ + { + "name": "key", + "x-originalName": "keys", + "in": "query", + "style": "form", + "explode": true, + "schema": { + "type": "array", + "nullable": true, + "items": { + "type": "string", + "format": "guid" + } + }, + "x-position": 1 + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FolderTreeItemViewModel" + } + } + } + } + } + } + } + }, + "/umbraco/api/v1/dictionary-item/tree/root": { + "get": { + "tags": [ + "DictionaryItem" + ], + "operationId": "RootDictionaryItemTree_Root", + "parameters": [ + { + "name": "skip", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + }, + "x-position": 1 + }, + { + "name": "take", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 100 + }, + "x-position": 2 + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedViewModelOfEntityTreeItemViewModel" + } + } + } + } + } + } + }, + "/umbraco/api/v1/analytics": { + "get": { + "operationId": "GetAnalytics_Get", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AnalyticsLevelViewModel" + } + } + }, + "description": "" + } + }, + "tags": [ + "Analytics" + ] + }, + "post": { + "operationId": "SetAnalytics_SetConsentLevel", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AnalyticsLevelViewModel" + } + } + }, + "required": true, + "x-name": "analyticsLevelViewModel", + "x-position": 1 + }, + "responses": { + "200": { + "description": "" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + }, + "description": "" + } + }, + "tags": [ + "Analytics" + ] + } + }, + "/umbraco/api/v1/analytics/all": { + "get": { + "operationId": "AllAnalytics_GetAll", + "parameters": [ + { + "in": "query", + "name": "skip", + "schema": { + "format": "int32", + "type": "integer" + }, + "x-position": 1 + }, + { + "in": "query", + "name": "take", + "schema": { + "format": "int32", + "type": "integer" + }, + "x-position": 2 + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedViewModelOfTelemetryLevel" + } + } + }, + "description": "" + } + }, + "tags": [ + "Analytics" + ] + } + }, + "/umbraco/api/v1/culture": { + "get": { + "operationId": "AllCulture_GetAll", + "parameters": [ + { + "in": "query", + "name": "skip", + "schema": { + "format": "int32", + "type": "integer" + }, + "x-position": 1 + }, + { + "in": "query", + "name": "take", + "schema": { + "format": "int32", + "type": "integer" + }, + "x-position": 2 + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedViewModelOfCultureViewModel" + } + } + }, + "description": "" + } + }, + "summary": "Returns all cultures available for creating languages.", + "tags": [ + "Culture" + ] + } + }, + "/umbraco/api/v1/data-type/tree/children": { + "get": { + "tags": [ + "DataType" + ], + "operationId": "ChildrenDataTypeTree_Children", + "parameters": [ + { + "name": "parentKey", + "in": "query", + "schema": { + "type": "string", + "format": "guid" + }, + "x-position": 1 + }, + { + "name": "skip", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + }, + "x-position": 2 + }, + { + "name": "take", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 100 + }, + "x-position": 3 + }, + { + "name": "foldersOnly", + "in": "query", + "schema": { + "type": "boolean", + "default": false + }, + "x-position": 4 + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedViewModelOfFolderTreeItemViewModel" + } + } + } + } + } + } + }, + "/umbraco/api/v1/data-type/tree/items": { + "get": { + "tags": [ + "DataType" + ], + "operationId": "ItemsDataTypeTree_Items", + "parameters": [ + { + "name": "key", + "x-originalName": "keys", + "in": "query", + "style": "form", + "explode": true, + "schema": { + "type": "array", + "nullable": true, + "items": { + "type": "string", + "format": "guid" + } + }, + "x-position": 1 + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FolderTreeItemViewModel" + } + } + } + } + } + } + } + }, + "/umbraco/api/v1/data-type/tree/root": { + "get": { + "tags": [ + "DataType" + ], + "operationId": "RootDataTypeTree_Root", + "parameters": [ + { + "name": "skip", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + }, + "x-position": 1 + }, + { + "name": "take", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 100 + }, + "x-position": 2 + }, + { + "name": "foldersOnly", + "in": "query", + "schema": { + "type": "boolean", + "default": false + }, + "x-position": 3 + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedViewModelOfFolderTreeItemViewModel" } } } @@ -286,6 +3754,278 @@ } } }, + "UpgradeSettingsViewModel": { + "type": "object", + "additionalProperties": false, + "properties": { + "currentState": { + "type": "string" + }, + "newState": { + "type": "string" + }, + "newVersion": { + "type": "string" + }, + "oldVersion": { + "type": "string" + }, + "reportUrl": { + "type": "string" + } + } + }, + "PagedViewModelOfEntityTreeItemViewModel": { + "type": "object", + "additionalProperties": false, + "properties": { + "total": { + "type": "integer", + "format": "int64" + }, + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EntityTreeItemViewModel" + } + } + } + }, + "EntityTreeItemViewModel": { + "allOf": [ + { + "$ref": "#/components/schemas/TreeItemViewModel" + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "key": { + "type": "string", + "format": "guid" + }, + "isContainer": { + "type": "boolean" + }, + "parentKey": { + "type": "string", + "format": "guid", + "nullable": true + } + } + } + ] + }, + "TreeItemViewModel": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": "string" + }, + "type": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "hasChildren": { + "type": "boolean" + } + } + }, + "PagedViewModelOfFileSystemTreeItemViewModel": { + "type": "object", + "additionalProperties": false, + "properties": { + "total": { + "type": "integer", + "format": "int64" + }, + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FileSystemTreeItemViewModel" + } + } + } + }, + "FileSystemTreeItemViewModel": { + "allOf": [ + { + "$ref": "#/components/schemas/TreeItemViewModel" + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "string" + }, + "isFolder": { + "type": "boolean" + } + } + } + ] + }, + "ServerStatusViewModel": { + "type": "object", + "additionalProperties": false, + "properties": { + "serverStatus": { + "$ref": "#/components/schemas/RuntimeLevel" + } + } + }, + "RuntimeLevel": { + "type": "string", + "description": "Describes the levels in which the runtime can run.\n ", + "x-enumNames": [ + "Unknown", + "Boot", + "Install", + "Upgrade", + "Run", + "BootFailed" + ], + "enum": [ + "Unknown", + "Boot", + "Install", + "Upgrade", + "Run", + "BootFailed" + ] + }, + "VersionViewModel": { + "type": "object", + "additionalProperties": false, + "properties": { + "version": { + "type": "string" + } + } + }, + "FolderTreeItemViewModel": { + "allOf": [ + { + "$ref": "#/components/schemas/EntityTreeItemViewModel" + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "isFolder": { + "type": "boolean" + } + } + } + ] + }, + "ProfilingStatusViewModel": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean" + } + } + }, + "PagedViewModelOfContentTreeItemViewModel": { + "type": "object", + "additionalProperties": false, + "properties": { + "total": { + "type": "integer", + "format": "int64" + }, + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ContentTreeItemViewModel" + } + } + } + }, + "ContentTreeItemViewModel": { + "allOf": [ + { + "$ref": "#/components/schemas/EntityTreeItemViewModel" + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "noAccess": { + "type": "boolean" + } + } + } + ] + }, + "PagedViewModelOfRecycleBinItemViewModel": { + "type": "object", + "additionalProperties": false, + "properties": { + "total": { + "type": "integer", + "format": "int64" + }, + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RecycleBinItemViewModel" + } + } + } + }, + "RecycleBinItemViewModel": { + "type": "object", + "additionalProperties": false, + "properties": { + "key": { + "type": "string", + "format": "guid" + }, + "name": { + "type": "string" + }, + "type": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "hasChildren": { + "type": "boolean" + }, + "isContainer": { + "type": "boolean" + }, + "parentKey": { + "type": "string", + "format": "guid", + "nullable": true + } + } + }, + "PagedViewModelOfFolderTreeItemViewModel": { + "type": "object", + "additionalProperties": false, + "properties": { + "total": { + "type": "integer", + "format": "int64" + }, + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FolderTreeItemViewModel" + } + } + } + }, "InstallSettingsViewModel": { "type": "object", "additionalProperties": false, @@ -477,62 +4217,254 @@ } } }, - "UpgradeSettingsViewModel": { + "PagedViewModelOfExamineIndexViewModel": { "type": "object", "additionalProperties": false, "properties": { - "currentState": { + "total": { + "type": "integer", + "format": "int64" + }, + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ExamineIndexViewModel" + } + } + } + }, + "ExamineIndexViewModel": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { "type": "string" }, - "newState": { + "healthStatus": { + "type": "string", + "nullable": true + }, + "isHealthy": { + "type": "boolean" + }, + "canRebuild": { + "type": "boolean" + }, + "searcherName": { "type": "string" }, - "newVersion": { - "type": "string" + "documentCount": { + "type": "integer", + "format": "int64" }, - "oldVersion": { - "type": "string" + "fieldCount": { + "type": "integer", + "format": "int32" + } + } + }, + "PagedViewModelOfSearcherViewModel": { + "type": "object", + "additionalProperties": false, + "properties": { + "total": { + "type": "integer", + "format": "int64" }, - "reportUrl": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SearcherViewModel" + } + } + } + }, + "SearcherViewModel": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { "type": "string" } } }, - "ServerStatusViewModel": { + "PagedViewModelOfPagedViewModelOfSearchResultViewModel": { "type": "object", "additionalProperties": false, "properties": { - "serverStatus": { - "$ref": "#/components/schemas/RuntimeLevel" + "total": { + "type": "integer", + "format": "int64" + }, + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PagedViewModelOfSearchResultViewModel" + } } } }, - "RuntimeLevel": { - "type": "string", - "description": "Describes the levels in which the runtime can run.\n ", - "x-enumNames": [ - "Unknown", - "Boot", - "Install", - "Upgrade", - "Run", - "BootFailed" - ], - "enum": [ - "Unknown", - "Boot", - "Install", - "Upgrade", - "Run", - "BootFailed" + "PagedViewModelOfSearchResultViewModel": { + "type": "object", + "additionalProperties": false, + "properties": { + "total": { + "type": "integer", + "format": "int64" + }, + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SearchResultViewModel" + } + } + } + }, + "SearchResultViewModel": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string" + }, + "score": { + "type": "number", + "format": "float" + }, + "fieldCount": { + "type": "integer", + "format": "int32" + }, + "fields": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FieldViewModel" + } + } + } + }, + "FieldViewModel": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": "string" + }, + "values": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "PagedViewModelOfDocumentTreeItemViewModel": { + "type": "object", + "additionalProperties": false, + "properties": { + "total": { + "type": "integer", + "format": "int64" + }, + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DocumentTreeItemViewModel" + } + } + } + }, + "DocumentTreeItemViewModel": { + "allOf": [ + { + "$ref": "#/components/schemas/ContentTreeItemViewModel" + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "isProtected": { + "type": "boolean" + }, + "isPublished": { + "type": "boolean" + }, + "isEdited": { + "type": "boolean" + } + } + } ] }, - "VersionViewModel": { + "PagedViewModelOfDocumentTypeTreeItemViewModel": { "type": "object", "additionalProperties": false, "properties": { - "version": { - "type": "string" + "total": { + "type": "integer", + "format": "int64" + }, + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DocumentTypeTreeItemViewModel" + } + } + } + }, + "DocumentTypeTreeItemViewModel": { + "allOf": [ + { + "$ref": "#/components/schemas/FolderTreeItemViewModel" + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "isElement": { + "type": "boolean" + } + } + } + ] + }, + "DocumentBlueprintTreeItemViewModel": { + "allOf": [ + { + "$ref": "#/components/schemas/EntityTreeItemViewModel" + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "documentTypeKey": { + "type": "string", + "format": "guid" + }, + "documentTypeAlias": { + "type": "string" + }, + "documentTypeName": { + "type": "string", + "nullable": true + } + } + } + ] + }, + "PagedViewModelOfDocumentBlueprintTreeItemViewModel": { + "type": "object", + "additionalProperties": false, + "properties": { + "total": { + "type": "integer", + "format": "int64" + }, + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DocumentBlueprintTreeItemViewModel" + } } } } @@ -540,13 +4472,88 @@ }, "tags": [ { - "name": "Upgrade" + "name": "Analytics" + }, + { + "name": "Culture" + }, + { + "name": "DataType" + }, + { + "name": "Dictionary" + }, + { + "name": "DictionaryItem" + }, + { + "name": "Document" + }, + { + "name": "DocumentBlueprint" + }, + { + "name": "DocumentType" + }, + { + "name": "ExamineManagement" + }, + { + "name": "Help" + }, + { + "name": "Install" + }, + { + "name": "Language" + }, + { + "name": "Media" + }, + { + "name": "MediaType" + }, + { + "name": "MemberGroup" + }, + { + "name": "MemberType" + }, + { + "name": "PartialView" + }, + { + "name": "Profiling" + }, + { + "name": "PublishedCache" + }, + { + "name": "Relation" + }, + { + "name": "RelationType" + }, + { + "name": "Script" }, { "name": "Server" }, { - "name": "Install" + "name": "StaticFile" + }, + { + "name": "Stylesheet" + }, + { + "name": "Template" + }, + { + "name": "TrackedReferences" + }, + { + "name": "Upgrade" } ] } diff --git a/src/Umbraco.Cms.ManagementApi/Serialization/ISystemTextJsonSerializer.cs b/src/Umbraco.Cms.ManagementApi/Serialization/ISystemTextJsonSerializer.cs new file mode 100644 index 0000000000..717d9491e6 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Serialization/ISystemTextJsonSerializer.cs @@ -0,0 +1,7 @@ +using Umbraco.Cms.Core.Serialization; + +namespace Umbraco.Cms.ManagementApi.Serialization; + +public interface ISystemTextJsonSerializer : IJsonSerializer +{ +} diff --git a/src/Umbraco.Cms.ManagementApi/Serialization/SystemTextJsonSerializer.cs b/src/Umbraco.Cms.ManagementApi/Serialization/SystemTextJsonSerializer.cs new file mode 100644 index 0000000000..65deecadad --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Serialization/SystemTextJsonSerializer.cs @@ -0,0 +1,17 @@ +using System.Text.Json; + +namespace Umbraco.Cms.ManagementApi.Serialization; + +public class SystemTextJsonSerializer : ISystemTextJsonSerializer +{ + private JsonSerializerOptions _jsonSerializerOptions; + public SystemTextJsonSerializer() + { + _jsonSerializerOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; + } + public string Serialize(object? input) => JsonSerializer.Serialize(input, _jsonSerializerOptions); + + public T? Deserialize(string input) => JsonSerializer.Deserialize(input, _jsonSerializerOptions); + + public T? DeserializeSubset(string input, string key) => throw new NotSupportedException(); +} diff --git a/src/Umbraco.Cms.ManagementApi/Services/Entities/IUserStartNodeEntitiesService.cs b/src/Umbraco.Cms.ManagementApi/Services/Entities/IUserStartNodeEntitiesService.cs new file mode 100644 index 0000000000..2f05759b42 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Services/Entities/IUserStartNodeEntitiesService.cs @@ -0,0 +1,42 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.Models.Entities; + +namespace Umbraco.Cms.ManagementApi.Services.Entities; + +public interface IUserStartNodeEntitiesService +{ + /// + /// Calculates the applicable root entities for a given object type for users without root access. + /// + /// The object type. + /// The calculated start node IDs for the user. + /// A list of root entities for the user. + /// + /// The returned entities may include entities that outside of the user start node scope, but are needed to + /// for browsing to the actual user start nodes. These entities will be marked as "no access" entities. + /// + IEnumerable RootUserAccessEntities(UmbracoObjectTypes umbracoObjectType, int[] userStartNodeIds); + + /// + /// Calculates the applicable child entities from a list of candidate child entities for users without root access. + /// + /// The candidate child entities to filter (i.e. entities fetched with ). + /// The calculated start node paths for the user. + /// A list of child entities applicable entities for the user. + /// + /// The returned entities may include entities that outside of the user start node scope, but are needed to + /// for browsing to the actual user start nodes. These entities will be marked as "no access" entities. + /// Some candidate entities may be filtered out if they are not applicable for the user scope. + /// + IEnumerable ChildUserAccessEntities(IEnumerable candidateChildren, string[] userStartNodePaths); + + /// + /// Calculates the access level of a collection of entities for users without root access. + /// + /// The entities. + /// The calculated start node paths for the user. + /// The access level for each entity. + IEnumerable UserAccessEntities(IEnumerable entities, string[] userStartNodePaths); +} diff --git a/src/Umbraco.Cms.ManagementApi/Services/Entities/UserStartNodeEntitiesService.cs b/src/Umbraco.Cms.ManagementApi/Services/Entities/UserStartNodeEntitiesService.cs new file mode 100644 index 0000000000..70c01308d8 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Services/Entities/UserStartNodeEntitiesService.cs @@ -0,0 +1,76 @@ +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.Models.Entities; +using Umbraco.Extensions; + +namespace Umbraco.Cms.ManagementApi.Services.Entities; + +public class UserStartNodeEntitiesService : IUserStartNodeEntitiesService +{ + private readonly IEntityService _entityService; + + public UserStartNodeEntitiesService(IEntityService entityService) => _entityService = entityService; + + /// + public IEnumerable RootUserAccessEntities(UmbracoObjectTypes umbracoObjectType, int[] userStartNodeIds) + { + // root entities for users without root access should include: + // - the start nodes that are actual root entities (level == 1) + // - the root level ancestors to the rest of the start nodes (required for browsing to the actual start nodes - will be marked as "no access") + IEntitySlim[] userStartEntities = _entityService.GetAll(umbracoObjectType, userStartNodeIds).ToArray(); + + // find the start nodes that are at root level (level == 1) + IEntitySlim[] allowedTopmostEntities = userStartEntities.Where(entity => entity.Level == 1).ToArray(); + + // find the root level ancestors of the rest of the start nodes, and add those as well + var nonAllowedTopmostEntityIds = userStartEntities.Except(allowedTopmostEntities) + .Select(entity => int.TryParse(entity.Path.Split(Constants.CharArrays.Comma).Skip(1).FirstOrDefault(), out var id) ? id : 0) + .Where(id => id > 0) + .ToArray(); + IEntitySlim[] nonAllowedTopmostEntities = nonAllowedTopmostEntityIds.Any() + ? _entityService.GetAll(umbracoObjectType, nonAllowedTopmostEntityIds).ToArray() + : Array.Empty(); + + return allowedTopmostEntities + .Select(entity => new UserAccessEntity(entity, true)) + .Union( + nonAllowedTopmostEntities + .Select(entity => new UserAccessEntity(entity, false))) + .ToArray(); + } + + /// + public IEnumerable ChildUserAccessEntities(IEnumerable candidateChildren, string[] userStartNodePaths) + // child entities for users without root access should include: + // - children that are descendant-or-self of a user start node + // - children that are ancestors of a user start node (required for browsing to the actual start nodes - will be marked as "no access") + // all other candidate children should be discarded + => candidateChildren.Select(child => + { + // is descendant-or-self of a start node? + if (IsDescendantOrSelf(child, userStartNodePaths)) + { + return new UserAccessEntity(child, true); + } + + // is ancestor of a start node? + if (userStartNodePaths.Any(path => path.StartsWith(child.Path))) + { + return new UserAccessEntity(child, false); + } + + return null; + }).WhereNotNull().ToArray(); + + /// + public IEnumerable UserAccessEntities(IEnumerable entities, string[] userStartNodePaths) + // entities for users without root access should include: + // - entities that are descendant-or-self of a user start node as regular entities + // - all other entities as "no access" entities + => entities.Select(entity => new UserAccessEntity(entity, IsDescendantOrSelf(entity, userStartNodePaths))).ToArray(); + + private static bool IsDescendantOrSelf(IEntitySlim child, string[] userStartNodePaths) + => userStartNodePaths.Any(path => child.Path.StartsWith(path)); +} diff --git a/src/Umbraco.Cms.ManagementApi/Services/ExamineManagerService.cs b/src/Umbraco.Cms.ManagementApi/Services/ExamineManagerService.cs new file mode 100644 index 0000000000..21195bdb51 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Services/ExamineManagerService.cs @@ -0,0 +1,23 @@ +using Examine; + +namespace Umbraco.Cms.ManagementApi.Services; + +public class ExamineManagerService : IExamineManagerService +{ + private readonly IExamineManager _examineManager; + + public ExamineManagerService(IExamineManager examineManager) => _examineManager = examineManager; + + public bool TryFindSearcher(string searcherName, out ISearcher searcher) + { + // try to get the searcher from the indexes + if (!_examineManager.TryGetIndex(searcherName, out IIndex index)) + { + // if we didn't find anything try to find it by an explicitly declared searcher + return _examineManager.TryGetSearcher(searcherName, out searcher); + } + + searcher = index.Searcher; + return true; + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Services/IExamineManagerService.cs b/src/Umbraco.Cms.ManagementApi/Services/IExamineManagerService.cs new file mode 100644 index 0000000000..9a46ff5270 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Services/IExamineManagerService.cs @@ -0,0 +1,8 @@ +using Examine; + +namespace Umbraco.Cms.ManagementApi.Services; + +public interface IExamineManagerService +{ + bool TryFindSearcher(string searcherName, out ISearcher searcher); +} diff --git a/src/Umbraco.Cms.ManagementApi/Services/IJsonPatchService.cs b/src/Umbraco.Cms.ManagementApi/Services/IJsonPatchService.cs new file mode 100644 index 0000000000..5c196a08b7 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Services/IJsonPatchService.cs @@ -0,0 +1,9 @@ +using Json.Patch; +using Umbraco.Cms.ManagementApi.ViewModels.JsonPatch; + +namespace Umbraco.Cms.ManagementApi.Services; + +public interface IJsonPatchService +{ + PatchResult? Patch(JsonPatchViewModel[] patchViewModel, object objectToPatch); +} diff --git a/src/Umbraco.Cms.ManagementApi/Services/ILoadDictionaryItemService.cs b/src/Umbraco.Cms.ManagementApi/Services/ILoadDictionaryItemService.cs new file mode 100644 index 0000000000..dc9def29fa --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Services/ILoadDictionaryItemService.cs @@ -0,0 +1,8 @@ +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.ManagementApi.Services; + +public interface ILoadDictionaryItemService +{ + IDictionaryItem Load(string filePath, int? parentId); +} diff --git a/src/Umbraco.Cms.ManagementApi/Services/IUploadFileService.cs b/src/Umbraco.Cms.ManagementApi/Services/IUploadFileService.cs new file mode 100644 index 0000000000..32be53b04f --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Services/IUploadFileService.cs @@ -0,0 +1,9 @@ +using Microsoft.AspNetCore.Http; +using Umbraco.Cms.ManagementApi.Models; + +namespace Umbraco.Cms.ManagementApi.Services; + +public interface IUploadFileService +{ + FormFileUploadResult TryLoad(IFormFile file); +} diff --git a/src/Umbraco.Cms.ManagementApi/Services/JsonPatchService.cs b/src/Umbraco.Cms.ManagementApi/Services/JsonPatchService.cs new file mode 100644 index 0000000000..cb614832eb --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Services/JsonPatchService.cs @@ -0,0 +1,23 @@ +using System.Text.Json.Nodes; +using Json.Patch; +using Umbraco.Cms.ManagementApi.Serialization; +using Umbraco.Cms.ManagementApi.ViewModels.JsonPatch; + +namespace Umbraco.Cms.ManagementApi.Services; + +public class JsonPatchService : IJsonPatchService +{ + private readonly ISystemTextJsonSerializer _systemTextJsonSerializer; + + public JsonPatchService(ISystemTextJsonSerializer systemTextJsonSerializer) => _systemTextJsonSerializer = systemTextJsonSerializer; + + public PatchResult? Patch(JsonPatchViewModel[] patchViewModel, object objectToPatch) + { + var patchString = _systemTextJsonSerializer.Serialize(patchViewModel); + + var docString = _systemTextJsonSerializer.Serialize(objectToPatch); + JsonPatch? patch = _systemTextJsonSerializer.Deserialize(patchString); + var doc = JsonNode.Parse(docString); + return patch?.Apply(doc); + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Services/LoadDictionaryItemService.cs b/src/Umbraco.Cms.ManagementApi/Services/LoadDictionaryItemService.cs new file mode 100644 index 0000000000..5672188caf --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Services/LoadDictionaryItemService.cs @@ -0,0 +1,52 @@ +using System.Xml; +using System.Xml.Linq; +using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Infrastructure.Packaging; + +namespace Umbraco.Cms.ManagementApi.Services; + +public class LoadDictionaryItemService : ILoadDictionaryItemService +{ + private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; + private readonly ILocalizationService _localizationService; + private readonly PackageDataInstallation _packageDataInstallation; + private readonly ILogger _logger; + + public LoadDictionaryItemService( + IBackOfficeSecurityAccessor backOfficeSecurityAccessor, + ILocalizationService localizationService, + PackageDataInstallation packageDataInstallation, + ILogger logger) + { + _backOfficeSecurityAccessor = backOfficeSecurityAccessor; + _localizationService = localizationService; + _packageDataInstallation = packageDataInstallation; + _logger = logger; + } + public IDictionaryItem Load(string filePath, int? parentId) + { + var xmlDocument = new XmlDocument { XmlResolver = null }; + xmlDocument.Load(filePath); + + var userId = _backOfficeSecurityAccessor.BackOfficeSecurity?.GetUserId().Result ?? 0; + var element = XElement.Parse(xmlDocument.InnerXml); + + IDictionaryItem? parentDictionaryItem = _localizationService.GetDictionaryItemById(parentId ?? 0); + IEnumerable dictionaryItems = _packageDataInstallation.ImportDictionaryItem(element, userId, parentDictionaryItem?.Key); + + // Try to clean up the temporary file. + try + { + System.IO.File.Delete(filePath); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error cleaning up temporary udt file in {File}", filePath); + } + + return dictionaryItems.First(); + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Services/Paging/PaginationService.cs b/src/Umbraco.Cms.ManagementApi/Services/Paging/PaginationService.cs new file mode 100644 index 0000000000..34488a51c3 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Services/Paging/PaginationService.cs @@ -0,0 +1,39 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace Umbraco.Cms.ManagementApi.Services.Paging; + +// TODO: remove this class once EF core is in place with proper skip/take pagination implementation +// this service is used for converting skip/take to classic pagination with page number and page size. +// it is a temporary solution that should be removed once EF core is in place, thus we'll live +// with this code being statically referenced across multiple controllers. the alternative would be +// an injectable service, but that would require a greater clean-up effort later on. +internal static class PaginationService +{ + internal static bool ConvertSkipTakeToPaging(int skip, int take, out long pageNumber, out int pageSize, out ProblemDetails? error) + { + if (take <= 0) + { + throw new ArgumentException("Must be greater than zero", nameof(take)); + } + + if (skip % take != 0) + { + pageSize = 0; + pageNumber = 0; + error = new ProblemDetails + { + Title = "Invalid skip/take", + Detail = "Skip must be a multiple of take - i.e. skip = 10, take = 5", + Status = StatusCodes.Status400BadRequest, + Type = "Error", + }; + return false; + } + + pageSize = take; + pageNumber = skip / take; + error = null; + return true; + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Services/UploadFileService.cs b/src/Umbraco.Cms.ManagementApi/Services/UploadFileService.cs new file mode 100644 index 0000000000..20a43511ea --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Services/UploadFileService.cs @@ -0,0 +1,63 @@ +using System.Xml; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.FileProviders; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Hosting; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.Models; +using Umbraco.Extensions; + +namespace Umbraco.Cms.ManagementApi.Services; + +public class UploadFileService : IUploadFileService +{ + private readonly IHostingEnvironment _hostingEnvironment; + private readonly ILocalizedTextService _localizedTextService; + + public UploadFileService(IHostingEnvironment hostingEnvironment, ILocalizedTextService localizedTextService) + { + _hostingEnvironment = hostingEnvironment; + _localizedTextService = localizedTextService; + } + + public FormFileUploadResult TryLoad(IFormFile file) + { + var formFileUploadResult = new FormFileUploadResult(); + var fileName = file.FileName.Trim(Constants.CharArrays.DoubleQuote); + var ext = fileName.Substring(fileName.LastIndexOf('.') + 1).ToLower(); + var root = _hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.TempFileUploads); + formFileUploadResult.TemporaryPath = Path.Combine(root, fileName); + + if (!Path.GetFullPath(formFileUploadResult.TemporaryPath).StartsWith(Path.GetFullPath(root))) + { + formFileUploadResult.ErrorMessage = _localizedTextService.Localize("media", "invalidFileName"); + formFileUploadResult.CouldLoad = false; + return formFileUploadResult; + } + + if (!ext.InvariantEquals("udt")) + { + formFileUploadResult.ErrorMessage = _localizedTextService.Localize("media", "disallowedFileType"); + formFileUploadResult.CouldLoad = false; + return formFileUploadResult; + } + + using (FileStream stream = File.Create(formFileUploadResult.TemporaryPath)) + { + file.CopyToAsync(stream).GetAwaiter().GetResult(); + } + + formFileUploadResult.XmlDocument = new XmlDocument {XmlResolver = null}; + formFileUploadResult.XmlDocument.Load(formFileUploadResult.TemporaryPath); + + if (formFileUploadResult.XmlDocument.DocumentElement != null) + { + return formFileUploadResult; + } + + formFileUploadResult.ErrorMessage = _localizedTextService.Localize("speechBubbles", "fileErrorNotFound"); + formFileUploadResult.CouldLoad = false; + return formFileUploadResult; + + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Umbraco.Cms.ManagementApi.csproj b/src/Umbraco.Cms.ManagementApi/Umbraco.Cms.ManagementApi.csproj index 283b447ab0..52a41491b4 100644 --- a/src/Umbraco.Cms.ManagementApi/Umbraco.Cms.ManagementApi.csproj +++ b/src/Umbraco.Cms.ManagementApi/Umbraco.Cms.ManagementApi.csproj @@ -2,12 +2,12 @@ Umbraco CMS - Management API Contains the presentation layer for the Umbraco CMS Management API. - net7.0 false false + diff --git a/src/Umbraco.Cms.ManagementApi/ViewModels/Analytics/AnalyticsLevelViewModel.cs b/src/Umbraco.Cms.ManagementApi/ViewModels/Analytics/AnalyticsLevelViewModel.cs new file mode 100644 index 0000000000..73d442992e --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/ViewModels/Analytics/AnalyticsLevelViewModel.cs @@ -0,0 +1,10 @@ +using System.Text.Json.Serialization; +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.ManagementApi.ViewModels.Analytics; + +public class AnalyticsLevelViewModel +{ + [JsonConverter(typeof(JsonStringEnumConverter))] + public TelemetryLevel AnalyticsLevel { get; set; } +} diff --git a/src/Umbraco.Cms.ManagementApi/ViewModels/Culture/CultureViewModel.cs b/src/Umbraco.Cms.ManagementApi/ViewModels/Culture/CultureViewModel.cs new file mode 100644 index 0000000000..9d75f8c6da --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/ViewModels/Culture/CultureViewModel.cs @@ -0,0 +1,8 @@ +namespace Umbraco.Cms.ManagementApi.ViewModels.Culture; + +public class CultureViewModel +{ + public string Name { get; set; } = null!; + + public string EnglishName { get; set; } = null!; +} diff --git a/src/Umbraco.Cms.ManagementApi/ViewModels/Dictionary/DictionaryImportViewModel.cs b/src/Umbraco.Cms.ManagementApi/ViewModels/Dictionary/DictionaryImportViewModel.cs new file mode 100644 index 0000000000..011f5febc8 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/ViewModels/Dictionary/DictionaryImportViewModel.cs @@ -0,0 +1,8 @@ +namespace Umbraco.Cms.ManagementApi.ViewModels.Dictionary; + +public class DictionaryImportViewModel +{ + public List DictionaryItems { get; set; } = null!; + + public string? TempFileName { get; set; } +} diff --git a/src/Umbraco.Cms.ManagementApi/ViewModels/Dictionary/DictionaryItemViewModel.cs b/src/Umbraco.Cms.ManagementApi/ViewModels/Dictionary/DictionaryItemViewModel.cs new file mode 100644 index 0000000000..bed4fe6757 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/ViewModels/Dictionary/DictionaryItemViewModel.cs @@ -0,0 +1,8 @@ +namespace Umbraco.Cms.ManagementApi.ViewModels.Dictionary; + +public class DictionaryItemViewModel +{ + public Guid? ParentId { get; set; } + + public Guid Key { get; set; } +} diff --git a/src/Umbraco.Cms.ManagementApi/ViewModels/Dictionary/DictionaryItemsImportViewModel.cs b/src/Umbraco.Cms.ManagementApi/ViewModels/Dictionary/DictionaryItemsImportViewModel.cs new file mode 100644 index 0000000000..1813921495 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/ViewModels/Dictionary/DictionaryItemsImportViewModel.cs @@ -0,0 +1,8 @@ +namespace Umbraco.Cms.ManagementApi.ViewModels.Dictionary; + +public class DictionaryItemsImportViewModel +{ + public string? Name { get; set; } + + public int Level { get; set; } +} diff --git a/src/Umbraco.Cms.ManagementApi/ViewModels/Dictionary/DictionaryOverviewViewModel.cs b/src/Umbraco.Cms.ManagementApi/ViewModels/Dictionary/DictionaryOverviewViewModel.cs new file mode 100644 index 0000000000..8d2a394799 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/ViewModels/Dictionary/DictionaryOverviewViewModel.cs @@ -0,0 +1,31 @@ +using Umbraco.Cms.Core.Models.ContentEditing; + +namespace Umbraco.Cms.ManagementApi.ViewModels.Dictionary; + +public class DictionaryOverviewViewModel +{ + /// + /// Initializes a new instance of the class. + /// + public DictionaryOverviewViewModel() => Translations = new List(); + + /// + /// Gets or sets the key. + /// + public string? Name { get; set; } + + /// + /// Gets or sets the key. + /// + public Guid Key { get; set; } + + /// + /// Gets or sets the level. + /// + public int Level { get; set; } + + /// + /// Sets the translations. + /// + public List Translations { get; } +} diff --git a/src/Umbraco.Cms.ManagementApi/ViewModels/Dictionary/DictionaryTranslationOverviewViewModel.cs b/src/Umbraco.Cms.ManagementApi/ViewModels/Dictionary/DictionaryTranslationOverviewViewModel.cs new file mode 100644 index 0000000000..f7df3d64d4 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/ViewModels/Dictionary/DictionaryTranslationOverviewViewModel.cs @@ -0,0 +1,14 @@ +namespace Umbraco.Cms.ManagementApi.ViewModels.Dictionary; + +public class DictionaryTranslationOverviewViewModel +{ + /// + /// Gets or sets the display name. + /// + public string? DisplayName { get; set; } + + /// + /// Gets or sets a value indicating whether has translation. + /// + public bool HasTranslation { get; set; } +} diff --git a/src/Umbraco.Cms.ManagementApi/ViewModels/Dictionary/DictionaryTranslationViewModel.cs b/src/Umbraco.Cms.ManagementApi/ViewModels/Dictionary/DictionaryTranslationViewModel.cs new file mode 100644 index 0000000000..0e568e1967 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/ViewModels/Dictionary/DictionaryTranslationViewModel.cs @@ -0,0 +1,28 @@ +namespace Umbraco.Cms.ManagementApi.ViewModels.Dictionary; + +public class DictionaryTranslationViewModel +{ + public int Id { get; set; } + + public Guid Key { get; set; } + + /// + /// Gets or sets the display name. + /// + public string? DisplayName { get; set; } + + /// + /// Gets or sets the ISO code. + /// + public string? IsoCode { get; set; } + + /// + /// Gets or sets the translation. + /// + public string Translation { get; set; } = null!; + + /// + /// Gets or sets the language id. + /// + public int LanguageId { get; set; } +} diff --git a/src/Umbraco.Cms.ManagementApi/ViewModels/Dictionary/DictionaryViewModel.cs b/src/Umbraco.Cms.ManagementApi/ViewModels/Dictionary/DictionaryViewModel.cs new file mode 100644 index 0000000000..2be6a6df29 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/ViewModels/Dictionary/DictionaryViewModel.cs @@ -0,0 +1,56 @@ +using System.ComponentModel.DataAnnotations; +using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Models.Validation; + +namespace Umbraco.Cms.ManagementApi.ViewModels.Dictionary; + +/// +/// The dictionary display model +/// +public class DictionaryViewModel : INotificationModel +{ + /// + /// Initializes a new instance of the class. + /// + public DictionaryViewModel() + { + Notifications = new List(); + Translations = new List(); + ContentApps = new List(); + } + + /// + /// Gets or sets the parent id. + /// + public Guid? ParentId { get; set; } + + /// + /// Gets or sets the translations. + /// + public IEnumerable Translations { get; set; } = Enumerable.Empty(); + + /// + /// Apps for the dictionary item + /// + public IEnumerable ContentApps { get; set; } + + /// + /// + /// This is used to add custom localized messages/strings to the response for the app to use for localized UI purposes. + /// + public List Notifications { get; private set; } + + [RequiredForPersistence(AllowEmptyStrings = false, ErrorMessage = "Required")] + [Required] + public string Name { get; set; } = null!; + + /// + /// Gets or sets the Key for the object + /// + public Guid Key { get; set; } + + /// + /// The path of the entity + /// + public string Path { get; set; } = string.Empty; +} diff --git a/src/Umbraco.Cms.ManagementApi/ViewModels/ExamineManagement/ExamineIndexViewModel.cs b/src/Umbraco.Cms.ManagementApi/ViewModels/ExamineManagement/ExamineIndexViewModel.cs new file mode 100644 index 0000000000..546615a1fe --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/ViewModels/ExamineManagement/ExamineIndexViewModel.cs @@ -0,0 +1,18 @@ +namespace Umbraco.Cms.ManagementApi.ViewModels.ExamineManagement; + +public class ExamineIndexViewModel +{ + public string Name { get; init; } = null!; + + public string? HealthStatus { get; init; } + + public bool IsHealthy => HealthStatus == "Healthy"; + + public bool CanRebuild { get; init; } + + public string SearcherName { get; init; } = null!; + + public long DocumentCount { get; init; } + + public int FieldCount { get; init; } +} diff --git a/src/Umbraco.Cms.ManagementApi/ViewModels/ExamineManagement/FieldViewModel.cs b/src/Umbraco.Cms.ManagementApi/ViewModels/ExamineManagement/FieldViewModel.cs new file mode 100644 index 0000000000..e01e01bbd2 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/ViewModels/ExamineManagement/FieldViewModel.cs @@ -0,0 +1,8 @@ +namespace Umbraco.Cms.ManagementApi.ViewModels.ExamineManagement; + +public class FieldViewModel +{ + public string Name { get; init; } = null!; + + public IEnumerable Values { get; init; } = null!; +} diff --git a/src/Umbraco.Cms.ManagementApi/ViewModels/ExamineManagement/SearchResultViewModel.cs b/src/Umbraco.Cms.ManagementApi/ViewModels/ExamineManagement/SearchResultViewModel.cs new file mode 100644 index 0000000000..307c5ce873 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/ViewModels/ExamineManagement/SearchResultViewModel.cs @@ -0,0 +1,12 @@ +namespace Umbraco.Cms.ManagementApi.ViewModels.ExamineManagement; + +public class SearchResultViewModel +{ + public string Id { get; set; } = null!; + + public float Score { get; set; } + + public int FieldCount => Fields.Count(); + + public IEnumerable Fields { get; set; } = null!; +} diff --git a/src/Umbraco.Cms.ManagementApi/ViewModels/ExamineManagement/SearcherViewModel.cs b/src/Umbraco.Cms.ManagementApi/ViewModels/ExamineManagement/SearcherViewModel.cs new file mode 100644 index 0000000000..b40428db29 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/ViewModels/ExamineManagement/SearcherViewModel.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.ManagementApi.ViewModels.ExamineManagement; + +public class SearcherViewModel +{ + public string Name { get; set; } = null!; +} diff --git a/src/Umbraco.Cms.ManagementApi/ViewModels/Help/HelpPageViewModel.cs b/src/Umbraco.Cms.ManagementApi/ViewModels/Help/HelpPageViewModel.cs new file mode 100644 index 0000000000..f17c5de745 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/ViewModels/Help/HelpPageViewModel.cs @@ -0,0 +1,12 @@ +namespace Umbraco.Cms.ManagementApi.ViewModels.Help; + +public class HelpPageViewModel +{ + public string? Name { get; set; } + + public string? Description { get; set; } + + public string? Url { get; set; } + + public string? Type { get; set; } +} diff --git a/src/Umbraco.Cms.ManagementApi/ViewModels/Installer/ConsentLevelViewModel.cs b/src/Umbraco.Cms.ManagementApi/ViewModels/Installer/ConsentLevelViewModel.cs index 2774f5ba2e..5ea958b0a5 100644 --- a/src/Umbraco.Cms.ManagementApi/ViewModels/Installer/ConsentLevelViewModel.cs +++ b/src/Umbraco.Cms.ManagementApi/ViewModels/Installer/ConsentLevelViewModel.cs @@ -1,16 +1,12 @@ -using System.Runtime.Serialization; using System.Text.Json.Serialization; using Umbraco.Cms.Core.Models; namespace Umbraco.Cms.ManagementApi.ViewModels.Installer; -[DataContract(Name = "consentLevels")] public class ConsentLevelViewModel { - [DataMember(Name = "level")] [JsonConverter(typeof(JsonStringEnumConverter))] public TelemetryLevel Level { get; set; } - [DataMember(Name = "description")] public string Description { get; set; } = string.Empty; } diff --git a/src/Umbraco.Cms.ManagementApi/ViewModels/Installer/DatabaseInstallViewModel.cs b/src/Umbraco.Cms.ManagementApi/ViewModels/Installer/DatabaseInstallViewModel.cs index 1bc2f4c3e9..b7e510305a 100644 --- a/src/Umbraco.Cms.ManagementApi/ViewModels/Installer/DatabaseInstallViewModel.cs +++ b/src/Umbraco.Cms.ManagementApi/ViewModels/Installer/DatabaseInstallViewModel.cs @@ -1,36 +1,26 @@ using System.ComponentModel; using System.ComponentModel.DataAnnotations; -using System.Runtime.Serialization; namespace Umbraco.Cms.ManagementApi.ViewModels.Installer; -[DataContract(Name = "databaseInstall")] public class DatabaseInstallViewModel { - [DataMember(Name = "id")] [Required] - public Guid Id { get; init; } + public Guid Id { get; set; } - [DataMember(Name = "providerName")] [Required] - public string? ProviderName { get; init; } + public string? ProviderName { get; set; } - [DataMember(Name = "server")] - public string? Server { get; init; } + public string? Server { get; set; } - [DataMember(Name = "name")] - public string? Name { get; init; } + public string? Name { get; set; } - [DataMember(Name = "username")] - public string? Username { get; init; } + public string? Username { get; set; } - [DataMember(Name = "password")] [PasswordPropertyText] - public string? Password { get; init; } + public string? Password { get; set; } - [DataMember(Name = "useIntegratedAuthentication")] - public bool UseIntegratedAuthentication { get; init; } + public bool UseIntegratedAuthentication { get; set; } - [DataMember(Name = "connectionString")] - public string? ConnectionString { get; init; } + public string? ConnectionString { get; set; } } diff --git a/src/Umbraco.Cms.ManagementApi/ViewModels/Installer/DatabaseSettingsViewModel.cs b/src/Umbraco.Cms.ManagementApi/ViewModels/Installer/DatabaseSettingsViewModel.cs index 0d2c45f105..643f72372e 100644 --- a/src/Umbraco.Cms.ManagementApi/ViewModels/Installer/DatabaseSettingsViewModel.cs +++ b/src/Umbraco.Cms.ManagementApi/ViewModels/Installer/DatabaseSettingsViewModel.cs @@ -1,40 +1,26 @@ -using System.Runtime.Serialization; - namespace Umbraco.Cms.ManagementApi.ViewModels.Installer; -[DataContract(Name = "databaseSettings")] public class DatabaseSettingsViewModel { - [DataMember(Name = "id")] public Guid Id { get; set; } - [DataMember(Name = "sortOrder")] public int SortOrder { get; set; } - [DataMember(Name = "displayName")] public string DisplayName { get; set; } = string.Empty; - [DataMember(Name = "defaultDatabaseName")] public string DefaultDatabaseName { get; set; } = string.Empty; - [DataMember(Name = "providerName")] public string ProviderName { get; set; } = string.Empty; - [DataMember(Name = "isConfigured")] public bool IsConfigured { get; set; } - [DataMember(Name = "requiresServer")] public bool RequiresServer { get; set; } - [DataMember(Name = "serverPlaceholder")] public string ServerPlaceholder { get; set; } = string.Empty; - [DataMember(Name = "requiresCredentials")] public bool RequiresCredentials { get; set; } - [DataMember(Name = "supportsIntegratedAuthentication")] public bool SupportsIntegratedAuthentication { get; set; } - [DataMember(Name = "requiresConnectionTest")] public bool RequiresConnectionTest { get; set; } } diff --git a/src/Umbraco.Cms.ManagementApi/ViewModels/Installer/InstallSettingsViewModel.cs b/src/Umbraco.Cms.ManagementApi/ViewModels/Installer/InstallSettingsViewModel.cs index 156aa73e3e..49de39f45b 100644 --- a/src/Umbraco.Cms.ManagementApi/ViewModels/Installer/InstallSettingsViewModel.cs +++ b/src/Umbraco.Cms.ManagementApi/ViewModels/Installer/InstallSettingsViewModel.cs @@ -1,13 +1,8 @@ -using System.Runtime.Serialization; - namespace Umbraco.Cms.ManagementApi.ViewModels.Installer; -[DataContract(Name = "installSettings")] public class InstallSettingsViewModel { - [DataMember(Name = "user")] public UserSettingsViewModel User { get; set; } = null!; - [DataMember(Name = "databases")] public IEnumerable Databases { get; set; } = Enumerable.Empty(); } diff --git a/src/Umbraco.Cms.ManagementApi/ViewModels/Installer/InstallViewModel.cs b/src/Umbraco.Cms.ManagementApi/ViewModels/Installer/InstallViewModel.cs index ed815a521d..e6639206be 100644 --- a/src/Umbraco.Cms.ManagementApi/ViewModels/Installer/InstallViewModel.cs +++ b/src/Umbraco.Cms.ManagementApi/ViewModels/Installer/InstallViewModel.cs @@ -1,5 +1,4 @@ using System.ComponentModel.DataAnnotations; -using System.Runtime.Serialization; using System.Text.Json.Serialization; using Umbraco.Cms.Core.Models; @@ -7,15 +6,12 @@ namespace Umbraco.Cms.ManagementApi.ViewModels.Installer; public class InstallViewModel { - [DataMember(Name = "user")] [Required] - public UserInstallViewModel User { get; init; } = null!; + public UserInstallViewModel User { get; set; } = null!; - [DataMember(Name = "database")] [Required] - public DatabaseInstallViewModel Database { get; init; } = null!; + public DatabaseInstallViewModel Database { get; set; } = null!; - [DataMember(Name = "telemetryLevel")] [JsonConverter(typeof(JsonStringEnumConverter))] - public TelemetryLevel TelemetryLevel { get; init; } = TelemetryLevel.Basic; + public TelemetryLevel TelemetryLevel { get; set; } = TelemetryLevel.Basic; } diff --git a/src/Umbraco.Cms.ManagementApi/ViewModels/Installer/UpgradeSettingsViewModel.cs b/src/Umbraco.Cms.ManagementApi/ViewModels/Installer/UpgradeSettingsViewModel.cs index 8274246070..69c0e0ce48 100644 --- a/src/Umbraco.Cms.ManagementApi/ViewModels/Installer/UpgradeSettingsViewModel.cs +++ b/src/Umbraco.Cms.ManagementApi/ViewModels/Installer/UpgradeSettingsViewModel.cs @@ -1,23 +1,15 @@ -using System.Runtime.Serialization; +namespace Umbraco.Cms.ManagementApi.ViewModels.Installer; -namespace Umbraco.Cms.ManagementApi.ViewModels.Installer; - -[DataContract(Name = "upgradeSettingsViewModel")] public class UpgradeSettingsViewModel { - [DataMember(Name = "currentState")] public string CurrentState { get; set; } = string.Empty; - [DataMember(Name = "newState")] public string NewState { get; set; } = string.Empty; - [DataMember(Name = "newVersion")] public string NewVersion { get; set; } = string.Empty; - [DataMember(Name = "oldVersion")] public string OldVersion { get; set; } = string.Empty; - [DataMember(Name = "reportUrl")] public string ReportUrl => $"https://our.umbraco.com/contribute/releases/compare?from={OldVersion}&to={NewVersion}¬es=1"; } diff --git a/src/Umbraco.Cms.ManagementApi/ViewModels/Installer/UserInstallViewModel.cs b/src/Umbraco.Cms.ManagementApi/ViewModels/Installer/UserInstallViewModel.cs index dbdb859f63..808d2fe639 100644 --- a/src/Umbraco.Cms.ManagementApi/ViewModels/Installer/UserInstallViewModel.cs +++ b/src/Umbraco.Cms.ManagementApi/ViewModels/Installer/UserInstallViewModel.cs @@ -1,26 +1,21 @@ using System.ComponentModel; using System.ComponentModel.DataAnnotations; -using System.Runtime.Serialization; namespace Umbraco.Cms.ManagementApi.ViewModels.Installer; public class UserInstallViewModel { - [DataMember(Name = "name")] [Required] [StringLength(255)] - public string Name { get; init; } = null!; + public string Name { get; set; } = null!; - [DataMember(Name = "email")] [Required] [EmailAddress] - public string Email { get; init; } = null!; + public string Email { get; set; } = null!; - [DataMember(Name = "password")] [Required] [PasswordPropertyText] - public string Password { get; init; } = null!; + public string Password { get; set; } = null!; - [DataMember(Name = "subscribeToNewsletter")] - public bool SubscribeToNewsletter { get; init; } + public bool SubscribeToNewsletter { get; } } diff --git a/src/Umbraco.Cms.ManagementApi/ViewModels/Installer/UserSettingsViewModel.cs b/src/Umbraco.Cms.ManagementApi/ViewModels/Installer/UserSettingsViewModel.cs index b2be9e88c9..8265431f80 100644 --- a/src/Umbraco.Cms.ManagementApi/ViewModels/Installer/UserSettingsViewModel.cs +++ b/src/Umbraco.Cms.ManagementApi/ViewModels/Installer/UserSettingsViewModel.cs @@ -1,16 +1,10 @@ -using System.Runtime.Serialization; - namespace Umbraco.Cms.ManagementApi.ViewModels.Installer; -[DataContract(Name = "user")] public class UserSettingsViewModel { - [DataMember(Name = "minCharLength")] public int MinCharLength { get; set; } - [DataMember(Name = "minNonAlphaNumericLength")] public int MinNonAlphaNumericLength { get; set; } - [DataMember(Name = "consentLevels")] public IEnumerable ConsentLevels { get; set; } = Enumerable.Empty(); } diff --git a/src/Umbraco.Cms.ManagementApi/ViewModels/JsonPatch/JsonPatchViewModel.cs b/src/Umbraco.Cms.ManagementApi/ViewModels/JsonPatch/JsonPatchViewModel.cs new file mode 100644 index 0000000000..1fd1e539b2 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/ViewModels/JsonPatch/JsonPatchViewModel.cs @@ -0,0 +1,10 @@ +namespace Umbraco.Cms.ManagementApi.ViewModels.JsonPatch; + +public class JsonPatchViewModel +{ + public string Op { get; set; } = null!; + + public string Path { get; set; } = null!; + + public object Value { get; set; } = null!; +} diff --git a/src/Umbraco.Cms.ManagementApi/ViewModels/Language/LanguageViewModel.cs b/src/Umbraco.Cms.ManagementApi/ViewModels/Language/LanguageViewModel.cs new file mode 100644 index 0000000000..3849c08c57 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/ViewModels/Language/LanguageViewModel.cs @@ -0,0 +1,19 @@ +using System.ComponentModel.DataAnnotations; + +namespace Umbraco.Cms.ManagementApi.ViewModels.Language; + +public class LanguageViewModel +{ + public int Id { get; set; } + + [Required(AllowEmptyStrings = false)] + public string IsoCode { get; set; } = null!; + + public string? Name { get; set; } + + public bool IsDefault { get; set; } + + public bool IsMandatory { get; set; } + + public int? FallbackLanguageId { get; set; } +} diff --git a/src/Umbraco.Cms.ManagementApi/ViewModels/Move/MoveViewModel.cs b/src/Umbraco.Cms.ManagementApi/ViewModels/Move/MoveViewModel.cs new file mode 100644 index 0000000000..618e7d5b71 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/ViewModels/Move/MoveViewModel.cs @@ -0,0 +1,21 @@ +using System.ComponentModel.DataAnnotations; + +namespace Umbraco.Cms.ManagementApi.ViewModels.Move; + +/// +/// A model representing a model for moving or copying +/// +public class MoveViewModel +{ + /// + /// The Id of the node to move or copy to + /// + [Required] + public int ParentId { get; set; } + + /// + /// The id of the node to move or copy + /// + [Required] + public int Id { get; set; } +} diff --git a/src/Umbraco.Cms.ManagementApi/ViewModels/Pagination/PagedViewModel.cs b/src/Umbraco.Cms.ManagementApi/ViewModels/Pagination/PagedViewModel.cs index 7d9760bda4..9e3b6cf21b 100644 --- a/src/Umbraco.Cms.ManagementApi/ViewModels/Pagination/PagedViewModel.cs +++ b/src/Umbraco.Cms.ManagementApi/ViewModels/Pagination/PagedViewModel.cs @@ -5,4 +5,6 @@ public class PagedViewModel public long Total { get; set; } public IEnumerable Items { get; set; } = Enumerable.Empty(); + + public static PagedViewModel Empty() => new(); } diff --git a/src/Umbraco.Cms.ManagementApi/ViewModels/Profiling/ProfilingStatusViewModel.cs b/src/Umbraco.Cms.ManagementApi/ViewModels/Profiling/ProfilingStatusViewModel.cs new file mode 100644 index 0000000000..d165a613a4 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/ViewModels/Profiling/ProfilingStatusViewModel.cs @@ -0,0 +1,8 @@ +namespace Umbraco.Cms.ManagementApi.ViewModels.Profiling; + +public class ProfilingStatusViewModel +{ + public ProfilingStatusViewModel(bool enabled) => Enabled = enabled; + + public bool Enabled { get; } +} diff --git a/src/Umbraco.Cms.ManagementApi/ViewModels/RecycleBin/RecycleBinItemViewModel.cs b/src/Umbraco.Cms.ManagementApi/ViewModels/RecycleBin/RecycleBinItemViewModel.cs new file mode 100644 index 0000000000..1fe649132a --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/ViewModels/RecycleBin/RecycleBinItemViewModel.cs @@ -0,0 +1,19 @@ +namespace Umbraco.Cms.ManagementApi.ViewModels.RecycleBin; + +public class RecycleBinItemViewModel +{ + public Guid Key { get; set; } + + public string Name { get; set; } = string.Empty; + + public string Type { get; set; } = string.Empty; + + public string Icon { get; set; } = string.Empty; + + public bool HasChildren { get; set; } + + public bool IsContainer { get; set; } + + public Guid? ParentKey { get; set; } +} + diff --git a/src/Umbraco.Cms.ManagementApi/ViewModels/Relation/RelationViewModel.cs b/src/Umbraco.Cms.ManagementApi/ViewModels/Relation/RelationViewModel.cs new file mode 100644 index 0000000000..d09466c657 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/ViewModels/Relation/RelationViewModel.cs @@ -0,0 +1,43 @@ +using System.ComponentModel; +using System.Text.Json.Serialization; + +namespace Umbraco.Cms.ManagementApi.ViewModels.Relation; + +public class RelationViewModel +{ + /// + /// Gets or sets the Parent Id of the Relation (Source). + /// + [ReadOnly(true)] + public int ParentId { get; set; } + + /// + /// Gets or sets the Parent Name of the relation (Source). + /// + [ReadOnly(true)] + public string? ParentName { get; set; } + + /// + /// Gets or sets the Child Id of the Relation (Destination). + /// + [ReadOnly(true)] + public int ChildId { get; set; } + + /// + /// Gets or sets the Child Name of the relation (Destination). + /// + [ReadOnly(true)] + public string? ChildName { get; set; } + + /// + /// Gets or sets the date when the Relation was created. + /// + [ReadOnly(true)] + public DateTime CreateDate { get; set; } + + /// + /// Gets or sets a comment for the Relation. + /// + [ReadOnly(true)] + public string? Comment { get; set; } +} diff --git a/src/Umbraco.Cms.ManagementApi/ViewModels/TrackedReferences/RelationItemViewModel.cs b/src/Umbraco.Cms.ManagementApi/ViewModels/TrackedReferences/RelationItemViewModel.cs new file mode 100644 index 0000000000..e60c58f87f --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/ViewModels/TrackedReferences/RelationItemViewModel.cs @@ -0,0 +1,22 @@ +namespace Umbraco.Cms.ManagementApi.ViewModels.TrackedReferences; + +public class RelationItemViewModel +{ + public Guid NodeKey { get; set; } + + public string? NodeName { get; set; } + + public string? NodeType { get; set; } + + public string? ContentTypeIcon { get; set; } + + public string? ContentTypeAlias { get; set; } + + public string? ContentTypeName { get; set; } + + public string? RelationTypeName { get; set; } + + public bool RelationTypeIsBidirectional { get; set; } + + public bool RelationTypeIsDependency { get; set; } +} diff --git a/src/Umbraco.Cms.ManagementApi/ViewModels/Tree/ContentTreeItemViewModel.cs b/src/Umbraco.Cms.ManagementApi/ViewModels/Tree/ContentTreeItemViewModel.cs new file mode 100644 index 0000000000..b958b0bff5 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/ViewModels/Tree/ContentTreeItemViewModel.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.ManagementApi.ViewModels.Tree; + +public class ContentTreeItemViewModel : EntityTreeItemViewModel +{ + public bool NoAccess { get; set; } +} diff --git a/src/Umbraco.Cms.ManagementApi/ViewModels/Tree/DocumentBlueprintTreeItemViewModel.cs b/src/Umbraco.Cms.ManagementApi/ViewModels/Tree/DocumentBlueprintTreeItemViewModel.cs new file mode 100644 index 0000000000..24760f7f1b --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/ViewModels/Tree/DocumentBlueprintTreeItemViewModel.cs @@ -0,0 +1,10 @@ +namespace Umbraco.Cms.ManagementApi.ViewModels.Tree; + +public class DocumentBlueprintTreeItemViewModel : EntityTreeItemViewModel +{ + public Guid DocumentTypeKey { get; set; } + + public string DocumentTypeAlias { get; set; } = string.Empty; + + public string? DocumentTypeName { get; set; } = string.Empty; +} diff --git a/src/Umbraco.Cms.ManagementApi/ViewModels/Tree/DocumentTreeItemViewModel.cs b/src/Umbraco.Cms.ManagementApi/ViewModels/Tree/DocumentTreeItemViewModel.cs new file mode 100644 index 0000000000..bba2c90f15 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/ViewModels/Tree/DocumentTreeItemViewModel.cs @@ -0,0 +1,10 @@ +namespace Umbraco.Cms.ManagementApi.ViewModels.Tree; + +public class DocumentTreeItemViewModel : ContentTreeItemViewModel +{ + public bool IsProtected { get; set; } + + public bool IsPublished { get; set; } + + public bool IsEdited { get; set; } +} diff --git a/src/Umbraco.Cms.ManagementApi/ViewModels/Tree/DocumentTypeTreeItemViewModel.cs b/src/Umbraco.Cms.ManagementApi/ViewModels/Tree/DocumentTypeTreeItemViewModel.cs new file mode 100644 index 0000000000..0fbd3abb4c --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/ViewModels/Tree/DocumentTypeTreeItemViewModel.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.ManagementApi.ViewModels.Tree; + +public class DocumentTypeTreeItemViewModel : FolderTreeItemViewModel +{ + public bool IsElement { get; set; } +} diff --git a/src/Umbraco.Cms.ManagementApi/ViewModels/Tree/EntityTreeItemViewModel.cs b/src/Umbraco.Cms.ManagementApi/ViewModels/Tree/EntityTreeItemViewModel.cs new file mode 100644 index 0000000000..fd9c3abd48 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/ViewModels/Tree/EntityTreeItemViewModel.cs @@ -0,0 +1,10 @@ +namespace Umbraco.Cms.ManagementApi.ViewModels.Tree; + +public class EntityTreeItemViewModel : TreeItemViewModel +{ + public Guid Key { get; set; } + + public bool IsContainer { get; set; } + + public Guid? ParentKey { get; set; } +} diff --git a/src/Umbraco.Cms.ManagementApi/ViewModels/Tree/FileSystemTreeItemViewModel.cs b/src/Umbraco.Cms.ManagementApi/ViewModels/Tree/FileSystemTreeItemViewModel.cs new file mode 100644 index 0000000000..50973df0c0 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/ViewModels/Tree/FileSystemTreeItemViewModel.cs @@ -0,0 +1,8 @@ +namespace Umbraco.Cms.ManagementApi.ViewModels.Tree; + +public class FileSystemTreeItemViewModel : TreeItemViewModel +{ + public string Path { get; set; } = string.Empty; + + public bool IsFolder { get; set; } +} diff --git a/src/Umbraco.Cms.ManagementApi/ViewModels/Tree/FolderTreeItemViewModel.cs b/src/Umbraco.Cms.ManagementApi/ViewModels/Tree/FolderTreeItemViewModel.cs new file mode 100644 index 0000000000..4e435372d1 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/ViewModels/Tree/FolderTreeItemViewModel.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.ManagementApi.ViewModels.Tree; + +public class FolderTreeItemViewModel : EntityTreeItemViewModel +{ + public bool IsFolder { get; set; } +} diff --git a/src/Umbraco.Cms.ManagementApi/ViewModels/Tree/TreeItemViewModel.cs b/src/Umbraco.Cms.ManagementApi/ViewModels/Tree/TreeItemViewModel.cs new file mode 100644 index 0000000000..ac8fdc2cb9 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/ViewModels/Tree/TreeItemViewModel.cs @@ -0,0 +1,12 @@ +namespace Umbraco.Cms.ManagementApi.ViewModels.Tree; + +public class TreeItemViewModel +{ + public string Name { get; set; } = string.Empty; + + public string Type { get; set; } = string.Empty; + + public string Icon { get; set; } = string.Empty; + + public bool HasChildren { get; set; } +} diff --git a/src/Umbraco.Cms.Persistence.SqlServer/Umbraco.Cms.Persistence.SqlServer.csproj b/src/Umbraco.Cms.Persistence.SqlServer/Umbraco.Cms.Persistence.SqlServer.csproj index d2cfff9a2e..ce7a974d19 100644 --- a/src/Umbraco.Cms.Persistence.SqlServer/Umbraco.Cms.Persistence.SqlServer.csproj +++ b/src/Umbraco.Cms.Persistence.SqlServer/Umbraco.Cms.Persistence.SqlServer.csproj @@ -2,7 +2,6 @@ Umbraco CMS - Persistence - SQL Server Adds support for SQL Server to Umbraco CMS. - net7.0 diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Umbraco.Cms.Persistence.Sqlite.csproj b/src/Umbraco.Cms.Persistence.Sqlite/Umbraco.Cms.Persistence.Sqlite.csproj index c0a8b649fe..da91785ef0 100644 --- a/src/Umbraco.Cms.Persistence.Sqlite/Umbraco.Cms.Persistence.Sqlite.csproj +++ b/src/Umbraco.Cms.Persistence.Sqlite/Umbraco.Cms.Persistence.Sqlite.csproj @@ -2,7 +2,6 @@ Umbraco CMS - Persistence - SQLite Adds support for SQLite to Umbraco CMS. - net7.0 diff --git a/src/Umbraco.Cms.StaticAssets/Umbraco.Cms.StaticAssets.csproj b/src/Umbraco.Cms.StaticAssets/Umbraco.Cms.StaticAssets.csproj index e88b83bbbe..f1196689b9 100644 --- a/src/Umbraco.Cms.StaticAssets/Umbraco.Cms.StaticAssets.csproj +++ b/src/Umbraco.Cms.StaticAssets/Umbraco.Cms.StaticAssets.csproj @@ -2,7 +2,6 @@ Umbraco CMS - Static assets Contains the static assets needed to run Umbraco CMS. - net7.0 true / diff --git a/src/Umbraco.Cms.Targets/Umbraco.Cms.Targets.csproj b/src/Umbraco.Cms.Targets/Umbraco.Cms.Targets.csproj index 98d5b0741b..c8968e1955 100644 --- a/src/Umbraco.Cms.Targets/Umbraco.Cms.Targets.csproj +++ b/src/Umbraco.Cms.Targets/Umbraco.Cms.Targets.csproj @@ -2,7 +2,6 @@ Umbraco CMS Installs Umbraco CMS with minimal dependencies in your ASP.NET Core project. - net7.0 false false diff --git a/src/Umbraco.Cms/Umbraco.Cms.csproj b/src/Umbraco.Cms/Umbraco.Cms.csproj index 2b710029be..b037bd216b 100644 --- a/src/Umbraco.Cms/Umbraco.Cms.csproj +++ b/src/Umbraco.Cms/Umbraco.Cms.csproj @@ -1,8 +1,7 @@ - + Umbraco CMS Installs Umbraco CMS with all default dependencies in your ASP.NET Core project. - net7.0 false false diff --git a/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs b/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs index 2665c0738f..92c443fd77 100644 --- a/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs @@ -21,7 +21,6 @@ public class GlobalSettings internal const bool StaticHideTopLevelNodeFromPath = true; internal const bool StaticUseHttps = false; internal const int StaticVersionCheckPeriod = 7; - internal const string StaticUmbracoPath = Constants.System.DefaultUmbracoPath; internal const string StaticIconsPath = "umbraco/assets/icons"; internal const string StaticUmbracoCssPath = "~/css"; internal const string StaticUmbracoScriptsPath = "~/scripts"; @@ -80,8 +79,13 @@ public class GlobalSettings /// /// Gets or sets a value for the Umbraco back-office path. /// - [DefaultValue(StaticUmbracoPath)] - public string UmbracoPath { get; set; } = StaticUmbracoPath; + public string UmbracoPath + { + get => Constants.System.DefaultUmbracoPath; + [Obsolete($"{nameof(UmbracoPath)} is no longer configurable, property setter is scheduled for removal in V12")] + // NOTE: when removing this, also clean up the hardcoded removal of UmbracoPath in UmbracoJsonSchemaGenerator + set { } + } /// /// Gets or sets a value for the Umbraco icons path. diff --git a/src/Umbraco.Core/Constants-Icons.cs b/src/Umbraco.Core/Constants-Icons.cs index 40ab52aaa5..5cfc2808fc 100644 --- a/src/Umbraco.Core/Constants-Icons.cs +++ b/src/Umbraco.Core/Constants-Icons.cs @@ -119,11 +119,31 @@ public static partial class Constants /// public const string Packages = "icon-box"; + /// + /// System property editor icon + /// + public const string PartialView = "icon-article"; + /// /// System property editor icon /// public const string PropertyEditor = "icon-autofill"; + /// + /// Relation type icon + /// + public const string RelationType = "icon-trafic"; + + /// + /// Script type icon + /// + public const string Script = "icon-script"; + + /// + /// Stylesheet type icon + /// + public const string Stylesheet = "icon-brackets"; + /// /// System member icon /// diff --git a/src/Umbraco.Core/Constants-Telemetry.cs b/src/Umbraco.Core/Constants-Telemetry.cs index c40df0f8bd..0fc2cb1612 100644 --- a/src/Umbraco.Core/Constants-Telemetry.cs +++ b/src/Umbraco.Core/Constants-Telemetry.cs @@ -22,6 +22,7 @@ public static partial class Constants public static string OsLanguage = "OsLanguage"; public static string WebServer = "WebServer"; public static string ModelsBuilderMode = "ModelBuilderMode"; + [Obsolete($"UmbracoPath is no longer configurable, scheduled for removal in V12")] public static string CustomUmbracoPath = "CustomUmbracoPath"; public static string AspEnvironment = "AspEnvironment"; public static string IsDebug = "IsDebug"; diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs index d806950584..d783a67a52 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs @@ -324,6 +324,7 @@ namespace Umbraco.Cms.Core.DependencyInjection Services.AddUnique(); Services.AddUnique(provider => new CultureImpactFactory(provider.GetRequiredService>())); + Services.AddUnique(); } } } diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/es.xml b/src/Umbraco.Core/EmbeddedResources/Lang/es.xml index 4012566597..1ed96d8807 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/es.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/es.xml @@ -331,6 +331,7 @@ Selecciona miembro Selecciona grupo de miembros Selecciona nodo + Seleccionar idiomas Selecciona secciones Selecciona usuarios No se encontraron iconos @@ -1363,6 +1364,8 @@ Establecer permisos para nodos específicos Perfil Buscar en todos los hijos + Limite los idiomas a los que los usuarios tienen acceso para editar + Permitir el acceso a todos los idiomas Añadir secciones para dar acceso a usuarios Seleccionar grupos de usuarios Nodo de inicio no seleccionado diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/nl.xml b/src/Umbraco.Core/EmbeddedResources/Lang/nl.xml index 53ec669c1b..e19ef083e5 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/nl.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/nl.xml @@ -510,6 +510,7 @@ Selecteer lid groep Selecteer lid type Selecteer node + Selecteer talen Selecteer secties Selecteer gebruiker Selecteer gebruikers @@ -1916,6 +1917,8 @@ Echter, Runway biedt een gemakkelijke basis om je snel op weg te helpen. Als je Profiel Doorzoek alle subitems Geef de gebruiker toegang tot secties + Beperk de talen die gebruikers mogen bewerken + Toegang tot alle talen toestaan Selecteer een gebruikersgroep Geen startnode geselecteerd Geen startnodes geselecteerd diff --git a/src/Umbraco.Core/Extensions/StringExtensions.cs b/src/Umbraco.Core/Extensions/StringExtensions.cs index a404daedf9..d30ef9ccba 100644 --- a/src/Umbraco.Core/Extensions/StringExtensions.cs +++ b/src/Umbraco.Core/Extensions/StringExtensions.cs @@ -129,8 +129,7 @@ public static class StringExtensions } input = input.Trim(); - return (input.StartsWith("{") && input.EndsWith("}")) - || (input.StartsWith("[") && input.EndsWith("]")); + return (input[0] is '[' && input[^1] is ']') || (input[0] is '{' && input[^1] is '}'); } public static bool DetectIsEmptyJson(this string input) => @@ -418,7 +417,7 @@ public static class StringExtensions /// empty, or consists only of white-space characters, otherwise /// returns . /// - public static bool IsNullOrWhiteSpace(this string? value) => string.IsNullOrWhiteSpace(value); + public static bool IsNullOrWhiteSpace([NotNullWhen(false)] this string? value) => string.IsNullOrWhiteSpace(value); [return: NotNullIfNotNull("defaultValue")] public static string? IfNullOrWhiteSpace(this string? str, string? defaultValue) => diff --git a/src/Umbraco.Core/Models/ContentEditing/DictionaryTranslationDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/DictionaryTranslationDisplay.cs index afd36b6acc..70ab031538 100644 --- a/src/Umbraco.Core/Models/ContentEditing/DictionaryTranslationDisplay.cs +++ b/src/Umbraco.Core/Models/ContentEditing/DictionaryTranslationDisplay.cs @@ -9,6 +9,12 @@ namespace Umbraco.Cms.Core.Models.ContentEditing; [DataContract(Name = "dictionaryTranslation", Namespace = "")] public class DictionaryTranslationDisplay : DictionaryTranslationSave { + [DataMember(Name = "id")] + public int Id { get; set; } + + [DataMember(Name = "key")] + public Guid Key { get; set; } + /// /// Gets or sets the display name. /// diff --git a/src/Umbraco.Core/Models/Mapping/DictionaryMapDefinition.cs b/src/Umbraco.Core/Models/Mapping/DictionaryMapDefinition.cs index 3c79d1c12f..cc534f4c72 100644 --- a/src/Umbraco.Core/Models/Mapping/DictionaryMapDefinition.cs +++ b/src/Umbraco.Core/Models/Mapping/DictionaryMapDefinition.cs @@ -13,12 +13,22 @@ namespace Umbraco.Cms.Core.Models.Mapping; public class DictionaryMapDefinition : IMapDefinition { private readonly CommonMapper? _commonMapper; + private readonly IDictionaryService _dictionaryService; private readonly ILocalizationService _localizationService; public DictionaryMapDefinition(ILocalizationService localizationService, CommonMapper commonMapper) + : this( + localizationService, + commonMapper, + StaticServiceProvider.Instance.GetRequiredService()) + { + } + + public DictionaryMapDefinition(ILocalizationService localizationService, CommonMapper commonMapper, IDictionaryService dictionaryService) { _localizationService = localizationService; _commonMapper = commonMapper; + _dictionaryService = dictionaryService; } public void DefineMaps(IUmbracoMapper mapper) @@ -37,22 +47,6 @@ public class DictionaryMapDefinition : IMapDefinition target.Name = source.ItemKey; } - private static void GetParentId(Guid parentId, ILocalizationService localizationService, List ids) - { - IDictionaryItem? dictionary = localizationService.GetDictionaryItemById(parentId); - if (dictionary == null) - { - return; - } - - ids.Add(dictionary.Id); - - if (dictionary.ParentId.HasValue) - { - GetParentId(dictionary.ParentId.Value, localizationService, ids); - } - } - // Umbraco.Code.MapAll -Icon -Trashed -Alias private void Map(IDictionaryItem source, DictionaryDisplay target, MapperContext context) { @@ -66,22 +60,7 @@ public class DictionaryMapDefinition : IMapDefinition target.ContentApps.AddRange(_commonMapper.GetContentAppsForEntity(source)); } - // build up the path to make it possible to set active item in tree - // TODO: check if there is a better way - if (source.ParentId.HasValue) - { - var ids = new List { -1 }; - var parentIds = new List(); - GetParentId(source.ParentId.Value, _localizationService, parentIds); - parentIds.Reverse(); - ids.AddRange(parentIds); - ids.Add(source.Id); - target.Path = string.Join(",", ids); - } - else - { - target.Path = "-1," + source.Id; - } + target.Path = _dictionaryService.CalculatePath(source.ParentId, source.Id); // add all languages and the translations foreach (ILanguage lang in _localizationService.GetAllLanguages()) diff --git a/src/Umbraco.Core/Models/Mapping/LanguageMapDefinition.cs b/src/Umbraco.Core/Models/Mapping/LanguageMapDefinition.cs index 81096889c8..12d2f162f7 100644 --- a/src/Umbraco.Core/Models/Mapping/LanguageMapDefinition.cs +++ b/src/Umbraco.Core/Models/Mapping/LanguageMapDefinition.cs @@ -9,8 +9,7 @@ public class LanguageMapDefinition : IMapDefinition { mapper.Define((source, context) => new EntityBasic(), Map); mapper.Define((source, context) => new ContentEditing.Language(), Map); - mapper.Define, IEnumerable>( - (source, context) => new List(), Map); + mapper.Define, IEnumerable>((source, context) => new List(), Map); } // Umbraco.Code.MapAll -Udi -Path -Trashed -AdditionalData -Icon diff --git a/src/Umbraco.Core/Models/PagedModel.cs b/src/Umbraco.Core/Models/PagedModel.cs new file mode 100644 index 0000000000..7ccb582b56 --- /dev/null +++ b/src/Umbraco.Core/Models/PagedModel.cs @@ -0,0 +1,18 @@ +namespace Umbraco.New.Cms.Core.Models; + +public class PagedModel +{ + public PagedModel() + { + } + + public PagedModel(long total, IEnumerable items) + { + Total = total; + Items = items; + } + + public IEnumerable Items { get; init; } = Enumerable.Empty(); + + public long Total { get; init; } +} diff --git a/src/Umbraco.Core/Models/RelationItemModel.cs b/src/Umbraco.Core/Models/RelationItemModel.cs new file mode 100644 index 0000000000..ee45422586 --- /dev/null +++ b/src/Umbraco.Core/Models/RelationItemModel.cs @@ -0,0 +1,22 @@ +namespace Umbraco.Cms.Core.Models; + +public class RelationItemModel +{ + public Guid NodeKey { get; set; } + + public string? NodeName { get; set; } + + public string? NodeType { get; set; } + + public string? ContentTypeIcon { get; set; } + + public string? ContentTypeAlias { get; set; } + + public string? ContentTypeName { get; set; } + + public string? RelationTypeName { get; set; } + + public bool RelationTypeIsBidirectional { get; set; } + + public bool RelationTypeIsDependency { get; set; } +} diff --git a/src/Umbraco.Core/Models/UmbracoObjectTypes.cs b/src/Umbraco.Core/Models/UmbracoObjectTypes.cs index 600927db84..acc9888bf4 100644 --- a/src/Umbraco.Core/Models/UmbracoObjectTypes.cs +++ b/src/Umbraco.Core/Models/UmbracoObjectTypes.cs @@ -54,7 +54,7 @@ public enum UmbracoObjectTypes /// /// Member Group /// - [UmbracoObjectType(Constants.ObjectTypes.Strings.MemberGroup)] + [UmbracoObjectType(Constants.ObjectTypes.Strings.MemberGroup, typeof(IMemberGroup))] [FriendlyName("Member Group")] [UmbracoUdiType(Constants.UdiEntityType.MemberGroup)] MemberGroup, diff --git a/src/Umbraco.Core/Persistence/Repositories/IDictionaryRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IDictionaryRepository.cs index db2347e925..9d75bc1030 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IDictionaryRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IDictionaryRepository.cs @@ -6,6 +6,10 @@ public interface IDictionaryRepository : IReadWriteQueryRepository GetMany(params Guid[] uniqueIds) => Array.Empty(); + + IEnumerable GetManyByKeys(params string[] keys) => Array.Empty(); + IDictionaryItem? Get(string key); IEnumerable GetDictionaryItemDescendants(Guid? parentId); diff --git a/src/Umbraco.Core/Persistence/Repositories/ITrackedReferencesRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ITrackedReferencesRepository.cs index a69722c04a..bd6723e674 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ITrackedReferencesRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ITrackedReferencesRepository.cs @@ -46,4 +46,65 @@ public interface ITrackedReferencesRepository /// The total count of descending items. /// An enumerable list of objects. IEnumerable GetPagedDescendantsInReferences(int parentId, long pageIndex, int pageSize, bool filterMustBeIsDependency, out long totalRecords); + + /// + /// Gets a page of items which are in relation with the current item. + /// Basically, shows the items which depend on the current item. + /// + /// The identifier of the entity to retrieve relations for. + /// The amount of items to skip. + /// The amount of items to take. + /// + /// A boolean indicating whether to filter only the RelationTypes which are + /// dependencies (isDependency field is set to true). + /// + /// The total count of the items with reference to the current item. + /// An enumerable list of objects. + IEnumerable GetPagedRelationsForItem( + int id, + long skip, + long take, + bool filterMustBeIsDependency, + out long totalRecords) => + throw new NotImplementedException(); + + /// + /// Gets a page of items used in any kind of relation from selected integer ids. + /// + /// The identifiers of the entities to check for relations. + /// The amount of items to skip. + /// The amount of items to take. + /// + /// A boolean indicating whether to filter only the RelationTypes which are + /// dependencies (isDependency field is set to true). + /// + /// The total count of the items in any kind of relation. + /// An enumerable list of objects. + IEnumerable GetPagedItemsWithRelations( + int[] ids, + long skip, + long take, + bool filterMustBeIsDependency, + out long totalRecords) => + throw new NotImplementedException(); + + /// + /// Gets a page of the descending items that have any references, given a parent id. + /// + /// The unique identifier of the parent to retrieve descendants for. + /// The amount of items to skip. + /// The amount of items to take. + /// + /// A boolean indicating whether to filter only the RelationTypes which are + /// dependencies (isDependency field is set to true). + /// + /// The total count of descending items. + /// An enumerable list of objects. + IEnumerable GetPagedDescendantsInReferences( + int parentId, + long skip, + long take, + bool filterMustBeIsDependency, + out long totalRecords) => + throw new NotImplementedException(); } diff --git a/src/Umbraco.Core/PropertyEditors/ConfigurationFieldAttribute.cs b/src/Umbraco.Core/PropertyEditors/ConfigurationFieldAttribute.cs index c504a790be..07433d1504 100644 --- a/src/Umbraco.Core/PropertyEditors/ConfigurationFieldAttribute.cs +++ b/src/Umbraco.Core/PropertyEditors/ConfigurationFieldAttribute.cs @@ -106,7 +106,7 @@ public class ConfigurationFieldAttribute : Attribute } /// - /// Gets or sets the key of the field. + /// Gets the key of the field. /// /// /// When null or empty, the should derive a key @@ -120,7 +120,7 @@ public class ConfigurationFieldAttribute : Attribute public string? Name { get; } /// - /// Gets or sets the view to use to render the field editor. + /// Gets the view to use to render the field editor. /// public string? View { get; } diff --git a/src/Umbraco.Core/Serialization/IJsonSerializer.cs b/src/Umbraco.Core/Serialization/IJsonSerializer.cs index 5a31a2cf97..889dbbd0a0 100644 --- a/src/Umbraco.Core/Serialization/IJsonSerializer.cs +++ b/src/Umbraco.Core/Serialization/IJsonSerializer.cs @@ -6,5 +6,6 @@ public interface IJsonSerializer T? Deserialize(string input); + [Obsolete("This will be removed in v13")] T? DeserializeSubset(string input, string key); } diff --git a/src/Umbraco.Core/Services/DictionaryService.cs b/src/Umbraco.Core/Services/DictionaryService.cs new file mode 100644 index 0000000000..1c48ac4458 --- /dev/null +++ b/src/Umbraco.Core/Services/DictionaryService.cs @@ -0,0 +1,49 @@ +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Core.Services; + +public class DictionaryService : IDictionaryService +{ + private readonly ILocalizationService _localizationService; + + public DictionaryService(ILocalizationService localizationService) => _localizationService = localizationService; + + public string CalculatePath(Guid? parentId, int sourceId) + { + string path; + + // TODO: check if there is a better way + if (parentId.HasValue) + { + var ids = new List { -1 }; + var parentIds = new List(); + GetParentId(parentId.Value, parentIds); + parentIds.Reverse(); + ids.AddRange(parentIds); + ids.Add(sourceId); + path = string.Join(",", ids); + } + else + { + path = "-1," + sourceId; + } + + return path; + } + + private void GetParentId(Guid parentId, List ids) + { + IDictionaryItem? dictionary = _localizationService.GetDictionaryItemById(parentId); + if (dictionary == null) + { + return; + } + + ids.Add(dictionary.Id); + + if (dictionary.ParentId.HasValue) + { + GetParentId(dictionary.ParentId.Value, ids); + } + } +} diff --git a/src/Umbraco.Core/Services/EntityService.cs b/src/Umbraco.Core/Services/EntityService.cs index 591fa17909..b6fc244bd0 100644 --- a/src/Umbraco.Core/Services/EntityService.cs +++ b/src/Umbraco.Core/Services/EntityService.cs @@ -37,6 +37,8 @@ public class EntityService : RepositoryService, IEntityService { typeof(IMediaType).FullName!, UmbracoObjectTypes.MediaType }, { typeof(IMember).FullName!, UmbracoObjectTypes.Member }, { typeof(IMemberType).FullName!, UmbracoObjectTypes.MemberType }, + { typeof(IMemberGroup).FullName!, UmbracoObjectTypes.MemberGroup }, + { typeof(ITemplate).FullName!, UmbracoObjectTypes.Template }, }; } @@ -314,14 +316,18 @@ public class EntityService : RepositoryService, IEntityService out long totalRecords, IQuery? filter = null, Ordering? ordering = null) - { - using (ScopeProvider.CreateCoreScope(autoComplete: true)) - { - IQuery query = Query().Where(x => x.ParentId == id && x.Trashed == false); + => GetPagedChildren(id, objectType, pageIndex, pageSize, false, filter, ordering, out totalRecords); - return _entityRepository.GetPagedResultsByQuery(query, objectType.GetGuid(), pageIndex, pageSize, out totalRecords, filter, ordering); - } - } + /// + public IEnumerable GetPagedTrashedChildren( + int id, + UmbracoObjectTypes objectType, + long pageIndex, + int pageSize, + out long totalRecords, + IQuery? filter = null, + Ordering? ordering = null) + => GetPagedChildren(id, objectType, pageIndex, pageSize, true, filter, ordering, out totalRecords); /// public IEnumerable GetPagedDescendants( @@ -523,4 +529,23 @@ public class EntityService : RepositoryService, IEntityService return objType; } + + private IEnumerable GetPagedChildren( + int id, + UmbracoObjectTypes objectType, + long pageIndex, + int pageSize, + bool trashed, + IQuery? filter, + Ordering? ordering, + out long totalRecords) + { + using (ScopeProvider.CreateCoreScope(autoComplete: true)) + { + IQuery query = Query().Where(x => x.ParentId == id && x.Trashed == trashed); + + return _entityRepository.GetPagedResultsByQuery(query, objectType.GetGuid(), pageIndex, pageSize, out totalRecords, filter, ordering); + } + } } + diff --git a/src/Umbraco.Core/Services/IDictionaryService.cs b/src/Umbraco.Core/Services/IDictionaryService.cs new file mode 100644 index 0000000000..3a26e99a17 --- /dev/null +++ b/src/Umbraco.Core/Services/IDictionaryService.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Core.Services; + +public interface IDictionaryService +{ + string CalculatePath(Guid? parentId, int sourceId); +} diff --git a/src/Umbraco.Core/Services/IEntityService.cs b/src/Umbraco.Core/Services/IEntityService.cs index 74a416a8fe..5151d9ed1f 100644 --- a/src/Umbraco.Core/Services/IEntityService.cs +++ b/src/Umbraco.Core/Services/IEntityService.cs @@ -186,6 +186,22 @@ public interface IEntityService IQuery? filter = null, Ordering? ordering = null); + /// + /// Gets children of an entity. + /// + IEnumerable GetPagedTrashedChildren( + int id, + UmbracoObjectTypes objectType, + long pageIndex, + int pageSize, + out long totalRecords, + IQuery? filter = null, + Ordering? ordering = null) + { + totalRecords = 0; + return Array.Empty(); + } + /// /// Gets descendants of an entity. /// diff --git a/src/Umbraco.Core/Services/ILocalizationService.cs b/src/Umbraco.Core/Services/ILocalizationService.cs index 7a1b1b6fd1..dbfb01d3e1 100644 --- a/src/Umbraco.Core/Services/ILocalizationService.cs +++ b/src/Umbraco.Core/Services/ILocalizationService.cs @@ -1,4 +1,5 @@ using Umbraco.Cms.Core.Models; +using Umbraco.New.Cms.Core.Models; namespace Umbraco.Cms.Core.Services; @@ -49,6 +50,15 @@ public interface ILocalizationService : IService /// IDictionaryItem? GetDictionaryItemById(Guid id); + /// + /// Gets a collection of by their ids + /// + /// Ids of the + /// + /// A collection of + /// + IEnumerable GetDictionaryItemsByIds(params Guid[] ids) => Array.Empty(); + /// /// Gets a by its key /// @@ -58,6 +68,15 @@ public interface ILocalizationService : IService /// IDictionaryItem? GetDictionaryItemByKey(string key); + /// + /// Gets a collection of by their keys + /// + /// Keys of the + /// + /// A collection of + /// + IEnumerable GetDictionaryItemsByKeys(params string[] keys) => Array.Empty(); + /// /// Gets a list of children for a /// @@ -175,4 +194,10 @@ public interface ILocalizationService : IService /// /// The full dictionary key map. Dictionary GetDictionaryItemKeyMap(); + + PagedModel GetAllLanguagesPaged(int skip, int take) + { + ILanguage[] all = GetAllLanguages().Skip(skip).Take(take).ToArray(); + return new PagedModel(all.Length, all); + } } diff --git a/src/Umbraco.Core/Services/ITrackedReferencesService.cs b/src/Umbraco.Core/Services/ITrackedReferencesService.cs index 16b953c35a..94a8871e7f 100644 --- a/src/Umbraco.Core/Services/ITrackedReferencesService.cs +++ b/src/Umbraco.Core/Services/ITrackedReferencesService.cs @@ -1,4 +1,5 @@ using Umbraco.Cms.Core.Models; +using Umbraco.New.Cms.Core.Models; namespace Umbraco.Cms.Core.Services; @@ -43,4 +44,47 @@ public interface ITrackedReferencesService /// /// A paged result of objects. PagedResult GetPagedItemsWithRelations(int[] ids, long pageIndex, int pageSize, bool filterMustBeIsDependency); + + /// + /// Gets a paged result of items which are in relation with the current item. + /// Basically, shows the items which depend on the current item. + /// + /// The identifier of the entity to retrieve relations for. + /// The amount of items to skip + /// The amount of items to take. + /// + /// A boolean indicating whether to filter only the RelationTypes which are + /// dependencies (isDependency field is set to true). + /// + /// The total amount of items. + /// A paged result of objects. + PagedModel GetPagedRelationsForItem(int id, long skip, long take, bool filterMustBeIsDependency) => throw new NotImplementedException(); + + /// + /// Gets a paged result of the descending items that have any references, given a parent id. + /// + /// The unique identifier of the parent to retrieve descendants for. + /// The amount of items to skip + /// The amount of items to take. + /// + /// A boolean indicating whether to filter only the RelationTypes which are + /// dependencies (isDependency field is set to true). + /// + /// The total amount of items. + /// A paged result of objects. + PagedModel GetPagedDescendantsInReferences(int parentId, long skip, long take, bool filterMustBeIsDependency) => throw new NotImplementedException(); + + /// + /// Gets a paged result of items used in any kind of relation from selected integer ids. + /// + /// The identifiers of the entities to check for relations. + /// The amount of items to skip + /// The amount of items to take. + /// + /// A boolean indicating whether to filter only the RelationTypes which are + /// dependencies (isDependency field is set to true). + /// + /// The total amount of items. + /// A paged result of objects. + PagedModel GetPagedItemsWithRelations(int[] ids, long skip, long take, bool filterMustBeIsDependency) => throw new NotImplementedException(); } diff --git a/src/Umbraco.Core/Services/LocalizationService.cs b/src/Umbraco.Core/Services/LocalizationService.cs index 3046ddafb5..824aed8cf5 100644 --- a/src/Umbraco.Core/Services/LocalizationService.cs +++ b/src/Umbraco.Core/Services/LocalizationService.cs @@ -170,6 +170,29 @@ internal class LocalizationService : RepositoryService, ILocalizationService } } + /// + /// Gets a collection by their ids + /// + /// Ids of the + /// + /// A collection of + /// + public IEnumerable GetDictionaryItemsByIds(params Guid[] ids) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + IEnumerable items = _dictionaryRepository.GetMany(ids).ToArray(); + + // ensure the lazy Language callback is assigned + foreach (IDictionaryItem item in items) + { + EnsureDictionaryItemLanguageCallback(item); + } + + return items; + } + } + /// /// Gets a by its key /// @@ -189,6 +212,28 @@ internal class LocalizationService : RepositoryService, ILocalizationService } } + /// + /// Gets a collection of by their keys + /// + /// Keys of the + /// + /// A collection of + /// + public IEnumerable GetDictionaryItemsByKeys(params string[] keys) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + IEnumerable items = _dictionaryRepository.GetManyByKeys(keys).ToArray(); + + // ensure the lazy Language callback is assigned + foreach (IDictionaryItem item in items) + { + EnsureDictionaryItemLanguageCallback(item); + } + return items; + } + } + /// /// Gets a list of children for a /// diff --git a/src/Umbraco.Core/Services/TrackedReferencesService.cs b/src/Umbraco.Core/Services/TrackedReferencesService.cs index 32dc9c18cc..280e648327 100644 --- a/src/Umbraco.Core/Services/TrackedReferencesService.cs +++ b/src/Umbraco.Core/Services/TrackedReferencesService.cs @@ -1,23 +1,29 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; +using Umbraco.New.Cms.Core.Models; namespace Umbraco.Cms.Core.Services; public class TrackedReferencesService : ITrackedReferencesService { - private readonly IEntityService _entityService; private readonly ICoreScopeProvider _scopeProvider; private readonly ITrackedReferencesRepository _trackedReferencesRepository; + [Obsolete("Please use ctor that does not take an IEntityService, scheduled for removal in V12")] public TrackedReferencesService( ITrackedReferencesRepository trackedReferencesRepository, ICoreScopeProvider scopeProvider, - IEntityService entityService) + IEntityService entityService) : this(trackedReferencesRepository, scopeProvider) + { + } + + public TrackedReferencesService( + ITrackedReferencesRepository trackedReferencesRepository, + ICoreScopeProvider scopeProvider) { _trackedReferencesRepository = trackedReferencesRepository; _scopeProvider = scopeProvider; - _entityService = entityService; } /// @@ -58,4 +64,37 @@ public class TrackedReferencesService : ITrackedReferencesService out var totalItems); return new PagedResult(totalItems, pageIndex + 1, pageSize) { Items = items }; } + + public PagedModel GetPagedRelationsForItem(int id, long skip, long take, bool filterMustBeIsDependency) + { + using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true); + IEnumerable items = _trackedReferencesRepository.GetPagedRelationsForItem(id, skip, take, filterMustBeIsDependency, out var totalItems); + var pagedModel = new PagedModel(totalItems, items); + + return pagedModel; + } + + public PagedModel GetPagedDescendantsInReferences(int parentId, long skip, long take, bool filterMustBeIsDependency) + { + using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true); + + IEnumerable items = _trackedReferencesRepository.GetPagedDescendantsInReferences( + parentId, + skip, + take, + filterMustBeIsDependency, + out var totalItems); + var pagedModel = new PagedModel(totalItems, items); + + return pagedModel; + } + + public PagedModel GetPagedItemsWithRelations(int[] ids, long skip, long take, bool filterMustBeIsDependency) + { + using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true); + IEnumerable items = _trackedReferencesRepository.GetPagedItemsWithRelations(ids, skip, take, filterMustBeIsDependency, out var totalItems); + var pagedModel = new PagedModel(totalItems, items); + + return pagedModel; + } } diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 969a489d81..dd35456a07 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -3,7 +3,6 @@ Umbraco.Cms.Core Umbraco CMS - Core Contains the core assembly needed to run Umbraco CMS. - net7.0 Umbraco.Cms.Core diff --git a/src/Umbraco.Examine.Lucene/Umbraco.Examine.Lucene.csproj b/src/Umbraco.Examine.Lucene/Umbraco.Examine.Lucene.csproj index 5da47eb04a..61b7446724 100644 --- a/src/Umbraco.Examine.Lucene/Umbraco.Examine.Lucene.csproj +++ b/src/Umbraco.Examine.Lucene/Umbraco.Examine.Lucene.csproj @@ -3,7 +3,6 @@ Umbraco.Cms.Examine.Lucene Umbraco CMS - Examine - Lucene Adds Examine searching support using Lucene to Umbraco CMS. - net7.0 Umbraco.Cms.Infrastructure.Examine diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DictionaryRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DictionaryRepository.cs index 6cd2d8989b..909c9cfec2 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DictionaryRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DictionaryRepository.cs @@ -33,6 +33,13 @@ internal class DictionaryRepository : EntityRepositoryBase return uniqueIdRepo.Get(uniqueId); } + public IEnumerable GetMany(params Guid[] uniqueIds) + { + var uniqueIdRepo = new DictionaryByUniqueIdRepository(this, ScopeAccessor, AppCaches, + _loggerFactory.CreateLogger()); + return uniqueIdRepo.GetMany(uniqueIds); + } + public IDictionaryItem? Get(string key) { var keyRepo = new DictionaryByKeyRepository(this, ScopeAccessor, AppCaches, @@ -40,6 +47,13 @@ internal class DictionaryRepository : EntityRepositoryBase return keyRepo.Get(key); } + public IEnumerable GetManyByKeys(string[] keys) + { + var keyRepo = new DictionaryByKeyRepository(this, ScopeAccessor, AppCaches, + _loggerFactory.CreateLogger()); + return keyRepo.GetMany(keys); + } + public Dictionary GetDictionaryItemKeyMap() { var columns = new[] { "key", "id" }.Select(x => (object)SqlSyntax.GetQuotedColumnName(x)).ToArray(); diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepository.cs index c904b5b440..9841ae9d0c 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepository.cs @@ -680,7 +680,8 @@ internal class EntityRepository : RepositoryBase, IEntityRepositoryExtended private EntitySlim BuildEntity(BaseDto dto) { - if (dto.NodeObjectType == Constants.ObjectTypes.Document) + if (dto.NodeObjectType == Constants.ObjectTypes.Document + || dto.NodeObjectType == Constants.ObjectTypes.DocumentBlueprint) { return BuildDocumentEntity(dto); } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/SimpleGetRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/SimpleGetRepository.cs index 1fe1f1e82a..ad509afd5a 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/SimpleGetRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/SimpleGetRepository.cs @@ -55,7 +55,7 @@ internal abstract class SimpleGetRepository : EntityReposito protected override IEnumerable PerformGetAll(params TId[]? ids) { - Sql sql = Sql().From(); + Sql sql = Sql().From(); if (ids?.Any() ?? false) { diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TrackedReferencesRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TrackedReferencesRepository.cs index 97d1e8d0f4..fc62ac861b 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TrackedReferencesRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TrackedReferencesRepository.cs @@ -1,10 +1,7 @@ -using System; -using System.Collections.Generic; -using System.Linq; using NPoco; +using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Repositories; -using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; @@ -14,40 +11,44 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement internal class TrackedReferencesRepository : ITrackedReferencesRepository { private readonly IScopeAccessor _scopeAccessor; + private readonly IUmbracoMapper _umbracoMapper; - public TrackedReferencesRepository(IScopeAccessor scopeAccessor) + public TrackedReferencesRepository(IScopeAccessor scopeAccessor, IUmbracoMapper umbracoMapper) { _scopeAccessor = scopeAccessor; + _umbracoMapper = umbracoMapper; } /// /// Gets a page of items used in any kind of relation from selected integer ids. /// - public IEnumerable GetPagedItemsWithRelations(int[] ids, long pageIndex, int pageSize, bool filterMustBeIsDependency, out long totalRecords) + public IEnumerable GetPagedItemsWithRelations(int[] ids, long pageIndex, int pageSize, + bool filterMustBeIsDependency, out long totalRecords) { - Sql innerUnionSql = GetInnerUnionSql(); + Sql innerUnionSql = GetInnerUnionSql(); var sql = _scopeAccessor.AmbientScope?.Database.SqlContext.Sql().SelectDistinct( - "[x].[id] as nodeId", - "[n].[uniqueId] as nodeKey", - "[n].[text] as nodeName", - "[n].[nodeObjectType] as nodeObjectType", - "[ct].[icon] as contentTypeIcon", - "[ct].[alias] as contentTypeAlias", - "[ctn].[text] as contentTypeName", - "[x].[alias] as relationTypeAlias", - "[x].[name] as relationTypeName", - "[x].[isDependency] as relationTypeIsDependency", - "[x].[dual] as relationTypeIsBidirectional") - .From("n") - .InnerJoinNested(innerUnionSql, "x") - .On((n, x) => n.NodeId == x.Id, "n", "x") - .LeftJoin("c").On((left, right) => left.NodeId == right.NodeId, aliasLeft: "n", - aliasRight: "c") - .LeftJoin("ct") - .On((left, right) => left.ContentTypeId == right.NodeId, aliasLeft: "c", - aliasRight: "ct") - .LeftJoin("ctn").On((left, right) => left.NodeId == right.NodeId, - aliasLeft: "ct", aliasRight: "ctn"); + "[x].[id] as nodeId", + "[n].[uniqueId] as nodeKey", + "[n].[text] as nodeName", + "[n].[nodeObjectType] as nodeObjectType", + "[ct].[icon] as contentTypeIcon", + "[ct].[alias] as contentTypeAlias", + "[ctn].[text] as contentTypeName", + "[x].[alias] as relationTypeAlias", + "[x].[name] as relationTypeName", + "[x].[isDependency] as relationTypeIsDependency", + "[x].[dual] as relationTypeIsBidirectional") + .From("n") + .InnerJoinNested(innerUnionSql, "x") + .On((n, x) => n.NodeId == x.Id, "n", "x") + .LeftJoin("c").On((left, right) => left.NodeId == right.NodeId, + aliasLeft: "n", + aliasRight: "c") + .LeftJoin("ct") + .On((left, right) => left.ContentTypeId == right.NodeId, aliasLeft: "c", + aliasRight: "ct") + .LeftJoin("ctn").On((left, right) => left.NodeId == right.NodeId, + aliasLeft: "ct", aliasRight: "ctn"); if (ids.Any()) { sql = sql?.Where(x => ids.Contains(x.NodeId), "n"); @@ -75,20 +76,25 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement } var innerUnionSqlChild = _scopeAccessor.AmbientScope.Database.SqlContext.Sql().Select( - "[cr].childId as id", "[cr].parentId as otherId", "[rt].[alias]", "[rt].[name]", "[rt].[isDependency]", "[rt].[dual]") + "[cr].childId as id", "[cr].parentId as otherId", "[rt].[alias]", "[rt].[name]", + "[rt].[isDependency]", "[rt].[dual]") .From("cr").InnerJoin("rt") .On((cr, rt) => rt.Dual == false && rt.Id == cr.RelationType, "cr", "rt"); var innerUnionSqlDualParent = _scopeAccessor.AmbientScope.Database.SqlContext.Sql().Select( - "[dpr].parentId as id", "[dpr].childId as otherId", "[dprt].[alias]", "[dprt].[name]", "[dprt].[isDependency]", "[dprt].[dual]") + "[dpr].parentId as id", "[dpr].childId as otherId", "[dprt].[alias]", "[dprt].[name]", + "[dprt].[isDependency]", "[dprt].[dual]") .From("dpr").InnerJoin("dprt") - .On((dpr, dprt) => dprt.Dual == true && dprt.Id == dpr.RelationType, "dpr", + .On((dpr, dprt) => dprt.Dual == true && dprt.Id == dpr.RelationType, + "dpr", "dprt"); var innerUnionSql3 = _scopeAccessor.AmbientScope.Database.SqlContext.Sql().Select( - "[dcr].childId as id", "[dcr].parentId as otherId", "[dcrt].[alias]", "[dcrt].[name]", "[dcrt].[isDependency]", "[dcrt].[dual]") + "[dcr].childId as id", "[dcr].parentId as otherId", "[dcrt].[alias]", "[dcrt].[name]", + "[dcrt].[isDependency]", "[dcrt].[dual]") .From("dcr").InnerJoin("dcrt") - .On((dcr, dcrt) => dcrt.Dual == true && dcrt.Id == dcr.RelationType, "dcr", + .On((dcr, dcrt) => dcrt.Dual == true && dcrt.Id == dcr.RelationType, + "dcr", "dcrt"); @@ -100,7 +106,8 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement /// /// Gets a page of the descending items that have any references, given a parent id. /// - public IEnumerable GetPagedDescendantsInReferences(int parentId, long pageIndex, int pageSize, bool filterMustBeIsDependency, out long totalRecords) + public IEnumerable GetPagedDescendantsInReferences(int parentId, long pageIndex, int pageSize, + bool filterMustBeIsDependency, out long totalRecords) { var syntax = _scopeAccessor.AmbientScope?.Database.SqlContext.SqlSyntax; @@ -118,28 +125,30 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement Sql innerUnionSql = GetInnerUnionSql(); var sql = _scopeAccessor.AmbientScope?.Database.SqlContext.Sql().SelectDistinct( - "[x].[id] as nodeId", - "[n].[uniqueId] as nodeKey", - "[n].[text] as nodeName", - "[n].[nodeObjectType] as nodeObjectType", - "[ct].[icon] as contentTypeIcon", - "[ct].[alias] as contentTypeAlias", - "[ctn].[text] as contentTypeName", - "[x].[alias] as relationTypeAlias", - "[x].[name] as relationTypeName", - "[x].[isDependency] as relationTypeIsDependency", - "[x].[dual] as relationTypeIsBidirectional") - .From("n") - .InnerJoinNested(innerUnionSql, "x") - .On((n, x) => n.NodeId == x.Id, "n", "x") - .LeftJoin("c").On((left, right) => left.NodeId == right.NodeId, aliasLeft: "n", - aliasRight: "c") - .LeftJoin("ct") - .On((left, right) => left.ContentTypeId == right.NodeId, aliasLeft: "c", - aliasRight: "ct") - .LeftJoin("ctn").On((left, right) => left.NodeId == right.NodeId, - aliasLeft: "ct", aliasRight: "ctn"); - sql = sql?.WhereIn((System.Linq.Expressions.Expression>)(x => x.NodeId), subQuery, "n"); + "[x].[id] as nodeId", + "[n].[uniqueId] as nodeKey", + "[n].[text] as nodeName", + "[n].[nodeObjectType] as nodeObjectType", + "[ct].[icon] as contentTypeIcon", + "[ct].[alias] as contentTypeAlias", + "[ctn].[text] as contentTypeName", + "[x].[alias] as relationTypeAlias", + "[x].[name] as relationTypeName", + "[x].[isDependency] as relationTypeIsDependency", + "[x].[dual] as relationTypeIsBidirectional") + .From("n") + .InnerJoinNested(innerUnionSql, "x") + .On((n, x) => n.NodeId == x.Id, "n", "x") + .LeftJoin("c").On((left, right) => left.NodeId == right.NodeId, + aliasLeft: "n", + aliasRight: "c") + .LeftJoin("ct") + .On((left, right) => left.ContentTypeId == right.NodeId, aliasLeft: "c", + aliasRight: "ct") + .LeftJoin("ctn").On((left, right) => left.NodeId == right.NodeId, + aliasLeft: "ct", aliasRight: "ctn"); + sql = sql?.WhereIn((System.Linq.Expressions.Expression>)(x => x.NodeId), subQuery, + "n"); if (filterMustBeIsDependency) { @@ -160,32 +169,34 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement /// Gets a page of items which are in relation with the current item. /// Basically, shows the items which depend on the current item. /// - public IEnumerable GetPagedRelationsForItem(int id, long pageIndex, int pageSize, bool filterMustBeIsDependency, out long totalRecords) + public IEnumerable GetPagedRelationsForItem(int id, long pageIndex, int pageSize, + bool filterMustBeIsDependency, out long totalRecords) { Sql innerUnionSql = GetInnerUnionSql(); var sql = _scopeAccessor.AmbientScope?.Database.SqlContext.Sql().SelectDistinct( - "[x].[otherId] as nodeId", - "[n].[uniqueId] as nodeKey", - "[n].[text] as nodeName", - "[n].[nodeObjectType] as nodeObjectType", - "[ct].[icon] as contentTypeIcon", - "[ct].[alias] as contentTypeAlias", - "[ctn].[text] as contentTypeName", - "[x].[alias] as relationTypeAlias", - "[x].[name] as relationTypeName", - "[x].[isDependency] as relationTypeIsDependency", - "[x].[dual] as relationTypeIsBidirectional") - .From("n") - .InnerJoinNested(innerUnionSql, "x") - .On((n, x) => n.NodeId == x.OtherId, "n", "x") - .LeftJoin("c").On((left, right) => left.NodeId == right.NodeId, aliasLeft: "n", - aliasRight: "c") - .LeftJoin("ct") - .On((left, right) => left.ContentTypeId == right.NodeId, aliasLeft: "c", - aliasRight: "ct") - .LeftJoin("ctn").On((left, right) => left.NodeId == right.NodeId, - aliasLeft: "ct", aliasRight: "ctn") - .Where(x => x.Id == id, "x"); + "[x].[otherId] as nodeId", + "[n].[uniqueId] as nodeKey", + "[n].[text] as nodeName", + "[n].[nodeObjectType] as nodeObjectType", + "[ct].[icon] as contentTypeIcon", + "[ct].[alias] as contentTypeAlias", + "[ctn].[text] as contentTypeName", + "[x].[alias] as relationTypeAlias", + "[x].[name] as relationTypeName", + "[x].[isDependency] as relationTypeIsDependency", + "[x].[dual] as relationTypeIsBidirectional") + .From("n") + .InnerJoinNested(innerUnionSql, "x") + .On((n, x) => n.NodeId == x.OtherId, "n", "x") + .LeftJoin("c").On((left, right) => left.NodeId == right.NodeId, + aliasLeft: "n", + aliasRight: "c") + .LeftJoin("ct") + .On((left, right) => left.ContentTypeId == right.NodeId, aliasLeft: "c", + aliasRight: "ct") + .LeftJoin("ctn").On((left, right) => left.NodeId == right.NodeId, + aliasLeft: "ct", aliasRight: "ctn") + .Where(x => x.Id == id, "x"); if (filterMustBeIsDependency) { @@ -201,25 +212,184 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement return pagedResult?.Items.Select(MapDtoToEntity) ?? Enumerable.Empty(); } + public IEnumerable GetPagedRelationsForItem( + int id, + long skip, + long take, + bool filterMustBeIsDependency, + out long totalRecords) + { + Sql innerUnionSql = GetInnerUnionSql(); + var sql = _scopeAccessor.AmbientScope?.Database.SqlContext.Sql().SelectDistinct( + "[x].[otherId] as nodeId", + "[n].[uniqueId] as nodeKey", + "[n].[text] as nodeName", + "[n].[nodeObjectType] as nodeObjectType", + "[ct].[icon] as contentTypeIcon", + "[ct].[alias] as contentTypeAlias", + "[ctn].[text] as contentTypeName", + "[x].[alias] as relationTypeAlias", + "[x].[name] as relationTypeName", + "[x].[isDependency] as relationTypeIsDependency", + "[x].[dual] as relationTypeIsBidirectional") + .From("n") + .InnerJoinNested(innerUnionSql, "x") + .On((n, x) => n.NodeId == x.OtherId, "n", "x") + .LeftJoin("c").On((left, right) => left.NodeId == right.NodeId, + aliasLeft: "n", + aliasRight: "c") + .LeftJoin("ct") + .On((left, right) => left.ContentTypeId == right.NodeId, aliasLeft: "c", + aliasRight: "ct") + .LeftJoin("ctn").On((left, right) => left.NodeId == right.NodeId, + aliasLeft: "ct", aliasRight: "ctn") + .Where(x => x.Id == id, "x"); + + if (filterMustBeIsDependency) + { + sql = sql?.Where(rt => rt.IsDependency, "x"); + } + + // Ordering is required for paging + sql = sql?.OrderBy(x => x.Alias, "x"); + + RelationItemDto[] pagedResult = + _scopeAccessor.AmbientScope?.Database.SkipTake(skip, take, sql).ToArray() ?? + Array.Empty(); + totalRecords = pagedResult.Length; + + return _umbracoMapper.MapEnumerable(pagedResult); + } + + public IEnumerable GetPagedItemsWithRelations( + int[] ids, + long skip, + long take, + bool filterMustBeIsDependency, + out long totalRecords) + { + Sql innerUnionSql = GetInnerUnionSql(); + Sql? sql = _scopeAccessor.AmbientScope?.Database.SqlContext.Sql().SelectDistinct( + "[x].[id] as nodeId", + "[n].[uniqueId] as nodeKey", + "[n].[text] as nodeName", + "[n].[nodeObjectType] as nodeObjectType", + "[ct].[icon] as contentTypeIcon", + "[ct].[alias] as contentTypeAlias", + "[ctn].[text] as contentTypeName", + "[x].[alias] as relationTypeAlias", + "[x].[name] as relationTypeName", + "[x].[isDependency] as relationTypeIsDependency", + "[x].[dual] as relationTypeIsBidirectional") + .From("n") + .InnerJoinNested(innerUnionSql, "x") + .On((n, x) => n.NodeId == x.Id, "n", "x") + .LeftJoin("c") + .On((left, right) => left.NodeId == right.NodeId, aliasLeft: "n", aliasRight: "c") + .LeftJoin("ct") + .On((left, right) => left.ContentTypeId == right.NodeId, aliasLeft: "c", + aliasRight: "ct") + .LeftJoin("ctn") + .On((left, right) => left.NodeId == right.NodeId, aliasLeft: "ct", + aliasRight: "ctn"); + if (ids.Any()) + { + sql = sql?.Where(x => ids.Contains(x.NodeId), "n"); + } + + if (filterMustBeIsDependency) + { + sql = sql?.Where(rt => rt.IsDependency, "x"); + } + + // Ordering is required for paging + sql = sql?.OrderBy(x => x.Alias, "x"); + + RelationItemDto[] pagedResult = + _scopeAccessor.AmbientScope?.Database.SkipTake(skip, take, sql).ToArray() ?? + Array.Empty(); + totalRecords = pagedResult.Length; + + return _umbracoMapper.MapEnumerable(pagedResult); + } + + public IEnumerable GetPagedDescendantsInReferences( + int parentId, + long skip, + long take, + bool filterMustBeIsDependency, + out long totalRecords) + { + var syntax = _scopeAccessor.AmbientScope?.Database.SqlContext.SqlSyntax; + + // Gets the path of the parent with ",%" added + var subsubQuery = _scopeAccessor.AmbientScope?.Database.SqlContext.Sql() + .Select(syntax?.GetConcat("[node].[path]", "',%'")) + .From("node") + .Where(x => x.NodeId == parentId, "node"); + + // Gets the descendants of the parent node + Sql? subQuery = _scopeAccessor.AmbientScope?.Database.SqlContext.Sql() + .Select(x => x.NodeId) + .From() + .WhereLike(x => x.Path, subsubQuery); + + Sql innerUnionSql = GetInnerUnionSql(); + var sql = _scopeAccessor.AmbientScope?.Database.SqlContext.Sql().SelectDistinct( + "[x].[id] as nodeId", + "[n].[uniqueId] as nodeKey", + "[n].[text] as nodeName", + "[n].[nodeObjectType] as nodeObjectType", + "[ct].[icon] as contentTypeIcon", + "[ct].[alias] as contentTypeAlias", + "[ctn].[text] as contentTypeName", + "[x].[alias] as relationTypeAlias", + "[x].[name] as relationTypeName", + "[x].[isDependency] as relationTypeIsDependency", + "[x].[dual] as relationTypeIsBidirectional") + .From("n") + .InnerJoinNested(innerUnionSql, "x") + .On((n, x) => n.NodeId == x.Id, "n", "x") + .LeftJoin("c").On((left, right) => left.NodeId == right.NodeId, + aliasLeft: "n", aliasRight: "c") + .LeftJoin("ct") + .On((left, right) => left.ContentTypeId == right.NodeId, aliasLeft: "c", + aliasRight: "ct") + .LeftJoin("ctn") + .On((left, right) => left.NodeId == right.NodeId, aliasLeft: "ct", + aliasRight: "ctn"); + sql = sql?.WhereIn((System.Linq.Expressions.Expression>)(x => x.NodeId), subQuery, + "n"); + + if (filterMustBeIsDependency) + { + sql = sql?.Where(rt => rt.IsDependency, "x"); + } + + // Ordering is required for paging + sql = sql?.OrderBy(x => x.Alias, "x"); + + List? pagedResult = + _scopeAccessor.AmbientScope?.Database.SkipTake(skip, take, sql); + totalRecords = pagedResult?.Count ?? 0; + + return _umbracoMapper.MapEnumerable(pagedResult ?? + new List()); + } + private class UnionHelperDto { - [Column("id")] - public int Id { get; set; } + [Column("id")] public int Id { get; set; } - [Column("otherId")] - public int OtherId { get; set; } + [Column("otherId")] public int OtherId { get; set; } - [Column("alias")] - public string? Alias { get; set; } + [Column("alias")] public string? Alias { get; set; } - [Column("name")] - public string? Name { get; set; } + [Column("name")] public string? Name { get; set; } - [Column("isDependency")] - public bool IsDependency { get; set; } + [Column("isDependency")] public bool IsDependency { get; set; } - [Column("dual")] - public bool Dual { get; set; } + [Column("dual")] public bool Dual { get; set; } } private RelationItem MapDtoToEntity(RelationItemDto dto) diff --git a/src/Umbraco.Infrastructure/PropertyEditors/DropDownFlexibleConfigurationEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/DropDownFlexibleConfigurationEditor.cs index 55d7f3c0af..ec5c98e99a 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/DropDownFlexibleConfigurationEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/DropDownFlexibleConfigurationEditor.cs @@ -26,7 +26,7 @@ internal class DropDownFlexibleConfigurationEditor : ConfigurationEditor : UserManager)); } + + var result = await VerifyPasswordAsync(userPasswordStore, user, password); - var hash = await userPasswordStore.GetPasswordHashAsync(user, CancellationToken.None); - - return await VerifyPasswordAsync(userPasswordStore, user, password) == PasswordVerificationResult.Success; + return result == PasswordVerificationResult.Success || result == PasswordVerificationResult.SuccessRehashNeeded; } public virtual async Task> GetValidTwoFactorProvidersAsync(TUser user) diff --git a/src/Umbraco.Infrastructure/Telemetry/Providers/SystemInformationTelemetryProvider.cs b/src/Umbraco.Infrastructure/Telemetry/Providers/SystemInformationTelemetryProvider.cs index 8953b71ce1..0a5881528e 100644 --- a/src/Umbraco.Infrastructure/Telemetry/Providers/SystemInformationTelemetryProvider.cs +++ b/src/Umbraco.Infrastructure/Telemetry/Providers/SystemInformationTelemetryProvider.cs @@ -16,7 +16,6 @@ namespace Umbraco.Cms.Infrastructure.Telemetry.Providers; internal class SystemInformationTelemetryProvider : IDetailedTelemetryProvider, IUserDataService { - private readonly GlobalSettings _globalSettings; private readonly IHostEnvironment _hostEnvironment; private readonly HostingSettings _hostingSettings; private readonly ILocalizationService _localizationService; @@ -25,7 +24,7 @@ internal class SystemInformationTelemetryProvider : IDetailedTelemetryProvider, private readonly IUmbracoVersion _version; private readonly IServerRoleAccessor _serverRoleAccessor; - + [Obsolete($"Use the constructor that does not take an IOptionsMonitor parameter, scheduled for removal in V12")] public SystemInformationTelemetryProvider( IUmbracoVersion version, ILocalizationService localizationService, @@ -35,6 +34,18 @@ internal class SystemInformationTelemetryProvider : IDetailedTelemetryProvider, IHostEnvironment hostEnvironment, IUmbracoDatabaseFactory umbracoDatabaseFactory, IServerRoleAccessor serverRoleAccessor) + : this(version, localizationService, modelsBuilderSettings, hostingSettings, hostEnvironment, umbracoDatabaseFactory, serverRoleAccessor) + { + } + + public SystemInformationTelemetryProvider( + IUmbracoVersion version, + ILocalizationService localizationService, + IOptionsMonitor modelsBuilderSettings, + IOptionsMonitor hostingSettings, + IHostEnvironment hostEnvironment, + IUmbracoDatabaseFactory umbracoDatabaseFactory, + IServerRoleAccessor serverRoleAccessor) { _version = version; _localizationService = localizationService; @@ -42,7 +53,6 @@ internal class SystemInformationTelemetryProvider : IDetailedTelemetryProvider, _umbracoDatabaseFactory = umbracoDatabaseFactory; _serverRoleAccessor = serverRoleAccessor; - _globalSettings = globalSettings.CurrentValue; _hostingSettings = hostingSettings.CurrentValue; _modelsBuilderSettings = modelsBuilderSettings.CurrentValue; } @@ -57,8 +67,6 @@ internal class SystemInformationTelemetryProvider : IDetailedTelemetryProvider, private bool IsDebug => _hostingSettings.Debug; - private bool UmbracoPathCustomized => _globalSettings.UmbracoPath != Constants.System.DefaultUmbracoPath; - private string AspEnvironment => _hostEnvironment.EnvironmentName; private string ServerOs => RuntimeInformation.OSDescription; @@ -74,7 +82,6 @@ internal class SystemInformationTelemetryProvider : IDetailedTelemetryProvider, new(Constants.Telemetry.OsLanguage, CurrentCulture), new(Constants.Telemetry.WebServer, CurrentWebServer), new(Constants.Telemetry.ModelsBuilderMode, ModelsBuilderMode), - new(Constants.Telemetry.CustomUmbracoPath, UmbracoPathCustomized), new(Constants.Telemetry.AspEnvironment, AspEnvironment), new(Constants.Telemetry.IsDebug, IsDebug), new(Constants.Telemetry.DatabaseProvider, DatabaseProvider), new(Constants.Telemetry.CurrentServerRole, CurrentServerRole), diff --git a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj index fef3ecd9e5..65383daea7 100644 --- a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj +++ b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj @@ -3,7 +3,6 @@ Umbraco.Cms.Infrastructure Umbraco CMS - Infrastructure Contains the infrastructure assembly needed to run Umbraco CMS. - net7.0 Umbraco.Cms.Infrastructure diff --git a/src/Umbraco.New.Cms.Core/Services/Installer/ILanguageService.cs b/src/Umbraco.New.Cms.Core/Services/Installer/ILanguageService.cs new file mode 100644 index 0000000000..29bb2df265 --- /dev/null +++ b/src/Umbraco.New.Cms.Core/Services/Installer/ILanguageService.cs @@ -0,0 +1,11 @@ +using Umbraco.Cms.Core.Models; + +namespace Umbraco.New.Cms.Core.Services.Installer; + +public interface ILanguageService +{ + bool LanguageAlreadyExists(int id, string isoCode); + + bool CanUseLanguagesFallbackLanguage(ILanguage language); + bool CanGetProperFallbackLanguage(ILanguage existingById); +} diff --git a/src/Umbraco.New.Cms.Core/Services/Languages/LanguageService.cs b/src/Umbraco.New.Cms.Core/Services/Languages/LanguageService.cs new file mode 100644 index 0000000000..73a4ded3d5 --- /dev/null +++ b/src/Umbraco.New.Cms.Core/Services/Languages/LanguageService.cs @@ -0,0 +1,92 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.New.Cms.Core.Services.Installer; + +namespace Umbraco.New.Cms.Core.Services.Languages; + +public class LanguageService : ILanguageService +{ + private readonly ILocalizationService _localizationService; + + public LanguageService(ILocalizationService localizationService) + { + _localizationService = localizationService; + } + + public bool LanguageAlreadyExists(int id, string isoCode) + { + // this is prone to race conditions but the service will not let us proceed anyways + ILanguage? existingByCulture = _localizationService.GetLanguageByIsoCode(isoCode); + + // the localization service might return the generic language even when queried for specific ones (e.g. "da" when queried for "da-DK") + // - we need to handle that explicitly + if (existingByCulture?.IsoCode != isoCode) + { + existingByCulture = null; + } + + if (existingByCulture != null && id != existingByCulture.Id) + { + return true; + } + + ILanguage? existingById = id != default ? _localizationService.GetLanguageById(id) : null; + return existingById is not null; + } + + public bool CanUseLanguagesFallbackLanguage(ILanguage language) + { + if (!language.FallbackLanguageId.HasValue) + { + return false; + } + + var languages = _localizationService.GetAllLanguages().ToDictionary(x => x.Id, x => x); + return languages.ContainsKey(language.FallbackLanguageId.Value); + + } + + public bool CanGetProperFallbackLanguage(ILanguage existingById) + { + // modifying an existing language can create a fallback, verify + // note that the service will check again, dealing with race conditions + if (existingById.FallbackLanguageId.HasValue) + { + var languages = _localizationService.GetAllLanguages().ToDictionary(x => x.Id, x => x); + + if (CreatesCycle(existingById, languages)) + { + return false; + } + } + + return true; + } + + + // see LocalizationService + private bool CreatesCycle(ILanguage language, IDictionary languages) + { + // a new language is not referenced yet, so cannot be part of a cycle + if (!language.HasIdentity) + { + return false; + } + + var id = language.FallbackLanguageId; + while (true) // assuming languages does not already contains a cycle, this must end + { + if (!id.HasValue) + { + return false; // no fallback means no cycle + } + + if (id.Value == language.Id) + { + return true; // back to language = cycle! + } + + id = languages[id.Value].FallbackLanguageId; // else keep chaining + } + } +} diff --git a/src/Umbraco.New.Cms.Core/Umbraco.New.Cms.Core.csproj b/src/Umbraco.New.Cms.Core/Umbraco.New.Cms.Core.csproj index 25ce2bfabd..339bff9be6 100644 --- a/src/Umbraco.New.Cms.Core/Umbraco.New.Cms.Core.csproj +++ b/src/Umbraco.New.Cms.Core/Umbraco.New.Cms.Core.csproj @@ -2,7 +2,6 @@ Umbraco CMS - Core Contains the core assembly needed to run Umbraco CMS. - net7.0 false false diff --git a/src/Umbraco.New.Cms.Infrastructure/Persistence/Mappers/RelationModelMapDefinition.cs b/src/Umbraco.New.Cms.Infrastructure/Persistence/Mappers/RelationModelMapDefinition.cs new file mode 100644 index 0000000000..44dfc3bd4a --- /dev/null +++ b/src/Umbraco.New.Cms.Infrastructure/Persistence/Mappers/RelationModelMapDefinition.cs @@ -0,0 +1,26 @@ +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +namespace Umbraco.New.Cms.Infrastructure.Persistence.Mappers; + +public class RelationModelMapDefinition : IMapDefinition +{ + public void DefineMaps(IUmbracoMapper mapper) + { + mapper.Define((source, context) => new RelationItemModel(), Map); + } + + private void Map(RelationItemDto source, RelationItemModel target, MapperContext context) + { + target.NodeKey = source.ChildNodeKey; + target.NodeType = ObjectTypes.GetUdiType(source.ChildNodeObjectType); + target.NodeName = source.ChildNodeName; + target.RelationTypeName = source.RelationTypeName; + target.RelationTypeIsBidirectional = source.RelationTypeIsBidirectional; + target.RelationTypeIsDependency = source.RelationTypeIsDependency; + target.ContentTypeAlias = source.ChildContentTypeAlias; + target.ContentTypeIcon = source.ChildContentTypeIcon; + target.ContentTypeName = source.ChildContentTypeName; + } +} diff --git a/src/Umbraco.New.Cms.Infrastructure/Services/IIndexingRebuilderService.cs b/src/Umbraco.New.Cms.Infrastructure/Services/IIndexingRebuilderService.cs new file mode 100644 index 0000000000..7832a2b782 --- /dev/null +++ b/src/Umbraco.New.Cms.Infrastructure/Services/IIndexingRebuilderService.cs @@ -0,0 +1,11 @@ +using Examine; + +namespace Umbraco.New.Cms.Infrastructure.Services; + +public interface IIndexingRebuilderService +{ + bool CanRebuild(string indexName); + bool TryRebuild(IIndex index, string indexName); + + bool IsRebuilding(string indexName); +} diff --git a/src/Umbraco.New.Cms.Infrastructure/Services/IndexingRebuilderService.cs b/src/Umbraco.New.Cms.Infrastructure/Services/IndexingRebuilderService.cs new file mode 100644 index 0000000000..f6e9183d0a --- /dev/null +++ b/src/Umbraco.New.Cms.Infrastructure/Services/IndexingRebuilderService.cs @@ -0,0 +1,86 @@ +using Examine; +using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Infrastructure.Examine; + +namespace Umbraco.New.Cms.Infrastructure.Services; + +public class IndexingRebuilderService : IIndexingRebuilderService +{ + private const string TempKey = "temp_indexing_op_"; + private readonly IAppPolicyCache _runtimeCache; + private readonly IIndexRebuilder _indexRebuilder; + private readonly ILogger _logger; + + public IndexingRebuilderService( + AppCaches runtimeCache, + IIndexRebuilder indexRebuilder, + ILogger logger) + { + _indexRebuilder = indexRebuilder; + _logger = logger; + _runtimeCache = runtimeCache.RuntimeCache; + } + + public bool CanRebuild(string indexName) => _indexRebuilder.CanRebuild(indexName); + + public bool TryRebuild(IIndex index, string indexName) + { + // Remove it in case there's a handler there already + index.IndexOperationComplete -= Indexer_IndexOperationComplete; + + // Now add a single handler + index.IndexOperationComplete += Indexer_IndexOperationComplete; + + try + { + Set(indexName); + _indexRebuilder.RebuildIndex(indexName); + return true; + } + catch(Exception exception) + { + // Ensure it's not listening + index.IndexOperationComplete -= Indexer_IndexOperationComplete; + _logger.LogError(exception, "An error occurred rebuilding index"); + return false; + } + } + + private void Set(string indexName) + { + var cacheKey = TempKey + indexName; + + // put temp val in cache which is used as a rudimentary way to know when the indexing is done + _runtimeCache.Insert(cacheKey, () => "tempValue", TimeSpan.FromMinutes(5)); + } + + private void Clear(string? indexName) + { + var cacheKey = TempKey + indexName; + _runtimeCache.Clear(cacheKey); + } + + public bool IsRebuilding(string indexName) + { + var cacheKey = "temp_indexing_op_" + indexName; + return _runtimeCache.Get(cacheKey) is not null; + } + + private void Indexer_IndexOperationComplete(object? sender, EventArgs e) + { + var indexer = (IIndex?)sender; + + _logger.LogDebug("Logging operation completed for index {IndexName}", indexer?.Name); + + if (indexer is not null) + { + //ensure it's not listening anymore + indexer.IndexOperationComplete -= Indexer_IndexOperationComplete; + } + + _logger.LogInformation($"Rebuilding index '{indexer?.Name}' done."); + + Clear(indexer?.Name); + } +} diff --git a/src/Umbraco.New.Cms.Infrastructure/Umbraco.New.Cms.Infrastructure.csproj b/src/Umbraco.New.Cms.Infrastructure/Umbraco.New.Cms.Infrastructure.csproj index 5d8ee6698f..8dc5d3cc00 100644 --- a/src/Umbraco.New.Cms.Infrastructure/Umbraco.New.Cms.Infrastructure.csproj +++ b/src/Umbraco.New.Cms.Infrastructure/Umbraco.New.Cms.Infrastructure.csproj @@ -2,7 +2,6 @@ Umbraco CMS - Infrastructure Contains the infrastructure assembly needed to run Umbraco CMS. - net7.0 false false diff --git a/src/Umbraco.New.Cms.Web.Common/Routing/VersionedApiBackOfficeRouteAttribute.cs b/src/Umbraco.New.Cms.Web.Common/Routing/VersionedApiBackOfficeRouteAttribute.cs new file mode 100644 index 0000000000..a7abacdb03 --- /dev/null +++ b/src/Umbraco.New.Cms.Web.Common/Routing/VersionedApiBackOfficeRouteAttribute.cs @@ -0,0 +1,9 @@ +namespace Umbraco.New.Cms.Web.Common.Routing; + +public class VersionedApiBackOfficeRouteAttribute : BackOfficeRouteAttribute +{ + public VersionedApiBackOfficeRouteAttribute(string template) + : base($"api/v{{version:apiVersion}}/{template.TrimStart('/')}") + { + } +} diff --git a/src/Umbraco.New.Cms.Web.Common/Umbraco.New.Cms.Web.Common.csproj b/src/Umbraco.New.Cms.Web.Common/Umbraco.New.Cms.Web.Common.csproj index 01a4cb93de..292532ba0c 100644 --- a/src/Umbraco.New.Cms.Web.Common/Umbraco.New.Cms.Web.Common.csproj +++ b/src/Umbraco.New.Cms.Web.Common/Umbraco.New.Cms.Web.Common.csproj @@ -2,7 +2,6 @@ Umbraco CMS - Web Contains the web assembly needed to run Umbraco CMS. - net7.0 false false diff --git a/src/Umbraco.PublishedCache.NuCache/Umbraco.PublishedCache.NuCache.csproj b/src/Umbraco.PublishedCache.NuCache/Umbraco.PublishedCache.NuCache.csproj index 6cdec8ce08..a4230fca4d 100644 --- a/src/Umbraco.PublishedCache.NuCache/Umbraco.PublishedCache.NuCache.csproj +++ b/src/Umbraco.PublishedCache.NuCache/Umbraco.PublishedCache.NuCache.csproj @@ -3,7 +3,6 @@ Umbraco.Cms.PublishedCache.NuCache Umbraco CMS - Published cache - NuCache Contains the published cache assembly needed to run Umbraco CMS. - net7.0 Umbraco.Cms.Infrastructure.PublishedCache diff --git a/src/Umbraco.Web.BackOffice/Trees/TemplatesTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/TemplatesTreeController.cs index 985bad3f63..0753170d72 100644 --- a/src/Umbraco.Web.BackOffice/Trees/TemplatesTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/TemplatesTreeController.cs @@ -143,7 +143,7 @@ public class TemplatesTreeController : TreeController, ISearchableTree if (template.IsMasterTemplate == false) { //add delete option if it doesn't have children - menu.Items.Add(LocalizedTextService, hasSeparator: true, opensDialog: true); + menu.Items.Add(LocalizedTextService, hasSeparator: true, opensDialog: true, useLegacyIcon: false); } //add refresh diff --git a/src/Umbraco.Web.BackOffice/Umbraco.Web.BackOffice.csproj b/src/Umbraco.Web.BackOffice/Umbraco.Web.BackOffice.csproj index 450d146502..eea7d44c72 100644 --- a/src/Umbraco.Web.BackOffice/Umbraco.Web.BackOffice.csproj +++ b/src/Umbraco.Web.BackOffice/Umbraco.Web.BackOffice.csproj @@ -3,8 +3,6 @@ Umbraco.Cms.Web.BackOffice Umbraco CMS - Web - Backoffice Contains the backoffice assembly needed to run the backend of Umbraco CMS. - net7.0 - Library Umbraco.Cms.Web.BackOffice diff --git a/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj index b8e79d3b9d..f5a059d4ae 100644 --- a/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj +++ b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj @@ -3,7 +3,6 @@ Umbraco.Cms.Web.Common Umbraco CMS - Web Contains the web assembly needed to run Umbraco CMS. - net7.0 Umbraco.Cms.Web.Common diff --git a/src/Umbraco.Web.UI.Client/lib/bootstrap/less/tests/css-tests.html b/src/Umbraco.Web.UI.Client/lib/bootstrap/less/tests/css-tests.html index c69688c763..e3c3f725b7 100644 --- a/src/Umbraco.Web.UI.Client/lib/bootstrap/less/tests/css-tests.html +++ b/src/Umbraco.Web.UI.Client/lib/bootstrap/less/tests/css-tests.html @@ -940,7 +940,7 @@
- + diff --git a/src/Umbraco.Web.UI.Client/lib/bootstrap/less/tests/forms.html b/src/Umbraco.Web.UI.Client/lib/bootstrap/less/tests/forms.html index a63d728a00..f0f861e600 100644 --- a/src/Umbraco.Web.UI.Client/lib/bootstrap/less/tests/forms.html +++ b/src/Umbraco.Web.UI.Client/lib/bootstrap/less/tests/forms.html @@ -61,7 +61,7 @@
- +
diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbtabbedcontent.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbtabbedcontent.directive.js index 4eefa5176d..90f81bd827 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbtabbedcontent.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbtabbedcontent.directive.js @@ -190,8 +190,8 @@ } var contentLanguage = $scope.content.language; - - var otherCreatedVariants = $scope.contentNodeModel.variants.filter(x => x.compositeId !== $scope.content.compositeId && (x.state !== "NotCreated" || x.name !== null)).length === 0; + var variants = $scope.contentNodeModel && $scope.contentNodeModel.variants || []; + var otherCreatedVariants = variants.filter(x => x.compositeId !== $scope.content.compositeId && (x.state !== "NotCreated" || x.name !== null)).length === 0; var canEditCulture = !contentLanguage || // If the property culture equals the content culture it can be edited diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umblightbox.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umblightbox.directive.js index 0874c54cd2..268d62723e 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umblightbox.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umblightbox.directive.js @@ -77,7 +77,7 @@ (function() { 'use strict'; - function LightboxDirective() { + function LightboxDirective(focusLockService) { function link(scope, el, attr, ctrl) { @@ -88,6 +88,9 @@ el.appendTo("body"); + + focusLockService.addInertAttribute(); + // clean up scope.$on('$destroy', function() { // unbind watchers @@ -95,6 +98,9 @@ eventBindings[e](); } + focusLockService.removeInertAttribute(); + + document.getElementsByClassName("umb-lightbox__close")[0].blur(); el.remove(); }); } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbrangeslider.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbrangeslider.directive.js index 0c46ada020..405641a9ab 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbrangeslider.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbrangeslider.directive.js @@ -322,17 +322,13 @@ For extra details about options and events take a look here: https://refreshless }); } function setUpActivePipsHandling() { - let activePip = [null, null]; sliderInstance.noUiSlider.on('update', function (values,handle) { - if(activePip[handle]){ - activePip[handle].classList.remove("noUi-value-active"); - } sliderInstance.querySelectorAll('.noUi-value').forEach(pip => { + pip.classList.remove("noUi-value-active"); if (Number(values[handle]) === Number(pip.getAttribute('data-value'))) { - activePip[handle] = pip; + pip.classList.add("noUi-value-active"); } }); - activePip[handle].classList.add("noUi-value-active"); }); } function addPipClickHandler(){ diff --git a/src/Umbraco.Web.UI.Client/src/init.js b/src/Umbraco.Web.UI.Client/src/init.js index 1f8a29ee2f..918f3377ae 100644 --- a/src/Umbraco.Web.UI.Client/src/init.js +++ b/src/Umbraco.Web.UI.Client/src/init.js @@ -119,8 +119,15 @@ app.run(['$rootScope', '$route', '$location', '$cookies', 'urlHelper', 'appState event.preventDefault(); var returnPath = null; if (rejection.path == "/login" || rejection.path.startsWith("/login/")) { + // Check if a ReturnUrl is present on the querystring and redirect to it if set + var queryStrings = urlHelper.getQueryStringParams(); + if (typeof queryStrings.ReturnUrl !== 'undefined' && queryStrings.ReturnUrl.length > 0) { + returnPath = queryStrings.ReturnUrl; + } + else { //Set the current path before redirecting so we know where to redirect back to - returnPath = encodeURIComponent(window.location.href.replace(window.location.origin,'')); + returnPath = encodeURIComponent(window.location.href.replace(window.location.origin, '')); + } } $location.path(rejection.path) if (returnPath) { diff --git a/src/Umbraco.Web.UI.Client/src/installer/steps/database.html b/src/Umbraco.Web.UI.Client/src/installer/steps/database.html index e248f3221b..e75a82f9df 100644 --- a/src/Umbraco.Web.UI.Client/src/installer/steps/database.html +++ b/src/Umbraco.Web.UI.Client/src/installer/steps/database.html @@ -135,6 +135,7 @@ placeholder="umbraco-db-password" required ng-model="installer.current.model.password" + spellcheck="false" /> Enter the database password
diff --git a/src/Umbraco.Web.UI.Client/src/installer/steps/user.controller.js b/src/Umbraco.Web.UI.Client/src/installer/steps/user.controller.js index e65499ba6b..4726a53aaf 100644 --- a/src/Umbraco.Web.UI.Client/src/installer/steps/user.controller.js +++ b/src/Umbraco.Web.UI.Client/src/installer/steps/user.controller.js @@ -54,17 +54,13 @@ angular.module("umbraco.install").controller("Umbraco.Install.UserController", f const pips = consentSlider.querySelectorAll('.noUi-value'); - let activePip = [null, null]; consentSlider.noUiSlider.on('update', function (values,handle) { - if(activePip[handle]){ - activePip[handle].classList.remove("noUi-value-active"); - } consentSlider.querySelectorAll('.noUi-value').forEach(pip => { + pip.classList.remove("noUi-value-active"); if (Number(values[handle]) === Number(pip.getAttribute('data-value'))) { - activePip[handle] = pip; + pip.classList.add("noUi-value-active"); } }); - activePip[handle].classList.add("noUi-value-active"); }); $(consentSlider).on('$destroy', function () { diff --git a/src/Umbraco.Web.UI.Client/src/installer/steps/user.html b/src/Umbraco.Web.UI.Client/src/installer/steps/user.html index c0b254f80a..7e37c57328 100644 --- a/src/Umbraco.Web.UI.Client/src/installer/steps/user.html +++ b/src/Umbraco.Web.UI.Client/src/installer/steps/user.html @@ -61,6 +61,7 @@ required ng-model="installer.current.model.password" id="password" + spellcheck="false" /> At least {{installer.current.model.minCharLength}} characters diff --git a/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree-root.less b/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree-root.less index 83f1cd8d36..b0a230b4fb 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree-root.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree-root.less @@ -10,10 +10,14 @@ height: @editorHeaderHeight; } + h1, h5 { + margin: 0; + flex: 1; + } + h1 { font-size: @baseFontSize; font-weight: 700; - margin: 0; width: 100%; display: flex; } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-property-editor.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-property-editor.less index b2a5a055e1..6e16e76295 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-property-editor.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-property-editor.less @@ -1,6 +1,6 @@ .umb-property-editor { position: relative; - contain: layout; + contain: style; } .umb-property-editor--preview { @@ -63,4 +63,4 @@ margin-left: auto; margin-right: auto; } -} \ No newline at end of file +} diff --git a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-login.html b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-login.html index e466cfbca5..b53e29c365 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-login.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-login.html @@ -22,7 +22,7 @@ New password {{vm.invitedUserPasswordModel.passwordPolicyText}} - + Your new password cannot be blank! Minimum {{vm.invitedUserPasswordModel.passwordPolicies.minPasswordLength}} characters @@ -32,7 +32,7 @@
- + Required The confirmed password doesn't match the new password! @@ -156,7 +156,7 @@
- +