Merge branch 'v14/dev' into contrib

This commit is contained in:
Sebastiaan Janssen
2024-09-27 08:54:49 +02:00
147 changed files with 6317 additions and 1436 deletions

View File

@@ -29,7 +29,7 @@
<!-- Package Validation -->
<PropertyGroup>
<GenerateCompatibilitySuppressionFile>true</GenerateCompatibilitySuppressionFile>
<GenerateCompatibilitySuppressionFile>false</GenerateCompatibilitySuppressionFile>
<EnablePackageValidation>true</EnablePackageValidation>
<PackageValidationBaselineVersion>14.0.0</PackageValidationBaselineVersion>
<EnableStrictModeForCompatibleFrameworksInPackage>true</EnableStrictModeForCompatibleFrameworksInPackage>

View File

@@ -12,24 +12,24 @@
</ItemGroup>
<!-- Microsoft packages -->
<ItemGroup>
<PackageVersion Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.7" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="8.0.7" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.8" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="8.0.8" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.10.0" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="8.0.7" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.7" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.7" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.7" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="8.0.8" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.8" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.8" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.8" />
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.FileProviders.Embedded" Version="8.0.7" />
<PackageVersion Include="Microsoft.Extensions.FileProviders.Embedded" Version="8.0.8" />
<PackageVersion Include="Microsoft.Extensions.FileProviders.Physical" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Identity.Core" Version="8.0.7" />
<PackageVersion Include="Microsoft.Extensions.Identity.Stores" Version="8.0.7" />
<PackageVersion Include="Microsoft.Extensions.Identity.Core" Version="8.0.8" />
<PackageVersion Include="Microsoft.Extensions.Identity.Stores" Version="8.0.8" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="8.0.2" />
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0" />
@@ -47,7 +47,7 @@
<PackageVersion Include="Dazinator.Extensions.FileProviders" Version="2.0.0" />
<PackageVersion Include="Examine" Version="3.3.0" />
<PackageVersion Include="Examine.Core" Version="3.3.0" />
<PackageVersion Include="HtmlAgilityPack" Version="1.11.62" />
<PackageVersion Include="HtmlAgilityPack" Version="1.11.65" />
<PackageVersion Include="JsonPatch.Net" Version="3.1.1" />
<PackageVersion Include="K4os.Compression.LZ4" Version="1.3.8" />
<PackageVersion Include="MailKit" Version="4.7.1.1" />
@@ -75,7 +75,7 @@
<PackageVersion Include="Serilog.Sinks.Map" Version="1.0.2" />
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.5" />
<PackageVersion Include="SixLabors.ImageSharp.Web" Version="3.1.3" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.7.0" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.7.3" />
</ItemGroup>
<!-- Transitive pinned versions (only required because our direct dependencies have vulnerable versions of transitive dependencies) -->
<ItemGroup>
@@ -87,5 +87,7 @@
<PackageVersion Include="System.Security.Cryptography.Xml" Version="8.0.1" />
<!-- Both Dazinator.Extensions.FileProviders and MiniProfiler.AspNetCore.Mvc bring in a vulnerable version of System.Text.RegularExpressions -->
<PackageVersion Include="System.Text.RegularExpressions" Version="4.3.1" />
<!-- Both OpenIddict.AspNetCore, Npoco.SqlServer and Microsoft.EntityFrameworkCore.SqlServer bring in a vulnerable version of Microsoft.IdentityModel.JsonWebTokens -->
<PackageVersion Include="Microsoft.IdentityModel.JsonWebTokens" Version="7.7.1" />
</ItemGroup>
</Project>

View File

@@ -5,10 +5,6 @@ parameters:
displayName: Run SQL Server Integration Tests
type: boolean
default: false
- name: sqlServerAcceptanceTests
displayName: Run SQL Server Acceptance Tests
type: boolean
default: false
- name: myGetDeploy
displayName: Deploy to MyGet
type: boolean
@@ -517,10 +513,7 @@ stages:
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
# Test
- ${{ if eq(parameters.isNightly, true) }}:
pwsh: npm run test --ignore-certificate-errors
${{ else }}:
pwsh: npm run smokeTest --ignore-certificate-errors
- pwsh: npm run smokeTest --ignore-certificate-errors
displayName: Run Playwright tests
continueOnError: true
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
@@ -556,8 +549,6 @@ stages:
- job:
displayName: E2E Tests (SQL Server)
# condition: or(eq(stageDependencies.Build.A.outputs['build.NBGV_PublicRelease'], 'True'), ${{parameters.sqlServerAcceptanceTests}}) # Outcommented due to timeouts
condition: eq(${{parameters.sqlServerAcceptanceTests}}, True)
variables:
# Connection string
CONNECTIONSTRINGS__UMBRACODBDSN: Data Source=(localdb)\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\Umbraco.mdf;Integrated Security=True
@@ -593,7 +584,8 @@ stages:
- pwsh: |
"UMBRACO_USER_LOGIN=$(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSEREMAIL)
UMBRACO_USER_PASSWORD=$(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD)
URL=$(ASPNETCORE_URLS)" | Out-File .env
URL=$(ASPNETCORE_URLS)
STORAGE_STAGE_PATH=$(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/playwright/.auth/user.json" | Out-File .env
displayName: Generate .env
workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest
@@ -660,10 +652,7 @@ stages:
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
# Test
- ${{ if eq(parameters.isNightly, true) }}:
pwsh: npm run test --ignore-certificate-errors
${{ else }}:
pwsh: npm run smokeTest --ignore-certificate-errors
- pwsh: npm run smokeTest --ignore-certificate-errors
displayName: Run Playwright tests
continueOnError: true
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
@@ -714,7 +703,7 @@ stages:
dependsOn:
- Unit
- Integration
# - E2E
# - E2E
condition: and(succeeded(), or(eq(dependencies.Build.outputs['A.build.NBGV_PublicRelease'], 'True'), ${{parameters.myGetDeploy}}))
jobs:
- job:

View File

@@ -0,0 +1,400 @@
name: Nightly_E2E_Test_$(TeamProject)_$(Build.DefinitionName)_$(SourceBranchName)_$(Date:yyyyMMdd)$(Rev:.r)
pr: none
trigger: none
schedules:
- cron: '0 0 * * *'
displayName: Daily midnight build
branches:
include:
- v14/dev
- v15/dev
variables:
nodeVersion: 20
solution: umbraco.sln
buildConfiguration: Release
UMBRACO__CMS__GLOBAL__ID: 00000000-0000-0000-0000-000000000042
DOTNET_NOLOGO: true
DOTNET_GENERATE_ASPNET_CERTIFICATE: false
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
DOTNET_CLI_TELEMETRY_OPTOUT: true
npm_config_cache: $(Pipeline.Workspace)/.npm_client
NODE_OPTIONS: --max_old_space_size=16384
parameters:
- name: runSmokeTests
displayName: Run the smoke tests
type: boolean
default: false
stages:
###############################################
## Build
###############################################
- stage: Build
jobs:
- job: A
displayName: Build Umbraco CMS
pool:
vmImage: 'ubuntu-latest'
steps:
- checkout: self
fetchDepth: 0
submodules: true
- task: UseDotNet@2
displayName: Use .NET SDK from global.json
inputs:
useGlobalJson: true
- template: templates/backoffice-install.yml
- script: npm run build:for:cms
displayName: Run build (Bellissima)
workingDirectory: src/Umbraco.Web.UI.Client
- script: npm ci --no-fund --no-audit --prefer-offline
displayName: Run npm ci (Login)
workingDirectory: src/Umbraco.Web.UI.Login
- script: npm run build
displayName: Run npm build (Login)
workingDirectory: src/Umbraco.Web.UI.Login
- task: DotNetCoreCLI@2
displayName: Run dotnet restore
inputs:
command: restore
projects: $(solution)
- task: DotNetCoreCLI@2
name: build
displayName: Run dotnet build and generate NuGet packages
inputs:
command: build
projects: $(solution)
arguments: '--configuration $(buildConfiguration) --no-restore --property:ContinuousIntegrationBuild=true --property:GeneratePackageOnBuild=true --property:PackageOutputPath=$(Build.ArtifactStagingDirectory)/nupkg'
- task: PublishPipelineArtifact@1
displayName: Publish nupkg
inputs:
targetPath: $(Build.ArtifactStagingDirectory)/nupkg
artifactName: nupkg
- task: PublishPipelineArtifact@1
displayName: Publish build artifacts
inputs:
targetPath: $(Build.SourcesDirectory)
artifactName: build_output
- stage: E2E
displayName: E2E Tests
dependsOn: Build
variables:
npm_config_cache: $(Pipeline.Workspace)/.npm_e2e
# Enable console logging in Release mode
SERILOG__WRITETO__0__NAME: Async
SERILOG__WRITETO__0__ARGS__CONFIGURE__0__NAME: Console
# Set unattended install settings
UMBRACO__CMS__UNATTENDED__INSTALLUNATTENDED: true
UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERNAME: Playwright Test
UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD: UmbracoAcceptance123!
UMBRACO__CMS__UNATTENDED__UNATTENDEDUSEREMAIL: playwright@umbraco.com
# Custom Umbraco settings
UMBRACO__CMS__CONTENT__CONTENTVERSIONCLEANUPPOLICY__ENABLECLEANUP: false
UMBRACO__CMS__GLOBAL__DISABLEELECTIONFORSINGLESERVER: true
UMBRACO__CMS__GLOBAL__INSTALLMISSINGDATABASE: true
UMBRACO__CMS__GLOBAL__ID: 00000000-0000-0000-0000-000000000042
UMBRACO__CMS__GLOBAL__VERSIONCHECKPERIOD: 0
UMBRACO__CMS__GLOBAL__USEHTTPS: true
UMBRACO__CMS__HEALTHCHECKS__NOTIFICATION__ENABLED: false
UMBRACO__CMS__KEEPALIVE__DISABLEKEEPALIVETASK: true
UMBRACO__CMS__WEBROUTING__UMBRACOAPPLICATIONURL: https://localhost:44331/
ASPNETCORE_URLS: https://localhost:44331
jobs:
# E2E Tests
- job:
displayName: E2E Tests (SQLite)
timeoutInMinutes: 180
variables:
# Connection string
CONNECTIONSTRINGS__UMBRACODBDSN: Data Source=Umbraco;Mode=Memory;Cache=Shared;Foreign Keys=True;Pooling=True
CONNECTIONSTRINGS__UMBRACODBDSN_PROVIDERNAME: Microsoft.Data.Sqlite
strategy:
matrix:
Linux:
vmImage: 'ubuntu-latest'
Windows:
vmImage: 'windows-latest'
pool:
vmImage: $(vmImage)
steps:
# Setup test environment
- task: DownloadPipelineArtifact@2
displayName: Download NuGet artifacts
inputs:
artifact: nupkg
path: $(Agent.BuildDirectory)/app/nupkg
- task: NodeTool@0
displayName: Use Node.js $(nodeVersion)
retryCountOnTaskFailure: 3
inputs:
versionSpec: $(nodeVersion)
- task: UseDotNet@2
displayName: Use .NET SDK from global.json
inputs:
useGlobalJson: true
- pwsh: |
"UMBRACO_USER_LOGIN=$(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSEREMAIL)
UMBRACO_USER_PASSWORD=$(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD)
URL=$(ASPNETCORE_URLS)
STORAGE_STAGE_PATH=$(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/playwright/.auth/user.json" | Out-File .env
displayName: Generate .env
workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest
# Cache and restore NPM packages
- task: Cache@2
displayName: Cache NPM packages
inputs:
key: 'npm_e2e | "$(Agent.OS)" | $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/package-lock.json'
restoreKeys: |
npm_e2e | "$(Agent.OS)"
npm_e2e
path: $(npm_config_cache)
- script: npm ci --no-fund --no-audit --prefer-offline
workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest
displayName: Restore NPM packages
# Build application
- pwsh: |
$cmsVersion = "$(Build.BuildNumber)" -replace "\+",".g"
dotnet new nugetconfig
dotnet nuget add source ./nupkg --name Local
dotnet new install Umbraco.Templates::$cmsVersion
dotnet new umbraco --name UmbracoProject --version $cmsVersion --exclude-gitignore --no-restore --no-update-check
dotnet restore UmbracoProject
cp $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest.UmbracoProject/*.cs UmbracoProject
dotnet build UmbracoProject --configuration $(buildConfiguration) --no-restore
dotnet dev-certs https
displayName: Build application
workingDirectory: $(Agent.BuildDirectory)/app
# Run application
- bash: |
nohup dotnet run --project UmbracoProject --configuration $(buildConfiguration) --no-build --no-launch-profile > $(Build.ArtifactStagingDirectory)/playwright.log 2>&1 &
echo "##vso[task.setvariable variable=AcceptanceTestProcessId]$!"
displayName: Run application (Linux)
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
workingDirectory: $(Agent.BuildDirectory)/app
- pwsh: |
$process = Start-Process dotnet "run --project UmbracoProject --configuration $(buildConfiguration) --no-build --no-launch-profile 2>&1" -PassThru -NoNewWindow -RedirectStandardOutput $(Build.ArtifactStagingDirectory)/playwright.log
Write-Host "##vso[task.setvariable variable=AcceptanceTestProcessId]$($process.Id)"
displayName: Run application (Windows)
condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT'))
workingDirectory: $(Agent.BuildDirectory)/app
# Ensures we have the package wait-on installed
- pwsh: npm install wait-on
displayName: Install wait-on package
# Wait for application to start responding to requests
- pwsh: npx wait-on -v --interval 1000 --timeout 120000 $(ASPNETCORE_URLS)
displayName: Wait for application
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
# Install Playwright and dependencies
- pwsh: npx playwright install --with-deps
displayName: Install Playwright
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
# Test
- ${{ if eq(parameters.runSmokeTests, true) }}:
pwsh: npm run smokeTest --ignore-certificate-errors
${{ else }}:
pwsh: npm run test --ignore-certificate-errors
displayName: Run Playwright tests
continueOnError: true
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
env:
CI: true
CommitId: $(Build.SourceVersion)
AgentOs: $(Agent.OS)
# Stop application
- bash: kill -15 $(AcceptanceTestProcessId)
displayName: Stop application (Linux)
condition: and(succeeded(), ne(variables.AcceptanceTestProcessId, ''), eq(variables['Agent.OS'], 'Linux'))
- pwsh: Stop-Process -Id $(AcceptanceTestProcessId)
displayName: Stop application (Windows)
condition: and(succeeded(), ne(variables.AcceptanceTestProcessId, ''), eq(variables['Agent.OS'], 'Windows_NT'))
# Copy artifacts
- pwsh: |
if (Test-Path tests/Umbraco.Tests.AcceptanceTest/results/*) {
Copy-Item tests/Umbraco.Tests.AcceptanceTest/results $(Build.ArtifactStagingDirectory) -Recurse
}
displayName: Copy Playwright results
condition: succeededOrFailed()
# Publish
- task: PublishPipelineArtifact@1
displayName: Publish test artifacts
condition: succeededOrFailed()
inputs:
targetPath: $(Build.ArtifactStagingDirectory)
artifact: 'Acceptance Tests - $(Agent.JobName) - Attempt #$(System.JobAttempt)'
- job:
displayName: E2E Tests (SQL Server)
timeoutInMinutes: 180
variables:
# Connection string
CONNECTIONSTRINGS__UMBRACODBDSN: Data Source=(localdb)\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\Umbraco.mdf;Integrated Security=True
CONNECTIONSTRINGS__UMBRACODBDSN_PROVIDERNAME: Microsoft.Data.SqlClient
strategy:
matrix:
Linux:
vmImage: 'ubuntu-latest'
SA_PASSWORD: $(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD)
CONNECTIONSTRINGS__UMBRACODBDSN: 'Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);TrustServerCertificate=True'
Windows:
vmImage: 'windows-latest'
pool:
vmImage: $(vmImage)
steps:
# Setup test environment
- task: DownloadPipelineArtifact@2
displayName: Download NuGet artifacts
inputs:
artifact: nupkg
path: $(Agent.BuildDirectory)/app/nupkg
- task: NodeTool@0
displayName: Use Node.js $(nodeVersion)
inputs:
versionSpec: $(nodeVersion)
- task: UseDotNet@2
displayName: Use .NET SDK from global.json
inputs:
useGlobalJson: true
- pwsh: |
"UMBRACO_USER_LOGIN=$(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSEREMAIL)
UMBRACO_USER_PASSWORD=$(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD)
URL=$(ASPNETCORE_URLS)
STORAGE_STAGE_PATH=$(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/playwright/.auth/user.json" | Out-File .env
displayName: Generate .env
workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest
# Cache and restore NPM packages
- task: Cache@2
displayName: Cache NPM packages
inputs:
key: 'npm_e2e | "$(Agent.OS)" | $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/package-lock.json'
restoreKeys: |
npm_e2e | "$(Agent.OS)"
npm_e2e
path: $(npm_config_cache)
- script: npm ci --no-fund --no-audit --prefer-offline
workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest
displayName: Restore NPM packages
# Build application
- pwsh: |
$cmsVersion = "$(Build.BuildNumber)" -replace "\+",".g"
dotnet new nugetconfig
dotnet nuget add source ./nupkg --name Local
dotnet new install Umbraco.Templates::$cmsVersion
dotnet new umbraco --name UmbracoProject --version $cmsVersion --exclude-gitignore --no-restore --no-update-check
dotnet restore UmbracoProject
cp $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest.UmbracoProject/*.cs UmbracoProject
dotnet build UmbracoProject --configuration $(buildConfiguration) --no-restore
dotnet dev-certs https
displayName: Build application
workingDirectory: $(Agent.BuildDirectory)/app
# Start SQL Server
- powershell: docker run --name mssql -d -p 1433:1433 -e "ACCEPT_EULA=Y" -e "MSSQL_SA_PASSWORD=$(SA_PASSWORD)" mcr.microsoft.com/mssql/server:2022-latest
displayName: Start SQL Server Docker image (Linux)
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
- pwsh: SqlLocalDB start MSSQLLocalDB
displayName: Start SQL Server LocalDB (Windows)
condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT'))
# Run application
- bash: |
nohup dotnet run --project UmbracoProject --configuration $(buildConfiguration) --no-build --no-launch-profile > $(Build.ArtifactStagingDirectory)/playwright.log 2>&1 &
echo "##vso[task.setvariable variable=AcceptanceTestProcessId]$!"
displayName: Run application (Linux)
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
workingDirectory: $(Agent.BuildDirectory)/app
- pwsh: |
$process = Start-Process dotnet "run --project UmbracoProject --configuration $(buildConfiguration) --no-build --no-launch-profile 2>&1" -PassThru -NoNewWindow -RedirectStandardOutput $(Build.ArtifactStagingDirectory)/playwright.log
Write-Host "##vso[task.setvariable variable=AcceptanceTestProcessId]$($process.Id)"
displayName: Run application (Windows)
condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT'))
workingDirectory: $(Agent.BuildDirectory)/app
# Ensures we have the package wait-on installed
- pwsh: npm install wait-on
displayName: Install wait-on package
# Wait for application to start responding to requests
- pwsh: npx wait-on -v --interval 1000 --timeout 120000 $(ASPNETCORE_URLS)
displayName: Wait for application
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
# Install Playwright and dependencies
- pwsh: npx playwright install --with-deps
displayName: Install Playwright
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
# Test
- ${{ if eq(parameters.runSmokeTests, true) }}:
pwsh: npm run smokeTest --ignore-certificate-errors
${{ else }}:
pwsh: npm run test --ignore-certificate-errors
displayName: Run Playwright tests
continueOnError: true
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
env:
CI: true
CommitId: $(Build.SourceVersion)
AgentOs: $(Agent.OS)
# Stop application
- bash: kill -15 $(AcceptanceTestProcessId)
displayName: Stop application (Linux)
condition: and(succeeded(), ne(variables.AcceptanceTestProcessId, ''), eq(variables['Agent.OS'], 'Linux'))
- pwsh: Stop-Process -Id $(AcceptanceTestProcessId)
displayName: Stop application (Windows)
condition: and(succeeded(), ne(variables.AcceptanceTestProcessId, ''), eq(variables['Agent.OS'], 'Windows_NT'))
# Stop SQL Server
- pwsh: docker stop mssql
displayName: Stop SQL Server Docker image (Linux)
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
- pwsh: SqlLocalDB stop MSSQLLocalDB
displayName: Stop SQL Server LocalDB (Windows)
condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT'))
# Copy artifacts
- pwsh: |
if (Test-Path tests/Umbraco.Tests.AcceptanceTest/results/*) {
Copy-Item tests/Umbraco.Tests.AcceptanceTest/results $(Build.ArtifactStagingDirectory) -Recurse
}
displayName: Copy Playwright results
condition: succeededOrFailed()
# Publish
- task: PublishPipelineArtifact@1
displayName: Publish test artifacts
condition: succeededOrFailed()
inputs:
targetPath: $(Build.ArtifactStagingDirectory)
artifact: 'Acceptance Tests - $(Agent.JobName) - Attempt #$(System.JobAttempt)'

View File

@@ -26,7 +26,7 @@ steps:
useSameBranch: true
waitForQueuedBuildsToFinish: false
storeInEnvironmentVariable: false
templateParameters: 'sqlServerIntegrationTests: true, sqlServerAcceptanceTests: true, forceReleaseTestFilter: true, myGetDeploy: true, isNightly: true'
templateParameters: 'sqlServerIntegrationTests: true, forceReleaseTestFilter: true, myGetDeploy: true, isNightly: true'
authenticationMethod: 'OAuth Token'
enableBuildInQueueCondition: false
dependentOnSuccessfulBuildCondition: false

View File

@@ -13,6 +13,9 @@
<PackageReference Include="Swashbuckle.AspNetCore" />
<PackageReference Include="OpenIddict.Abstractions" />
<PackageReference Include="OpenIddict.AspNetCore" />
<!-- Both OpenIddict.AspNetCore, Npoco.SqlServer and Microsoft.EntityFrameworkCore.SqlServer bring in a vulnerable version of Microsoft.IdentityModel.JsonWebTokens -->
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens"/>
</ItemGroup>
<ItemGroup>

View File

@@ -1,5 +1,7 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.Content;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models.ContentEditing;
using Umbraco.Cms.Core.Models.ContentEditing.Validation;
using Umbraco.Cms.Core.Services.OperationStatus;
@@ -9,6 +11,7 @@ namespace Umbraco.Cms.Api.Management.Controllers.Content;
public abstract class ContentControllerBase : ManagementApiControllerBase
{
protected IActionResult ContentEditingOperationStatusResult(ContentEditingOperationStatus status)
=> OperationStatusResult(status, problemDetailsBuilder => status switch
{
@@ -96,7 +99,8 @@ public abstract class ContentControllerBase : ManagementApiControllerBase
}
var errors = new SortedDictionary<string, string[]>();
var missingPropertyAliases = new List<string>();
var missingPropertyModels = new List<PropertyValidationResponseModel>();
foreach (PropertyValidationError validationError in validationResult.ValidationErrors)
{
TValueModel? requestValue = requestModel.Values.FirstOrDefault(value =>
@@ -105,7 +109,7 @@ public abstract class ContentControllerBase : ManagementApiControllerBase
&& value.Segment == validationError.Segment);
if (requestValue is null)
{
missingPropertyAliases.Add(validationError.Alias);
missingPropertyModels.Add(MapMissingProperty(validationError));
continue;
}
@@ -119,7 +123,16 @@ public abstract class ContentControllerBase : ManagementApiControllerBase
.WithTitle("Validation failed")
.WithDetail("One or more properties did not pass validation")
.WithRequestModelErrors(errors)
.WithExtension("missingProperties", missingPropertyAliases.ToArray())
.WithExtension("missingValues", missingPropertyModels.ToArray())
.Build()));
}
private PropertyValidationResponseModel MapMissingProperty(PropertyValidationError source) =>
new()
{
Alias = source.Alias,
Segment = source.Segment,
Culture = source.Culture,
Messages = source.ErrorMessages,
};
}

View File

@@ -18,18 +18,21 @@ public abstract class CreateDocumentControllerBase : DocumentControllerBase
protected async Task<IActionResult> HandleRequest(CreateDocumentRequestModel requestModel, Func<Task<IActionResult>> authorizedHandler)
{
IEnumerable<string> cultures = requestModel.Variants
.Where(v => v.Culture is not null)
.Select(v => v.Culture!);
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
User,
ContentPermissionResource.WithKeys(ActionNew.ActionLetter, requestModel.Parent?.Id, cultures),
AuthorizationPolicies.ContentPermissionByResource);
// TODO This have temporarily been uncommented, to support the client sends values from all cultures, even when the user do not have access to the languages.
// The values are ignored in the ContentEditingService
if (!authorizationResult.Succeeded)
{
return Forbidden();
}
// IEnumerable<string> cultures = requestModel.Variants
// .Where(v => v.Culture is not null)
// .Select(v => v.Culture!);
// AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
// User,
// ContentPermissionResource.WithKeys(ActionNew.ActionLetter, requestModel.Parent?.Id, cultures),
// AuthorizationPolicies.ContentPermissionByResource);
//
// if (!authorizationResult.Succeeded)
// {
// return Forbidden();
// }
return await authorizedHandler();
}

View File

@@ -21,16 +21,21 @@ public class SearchDocumentItemController : DocumentItemControllerBase
_documentPresentationFactory = documentPresentationFactory;
}
[NonAction]
[Obsolete("Scheduled to be removed in v16, use the non obsoleted method instead")]
public async Task<IActionResult> Search(CancellationToken cancellationToken, string query, int skip = 0, int take = 100)
=> await SearchFromParent(cancellationToken, query, skip, take);
[HttpGet("search")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedModel<DocumentItemResponseModel>), StatusCodes.Status200OK)]
public async Task<IActionResult> Search(CancellationToken cancellationToken, string query, int skip = 0, int take = 100)
public async Task<IActionResult> SearchFromParent(CancellationToken cancellationToken, string query, int skip = 0, int take = 100, Guid? parentId = null)
{
PagedModel<IEntitySlim> searchResult = _indexedEntitySearchService.Search(UmbracoObjectTypes.Document, query, skip, take);
PagedModel<IEntitySlim> searchResult = _indexedEntitySearchService.Search(UmbracoObjectTypes.Document, query, parentId, skip, take);
var result = new PagedModel<DocumentItemResponseModel>
{
Items = searchResult.Items.OfType<IDocumentEntitySlim>().Select(_documentPresentationFactory.CreateItemResponseModel),
Total = searchResult.Total
Total = searchResult.Total,
};
return await Task.FromResult(Ok(result));

View File

@@ -17,18 +17,21 @@ public abstract class UpdateDocumentControllerBase : DocumentControllerBase
protected async Task<IActionResult> HandleRequest(Guid id, UpdateDocumentRequestModel requestModel, Func<Task<IActionResult>> authorizedHandler)
{
IEnumerable<string> cultures = requestModel.Variants
.Where(v => v.Culture is not null)
.Select(v => v.Culture!);
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
User,
ContentPermissionResource.WithKeys(ActionUpdate.ActionLetter, id, cultures),
AuthorizationPolicies.ContentPermissionByResource);
// TODO This have temporarily been uncommented, to support the client sends values from all cultures, even when the user do not have access to the languages.
// The values are ignored in the ContentEditingService
if (!authorizationResult.Succeeded)
{
return Forbidden();
}
// IEnumerable<string> cultures = requestModel.Variants
// .Where(v => v.Culture is not null)
// .Select(v => v.Culture!);
// AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
// User,
// ContentPermissionResource.WithKeys(ActionUpdate.ActionLetter, id, cultures),
// AuthorizationPolicies.ContentPermissionByResource);
//
// if (!authorizationResult.Succeeded)
// {
// return Forbidden();
// }
return await authorizedHandler();
}

View File

@@ -21,16 +21,21 @@ public class SearchMediaItemController : MediaItemControllerBase
_mediaPresentationFactory = mediaPresentationFactory;
}
[NonAction]
[Obsolete("Scheduled to be removed in v16, use the non obsoleted method instead")]
public async Task<IActionResult> Search(CancellationToken cancellationToken, string query, int skip = 0, int take = 100)
=> await SearchFromParent(cancellationToken, query, skip, take, null);
[HttpGet("search")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedModel<MediaItemResponseModel>), StatusCodes.Status200OK)]
public async Task<IActionResult> Search(CancellationToken cancellationToken, string query, int skip = 0, int take = 100)
public async Task<IActionResult> SearchFromParent(CancellationToken cancellationToken, string query, int skip = 0, int take = 100, Guid? parentId = null)
{
PagedModel<IEntitySlim> searchResult = _indexedEntitySearchService.Search(UmbracoObjectTypes.Media, query, skip, take);
PagedModel<IEntitySlim> searchResult = _indexedEntitySearchService.Search(UmbracoObjectTypes.Media, query, parentId, skip, take);
var result = new PagedModel<MediaItemResponseModel>
{
Items = searchResult.Items.OfType<IMediaEntitySlim>().Select(_mediaPresentationFactory.CreateItemResponseModel),
Total = searchResult.Total
Total = searchResult.Total,
};
return await Task.FromResult(Ok(result));

View File

@@ -1,19 +1,37 @@
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Services;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Management.Controllers.Security;
public class BackOfficeDefaultController : Controller
{
private readonly IRuntime _umbracoRuntime;
[ActivatorUtilitiesConstructor]
public BackOfficeDefaultController(IRuntime umbracoRuntime)
=> _umbracoRuntime = umbracoRuntime;
[Obsolete("Use the non obsoleted constructor instead. Scheduled to be removed in v17")]
public BackOfficeDefaultController()
: this(StaticServiceProvider.Instance.GetRequiredService<IRuntime>())
{
}
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> Index(CancellationToken cancellationToken)
{
// force authentication to occur since this is not an authorized endpoint
AuthenticateResult result = await this.AuthenticateBackOfficeAsync();
// a user can not be authenticated if no users have been created yet, or the user repository is unavailable
AuthenticateResult result = _umbracoRuntime.State.Level < RuntimeLevel.Upgrade
? AuthenticateResult.Fail("RuntimeLevel " + _umbracoRuntime.State.Level + " does not support authentication")
: await this.AuthenticateBackOfficeAsync();
// if we are not authenticated then we need to redirect to the login page
if (!result.Succeeded)

View File

@@ -1,9 +1,11 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Api.Management.ViewModels.Server;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.DependencyInjection;
namespace Umbraco.Cms.Api.Management.Controllers.Server;
@@ -11,8 +13,20 @@ namespace Umbraco.Cms.Api.Management.Controllers.Server;
public class ConfigurationServerController : ServerControllerBase
{
private readonly SecuritySettings _securitySettings;
private readonly GlobalSettings _globalSettings;
public ConfigurationServerController(IOptions<SecuritySettings> securitySettings) => _securitySettings = securitySettings.Value;
[Obsolete("Use the constructor that accepts all arguments. Will be removed in V16.")]
public ConfigurationServerController(IOptions<SecuritySettings> securitySettings)
: this(securitySettings, StaticServiceProvider.Instance.GetRequiredService<IOptions<GlobalSettings>>())
{
}
[ActivatorUtilitiesConstructor]
public ConfigurationServerController(IOptions<SecuritySettings> securitySettings, IOptions<GlobalSettings> globalSettings)
{
_securitySettings = securitySettings.Value;
_globalSettings = globalSettings.Value;
}
[HttpGet("configuration")]
[MapToApiVersion("1.0")]
@@ -22,6 +36,7 @@ public class ConfigurationServerController : ServerControllerBase
var responseModel = new ServerConfigurationResponseModel
{
AllowPasswordReset = _securitySettings.AllowPasswordReset,
VersionCheckPeriod = _globalSettings.VersionCheckPeriod
};
return Task.FromResult<IActionResult>(Ok(responseModel));

View File

@@ -0,0 +1,45 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.Server;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Web.Common.Authorization;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Management.Controllers.Server;
[ApiVersion("1.0")]
[Authorize(Policy = AuthorizationPolicies.RequireAdminAccess)]
public class UpgradeCheckServerController : ServerControllerBase
{
private readonly IUpgradeService _upgradeService;
private readonly IUmbracoVersion _umbracoVersion;
public UpgradeCheckServerController(IUpgradeService upgradeService, IUmbracoVersion umbracoVersion)
{
_upgradeService = upgradeService;
_umbracoVersion = umbracoVersion;
}
[HttpGet("upgrade-check")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(UpgradeCheckResponseModel), StatusCodes.Status200OK)]
public async Task<IActionResult> UpgradeCheck(CancellationToken cancellationToken)
{
UpgradeResult upgradeResult = await _upgradeService.CheckUpgrade(_umbracoVersion.SemanticVersion);
var responseModel = new UpgradeCheckResponseModel
{
Type = upgradeResult.UpgradeType,
Comment = upgradeResult.Comment,
Url = upgradeResult.UpgradeUrl.IsNullOrWhiteSpace()
? string.Empty
: $"{upgradeResult.UpgradeUrl}?version={_umbracoVersion.Version.ToString(3)}"
};
return Ok(responseModel);
}
}

View File

@@ -33,6 +33,7 @@ internal static class ApplicationBuilderExtensions
{
innerBuilder.UseExceptionHandler(exceptionBuilder => exceptionBuilder.Run(async context =>
{
var isDebug = context.RequestServices.GetRequiredService<IHostingEnvironment>().IsDebugMode;
Exception? exception = context.Features.Get<IExceptionHandlerPathFeature>()?.Error;
if (exception is null)
{
@@ -42,16 +43,16 @@ internal static class ApplicationBuilderExtensions
var response = new ProblemDetails
{
Title = exception.Message,
Detail = exception.StackTrace,
Detail = isDebug ? exception.StackTrace : null,
Status = StatusCodes.Status500InternalServerError,
Instance = exception.GetType().Name,
Instance = isDebug ? exception.GetType().Name : null,
Type = "Error"
};
await context.Response.WriteAsJsonAsync(response);
}));
});
internal static IApplicationBuilder UseEndpoints(this IApplicationBuilder applicationBuilder)
internal static IApplicationBuilder UseEndpoints(this IApplicationBuilder applicationBuilder)
{
IServiceProvider provider = applicationBuilder.ApplicationServices;

View File

@@ -29,6 +29,7 @@ internal static class BackOfficeAuthPolicyBuilderExtensions
builder.Services.AddSingleton<IAuthorizationHandler, UserGroupPermissionHandler>();
builder.Services.AddSingleton<IAuthorizationHandler, UserPermissionHandler>();
builder.Services.AddSingleton<IAuthorizationHandler, AllowedApplicationHandler>();
builder.Services.AddSingleton<IAuthorizationHandler, BackOfficeHandler>();
builder.Services.AddAuthorization(CreatePolicies);
return builder;
@@ -46,7 +47,7 @@ internal static class BackOfficeAuthPolicyBuilderExtensions
options.AddPolicy(AuthorizationPolicies.BackOfficeAccess, policy =>
{
policy.AuthenticationSchemes.Add(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme);
policy.RequireAuthenticatedUser();
policy.Requirements.Add(new BackOfficeRequirement());
});
options.AddPolicy(AuthorizationPolicies.RequireAdminAccess, policy =>

View File

@@ -27,13 +27,14 @@ internal sealed class DocumentNotificationPresentationFactory : IDocumentNotific
.ToArray()
?? Array.Empty<string>();
var availableActionIds = _actionCollection.Where(a => a.ShowInNotifier).Select(a => a.Letter.ToString()).ToArray();
return await Task.FromResult(
availableActionIds.Select(actionId => new DocumentNotificationResponseModel
return await Task.FromResult(_actionCollection
.Where(action => action.ShowInNotifier)
.Select(action => new DocumentNotificationResponseModel
{
ActionId = actionId,
Subscribed = subscribedActionIds.Contains(actionId)
}).ToArray());
ActionId = action.Letter,
Alias = action.Alias,
Subscribed = subscribedActionIds.Contains(action.Letter)
})
.ToArray());
}
}

View File

@@ -9875,6 +9875,14 @@
"format": "int32",
"default": 100
}
},
{
"name": "parentId",
"in": "query",
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"responses": {
@@ -15545,6 +15553,14 @@
"format": "int32",
"default": 100
}
},
{
"name": "parentId",
"in": "query",
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"responses": {
@@ -25176,6 +25192,41 @@
]
}
},
"/umbraco/management/api/v1/server/upgrade-check": {
"get": {
"tags": [
"Server"
],
"operationId": "GetServerUpgradeCheck",
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"oneOf": [
{
"$ref": "#/components/schemas/UpgradeCheckResponseModel"
}
]
}
}
}
},
"401": {
"description": "The resource is protected and requires an authentication token"
},
"403": {
"description": "The authenticated user do not have access to this resource"
}
},
"security": [
{
"Backoffice User": [ ]
}
]
}
},
"/umbraco/management/api/v1/item/static-file": {
"get": {
"tags": [
@@ -36244,6 +36295,7 @@
"DocumentNotificationResponseModel": {
"required": [
"actionId",
"alias",
"subscribed"
],
"type": "object",
@@ -36251,6 +36303,9 @@
"actionId": {
"type": "string"
},
"alias": {
"type": "string"
},
"subscribed": {
"type": "boolean"
}
@@ -37907,7 +37962,9 @@
},
"providerProperties": {
"type": "object",
"additionalProperties": { },
"additionalProperties": {
"nullable": true
},
"nullable": true
}
},
@@ -42446,12 +42503,17 @@
},
"ServerConfigurationResponseModel": {
"required": [
"allowPasswordReset"
"allowPasswordReset",
"versionCheckPeriod"
],
"type": "object",
"properties": {
"allowPasswordReset": {
"type": "boolean"
},
"versionCheckPeriod": {
"type": "integer",
"format": "int32"
}
},
"additionalProperties": false
@@ -44514,6 +44576,26 @@
},
"additionalProperties": false
},
"UpgradeCheckResponseModel": {
"required": [
"comment",
"type",
"url"
],
"type": "object",
"properties": {
"type": {
"type": "string"
},
"comment": {
"type": "string"
},
"url": {
"type": "string"
}
},
"additionalProperties": false
},
"UpgradeSettingsResponseModel": {
"required": [
"currentState",
@@ -45275,4 +45357,4 @@
}
}
}
}
}

View File

@@ -1,5 +1,6 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Infrastructure;
using Umbraco.Cms.Api.Management.Security.Authorization.User;
namespace Umbraco.Cms.Api.Management.Security.Authorization.DenyLocalLogin;
@@ -24,12 +25,12 @@ public class DenyLocalLoginHandler : MustSatisfyRequirementAuthorizationHandler<
if (isDenied is false)
{
// AuthorizationPolicies.BackOfficeAccess policy adds this requirement by policy.RequireAuthenticatedUser()
// AuthorizationPolicies.BackOfficeAccess policy adds this requirement by policy.Requirements.Add(new BackOfficeRequirement());
// Since we want to "allow anonymous" for some endpoints (i.e. BackOfficeController.Login()), it is necessary to succeed this requirement
IEnumerable<DenyAnonymousAuthorizationRequirement> denyAnonymousUserRequirements = context.PendingRequirements.OfType<DenyAnonymousAuthorizationRequirement>();
foreach (DenyAnonymousAuthorizationRequirement denyAnonymousUserRequirement in denyAnonymousUserRequirements)
IEnumerable<BackOfficeRequirement> backOfficeRequirements = context.PendingRequirements.OfType<BackOfficeRequirement>();
foreach (BackOfficeRequirement backOfficeRequirement in backOfficeRequirements)
{
context.Succeed(denyAnonymousUserRequirement);
context.Succeed(backOfficeRequirement);
}
}

View File

@@ -0,0 +1,35 @@
using Microsoft.AspNetCore.Authorization;
using Umbraco.Cms.Core.Security;
namespace Umbraco.Cms.Api.Management.Security.Authorization.User;
/// <summary>
/// Ensures authorization is successful for a back office user.
/// </summary>
public class BackOfficeHandler : MustSatisfyRequirementAuthorizationHandler<BackOfficeRequirement>
{
private readonly IBackOfficeSecurityAccessor _backOfficeSecurity;
public BackOfficeHandler(IBackOfficeSecurityAccessor backOfficeSecurity)
{
_backOfficeSecurity = backOfficeSecurity;
}
protected override Task<bool> IsAuthorized(AuthorizationHandlerContext context, BackOfficeRequirement requirement)
{
if (context.HasFailed is false && context.HasSucceeded is true)
{
return Task.FromResult(true);
}
if (!_backOfficeSecurity.BackOfficeSecurity?.IsAuthenticated() ?? false)
{
return Task.FromResult(false);
}
var userApprovalSucceeded = !requirement.RequireApproval ||
(_backOfficeSecurity.BackOfficeSecurity?.CurrentUser?.IsApproved ?? false);
return Task.FromResult(userApprovalSucceeded);
}
}

View File

@@ -0,0 +1,20 @@
using Microsoft.AspNetCore.Authorization;
namespace Umbraco.Cms.Api.Management.Security.Authorization.User;
/// <summary>
/// Authorization requirement for the <see cref="BackOfficeHandler" />.
/// </summary>
public class BackOfficeRequirement : IAuthorizationRequirement
{
/// <summary>
/// Initializes a new instance of the <see cref="BackOfficeRequirement" /> class.
/// </summary>
/// <param name="requireApproval">Flag for whether back-office user approval is required.</param>
public BackOfficeRequirement(bool requireApproval = true) => RequireApproval = requireApproval;
/// <summary>
/// Gets a value indicating whether back-office user approval is required.
/// </summary>
public bool RequireApproval { get; }
}

View File

@@ -0,0 +1,12 @@
namespace Umbraco.Cms.Api.Management.ViewModels.Content;
public class PropertyValidationResponseModel
{
public string[] Messages { get; set; } = Array.Empty<string>();
public string Alias { get; set; } = string.Empty;
public string? Culture { get; set; }
public string? Segment { get; set; }
}

View File

@@ -4,5 +4,7 @@ public class DocumentNotificationResponseModel
{
public required string ActionId { get; set; }
public required string Alias { get; set; }
public required bool Subscribed { get; set; }
}

View File

@@ -3,4 +3,6 @@
public class ServerConfigurationResponseModel
{
public bool AllowPasswordReset { get; set; }
public int VersionCheckPeriod { get; set; }
}

View File

@@ -0,0 +1,10 @@
namespace Umbraco.Cms.Api.Management.ViewModels.Server;
public class UpgradeCheckResponseModel
{
public required string Type { get; init; }
public required string Comment { get; init; }
public required string Url { get; init; }
}

View File

@@ -2,6 +2,7 @@
namespace Umbraco.Cms.Api.Management.ViewModels.Server;
[Obsolete("Not used. Will be removed in V15.")]
public class VersionResponseModel
{
[Required]

View File

@@ -7,6 +7,9 @@
<!-- Take top-level depedendency on Azure.Identity, because Microsoft.EntityFrameworkCore.SqlServer depends on a vulnerable version -->
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" />
<!-- Both OpenIddict.AspNetCore, Npoco.SqlServer and Microsoft.EntityFrameworkCore.SqlServer bring in a vulnerable version of Microsoft.IdentityModel.JsonWebTokens -->
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens"/>
</ItemGroup>
<ItemGroup>

View File

@@ -18,6 +18,9 @@
<!-- Take top-level depedendency on Azure.Identity, because NPoco.SqlServer depends on a vulnerable version -->
<PackageReference Include="Azure.Identity" />
<PackageReference Include="NPoco.SqlServer" />
<!-- Both OpenIddict.AspNetCore, Npoco.SqlServer and Microsoft.EntityFrameworkCore.SqlServer bring in a vulnerable version of Microsoft.IdentityModel.JsonWebTokens -->
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens"/>
</ItemGroup>
<ItemGroup>

View File

@@ -40,9 +40,14 @@
<Exec WorkingDirectory="$(ProjectDir)..\Umbraco.Web.UI.Client\" Command="npm run build:for:cms" />
</Target>
<Target Name="BuildLogin" DependsOnTargets="BuildBellissima">
<Exec WorkingDirectory="$(ProjectDir)..\Umbraco.Web.UI.Login\" Command="npm ci --no-fund --no-audit --prefer-offline" />
<Exec WorkingDirectory="$(ProjectDir)..\Umbraco.Web.UI.Login\" Command="npm run build" />
<Target Name="BuildBelle">
<Exec WorkingDirectory="$(ProjectDir)..\Umbraco.Web.UI.Client\" Command="npm ci --no-fund --no-audit --prefer-offline" Timeout="600000" />
<Exec WorkingDirectory="$(ProjectDir)..\Umbraco.Web.UI.Client\" Command="npm run build:skip-tests" Timeout="600000" />
</Target>
<Target Name="BuildLogin">
<Exec WorkingDirectory="$(ProjectDir)..\Umbraco.Web.UI.Login\" Command="npm ci --no-fund --no-audit --prefer-offline" Timeout="600000" />
<Exec WorkingDirectory="$(ProjectDir)..\Umbraco.Web.UI.Login\" Command="npm run build" Timeout="600000" />
</Target>
<Target Name="CleanStaticAssetsPreconditions" AfterTargets="Clean" Condition="'$(UmbracoBuild)' == ''">

View File

@@ -47,7 +47,7 @@
<!-- Generate JSON schema on build (and before copying to project) -->
<Target Name="GenerateAppsettingsSchema" BeforeTargets="Build;CopyUmbracoJsonSchemaFiles" Condition="!Exists('$(_UmbracoCmsJsonSchemaReference)')">
<Message Text="Generating $(_UmbracoCmsJsonSchemaReference) because it doesn't exist" Importance="high" />
<Exec WorkingDirectory="$(MSBuildThisFileDirectory)..\..\tools\Umbraco.JsonSchema" Command="dotnet run --configuration $(Configuration) -- --outputFile &quot;$(MSBuildThisFileDirectory)$(_UmbracoCmsJsonSchemaReference)&quot;" />
<Exec WorkingDirectory="$(MSBuildThisFileDirectory)..\..\tools\Umbraco.JsonSchema" Command="dotnet run --configuration $(Configuration) -- --outputFile &quot;$(MSBuildThisFileDirectory)$(_UmbracoCmsJsonSchemaReference)&quot;" Timeout="600000" />
</Target>
<!-- Remove generated JSON schema on clean -->

View File

@@ -13,7 +13,7 @@ public static partial class Constants
public static readonly string[] UmbracoCoreAssemblyNames =
{
"Umbraco.Core", "Umbraco.Infrastructure", "Umbraco.PublishedCache.NuCache", "Umbraco.Examine.Lucene",
"Umbraco.Web.Common", "Umbraco.Web.BackOffice", "Umbraco.Web.Website",
"Umbraco.Web.Common", "Umbraco.Cms.Api.Common","Umbraco.Cms.Api.Delivery","Umbraco.Cms.Api.Management", "Umbraco.Web.Website",
};
}
}

View File

@@ -8,7 +8,7 @@ using Umbraco.Cms.Core.Models.Entities;
namespace Umbraco.Extensions;
/// <summary>
/// Provides extension methods that return udis for Umbraco entities.
/// Provides extension methods that return UDIs for Umbraco entities.
/// </summary>
public static class UdiGetterExtensions
{
@@ -19,11 +19,177 @@ public static class UdiGetterExtensions
/// <returns>
/// The entity identifier of the entity.
/// </returns>
public static GuidUdi GetUdi(this ITemplate entity)
public static Udi GetUdi(this IEntity entity)
{
ArgumentNullException.ThrowIfNull(entity);
return new GuidUdi(Constants.UdiEntityType.Template, entity.Key).EnsureClosed();
return entity switch
{
// Concrete types
EntityContainer container => container.GetUdi(),
Script script => script.GetUdi(),
Stylesheet stylesheet => stylesheet.GetUdi(),
// Interfaces
IContentBase contentBase => contentBase.GetUdi(),
IContentTypeComposition contentTypeComposition => contentTypeComposition.GetUdi(),
IDataType dataType => dataType.GetUdi(),
IDictionaryItem dictionaryItem => dictionaryItem.GetUdi(),
ILanguage language => language.GetUdi(),
IMemberGroup memberGroup => memberGroup.GetUdi(),
IPartialView partialView => partialView.GetUdi(),
IRelationType relationType => relationType.GetUdi(),
ITemplate template => template.GetUdi(),
IWebhook webhook => webhook.GetUdi(),
_ => throw new NotSupportedException($"Entity type {entity.GetType().FullName} is not supported."),
};
}
/// <summary>
/// Gets the entity identifier of the entity.
/// </summary>
/// <param name="entity">The entity.</param>
/// <returns>
/// The entity identifier of the entity.
/// </returns>
public static GuidUdi GetUdi(this EntityContainer entity)
{
ArgumentNullException.ThrowIfNull(entity);
string entityType;
if (entity.ContainedObjectType == Constants.ObjectTypes.DataType)
{
entityType = Constants.UdiEntityType.DataTypeContainer;
}
else if (entity.ContainedObjectType == Constants.ObjectTypes.DocumentType)
{
entityType = Constants.UdiEntityType.DocumentTypeContainer;
}
else if (entity.ContainedObjectType == Constants.ObjectTypes.MediaType)
{
entityType = Constants.UdiEntityType.MediaTypeContainer;
}
else if (entity.ContainedObjectType == Constants.ObjectTypes.DocumentBlueprint)
{
entityType = Constants.UdiEntityType.DocumentBlueprintContainer;
}
else
{
throw new NotSupportedException($"Contained object type {entity.ContainedObjectType} is not supported.");
}
return new GuidUdi(entityType, entity.Key).EnsureClosed();
}
/// <summary>
/// Gets the entity identifier of the entity.
/// </summary>
/// <param name="entity">The entity.</param>
/// <returns>
/// The entity identifier of the entity.
/// </returns>
public static StringUdi GetUdi(this Script entity)
{
ArgumentNullException.ThrowIfNull(entity);
return GetUdiFromPath(Constants.UdiEntityType.Script, entity.Path);
}
/// <summary>
/// Gets the entity identifier of the entity.
/// </summary>
/// <param name="entity">The entity.</param>
/// <returns>
/// The entity identifier of the entity.
/// </returns>
public static StringUdi GetUdi(this Stylesheet entity)
{
ArgumentNullException.ThrowIfNull(entity);
return GetUdiFromPath(Constants.UdiEntityType.Stylesheet, entity.Path);
}
/// <summary>
/// Gets the entity identifier of the entity.
/// </summary>
/// <param name="entity">The entity.</param>
/// <returns>
/// The entity identifier of the entity.
/// </returns>
public static GuidUdi GetUdi(this IContentBase entity)
{
ArgumentNullException.ThrowIfNull(entity);
return entity switch
{
IContent content => content.GetUdi(),
IMedia media => media.GetUdi(),
IMember member => member.GetUdi(),
_ => throw new NotSupportedException($"Content base type {entity.GetType().FullName} is not supported."),
};
}
/// <summary>
/// Gets the entity identifier of the entity.
/// </summary>
/// <param name="entity">The entity.</param>
/// <returns>
/// The entity identifier of the entity.
/// </returns>
public static GuidUdi GetUdi(this IContent entity)
{
ArgumentNullException.ThrowIfNull(entity);
string entityType = entity.Blueprint ? Constants.UdiEntityType.DocumentBlueprint : Constants.UdiEntityType.Document;
return new GuidUdi(entityType, entity.Key).EnsureClosed();
}
/// <summary>
/// Gets the entity identifier of the entity.
/// </summary>
/// <param name="entity">The entity.</param>
/// <returns>
/// The entity identifier of the entity.
/// </returns>
public static GuidUdi GetUdi(this IMedia entity)
{
ArgumentNullException.ThrowIfNull(entity);
return new GuidUdi(Constants.UdiEntityType.Media, entity.Key).EnsureClosed();
}
/// <summary>
/// Gets the entity identifier of the entity.
/// </summary>
/// <param name="entity">The entity.</param>
/// <returns>
/// The entity identifier of the entity.
/// </returns>
public static GuidUdi GetUdi(this IMember entity)
{
ArgumentNullException.ThrowIfNull(entity);
return new GuidUdi(Constants.UdiEntityType.Member, entity.Key).EnsureClosed();
}
/// <summary>
/// Gets the entity identifier of the entity.
/// </summary>
/// <param name="entity">The entity.</param>
/// <returns>
/// The entity identifier of the entity.
/// </returns>
public static GuidUdi GetUdi(this IContentTypeComposition entity)
{
ArgumentNullException.ThrowIfNull(entity);
return entity switch
{
IContentType contentType => contentType.GetUdi(),
IMediaType mediaType => mediaType.GetUdi(),
IMemberType memberType => memberType.GetUdi(),
_ => throw new NotSupportedException($"Composition type {entity.GetType().FullName} is not supported."),
};
}
/// <summary>
@@ -68,42 +234,6 @@ public static class UdiGetterExtensions
return new GuidUdi(Constants.UdiEntityType.MemberType, entity.Key).EnsureClosed();
}
/// <summary>
/// Gets the entity identifier of the entity.
/// </summary>
/// <param name="entity">The entity.</param>
/// <returns>
/// The entity identifier of the entity.
/// </returns>
public static GuidUdi GetUdi(this IMemberGroup entity)
{
ArgumentNullException.ThrowIfNull(entity);
return new GuidUdi(Constants.UdiEntityType.MemberGroup, entity.Key).EnsureClosed();
}
/// <summary>
/// Gets the entity identifier of the entity.
/// </summary>
/// <param name="entity">The entity.</param>
/// <returns>
/// The entity identifier of the entity.
/// </returns>
public static GuidUdi GetUdi(this IContentTypeComposition entity)
{
ArgumentNullException.ThrowIfNull(entity);
string entityType = entity switch
{
IContentType => Constants.UdiEntityType.DocumentType,
IMediaType => Constants.UdiEntityType.MediaType,
IMemberType => Constants.UdiEntityType.MemberType,
_ => throw new NotSupportedException(string.Format("Composition type {0} is not supported.", entity.GetType().FullName)),
};
return new GuidUdi(entityType, entity.Key).EnsureClosed();
}
/// <summary>
/// Gets the entity identifier of the entity.
/// </summary>
@@ -118,129 +248,6 @@ public static class UdiGetterExtensions
return new GuidUdi(Constants.UdiEntityType.DataType, entity.Key).EnsureClosed();
}
/// <summary>
/// Gets the entity identifier of the entity.
/// </summary>
/// <param name="entity">The entity.</param>
/// <returns>
/// The entity identifier of the entity.
/// </returns>
public static GuidUdi GetUdi(this EntityContainer entity)
{
ArgumentNullException.ThrowIfNull(entity);
string entityType;
if (entity.ContainedObjectType == Constants.ObjectTypes.DataType)
{
entityType = Constants.UdiEntityType.DataTypeContainer;
}
else if (entity.ContainedObjectType == Constants.ObjectTypes.DocumentType)
{
entityType = Constants.UdiEntityType.DocumentTypeContainer;
}
else if (entity.ContainedObjectType == Constants.ObjectTypes.MediaType)
{
entityType = Constants.UdiEntityType.MediaTypeContainer;
}
else if (entity.ContainedObjectType == Constants.ObjectTypes.DocumentBlueprint)
{
entityType = Constants.UdiEntityType.DocumentBlueprintContainer;
}
else
{
throw new NotSupportedException(string.Format("Contained object type {0} is not supported.", entity.ContainedObjectType));
}
return new GuidUdi(entityType, entity.Key).EnsureClosed();
}
/// <summary>
/// Gets the entity identifier of the entity.
/// </summary>
/// <param name="entity">The entity.</param>
/// <returns>
/// The entity identifier of the entity.
/// </returns>
public static GuidUdi GetUdi(this IMedia entity)
{
ArgumentNullException.ThrowIfNull(entity);
return new GuidUdi(Constants.UdiEntityType.Media, entity.Key).EnsureClosed();
}
/// <summary>
/// Gets the entity identifier of the entity.
/// </summary>
/// <param name="entity">The entity.</param>
/// <returns>
/// The entity identifier of the entity.
/// </returns>
public static GuidUdi GetUdi(this IContent entity)
{
ArgumentNullException.ThrowIfNull(entity);
string entityType = entity.Blueprint ? Constants.UdiEntityType.DocumentBlueprint : Constants.UdiEntityType.Document;
return new GuidUdi(entityType, entity.Key).EnsureClosed();
}
/// <summary>
/// Gets the entity identifier of the entity.
/// </summary>
/// <param name="entity">The entity.</param>
/// <returns>
/// The entity identifier of the entity.
/// </returns>
public static GuidUdi GetUdi(this IMember entity)
{
ArgumentNullException.ThrowIfNull(entity);
return new GuidUdi(Constants.UdiEntityType.Member, entity.Key).EnsureClosed();
}
/// <summary>
/// Gets the entity identifier of the entity.
/// </summary>
/// <param name="entity">The entity.</param>
/// <returns>
/// The entity identifier of the entity.
/// </returns>
public static StringUdi GetUdi(this Stylesheet entity)
{
ArgumentNullException.ThrowIfNull(entity);
return GetUdiFromPath(Constants.UdiEntityType.Stylesheet, entity.Path);
}
/// <summary>
/// Gets the entity identifier of the entity.
/// </summary>
/// <param name="entity">The entity.</param>
/// <returns>
/// The entity identifier of the entity.
/// </returns>
public static StringUdi GetUdi(this Script entity)
{
ArgumentNullException.ThrowIfNull(entity);
return GetUdiFromPath(Constants.UdiEntityType.Script, entity.Path);
}
/// <summary>
/// Gets the UDI from a path.
/// </summary>
/// <param name="entityType">The type of the entity.</param>
/// <param name="path">The path.</param>
/// <returns>
/// The entity identifier of the entity.
/// </returns>
private static StringUdi GetUdiFromPath(string entityType, string path)
{
string id = path.TrimStart(Constants.CharArrays.ForwardSlash).Replace("\\", "/");
return new StringUdi(entityType, id).EnsureClosed();
}
/// <summary>
/// Gets the entity identifier of the entity.
/// </summary>
@@ -262,11 +269,11 @@ public static class UdiGetterExtensions
/// <returns>
/// The entity identifier of the entity.
/// </returns>
public static StringUdi GetUdi(this IPartialView entity)
public static StringUdi GetUdi(this ILanguage entity)
{
ArgumentNullException.ThrowIfNull(entity);
return GetUdiFromPath(Constants.UdiEntityType.PartialView, entity.Path);
return new StringUdi(Constants.UdiEntityType.Language, entity.IsoCode).EnsureClosed();
}
/// <summary>
@@ -276,19 +283,25 @@ public static class UdiGetterExtensions
/// <returns>
/// The entity identifier of the entity.
/// </returns>
public static GuidUdi GetUdi(this IContentBase entity)
public static GuidUdi GetUdi(this IMemberGroup entity)
{
ArgumentNullException.ThrowIfNull(entity);
string type = entity switch
{
IContent => Constants.UdiEntityType.Document,
IMedia => Constants.UdiEntityType.Media,
IMember => Constants.UdiEntityType.Member,
_ => throw new NotSupportedException(string.Format("Content base type {0} is not supported.", entity.GetType().FullName)),
};
return new GuidUdi(Constants.UdiEntityType.MemberGroup, entity.Key).EnsureClosed();
}
return new GuidUdi(type, entity.Key).EnsureClosed();
/// <summary>
/// Gets the entity identifier of the entity.
/// </summary>
/// <param name="entity">The entity.</param>
/// <returns>
/// The entity identifier of the entity.
/// </returns>
public static StringUdi GetUdi(this IPartialView entity)
{
ArgumentNullException.ThrowIfNull(entity);
return GetUdiFromPath(Constants.UdiEntityType.PartialView, entity.Path);
}
/// <summary>
@@ -305,6 +318,20 @@ public static class UdiGetterExtensions
return new GuidUdi(Constants.UdiEntityType.RelationType, entity.Key).EnsureClosed();
}
/// <summary>
/// Gets the entity identifier of the entity.
/// </summary>
/// <param name="entity">The entity.</param>
/// <returns>
/// The entity identifier of the entity.
/// </returns>
public static GuidUdi GetUdi(this ITemplate entity)
{
ArgumentNullException.ThrowIfNull(entity);
return new GuidUdi(Constants.UdiEntityType.Template, entity.Key).EnsureClosed();
}
/// <summary>
/// Gets the entity identifier of the entity.
/// </summary>
@@ -320,56 +347,17 @@ public static class UdiGetterExtensions
}
/// <summary>
/// Gets the entity identifier of the entity.
/// Gets the UDI from a path.
/// </summary>
/// <param name="entity">The entity.</param>
/// <param name="entityType">The type of the entity.</param>
/// <param name="path">The path.</param>
/// <returns>
/// The entity identifier of the entity.
/// </returns>
public static StringUdi GetUdi(this ILanguage entity)
private static StringUdi GetUdiFromPath(string entityType, string path)
{
ArgumentNullException.ThrowIfNull(entity);
string id = path.TrimStart(Constants.CharArrays.ForwardSlash).Replace("\\", "/");
return new StringUdi(Constants.UdiEntityType.Language, entity.IsoCode).EnsureClosed();
}
/// <summary>
/// Gets the entity identifier of the entity.
/// </summary>
/// <param name="entity">The entity.</param>
/// <returns>
/// The entity identifier of the entity.
/// </returns>
public static Udi GetUdi(this IEntity entity)
{
ArgumentNullException.ThrowIfNull(entity);
return entity switch
{
// Concrete types
EntityContainer container => container.GetUdi(),
Stylesheet stylesheet => stylesheet.GetUdi(),
Script script => script.GetUdi(),
// Content types
IContentType contentType => contentType.GetUdi(),
IMediaType mediaType => mediaType.GetUdi(),
IMemberType memberType => memberType.GetUdi(),
IContentTypeComposition contentTypeComposition => contentTypeComposition.GetUdi(),
// Content
IContent content => content.GetUdi(),
IMedia media => media.GetUdi(),
IMember member => member.GetUdi(),
IContentBase contentBase => contentBase.GetUdi(),
// Other
IDataType dataTypeComposition => dataTypeComposition.GetUdi(),
IDictionaryItem dictionaryItem => dictionaryItem.GetUdi(),
ILanguage language => language.GetUdi(),
IMemberGroup memberGroup => memberGroup.GetUdi(),
IPartialView partialView => partialView.GetUdi(),
IRelationType relationType => relationType.GetUdi(),
ITemplate template => template.GetUdi(),
IWebhook webhook => webhook.GetUdi(),
_ => throw new NotSupportedException(string.Format("Entity type {0} is not supported.", entity.GetType().FullName)),
};
return new StringUdi(entityType, id).EnsureClosed();
}
}

View File

@@ -6,5 +6,16 @@ namespace Umbraco.Cms.Core.Media.EmbedProviders;
/// Wrapper class for OEmbed response.
/// </summary>
[DataContract]
public class OEmbedResponse : OEmbedResponseBase<double>;
public class OEmbedResponse : OEmbedResponseBase<double>
{
// these is only here to avoid breaking changes. In theory it should still be source code compatible to remove them.
public new double? ThumbnailHeight => base.ThumbnailHeight;
public new double? ThumbnailWidth => base.ThumbnailWidth;
public new double? Height => base.Height;
public new double? Width => base.Width;
}

View File

@@ -34,7 +34,7 @@ public abstract class OEmbedResponseBase<T>
public string? ThumbnailUrl { get; set; }
[DataMember(Name = "thumbnail_height")]
public T? ThumbnailHeight { get; set; }
public virtual T? ThumbnailHeight { get; set; }
[DataMember(Name = "thumbnail_width")]
public T? ThumbnailWidth { get; set; }

View File

@@ -16,14 +16,10 @@ public class UpgradeCheckRepository : IUpgradeCheckRepository
{
try
{
if (_httpClient == null)
{
_httpClient = new HttpClient();
}
_httpClient ??= new HttpClient { Timeout = TimeSpan.FromSeconds(1) };
using var content = new StringContent(_jsonSerializer.Serialize(new CheckUpgradeDto(version)), Encoding.UTF8, "application/json");
_httpClient.Timeout = TimeSpan.FromSeconds(1);
using HttpResponseMessage task = await _httpClient.PostAsync(RestApiUpgradeChecklUrl, content);
var json = await task.Content.ReadAsStringAsync();
UpgradeResult? result = _jsonSerializer.Deserialize<UpgradeResult>(json);

View File

@@ -7,7 +7,7 @@ namespace Umbraco.Cms.Core.PropertyEditors.Validators;
/// <summary>
/// A validator that validates that the value is not null or empty (if it is a string)
/// </summary>
public sealed class RequiredValidator : IValueRequiredValidator, IValueValidator
public class RequiredValidator : IValueRequiredValidator, IValueValidator
{
[Obsolete($"Use the constructor that does not accept {nameof(ILocalizedTextService)}. Will be removed in V15.")]
public RequiredValidator(ILocalizedTextService textService)
@@ -24,7 +24,7 @@ public sealed class RequiredValidator : IValueRequiredValidator, IValueValidator
ValidateRequired(value, valueType);
/// <inheritdoc cref="IValueRequiredValidator.ValidateRequired" />
public IEnumerable<ValidationResult> ValidateRequired(object? value, string? valueType)
public virtual IEnumerable<ValidationResult> ValidateRequired(object? value, string? valueType)
{
if (value == null)
{

View File

@@ -1,9 +1,13 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.ContentEditing;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Core.Scoping;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.Services;
@@ -12,7 +16,11 @@ internal sealed class ContentEditingService
{
private readonly ITemplateService _templateService;
private readonly ILogger<ContentEditingService> _logger;
private readonly IUserService _userService;
private readonly ILocalizationService _localizationService;
private readonly ILanguageService _languageService;
[Obsolete("Use non-obsolete constructor. This will be removed in Umbraco 16.")]
public ContentEditingService(
IContentService contentService,
IContentTypeService contentTypeService,
@@ -24,10 +32,46 @@ internal sealed class ContentEditingService
IUserIdKeyResolver userIdKeyResolver,
ITreeEntitySortingService treeEntitySortingService,
IContentValidationService contentValidationService)
: this(
contentService,
contentTypeService,
propertyEditorCollection,
dataTypeService,
templateService,
logger,
scopeProvider,
userIdKeyResolver,
treeEntitySortingService,
contentValidationService,
StaticServiceProvider.Instance.GetRequiredService<IUserService>(),
StaticServiceProvider.Instance.GetRequiredService<ILocalizationService>(),
StaticServiceProvider.Instance.GetRequiredService<ILanguageService>()
)
{
}
public ContentEditingService(
IContentService contentService,
IContentTypeService contentTypeService,
PropertyEditorCollection propertyEditorCollection,
IDataTypeService dataTypeService,
ITemplateService templateService,
ILogger<ContentEditingService> logger,
ICoreScopeProvider scopeProvider,
IUserIdKeyResolver userIdKeyResolver,
ITreeEntitySortingService treeEntitySortingService,
IContentValidationService contentValidationService,
IUserService userService,
ILocalizationService localizationService,
ILanguageService languageService)
: base(contentService, contentTypeService, propertyEditorCollection, dataTypeService, logger, scopeProvider, userIdKeyResolver, contentValidationService, treeEntitySortingService)
{
_templateService = templateService;
_logger = logger;
_userService = userService;
_localizationService = localizationService;
_languageService = languageService;
}
public async Task<IContent?> GetAsync(Guid key)
@@ -65,7 +109,7 @@ internal sealed class ContentEditingService
ContentEditingOperationStatus validationStatus = result.Status;
ContentValidationResult validationResult = result.Result.ValidationResult;
IContent content = result.Result.Content!;
IContent content = await EnsureOnlyAllowedFieldsAreUpdated(result.Result.Content!, userKey);
ContentEditingOperationStatus updateTemplateStatus = await UpdateTemplateAsync(content, createModel.TemplateKey);
if (updateTemplateStatus != ContentEditingOperationStatus.Success)
{
@@ -78,6 +122,53 @@ internal sealed class ContentEditingService
: Attempt.FailWithStatus(saveStatus, new ContentCreateResult { Content = content });
}
/// <summary>
/// A temporary method that ensures the data is sent in is overridden by the original data, in cases where the user do not have permissions to change the data.
/// </summary>
private async Task<IContent> EnsureOnlyAllowedFieldsAreUpdated(IContent contentWithPotentialUnallowedChanges, Guid userKey)
{
if (contentWithPotentialUnallowedChanges.ContentType.VariesByCulture() is false)
{
return contentWithPotentialUnallowedChanges;
}
IContent? existingContent = await GetAsync(contentWithPotentialUnallowedChanges.Key);
IUser? user = await _userService.GetAsync(userKey);
if (user is null)
{
return contentWithPotentialUnallowedChanges;
}
var allowedLanguageIds = user.CalculateAllowedLanguageIds(_localizationService)!;
var allowedCultures = (await _languageService.GetIsoCodesByIdsAsync(allowedLanguageIds)).ToHashSet();
foreach (var culture in contentWithPotentialUnallowedChanges.EditedCultures ?? contentWithPotentialUnallowedChanges.PublishedCultures)
{
if (allowedCultures.Contains(culture))
{
continue;
}
// else override the updates values with the original values.
foreach (IProperty property in contentWithPotentialUnallowedChanges.Properties)
{
if (property.PropertyType.VariesByCulture() is false)
{
continue;
}
var value = existingContent?.Properties.First(x=>x.Alias == property.Alias).GetValue(culture, null, false);
property.SetValue(value, culture, null);
}
}
return contentWithPotentialUnallowedChanges;
}
public async Task<Attempt<ContentUpdateResult, ContentEditingOperationStatus>> UpdateAsync(Guid key, ContentUpdateModel updateModel, Guid userKey)
{
IContent? content = ContentService.GetById(key);
@@ -102,6 +193,8 @@ internal sealed class ContentEditingService
ContentEditingOperationStatus validationStatus = result.Status;
ContentValidationResult validationResult = result.Result.ValidationResult;
content = await EnsureOnlyAllowedFieldsAreUpdated(content, userKey);
ContentEditingOperationStatus updateTemplateStatus = await UpdateTemplateAsync(content, updateModel.TemplateKey);
if (updateTemplateStatus != ContentEditingOperationStatus.Success)
{

View File

@@ -523,8 +523,6 @@ public class FileService : RepositoryService, IFileService
/// </summary>
/// <param name="templates">List of <see cref="Template" /> to save</param>
/// <param name="userId">Optional id of the user</param>
// FIXME: we need to re-implement PackageDataInstallation.ImportTemplates so it imports templates in the correct order
// instead of relying on being able to save invalid templates (child templates whose master has yet to be created)
[Obsolete("Please use ITemplateService for template operations - will be removed in Umbraco 15")]
public void SaveTemplate(IEnumerable<ITemplate> templates, int userId = Constants.Security.SuperUserId)
{

View File

@@ -196,7 +196,7 @@ public interface IDataTypeService : IService
/// </summary>
/// <param name="propertyEditorAlias">Alias of the property editor</param>
/// <returns>Collection of <see cref="IDataType" /> configured for the property editor</returns>
Task<IEnumerable<IDataType>> GetByEditorAliasAsync(string propertyEditorAlias);
Task<IEnumerable<IDataType>> GetByEditorAliasAsync(string propertyEditorAlias) => Task.FromResult(GetByEditorAlias(propertyEditorAlias));
/// <summary>
/// Gets all <see cref="IDataType" /> for a given editor UI alias
@@ -246,5 +246,5 @@ public interface IDataTypeService : IService
/// </summary>
/// <param name="propertyEditorAlias">Aliases of the property editors</param>
/// <returns>Collection of <see cref="IDataType" /> configured for the property editors</returns>
Task<IEnumerable<IDataType>> GetByEditorAliasAsync(string[] propertyEditorAlias);
Task<IEnumerable<IDataType>> GetByEditorAliasAsync(string[] propertyEditorAlias) => Task.FromResult(propertyEditorAlias.SelectMany(x=>GetByEditorAlias(x)));
}

View File

@@ -13,4 +13,8 @@ namespace Umbraco.Cms.Core.Services;
public interface IIndexedEntitySearchService
{
PagedModel<IEntitySlim> Search(UmbracoObjectTypes objectType, string query, int skip = 0, int take = 100, bool ignoreUserStartNodes = false);
// default implementation to avoid breaking changes falls back to old behaviour
PagedModel<IEntitySlim> Search(UmbracoObjectTypes objectType, string query, Guid? parentId, int skip = 0, int take = 100, bool ignoreUserStartNodes = false)
=> Search(objectType,query, skip, take, ignoreUserStartNodes);
}

View File

@@ -65,16 +65,28 @@ public interface IPackageDataInstallation
/// <returns>An enumerable list of generated languages</returns>
IReadOnlyList<ILanguage> ImportLanguages(IEnumerable<XElement> languageElements, int userId);
[Obsolete("Use Async version instead, Scheduled to be removed in v17")]
IEnumerable<ITemplate> ImportTemplate(XElement templateElement, int userId);
Task<IEnumerable<ITemplate>> ImportTemplateAsync(XElement templateElement, int userId) => Task.FromResult(ImportTemplate(templateElement, userId));
/// <summary>
/// Imports and saves package xml as <see cref="ITemplate"/>
/// </summary>
/// <param name="templateElements">Xml to import</param>
/// <param name="userId">Optional user id</param>
/// <returns>An enumerable list of generated Templates</returns>
[Obsolete("Use Async version instead, Scheduled to be removed in v17")]
IReadOnlyList<ITemplate> ImportTemplates(IReadOnlyCollection<XElement> templateElements, int userId);
/// <summary>
/// Imports and saves package xml as <see cref="ITemplate"/>
/// </summary>
/// <param name="templateElements">Xml to import</param>
/// <param name="userId">Optional user id</param>
/// <returns>An enumerable list of generated Templates</returns>
Task<IReadOnlyList<ITemplate>> ImportTemplatesAsync(IReadOnlyCollection<XElement> templateElements, int userId) => Task.FromResult(ImportTemplates(templateElements, userId));
Guid GetContentTypeKey(XElement contentType);
string? GetEntityTypeAlias(XElement entityType);

View File

@@ -5,10 +5,7 @@ public enum ContentTypeOperationStatus
Success,
DuplicateAlias,
InvalidAlias,
NameCannotBeEmpty,
NameTooLong,
InvalidPropertyTypeAlias,
PropertyTypeAliasCannotEqualContentTypeAlias,
DuplicatePropertyTypeAlias,
DataTypeNotFound,
InvalidInheritance,
@@ -21,6 +18,9 @@ public enum ContentTypeOperationStatus
NotFound,
NotAllowed,
CancelledByNotification,
PropertyTypeAliasCannotEqualContentTypeAlias,
NameCannotBeEmpty,
NameTooLong,
InvalidElementFlagDocumentHasContent,
InvalidElementFlagElementIsUsedInPropertyEditorConfiguration,
InvalidElementFlagComparedToParent,

View File

@@ -356,8 +356,16 @@ public class BackOfficeExamineSearcher : IBackOfficeExamineSearcher
throw new ArgumentNullException(nameof(entityService));
}
UdiParser.TryParse(searchFrom, true, out Udi? udi);
searchFrom = udi == null ? searchFrom : entityService.GetId(udi).Result.ToString();
if (Guid.TryParse(searchFrom, out Guid guid))
{
searchFrom = entityService.GetId(guid, objectType).Result.ToString();
}
else
{
// fallback to Udi for legacy reasons as the calling methods take string?
UdiParser.TryParse(searchFrom, true, out Udi? udi);
searchFrom = udi == null ? searchFrom : entityService.GetId(udi).Result.ToString();
}
TreeEntityPath? entityPath =
int.TryParse(searchFrom, NumberStyles.Integer, CultureInfo.InvariantCulture, out var searchFromId) &&

View File

@@ -26,6 +26,7 @@ using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Core.Packaging;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Core.PropertyEditors.Validators;
using Umbraco.Cms.Core.PropertyEditors.ValueConverters;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Routing;
@@ -237,6 +238,8 @@ public static partial class UmbracoBuilderExtensions
builder.Services.AddSingleton<IBlockEditorElementTypeCache, BlockEditorElementTypeCache>();
builder.Services.AddSingleton<IRichTextRequiredValidator, RichTextRequiredValidator>();
return builder;
}

View File

@@ -113,7 +113,9 @@ public static partial class UmbracoBuilderExtensions
factory.GetRequiredService<IShortStringHelper>(),
factory.GetRequiredService<IConfigurationEditorJsonSerializer>(),
factory.GetRequiredService<IMediaService>(),
factory.GetRequiredService<IMediaTypeService>());
factory.GetRequiredService<IMediaTypeService>(),
factory.GetRequiredService<ITemplateContentParserService>(),
factory.GetRequiredService<ITemplateService>());
private static LocalizedTextServiceFileSources CreateLocalizedTextServiceFileSourcesFactory(
IServiceProvider container)

View File

@@ -53,7 +53,6 @@ public class FilePermissionHelper : IFilePermissionHelper
{
hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Bin),
hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Umbraco),
hostingEnvironment.MapPathWebRoot(_globalSettings.UmbracoPath),
hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Packages),
};
}

View File

@@ -63,18 +63,19 @@ public class UmbracoPlan : MigrationPlan
To<V_13_0_0.ChangeWebhookUrlColumnsToNvarcharMax>("{21C42760-5109-4C03-AB4F-7EA53577D1F5}");
To<V_13_0_0.AddExceptionOccured>("{6158F3A3-4902-4201-835E-1ED7F810B2D8}");
To<V_13_3_0.AlignUpgradedDatabase>("{985AF2BA-69D3-4DBA-95E0-AD3FA7459FA7}");
To<V_13_5_0.ChangeRedirectUrlToNvarcharMax>("{CC47C751-A81B-489A-A2BC-0240245DB687}");
// To 14.0.0
To<V_14_0_0.AddPropertyEditorUiAliasColumn>("{419827A0-4FCE-464B-A8F3-247C6092AF55}");
To<V_14_0_0.AddGuidsToUserGroups>("{69E12556-D9B3-493A-8E8A-65EC89FB658D}");
To<V_14_0_0.AddUserGroup2PermisionTable>("{F2B16CD4-F181-4BEE-81C9-11CF384E6025}");
To<V_14_0_0.AddGuidsToUsers>("{A8E01644-9F2E-4988-8341-587EF5B7EA69}");
To<NoopMigration>("{69E12556-D9B3-493A-8E8A-65EC89FB658D}");
To<NoopMigration>("{F2B16CD4-F181-4BEE-81C9-11CF384E6025}");
To<NoopMigration>("{A8E01644-9F2E-4988-8341-587EF5B7EA69}");
To<V_14_0_0.UpdateDefaultGuidsOfCreatedPackages>("{E073DBC0-9E8E-4C92-8210-9CB18364F46E}");
To<V_14_0_0.RenameTechnologyLeakingPropertyEditorAliases>("{80D282A4-5497-47FF-991F-BC0BCE603121}");
To<V_14_0_0.MigrateSchduledPublishesToUtc>("{96525697-E9DC-4198-B136-25AD033442B8}");
To<V_14_0_0.AddListViewKeysToDocumentTypes>("{7FC5AC9B-6F56-415B-913E-4A900629B853}");
To<V_14_0_0.MigrateDataTypeConfigurations>("{1539A010-2EB5-4163-8518-4AE2AA98AFC6}");
To<V_14_0_0.MigrateCharPermissionsToStrings>("{C567DE81-DF92-4B99-BEA8-CD34EF99DA5D}");
To<NoopMigration>("{C567DE81-DF92-4B99-BEA8-CD34EF99DA5D}");
To<V_14_0_0.DeleteMacroTables>("{0D82C836-96DD-480D-A924-7964E458BD34}");
To<V_14_0_0.MoveDocumentBlueprintsToFolders>("{1A0FBC8A-6FC6-456C-805C-B94816B2E570}");
To<V_14_0_0.MigrateTours>("{302DE171-6D83-4B6B-B3C0-AC8808A16CA1}");
@@ -91,5 +92,8 @@ public class UmbracoPlan : MigrationPlan
// To 14.2.0
To<V_14_2_0.AddMissingDateTimeConfiguration>("{20ED404C-6FF9-4F91-8AC9-2B298E0002EB}");
// To 14.3.0
To<V_13_5_0.ChangeRedirectUrlToNvarcharMax>("{EEF792FC-318C-4921-9859-51EBF07A53A3}"); // Execute again, to ensure all that migrated to 14.0.0 without 13.5 will have this
}
}

View File

@@ -53,5 +53,9 @@ public class UmbracoPremigrationPlan : MigrationPlan
// To 14.0.0
To<V_14_0_0.UpdateToOpenIddictV5>("{76FBF80E-37E6-462E-ADC1-25668F56151D}");
To<V_14_0_0.AddGuidsToUserGroups>("{37CF4AC3-8489-44BC-A7E8-64908FEEC656}");
To<V_14_0_0.AddUserGroup2PermisionTable>("{7BCB5352-B2ED-4D4B-B27D-ECDED930B50A}");
To<V_14_0_0.AddGuidsToUsers>("{3E69BF9B-BEAB-41B1-BB11-15383CCA1C7F}");
To<V_14_0_0.MigrateCharPermissionsToStrings>("{F12C609B-86B9-4386-AFA4-78E02857247C}");
}
}

View File

@@ -120,23 +120,48 @@ public class AlignUpgradedDatabase : MigrationBase
// We need to do this to ensure we don't try to rename the constraint if it doesn't exist.
const string tableName = "umbracoContentVersion";
const string columnName = "VersionDate";
const string newColumnName = "versionDate";
const string expectedConstraintName = "DF_umbracoContentVersion_versionDate";
ColumnInfo? versionDateColumn = columns
.FirstOrDefault(x => x is { TableName: tableName, ColumnName: columnName });
if (versionDateColumn is null)
// we only want to rename the column if necessary
if (versionDateColumn is not null)
{
// The column was not found I.E. the column is correctly named
return;
RenameColumn(tableName, columnName, newColumnName, columns);
}
RenameColumn(tableName, columnName, "versionDate", columns);
// Renames the default constraint for the column,
// apparently the content version table used to be prefixed with cms and not umbraco
// We don't have a fluid way to rename the default constraint so we have to use raw SQL
// This should be okay though since we are only running this migration on SQL Server
Sql<ISqlContext> constraintNameQuery = Database.SqlContext.Sql(@$"
SELECT obj_Constraint.NAME AS 'constraintName'
FROM sys.objects obj_table
JOIN sys.objects obj_Constraint
ON obj_table.object_id = obj_Constraint.parent_object_id
JOIN sys.sysconstraints constraints
ON constraints.constid = obj_Constraint.object_id
JOIN sys.columns columns
ON columns.object_id = obj_table.object_id
AND columns.column_id = constraints.colid
WHERE obj_table.NAME = '{tableName}'
AND columns.NAME = '{newColumnName}'
AND obj_Constraint.type = 'D'
");
var currentConstraintName = Database.ExecuteScalar<string>(constraintNameQuery);
// only rename the constraint if necessary
if (currentConstraintName == expectedConstraintName)
{
return;
}
Sql<ISqlContext> renameConstraintQuery = Database.SqlContext.Sql(
"EXEC sp_rename N'DF_cmsContentVersion_VersionDate', N'DF_umbracoContentVersion_versionDate', N'OBJECT'");
$"EXEC sp_rename N'{currentConstraintName}', N'{expectedConstraintName}', N'OBJECT'");
Database.Execute(renameConstraintQuery);
}

View File

@@ -0,0 +1,41 @@
using System.Linq.Expressions;
using System.Text;
using NPoco;
using Umbraco.Cms.Core;
using Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Column;
using Umbraco.Cms.Infrastructure.Persistence;
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_13_5_0;
public class ChangeRedirectUrlToNvarcharMax : MigrationBase
{
public ChangeRedirectUrlToNvarcharMax(IMigrationContext context) : base(context)
{
}
protected override void Migrate()
{
// We don't need to run this migration for SQLite, since ntext is not a thing there, text is just text.
if (DatabaseType == DatabaseType.SQLite)
{
return;
}
string tableName = RedirectUrlDto.TableName;
string colName = "url";
// Determine the current datatype of the column within the database
string colDataType = Database.ExecuteScalar<string>($"SELECT TOP(1) CHARACTER_MAXIMUM_LENGTH FROM INFORMATION_SCHEMA.COLUMNS" +
$" WHERE TABLE_NAME = '{tableName}' AND COLUMN_NAME = '{colName}'");
// 255 is the old length, -1 indicate MAX length
if (colDataType == "255")
{
// Upgrade to MAX length
Database.Execute($"Drop Index IX_umbracoRedirectUrl_culture_hash on {Constants.DatabaseSchema.Tables.RedirectUrl}");
Database.Execute($"ALTER TABLE {tableName} ALTER COLUMN {colName} nvarchar(MAX) NOT NULL");
Database.Execute($"CREATE INDEX IX_umbracoRedirectUrl_culture_hash ON {Constants.DatabaseSchema.Tables.RedirectUrl} (urlHash, contentKey, culture, createDateUtc)");
}
}
}

View File

@@ -20,15 +20,24 @@ public class AddGuidsToUserGroups : UnscopedMigrationBase
protected override void Migrate()
{
// If the new column already exists we'll do nothing.
if (ColumnExists(Constants.DatabaseSchema.Tables.UserGroup, NewColumnName))
{
Context.Complete();
return;
}
// SQL server can simply add the column, but for SQLite this won't work,
// so we'll have to create a new table and copy over data.
if (DatabaseType != DatabaseType.SQLite)
{
MigrateSqlServer();
Context.Complete();
return;
}
MigrateSqlite();
Context.Complete();
}
private void MigrateSqlServer()
@@ -37,11 +46,6 @@ public class AddGuidsToUserGroups : UnscopedMigrationBase
using IDisposable notificationSuppression = scope.Notifications.Suppress();
ScopeDatabase(scope);
if (ColumnExists(Constants.DatabaseSchema.Tables.UserGroup, NewColumnName))
{
return;
}
var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToList();
AddColumnIfNotExists<UserGroupDto>(columns, NewColumnName);
@@ -68,12 +72,6 @@ public class AddGuidsToUserGroups : UnscopedMigrationBase
using IDisposable notificationSuppression = scope.Notifications.Suppress();
ScopeDatabase(scope);
// If the new column already exists we'll do nothing.
if (ColumnExists(Constants.DatabaseSchema.Tables.UserGroup, NewColumnName))
{
return;
}
// This isn't pretty,
// But since you cannot alter columns, we have to copy the data over and delete the old table.
// However we cannot do this due to foreign keys, so temporarily disable these keys while migrating.

View File

@@ -26,6 +26,12 @@ internal class AddGuidsToUsers : UnscopedMigrationBase
protected override void Migrate()
{
if (ColumnExists(Constants.DatabaseSchema.Tables.User, NewColumnName))
{
Context.Complete();
return;
}
InvalidateBackofficeUserAccess = true;
using IScope scope = _scopeProvider.CreateScope();
using IDisposable notificationSuppression = scope.Notifications.Suppress();
@@ -75,11 +81,6 @@ internal class AddGuidsToUsers : UnscopedMigrationBase
private void MigrateSqlite()
{
if (ColumnExists(Constants.DatabaseSchema.Tables.User, NewColumnName))
{
return;
}
/*
* We commit the initial transaction started by the scope. This is required in order to disable the foreign keys.
* We then begin a new transaction, this transaction will be committed or rolled back by the scope, like normal.

View File

@@ -2,11 +2,13 @@ using System.Globalization;
using System.Net;
using System.Xml.Linq;
using System.Xml.XPath;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Collections;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Hosting;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Entities;
@@ -34,6 +36,8 @@ namespace Umbraco.Cms.Infrastructure.Packaging
private readonly IConfigurationEditorJsonSerializer _serializer;
private readonly IMediaService _mediaService;
private readonly IMediaTypeService _mediaTypeService;
private readonly ITemplateContentParserService _templateContentParserService;
private readonly ITemplateService _templateService;
private readonly IEntityService _entityService;
private readonly IContentTypeService _contentTypeService;
private readonly IContentService _contentService;
@@ -52,7 +56,9 @@ namespace Umbraco.Cms.Infrastructure.Packaging
IShortStringHelper shortStringHelper,
IConfigurationEditorJsonSerializer serializer,
IMediaService mediaService,
IMediaTypeService mediaTypeService)
IMediaTypeService mediaTypeService,
ITemplateContentParserService templateContentParserService,
ITemplateService templateService)
{
_dataValueEditorFactory = dataValueEditorFactory;
_logger = logger;
@@ -68,6 +74,44 @@ namespace Umbraco.Cms.Infrastructure.Packaging
_serializer = serializer;
_mediaService = mediaService;
_mediaTypeService = mediaTypeService;
_templateContentParserService = templateContentParserService;
_templateService = templateService;
}
[Obsolete("Please use new constructor, scheduled for removal in v15")]
public PackageDataInstallation(
IDataValueEditorFactory dataValueEditorFactory,
ILogger<PackageDataInstallation> logger,
IFileService fileService,
ILocalizationService localizationService,
IDataTypeService dataTypeService,
IEntityService entityService,
IContentTypeService contentTypeService,
IContentService contentService,
PropertyEditorCollection propertyEditors,
IScopeProvider scopeProvider,
IShortStringHelper shortStringHelper,
IConfigurationEditorJsonSerializer serializer,
IMediaService mediaService,
IMediaTypeService mediaTypeService)
: this(
dataValueEditorFactory,
logger,
fileService,
localizationService,
dataTypeService,
entityService,
contentTypeService,
contentService,
propertyEditors,
scopeProvider,
shortStringHelper,
serializer,
mediaService,
mediaTypeService,
StaticServiceProvider.Instance.GetRequiredService<ITemplateContentParserService>(),
StaticServiceProvider.Instance.GetRequiredService<ITemplateService>())
{
}
// Also remove factory service registration when this constructor is removed
@@ -103,7 +147,9 @@ namespace Umbraco.Cms.Infrastructure.Packaging
shortStringHelper,
serializer,
mediaService,
mediaTypeService)
mediaTypeService,
StaticServiceProvider.Instance.GetRequiredService<ITemplateContentParserService>(),
StaticServiceProvider.Instance.GetRequiredService<ITemplateService>())
{ }
#region Install/Uninstall
@@ -1663,16 +1709,25 @@ namespace Umbraco.Cms.Infrastructure.Packaging
#region Templates
[Obsolete("Use Async version instead, Scheduled to be removed in v17")]
public IEnumerable<ITemplate> ImportTemplate(XElement templateElement, int userId)
=> ImportTemplates(new[] { templateElement }, userId);
public async Task<IEnumerable<ITemplate>> ImportTemplateAsync(XElement templateElement, int userId)
=> ImportTemplatesAsync(new[] {templateElement}, userId).GetAwaiter().GetResult();
[Obsolete("Use Async version instead, Scheduled to be removed in v17")]
public IReadOnlyList<ITemplate> ImportTemplates(IReadOnlyCollection<XElement> templateElements, int userId)
=> ImportTemplatesAsync(templateElements, userId).GetAwaiter().GetResult();
/// <summary>
/// Imports and saves package xml as <see cref="ITemplate"/>
/// </summary>
/// <param name="templateElements">Xml to import</param>
/// <param name="userId">Optional user id</param>
/// <returns>An enumerable list of generated Templates</returns>
public IReadOnlyList<ITemplate> ImportTemplates(IReadOnlyCollection<XElement> templateElements, int userId)
public async Task<IReadOnlyList<ITemplate>> ImportTemplatesAsync(IReadOnlyCollection<XElement> templateElements, int userId)
{
var templates = new List<ITemplate>();
@@ -1682,20 +1737,19 @@ namespace Umbraco.Cms.Infrastructure.Packaging
{
var dependencies = new List<string>();
XElement elementCopy = tempElement;
//Ensure that the Master of the current template is part of the import, otherwise we ignore this dependency as part of the dependency sorting.
if (string.IsNullOrEmpty((string?)elementCopy.Element("Master")) == false &&
templateElements.Any(x => (string?)x.Element("Alias") == (string?)elementCopy.Element("Master")))
//Ensure that the Master of the current template is part of the import, otherwise we ignore this dependency as part of the dependency sorting.'
var masterTemplate = _templateContentParserService.MasterTemplateAlias(tempElement.Value);
if (masterTemplate is not null && templateElements.Any(x => (string?)x.Element("Alias") == masterTemplate))
{
dependencies.Add((string)elementCopy.Element("Master")!);
dependencies.Add(masterTemplate);
}
else if (string.IsNullOrEmpty((string?)elementCopy.Element("Master")) == false &&
templateElements.Any(x =>
(string?)x.Element("Alias") == (string?)elementCopy.Element("Master")) == false)
else
{
_logger.LogInformation(
"Template '{TemplateAlias}' has an invalid Master '{TemplateMaster}', so the reference has been ignored.",
(string?)elementCopy.Element("Alias"),
(string?)elementCopy.Element("Master"));
masterTemplate);
}
graph.AddItem(TopoGraph.CreateNode((string)elementCopy.Element("Alias")!, elementCopy, dependencies));
@@ -1712,9 +1766,9 @@ namespace Umbraco.Cms.Infrastructure.Packaging
var design = templateElement.Element("Design")?.Value;
XElement? masterElement = templateElement.Element("Master");
var existingTemplate = _fileService.GetTemplate(alias) as Template;
var existingTemplate = await _templateService.GetAsync(alias) as Template;
Template? template = existingTemplate ?? new Template(_shortStringHelper, templateName, alias);
Template template = existingTemplate ?? new Template(_shortStringHelper, templateName, alias);
// For new templates, use the serialized key if avaialble.
if (existingTemplate == null && Guid.TryParse(templateElement.Element("Key")?.Value, out Guid key))
@@ -1737,9 +1791,16 @@ namespace Umbraco.Cms.Infrastructure.Packaging
templates.Add(template);
}
if (templates.Any())
foreach (ITemplate template in templates)
{
_fileService.SaveTemplate(templates, userId);
if (template.Id > 0)
{
await _templateService.UpdateAsync(template, Constants.Security.SuperUserKey);
}
else
{
await _templateService.CreateAsync(template, Constants.Security.SuperUserKey);
}
}
return templates;

View File

@@ -38,6 +38,7 @@ internal class RedirectUrlDto
[Column("url")]
[NullSetting(NullSetting = NullSettings.NotNull)]
[SpecialDbType(SpecialDbTypes.NVARCHARMAX)]
public string Url { get; set; } = null!;
[Column("culture")]

View File

@@ -2,6 +2,7 @@
// See LICENSE for more details.
using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Cache.PropertyEditors;
@@ -10,6 +11,7 @@ using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Blocks;
using Umbraco.Cms.Core.Models.Editors;
using Umbraco.Cms.Core.PropertyEditors.Validators;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
@@ -66,14 +68,17 @@ public class RichTextPropertyEditor : DataEditor
internal class RichTextPropertyValueEditor : BlockValuePropertyValueEditorBase<RichTextBlockValue, RichTextBlockLayoutItem>
{
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
private readonly ILocalizedTextService _localizedTextService;
private readonly IHtmlSanitizer _htmlSanitizer;
private readonly HtmlImageSourceParser _imageSourceParser;
private readonly HtmlLocalLinkParser _localLinkParser;
private readonly RichTextEditorPastedImages _pastedImages;
private readonly IJsonSerializer _jsonSerializer;
private readonly IBlockEditorElementTypeCache _elementTypeCache;
private readonly IRichTextRequiredValidator _richTextRequiredValidator;
private readonly ILogger<RichTextPropertyValueEditor> _logger;
[Obsolete("Use non-obsolete constructor. This is schedules for removal in v16.")]
public RichTextPropertyValueEditor(
DataEditorAttribute attribute,
PropertyEditorCollection propertyEditors,
@@ -91,20 +96,64 @@ public class RichTextPropertyEditor : DataEditor
IBlockEditorElementTypeCache elementTypeCache,
IPropertyValidationService propertyValidationService,
DataValueReferenceFactoryCollection dataValueReferenceFactoryCollection)
: this(
attribute,
propertyEditors,
dataTypeReadCache,
logger,
backOfficeSecurityAccessor,
localizedTextService,
shortStringHelper,
imageSourceParser,
localLinkParser,
pastedImages,
jsonSerializer,
ioHelper,
htmlSanitizer,
elementTypeCache,
propertyValidationService,
dataValueReferenceFactoryCollection,
StaticServiceProvider.Instance.GetRequiredService<IRichTextRequiredValidator>())
{
}
public RichTextPropertyValueEditor(
DataEditorAttribute attribute,
PropertyEditorCollection propertyEditors,
IDataTypeConfigurationCache dataTypeReadCache,
ILogger<RichTextPropertyValueEditor> logger,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
ILocalizedTextService localizedTextService,
IShortStringHelper shortStringHelper,
HtmlImageSourceParser imageSourceParser,
HtmlLocalLinkParser localLinkParser,
RichTextEditorPastedImages pastedImages,
IJsonSerializer jsonSerializer,
IIOHelper ioHelper,
IHtmlSanitizer htmlSanitizer,
IBlockEditorElementTypeCache elementTypeCache,
IPropertyValidationService propertyValidationService,
DataValueReferenceFactoryCollection dataValueReferenceFactoryCollection,
IRichTextRequiredValidator richTextRequiredValidator)
: base(attribute, propertyEditors, dataTypeReadCache, localizedTextService, logger, shortStringHelper, jsonSerializer, ioHelper, dataValueReferenceFactoryCollection)
{
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
_localizedTextService = localizedTextService;
_imageSourceParser = imageSourceParser;
_localLinkParser = localLinkParser;
_pastedImages = pastedImages;
_htmlSanitizer = htmlSanitizer;
_elementTypeCache = elementTypeCache;
_richTextRequiredValidator = richTextRequiredValidator;
_jsonSerializer = jsonSerializer;
_logger = logger;
Validators.Add(new RichTextEditorBlockValidator(propertyValidationService, CreateBlockEditorValues(), elementTypeCache, jsonSerializer, logger));
}
public override IValueRequiredValidator RequiredValidator => _richTextRequiredValidator;
/// <inheritdoc />
public override object? ConfigurationObject
{

View File

@@ -0,0 +1,7 @@
using Umbraco.Cms.Core.PropertyEditors;
namespace Umbraco.Cms.Core.PropertyEditors.Validators;
internal interface IRichTextRequiredValidator : IValueRequiredValidator
{
}

View File

@@ -0,0 +1,30 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Core.PropertyEditors.Validators;
internal class RichTextRequiredValidator : RequiredValidator, IRichTextRequiredValidator
{
private readonly IJsonSerializer _jsonSerializer;
private readonly ILogger<RichTextRequiredValidator> _logger;
public RichTextRequiredValidator(ILocalizedTextService textService, IJsonSerializer jsonSerializer, ILogger<RichTextRequiredValidator> logger) : base(textService)
{
_jsonSerializer = jsonSerializer;
_logger = logger;
}
public override IEnumerable<ValidationResult> ValidateRequired(object? value, string? valueType) => base.ValidateRequired(GetValue(value), valueType);
private object? GetValue(object? value)
{
if(RichTextPropertyEditorHelper.TryParseRichTextEditorValue(value, _jsonSerializer, _logger, out RichTextEditorValue? richTextEditorValue))
{
return richTextEditorValue?.Markup;
}
return value;
}
}

View File

@@ -20,6 +20,15 @@ internal sealed class IndexedEntitySearchService : IIndexedEntitySearchService
}
public PagedModel<IEntitySlim> Search(UmbracoObjectTypes objectType, string query, int skip = 0, int take = 100, bool ignoreUserStartNodes = false)
=> Search(objectType, query, null, skip, take, ignoreUserStartNodes);
public PagedModel<IEntitySlim> Search(
UmbracoObjectTypes objectType,
string query,
Guid? parentId,
int skip = 0,
int take = 100,
bool ignoreUserStartNodes = false)
{
UmbracoEntityTypes entityType = objectType switch
{
@@ -37,7 +46,8 @@ internal sealed class IndexedEntitySearchService : IIndexedEntitySearchService
pageSize,
pageNumber,
out var totalFound,
ignoreUserStartNodes: ignoreUserStartNodes);
ignoreUserStartNodes: ignoreUserStartNodes,
searchFrom: parentId?.ToString());
Guid[] keys = searchResults.Select(
result =>

View File

@@ -1,3 +1,6 @@
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Runtime.Loader;
using System.Xml.Linq;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
@@ -354,8 +357,16 @@ public class PackagingService : IPackagingService
if (!string.IsNullOrEmpty(packageManifest.Version))
{
// Always use package version from manifest
installedPackage.Version = packageManifest.Version;
}
else if (string.IsNullOrEmpty(installedPackage.Version) &&
string.IsNullOrEmpty(installedPackage.PackageId) is false &&
TryGetAssemblyInformationalVersion(installedPackage.PackageId, out string? version))
{
// Use version of the assembly with the same name as the package ID
installedPackage.Version = version;
}
}
// Return all packages with an ID or name in the package manifest or package migrations
@@ -414,4 +425,20 @@ public class PackagingService : IPackagingService
return packageFile.CreateReadStream();
}
private static bool TryGetAssemblyInformationalVersion(string name, [NotNullWhen(true)] out string? version)
{
foreach (Assembly assembly in AssemblyLoadContext.Default.Assemblies)
{
AssemblyName assemblyName = assembly.GetName();
if (string.Equals(assemblyName.Name, name, StringComparison.OrdinalIgnoreCase) &&
assembly.TryGetInformationalVersion(out version))
{
return true;
}
}
version = null;
return false;
}
}

View File

@@ -351,6 +351,12 @@ internal class Property : PublishedPropertyBase
public CacheValue For(string? culture, string? segment)
{
// As noted on IPropertyValue, null value means invariant
// But as we need an actual string value to build a CompositeStringStringKey
// We need to convert null to empty
culture ??= string.Empty;
segment ??= string.Empty;
if (culture == string.Empty && segment == string.Empty)
{
return this;

View File

@@ -43,14 +43,14 @@ public class AspNetCoreHostingEnvironment : IHostingEnvironment
_webHostEnvironment = webHostEnvironment ?? throw new ArgumentNullException(nameof(webHostEnvironment));
_urlProviderMode = _webRoutingSettings.CurrentValue.UrlProviderMode;
SetSiteName(hostingSettings.CurrentValue.SiteName);
SetSiteNameAndDebugMode(hostingSettings.CurrentValue);
// We have to ensure that the OptionsMonitor is an actual options monitor since we have a hack
// where we initially use an OptionsMonitorAdapter, which doesn't implement OnChange.
// See summery of OptionsMonitorAdapter for more information.
if (hostingSettings is OptionsMonitor<HostingSettings>)
{
hostingSettings.OnChange(settings => SetSiteName(settings.SiteName));
hostingSettings.OnChange(settings => SetSiteNameAndDebugMode(settings));
}
ApplicationPhysicalPath = webHostEnvironment.ContentRootPath;
@@ -95,7 +95,7 @@ public class AspNetCoreHostingEnvironment : IHostingEnvironment
_hostingSettings.CurrentValue.ApplicationVirtualPath?.EnsureStartsWith('/') ?? "/";
/// <inheritdoc />
public bool IsDebugMode => _hostingSettings.CurrentValue.Debug;
public bool IsDebugMode { get; private set; }
public string LocalTempPath
{
@@ -188,8 +188,12 @@ public class AspNetCoreHostingEnvironment : IHostingEnvironment
}
}
private void SetSiteName(string? siteName) =>
SiteName = string.IsNullOrWhiteSpace(siteName)
private void SetSiteNameAndDebugMode(HostingSettings hostingSettings)
{
SiteName = string.IsNullOrWhiteSpace(hostingSettings.SiteName)
? _webHostEnvironment.ApplicationName
: siteName;
: hostingSettings.SiteName;
IsDebugMode = hostingSettings.Debug;
}
}

View File

@@ -30,6 +30,8 @@
<PackageReference Include="System.Net.Http" />
<!-- Take top-level depedendency on System.Text.RegularExpressions, because both Dazinator.Extensions.FileProviders and MiniProfiler.AspNetCore.Mvc depend on a vulnerable version -->
<PackageReference Include="System.Text.RegularExpressions" />
<!-- Both OpenIddict.AspNetCore, Npoco.SqlServer and Microsoft.EntityFrameworkCore.SqlServer bring in a vulnerable version of Microsoft.IdentityModel.JsonWebTokens -->
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens"/>
</ItemGroup>
<ItemGroup>

View File

@@ -2,7 +2,10 @@ import {
UmbBundleExtensionInitializer,
UmbServerExtensionRegistrator
} from "@umbraco-cms/backoffice/extension-api";
import { umbExtensionsRegistry } from "@umbraco-cms/backoffice/extension-registry";
import {
UmbAppEntryPointExtensionInitializer,
umbExtensionsRegistry
} from "@umbraco-cms/backoffice/extension-registry";
import type { UmbElement } from "@umbraco-cms/backoffice/element-api";
import { UmbControllerBase } from "@umbraco-cms/backoffice/class-api";
import { UUIIconRegistryEssential } from "@umbraco-cms/backoffice/external/uui";
@@ -21,6 +24,7 @@ export class UmbSlimBackofficeController extends UmbControllerBase {
constructor(host: UmbElement) {
super(host);
new UmbBundleExtensionInitializer(host, umbExtensionsRegistry);
new UmbAppEntryPointExtensionInitializer(host, umbExtensionsRegistry);
new UmbServerExtensionRegistrator(host, umbExtensionsRegistry).registerPublicExtensions();
this.#uuiIconRegistry.attach(host);

View File

@@ -3,7 +3,9 @@ WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.CreateUmbracoBuilder()
.AddBackOffice()
.AddWebsite()
#if UseDeliveryApi
.AddDeliveryApi()
#endif
.AddComposers()
.Build();
@@ -23,6 +25,9 @@ app.UseUmbraco()
})
.WithEndpoints(u =>
{
/*#if (UmbracoRelease = 'LTS')
u.UseInstallerEndpoints();
#endif */
u.UseBackOfficeEndpoints();
u.UseWebsiteEndpoints();
});

View File

@@ -19,6 +19,7 @@
<Content Include="UmbracoPackage\**" Exclude="bin;obj" />
<Content Include="UmbracoPackageRcl\**" Exclude="bin;obj" />
<Content Include="UmbracoProject\**" Exclude="bin;obj" />
<Content Include="UmbracoDockerCompose\**" Exclude="bin;obj"/>
<Content Include="..\src\Umbraco.Web.UI\Views\Partials\blocklist\**">
<Link>UmbracoProject\Views\Partials\blocklist\%(RecursiveDir)%(Filename)%(Extension)</Link>
<PackagePath>UmbracoProject\Views\Partials\blocklist</PackagePath>
@@ -47,7 +48,7 @@
</_TemplateJsonFiles>
</ItemGroup>
<Copy SourceFiles="@(_TemplateJsonFiles)" DestinationFiles="%(DestinationFile)" />
<JsonPathUpdateValue JsonFile="%(_TemplateJsonFiles.DestinationFile)" Path="$.symbols.UmbracoVersion.defaultValue" Value="&quot;$(PackageVersion)&quot;" />
<JsonPathUpdateValue JsonFile="%(_TemplateJsonFiles.DestinationFile)" Path="$.symbols.FinalVersion.parameters.cases.[0].value" Value="&quot;$(PackageVersion)&quot;" />
<ItemGroup>
<_PackageFiles Include="%(_TemplateJsonFiles.DestinationFile)">
<PackagePath>%(_TemplateJsonFiles.RelativeDir)</PackagePath>

View File

@@ -0,0 +1 @@
DB_PASSWORD=Password1234

View File

@@ -0,0 +1,24 @@
{
"$schema": "https://json.schemastore.org/dotnetcli.host.json",
"symbolInfo": {
"ProjectName": {
"longName": "ProjectName",
"shortName": "P"
},
"DatabasePassword": {
"longName": "DatabasePassword",
"shortName": "dbpw"
},
"Port":
{
"longName": "Port",
"shortName": "p"
}
},
"usageExamples": [
"dotnet new umbraco-compose -P MyProject",
"dotnet new umbraco-compose --ProjectName MyProject",
"dotnet new umbraco-compose -P -MyProject -dbpw MyStr0ngP@ssword",
"dotnet new umbraco-compose -P -MyProject --DatabasePassword MyStr0ngP@ssword"
]
}

View File

@@ -0,0 +1,23 @@
{
"$schema": "https://json.schemastore.org/ide.host.json",
"order": 0,
"icon": "../../icon.png",
"description": {
"id": "UmbracoDockerCompose",
"text": "Umbraco Docker Compose - Docker compose for Umbraco CMS and associated database"
},
"symbolInfo": [
{
"id": "ProjectName",
"isVisible": true
},
{
"id": "DatabasePassword",
"isVisible": true
},
{
"id": "Port",
"isVisible": true
}
]
}

View File

@@ -0,0 +1,49 @@
{
"$schema": "https://json.schemastore.org/template.json",
"author": "Umbraco HQ",
"classifications": [
"Web",
"CMS",
"Umbraco"
],
"name": "Umbraco Docker Compose",
"description": "Creates the prerequisites for developing Umbraco in Docker containers",
"groupIdentity": "Umbraco.Templates.UmbracoDockerCompose",
"identity": "Umbraco.Templates.UmbracoDockerCompose",
"shortName": "umbraco-compose",
"tags": {
"type": "item"
},
"symbols": {
"ProjectName": {
"type": "parameter",
"description": "The name of the project the Docker Compose file will be created for",
"datatype": "string",
"replaces": "UmbracoProject",
"isRequired": true
},
"DatabasePassword": {
"type": "parameter",
"description": "The password to the database, will be stored in .env file",
"datatype": "string",
"replaces": "Password1234",
"defaultValue": "Password1234"
},
"Port": {
"type": "parameter",
"description": "The port forward on the docker container, this is the port you use to access the site",
"datatype": "string",
"replaces": "TEMPLATE_PORT",
"defaultValue": "44372"
},
"ImageName": {
"type": "generated",
"generator": "casing",
"parameters": {
"source": "ProjectName",
"toLower": true
},
"replaces": "umbraco_image"
}
}
}

View File

@@ -0,0 +1,21 @@
FROM mcr.microsoft.com/azure-sql-edge:latest
ENV ACCEPT_EULA=Y
USER root
RUN mkdir /var/opt/sqlserver
RUN chown mssql /var/opt/sqlserver
ENV MSSQL_BACKUP_DIR="/var/opt/mssql"
ENV MSSQL_DATA_DIR="/var/opt/mssql/data"
ENV MSSQL_LOG_DIR="/var/opt/mssql/log"
EXPOSE 1433/tcp
COPY setup.sql /
COPY startup.sh /
COPY healthcheck.sh /
ENTRYPOINT [ "/bin/bash", "startup.sh" ]
CMD [ "/opt/mssql/bin/sqlservr" ]

View File

@@ -0,0 +1,15 @@
value="$(/opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P "$SA_PASSWORD" -d master -Q "SELECT state_desc FROM sys.databases WHERE name = 'umbracoDb'" | awk 'NR==3')"
# This checks for any non-zero length string, and $value will be empty when the database does not exist.
if [ -n "$value" ]
then
echo "ONLINE"
return 0 # With docker 0 = success
else
echo "OFFLINE"
return 1 # And 1 = unhealthy
fi
# This is useful for debugging
# echo "Value is:"
# echo "$value"

View File

@@ -0,0 +1,10 @@
USE [master]
GO
IF NOT EXISTS (SELECT * FROM sys.databases WHERE name = 'UmbracoDb')
BEGIN
CREATE DATABASE [umbracoDb]
END;
GO
USE UmbracoDb;

View File

@@ -0,0 +1,23 @@
#!/bin/bash
set -e
# Taken from: https://github.com/CarlSargunar/Umbraco-Docker-Workshop
if [ "$1" = '/opt/mssql/bin/sqlservr' ]; then
# If this is the container's first run, initialize the application database
if [ ! -f /tmp/app-initialized ]; then
# Initialize the application database asynchronously in a background process. This allows a) the SQL Server process to be the main process in the container, which allows graceful shutdown and other goodies, and b) us to only start the SQL Server process once, as opposed to starting, stopping, then starting it again.
function initialize_app_database() {
# Wait a bit for SQL Server to start. SQL Server's process doesn't provide a clever way to check if it's up or not, and it needs to be up before we can import the application database
sleep 15s
#run the setup script to create the DB and the schema in the DB
/opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P "$SA_PASSWORD" -d master -i setup.sql
# Note that the container has been initialized so future starts won't wipe changes to the data
touch /tmp/app-initialized
}
initialize_app_database &
fi
fi
exec "$@"

View File

@@ -0,0 +1,102 @@
services:
umb_database:
container_name: umbraco_image_database
build:
context: ./Database
environment:
SA_PASSWORD: ${DB_PASSWORD}
MSSQL_SA_PASSWORD: ${DB_PASSWORD}
ports:
- "1433:1433"
- "1434:1434"
volumes:
- umb_database:/var/opt/mssql
networks:
- umbnet
healthcheck:
# This healthcheck is to make sure that the database is up and running before the umbraco container starts.
# It works by querying the database for the state of the umbracoDb database, ensuring it exists.
test: ./healthcheck.sh
interval: 5m
timeout: 5s
retries: 3
start_period: 15s # Bootstrap duration, for this duration failures does not count towards max retries.
start_interval: 5s # How long after the health check has started to run the healthcheck again.
umbraco_image:
image: umbraco_image
environment:
- ASPNETCORE_ENVIRONMENT=Development
- ConnectionStrings__umbracoDbDSN=Server=umb_database;Database=umbracoDb;User Id=sa;Password=${DB_PASSWORD};TrustServerCertificate=true;
- ConnectionStrings__umbracoDbDSN_ProviderName=Microsoft.Data.SqlClient
volumes:
- umb_media:/app/wwwroot/media
- umb_scripts:/app/wwwroot/scripts
- umb_styles:/app/wwwroot/css
- umb_logs:/app/umbraco/Logs
- umb_views:/app/Views
- umb_data:/app/umbraco
- umb_models:/app/umbraco/models
build:
context: .
dockerfile: UmbracoProject/Dockerfile
args:
- BUILD_CONFIGURATION=Debug
depends_on:
umb_database:
condition: service_healthy
restart: always
ports:
- "TEMPLATE_PORT:8080"
networks:
- umbnet
develop:
# This allows you to run docker compose watch, after doing so the container will rebuild when the models are changed.
# Once a restart only feature is implemented (https://github.com/docker/compose/issues/11446)
# It would be really nice to add a restart only watch to \Views, since the file watchers for recompilation of Razor views does not work with docker.
watch:
- path: ./UmbracoProject/umbraco/models
action: rebuild
# These volumes are all made as bind mounts, meaning that they are bound to the host machine's file system.
# This is to better facilitate local development in the IDE, so the views, models, etc... are available in the IDE.
# This can be changed by removing the driver and driver_opts from the volumes.
volumes:
umb_media:
driver: local
driver_opts:
type: none
device: ./UmbracoProject/wwwroot/media
o: bind
umb_scripts:
driver: local
driver_opts:
type: none
device: ./UmbracoProject/wwwroot/scripts
o: bind
umb_styles:
driver: local
driver_opts:
type: none
device: ./UmbracoProject/wwwroot/css
o: bind
umb_logs:
umb_views:
driver: local
driver_opts:
type: none
device: ./UmbracoProject/Views
o: bind
umb_data:
umb_models:
driver: local
driver_opts:
type: none
device: ./UmbracoProject/umbraco/models
o: bind
umb_database:
networks:
umbnet:
driver: bridge

View File

@@ -0,0 +1,25 @@
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/.idea
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/azds.yaml
**/bin
**/charts
**/docker-compose*
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE
README.md

View File

@@ -8,12 +8,25 @@
},
"UmbracoVersion": {
"longName": "version",
"shortName": "v"
"shortName": "v",
"isHidden": true
},
"UmbracoRelease": {
"longName": "release",
"shortName": "r"
},
"UseHttpsRedirect": {
"longName": "use-https-redirect",
"shortName": ""
},
"UseDeliveryApi": {
"longName": "use-delivery-api",
"shortName": "da"
},
"Docker": {
"longName": "add-docker",
"shortName": ""
},
"SkipRestore": {
"longName": "no-restore",
"shortName": ""
@@ -58,6 +71,18 @@
"longName": "PackageTestSiteName",
"shortName": "p",
"isHidden": true
},
"ModelsBuilderMode": {
"longName": "models-mode",
"shortName": "mm"
},
"StarterKit": {
"longName": "starter-kit",
"shortName": "sk"
},
"DevelopmentMode": {
"longName": "development-mode",
"shortName": "dm"
}
},
"usageExamples": [

View File

@@ -9,11 +9,22 @@
"symbolInfo": [
{
"id": "UmbracoVersion",
"isVisible": true
"isVisible": false
},
{
"id": "UseHttpsRedirect",
"isVisible": true,
"persistenceScope": "templateGroup",
"defaultValue": "true"
},
{
"id": "UseDeliveryApi",
"isVisible": true,
"persistenceScope": "templateGroup"
},
{
"id": "ModelsBuilderMode",
"isVisible": true,
"persistenceScope": "templateGroup"
},
{
@@ -54,6 +65,23 @@
{
"id": "NoNodesViewPath",
"isVisible": true
},
{
"id": "Docker",
"isVisible": true
},
{
"id": "StarterKit",
"isVisible": true
},
{
"id": "UmbracoRelease",
"isVisible": true
},
{
"id": "DevelopmentMode",
"isVisible": true,
"defaultValue": "IDEDevelopment"
}
]
}

View File

@@ -0,0 +1,47 @@
{
"$schema": "https://json.schemastore.org/template.json",
"symbols": {
"StarterKit": {
"displayName": "Starter kit",
"type": "parameter",
"datatype": "choice",
"description": "Choose a starter kit to install.",
"defaultValue": "None",
"replaces": "STARTER_KIT_NAME",
// The choice here should be the name of the starter kit package, since it will be used directly for package reference.
"choices": [
{
"choice": "None",
"description": "No starter kit."
},
{
"choice": "Umbraco.TheStarterKit",
"description": "The Umbraco starter kit.",
"displayName": "The Starter Kit"
}
]
},
// Used to determine the version of the starter kit to install.
// there should be cases for Latest, LTS and Custom for every starterkit added above.
// This has the benefit that all maintenance of starter kits in template can be done from this file.
"StarterKitVersion": {
"type": "generated",
"generator": "switch",
"replaces": "STARTER_KIT_VERSION",
"parameters": {
"evaluator": "C++",
"datatype": "string",
"cases": [
{
"condition": "(StarterKit == 'Umbraco.TheStarterKit' && (UmbracoRelease == 'Latest' || UmbracoRelease == 'Custom'))",
"value": "14.0.0"
},
{
"condition": "(StarterKit == 'Umbraco.TheStarterKit' && UmbracoRelease == 'LTS')",
"value": "13.0.0"
}
]
}
}
}
}

View File

@@ -18,6 +18,7 @@
"sourceName": "UmbracoProject",
"defaultName": "UmbracoProject1",
"preferNameDirectory": true,
"additionalConfigFiles": [ "starterkits.template.json"],
"sources": [
{
"modifiers": [
@@ -26,6 +27,13 @@
"exclude": [
".gitignore"
]
},
{
"condition": "(!Docker)",
"exclude": [
"Dockerfile",
".dockerignore"
]
}
]
}
@@ -46,13 +54,72 @@
"defaultValue": "net8.0",
"replaces": "net8.0"
},
"UmbracoRelease": {
"displayName": "Umbraco Version",
"description": "The Umbraco release to use, either latest or latest long term supported",
"type": "parameter",
"datatype": "choice",
"defaultValue": "Latest",
"choices": [
{
"choice": "Latest",
"description": "The latest umbraco release"
},
{
"choice": "LTS",
"description": "The most recent long term supported version",
"displayName": "Long Term Supported"
}
],
"isRequired": false
},
"UmbracoVersion": {
"displayName": "Umbraco version",
"description": "The version of Umbraco.Cms to add as PackageReference.",
"displayName": "Custom Version",
"description": "The selected custom version of Umbraco, this is obsoleted, and will be removed in a future version of the template.",
"type": "parameter",
"datatype": "string",
"defaultValue": "*",
"replaces": "UMBRACO_VERSION_FROM_TEMPLATE"
"defaultValue": "null",
"replaces": "CUSTOM_VERSION",
"isRequired": false
},
"FinalVersion" : {
"type": "generated",
"generator": "switch",
"datatype": "text",
"description": "The calculated version of Umbraco to use",
"replaces": "UMBRACO_VERSION_FROM_TEMPLATE",
"parameters": {
"evaluator": "C++",
"datatype": "text",
"cases": [
{
"condition": "(UmbracoRelease == 'Latest')",
"value": "*"
},
{
"condition": "(UmbracoRelease == 'LTS')",
"value": "13.5.0"
}
]
}
},
"DotnetVersion":
{
"type": "generated",
"generator": "switch",
"datatype": "text",
"description": "Not relevant at the moment, but if we need to change the dotnet version based on the Umbraco version, we can do it here",
"replaces": "DOTNET_VERSION_FROM_TEMPLATE",
"parameters": {
"evaluator": "C++",
"datatype": "text",
"cases": [
{
"condition": "(true)",
"value": "net8.0"
}
]
}
},
"UseHttpsRedirect": {
"displayName": "Use HTTPS redirect",
@@ -61,6 +128,20 @@
"datatype": "bool",
"defaultValue": "false"
},
"UseDeliveryApi": {
"displayName": "Use Delivery API",
"description": "Enables the Delivery API",
"type": "parameter",
"datatype": "bool",
"defaultValue": "false"
},
"Docker": {
"displayName": "Add Docker file",
"description": "Adds a docker file to the project.",
"type": "parameter",
"datatype": "bool",
"defaultValue": "false"
},
"SkipRestore": {
"displayName": "Skip restore",
"description": "If specified, skips the automatic restore of the project on create.",
@@ -244,6 +325,58 @@
"defaultValue": "",
"replaces": "PACKAGE_PROJECT_NAME_FROM_TEMPLATE"
},
"DevelopmentMode": {
"type": "parameter",
"displayName": "Development mode",
"datatype": "choice",
"description": "Choose the development mode to use for the project.",
"defaultValue": "BackofficeDevelopment",
"choices": [
{
"choice": "BackofficeDevelopment",
"description": "Enables backoffice development, allowing you to develop from within the backoffice, this is the default behaviour.",
"displayName": "Backoffice Development"
},
{
"choice": "IDEDevelopment",
"description": "Configures appsettings.Development.json to Development runtime mode and SourceCodeAuto models builder mode, and configures appsettings.json to Production runtime mode, Nothing models builder mode, and enables UseHttps",
"displayName": "IDE Development"
}
]
},
"ModelsBuilderMode": {
"type": "parameter",
"displayName": "Models builder mode",
"datatype": "choice",
"description": "Choose the models builder mode to use for the project. When development mode is set to IDEDevelopment this only changes the models builder mode appsetttings.development.json",
"defaultValue": "Default",
"replaces": "MODELS_MODE",
"choices": [
{
"choice": "Default",
"description": "Let DevelopmentMode determine the models builder mode."
},
{
"choice": "InMemoryAuto",
"description": "Generate models in memory, automatically updating when a content type change, this means no need for app rebuild, however models are only available in views.",
"displayName": "In Memory Auto"
},
{
"choice": "SourceCodeManual",
"description": "Generate models as source code, only updating when requested manually, this means a interaction and rebuild is required when content type(s) change, however models are available in code.",
"displayName": "Source Code Manual"
},
{
"choice": "SourceCodeAuto",
"description": "Generate models as source code, automatically updating when a content type change, this means a rebuild is required when content type(s) change, however models are available in code.",
"displayName": "Source Code Auto"
},
{
"choice": "Nothing",
"description": "No models are generated, this is recommended for production assuming generated models are used for development."
}
]
},
"Namespace": {
"type": "derived",
"valueSource": "name",

View File

@@ -0,0 +1,33 @@
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
USER $APP_UID
WORKDIR /app
EXPOSE 8080
EXPOSE 8081
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY ["UmbracoProject/UmbracoProject.csproj", "UmbracoProject/"]
RUN dotnet restore "UmbracoProject/UmbracoProject.csproj"
COPY . .
WORKDIR "/src/UmbracoProject"
RUN dotnet build "UmbracoProject.csproj" -c $BUILD_CONFIGURATION -o /app/build
FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "UmbracoProject.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
# We need to make sure that the user running the app has write access to the umbraco folder, in order to write logs and other files.
# Since these are volumes they are created as root by the docker daemon.
USER root
RUN mkdir umbraco
RUN mkdir umbraco/Logs
RUN chown $APP_UID umbraco --recursive
#if (UmbracoRelease = 'LTS')
RUN chown $APP_UID wwwroot/umbraco --recursive
#endif
USER $APP_UID
ENTRYPOINT ["dotnet", "UmbracoProject.dll"]

View File

@@ -1,13 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>DOTNET_VERSION_FROM_TEMPLATE</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace Condition="'$(name)' != '$(name{-VALUE-FORMS-}safe_namespace)'">Umbraco.Cms.Web.UI</RootNamespace>
</PropertyGroup>
<ItemGroup>
<!--#if (UmbracoVersion != "null")
<PackageReference Include="Umbraco.Cms" Version="CUSTOM_VERSION" />
#else
<PackageReference Include="Umbraco.Cms" Version="UMBRACO_VERSION_FROM_TEMPLATE" />
#endif-->
<!--#if (StarterKit != "None") -->
<PackageReference Include="STARTER_KIT_NAME" Version="STARTER_KIT_VERSION"/>
<!--#endif -->
</ItemGroup>
<ItemGroup>
@@ -21,11 +28,13 @@
<CopyRazorGenerateFilesToPublishDirectory>true</CopyRazorGenerateFilesToPublishDirectory>
</PropertyGroup>
<!--#if (ModelsBuilderMode == "InMemoryAuto" || (DevelopmentMode == "BackofficeDevelopment" && ModelsBuilderMode == "Default")) -->
<PropertyGroup>
<!-- Remove RazorCompileOnBuild and RazorCompileOnPublish when not using ModelsMode InMemoryAuto -->
<RazorCompileOnBuild>false</RazorCompileOnBuild>
<RazorCompileOnPublish>false</RazorCompileOnPublish>
</PropertyGroup>
<!--#endif -->
<Import Project="..\PACKAGE_PROJECT_NAME_FROM_TEMPLATE\buildTransitive\PACKAGE_PROJECT_NAME_FROM_TEMPLATE.targets" Condition="'$(PackageProjectName)' != ''" />
<ItemGroup Condition="'$(PackageProjectName)' != ''">

View File

@@ -25,6 +25,11 @@
//#endif
"Umbraco": {
"CMS": {
//#if (UseHttpsRedirect || DevelopmentMode == "IDEDevelopment")
"Global": {
"UseHttps": false
},
//#endif
//#if (UsingUnattenedInstall)
"Unattended": {
"InstallUnattended": true,
@@ -36,12 +41,22 @@
"Content": {
"MacroErrors": "Throw"
},
//#if (DevelopmentMode == "IDEDevelopment")
"Runtime": {
"Mode": "Development"
},
//#if (ModelsBuilderMode == "Default")
"ModelsBuilder": {
"ModelsMode": "SourceCodeAuto"
},
////#else
//"ModelsBuilder": {
// "ModelsMode": "MODELS_MODE"
//},
//#endif
//#endif
"Hosting": {
"Debug": true
},
"RuntimeMinification": {
"UseInMemoryCache": true,
"CacheBuster": "Timestamp"
}
}
}

View File

@@ -20,7 +20,7 @@
"CMS": {
"Global": {
"Id": "TELEMETRYID_FROM_TEMPLATE",
//#if (UseHttpsRedirect)
//#if (UseHttpsRedirect || DevelopmentMode == "IDEDevelopment")
"UseHttps": true,
//#endif
//#if (HasNoNodesViewPath)
@@ -37,6 +37,24 @@
"Unattended": {
"UpgradeUnattended": true
},
//#if (UseDeliveryApi)
"DeliveryApi": {
"Enabled": true
},
//#endif
//#if (ModelsBuilderMode != "Default" && DevelopmentMode == "BackOfficeDevelopment")
"ModelsBuilder": {
"ModelsMode": "MODELS_MODE"
},
//#endif
//#if (DevelopmentMode == "IDEDevelopment")
"Runtime": {
"Mode": "Production"
},
"ModelsBuilder": {
"ModelsMode": "Nothing"
},
//#endif
"Security": {
"AllowConcurrentLogins": false
}

View File

@@ -4,8 +4,8 @@
<Import Project="$([MSBuild]::GetPathOfFileAbove(Directory.Packages.props, $(MSBuildThisFileDirectory)..))" />
<ItemGroup>
<!-- Microsoft packages -->
<PackageVersion Include="BenchmarkDotNet" Version="0.13.12" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.7" />
<PackageVersion Include="BenchmarkDotNet" Version="0.14.0" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.8" />
<PackageVersion Include="Microsoft.Extensions.Logging.Debug" Version="8.0.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
<PackageVersion Include="System.Data.DataSetExtensions" Version="4.5.0" />
@@ -17,7 +17,7 @@
<!-- Third-party packages -->
<PackageVersion Include="AutoFixture.AutoMoq" Version="4.18.1" />
<PackageVersion Include="AutoFixture.NUnit3" Version="4.18.1" />
<PackageVersion Include="Bogus" Version="35.6.0" />
<PackageVersion Include="Bogus" Version="35.6.1" />
<PackageVersion Include="Moq" Version="4.18.4" />
<PackageVersion Include="NUnit" Version="3.14.0" />
<PackageVersion Include="NUnit3TestAdapter" Version="4.5.0" PrivateAssets="all" />

View File

@@ -7,24 +7,18 @@
"name": "acceptancetest",
"hasInstallScript": true,
"dependencies": {
"@umbraco/json-models-builders": "^2.0.14",
"@umbraco/playwright-testhelpers": "^2.0.0-beta.73",
"@umbraco/json-models-builders": "^2.0.20",
"@umbraco/playwright-testhelpers": "^2.0.0-beta.84",
"camelize": "^1.0.0",
"dotenv": "^16.3.1",
"faker": "^4.1.0",
"form-data": "^4.0.0",
"node-fetch": "^2.6.7",
"xhr2": "^0.2.1"
"node-fetch": "^2.6.7"
},
"devDependencies": {
"@playwright/test": "^1.43",
"@types/node": "^20.9.0",
"del": "^6.0.0",
"ncp": "^2.0.0",
"prompt": "^1.2.0",
"tslib": "^2.4.0",
"typescript": "^4.8.3",
"wait-on": "^7.2.0"
"typescript": "^4.8.3"
}
},
"node_modules/@colors/colors": {
@@ -36,190 +30,55 @@
"node": ">=0.1.90"
}
},
"node_modules/@hapi/hoek": {
"version": "9.3.0",
"resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz",
"integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==",
"dev": true
},
"node_modules/@hapi/topo": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz",
"integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==",
"dev": true,
"dependencies": {
"@hapi/hoek": "^9.0.0"
}
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
"integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
"dev": true,
"dependencies": {
"@nodelib/fs.stat": "2.0.5",
"run-parallel": "^1.1.9"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/@nodelib/fs.stat": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
"integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
"dev": true,
"engines": {
"node": ">= 8"
}
},
"node_modules/@nodelib/fs.walk": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
"integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
"dev": true,
"dependencies": {
"@nodelib/fs.scandir": "2.1.5",
"fastq": "^1.6.0"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/@playwright/test": {
"version": "1.43.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.43.1.tgz",
"integrity": "sha512-HgtQzFgNEEo4TE22K/X7sYTYNqEMMTZmFS8kTq6m8hXj+m1D8TgwgIbumHddJa9h4yl4GkKb8/bgAl2+g7eDgA==",
"version": "1.46.0",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.46.0.tgz",
"integrity": "sha512-/QYft5VArOrGRP5pgkrfKksqsKA6CEFyGQ/gjNe6q0y4tZ1aaPfq4gIjudr1s3D+pXyrPRdsy4opKDrjBabE5w==",
"dev": true,
"dependencies": {
"playwright": "1.43.1"
"playwright": "1.46.0"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=16"
"node": ">=18"
}
},
"node_modules/@sideway/address": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.4.tgz",
"integrity": "sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==",
"dev": true,
"dependencies": {
"@hapi/hoek": "^9.0.0"
}
},
"node_modules/@sideway/formula": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz",
"integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==",
"dev": true
},
"node_modules/@sideway/pinpoint": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz",
"integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==",
"dev": true
},
"node_modules/@types/node": {
"version": "20.10.7",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.7.tgz",
"integrity": "sha512-fRbIKb8C/Y2lXxB5eVMj4IU7xpdox0Lh8bUPEdtLysaylsml1hOOx1+STloRs/B9nf7C6kPRmmg/V7aQW7usNg==",
"version": "20.14.15",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.15.tgz",
"integrity": "sha512-Fz1xDMCF/B00/tYSVMlmK7hVeLh7jE5f3B7X1/hmV0MJBwE27KlS7EvD/Yp+z1lm8mVhwV5w+n8jOZG8AfTlKw==",
"dev": true,
"dependencies": {
"undici-types": "~5.26.4"
}
},
"node_modules/@umbraco/json-models-builders": {
"version": "2.0.14",
"resolved": "https://registry.npmjs.org/@umbraco/json-models-builders/-/json-models-builders-2.0.14.tgz",
"integrity": "sha512-fP6hVSSph1iFQ1c65UH80AM6QK3r1CzuIiYOvZh+QOoVzpVFtH1VCHL3J2k8AwaHWLVAEopcvtvH5kkl7Luqww==",
"version": "2.0.20",
"resolved": "https://registry.npmjs.org/@umbraco/json-models-builders/-/json-models-builders-2.0.20.tgz",
"integrity": "sha512-LmTtklne1HlhMr1nALA+P5FrjIC9jL3A6Pcxj4dy+IPnTgnU2vMYaQIfE8wwz5Z5fZ5AAhWx/Zpdi8xCTbVSuQ==",
"license": "MIT",
"dependencies": {
"camelize": "^1.0.1"
}
},
"node_modules/@umbraco/playwright-testhelpers": {
"version": "2.0.0-beta.73",
"resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-2.0.0-beta.73.tgz",
"integrity": "sha512-CCURatZa7Ipui9ZTqdZmkpx89Sr5AJLoXogniq6mv84mSVGeCQFYzHvw1op2UE8nkKY5/wyqfrCihjrbW5v8lw==",
"version": "2.0.0-beta.84",
"resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-2.0.0-beta.84.tgz",
"integrity": "sha512-vH13Lg48knTkkLVTwhMXUKTOdjtmixFj0wF5Qhgb++13u4AVDb+oW+TbFwTjSYaLeNMraq5Uhwmto/XuJPs2Rw==",
"license": "MIT",
"dependencies": {
"@umbraco/json-models-builders": "2.0.14",
"@umbraco/json-models-builders": "2.0.20",
"node-fetch": "^2.6.7"
}
},
"node_modules/aggregate-error": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz",
"integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==",
"dev": true,
"dependencies": {
"clean-stack": "^2.0.0",
"indent-string": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/array-union": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
"integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/async": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz",
"integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==",
"dev": true
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/axios": {
"version": "1.7.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz",
"integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==",
"dev": true,
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true
},
"node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/braces": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"dev": true,
"dependencies": {
"fill-range": "^7.1.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/camelize": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz",
@@ -228,15 +87,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/clean-stack": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
"integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==",
"dev": true,
"engines": {
"node": ">=6"
}
},
"node_modules/colors": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz",
@@ -246,23 +96,6 @@
"node": ">=0.1.90"
}
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"dev": true
},
"node_modules/cycle": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/cycle/-/cycle-1.0.3.tgz",
@@ -272,57 +105,15 @@
"node": ">=0.4.0"
}
},
"node_modules/del": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/del/-/del-6.1.1.tgz",
"integrity": "sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg==",
"dev": true,
"dependencies": {
"globby": "^11.0.1",
"graceful-fs": "^4.2.4",
"is-glob": "^4.0.1",
"is-path-cwd": "^2.2.0",
"is-path-inside": "^3.0.2",
"p-map": "^4.0.0",
"rimraf": "^3.0.2",
"slash": "^3.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/dir-glob": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
"integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
"dev": true,
"dependencies": {
"path-type": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/dotenv": {
"version": "16.3.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz",
"integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==",
"version": "16.4.5",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz",
"integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/motdotla/dotenv?sponsor=1"
"url": "https://dotenvx.com"
}
},
"node_modules/eyes": {
@@ -334,343 +125,24 @@
"node": "> 0.1.90"
}
},
"node_modules/faker": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/faker/-/faker-4.1.0.tgz",
"integrity": "sha512-ILKg69P6y/D8/wSmDXw35Ly0re8QzQ8pMfBCflsGiZG2ZjMUNLYNexA6lz5pkmJlepVdsiDFUxYAzPQ9/+iGLA=="
},
"node_modules/fast-glob": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
"integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==",
"dev": true,
"dependencies": {
"@nodelib/fs.stat": "^2.0.2",
"@nodelib/fs.walk": "^1.2.3",
"glob-parent": "^5.1.2",
"merge2": "^1.3.0",
"micromatch": "^4.0.4"
},
"engines": {
"node": ">=8.6.0"
}
},
"node_modules/fastq": {
"version": "1.16.0",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.16.0.tgz",
"integrity": "sha512-ifCoaXsDrsdkWTtiNJX5uzHDsrck5TzfKKDcuFFTIrrc/BS076qgEIfoIy1VeZqViznfKiysPYTh/QeHtnIsYA==",
"dev": true,
"dependencies": {
"reusify": "^1.0.4"
}
},
"node_modules/fill-range": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"dev": true,
"dependencies": {
"to-regex-range": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/follow-redirects": {
"version": "1.15.6",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
"integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
"dev": true,
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
"dev": true
},
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"dev": true,
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
"engines": {
"node": "*"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/glob-parent": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true,
"dependencies": {
"is-glob": "^4.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/globby": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz",
"integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==",
"dev": true,
"dependencies": {
"array-union": "^2.1.0",
"dir-glob": "^3.0.1",
"fast-glob": "^3.2.9",
"ignore": "^5.2.0",
"merge2": "^1.4.1",
"slash": "^3.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"dev": true
},
"node_modules/ignore": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz",
"integrity": "sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==",
"dev": true,
"engines": {
"node": ">= 4"
}
},
"node_modules/indent-string": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
"integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
"dev": true,
"dependencies": {
"once": "^1.3.0",
"wrappy": "1"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"dev": true
},
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-glob": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"dev": true,
"dependencies": {
"is-extglob": "^2.1.1"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true,
"engines": {
"node": ">=0.12.0"
}
},
"node_modules/is-path-cwd": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz",
"integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==",
"dev": true,
"engines": {
"node": ">=6"
}
},
"node_modules/is-path-inside": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
"integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/isstream": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
"integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==",
"dev": true
},
"node_modules/joi": {
"version": "17.11.0",
"resolved": "https://registry.npmjs.org/joi/-/joi-17.11.0.tgz",
"integrity": "sha512-NgB+lZLNoqISVy1rZocE9PZI36bL/77ie924Ri43yEvi9GUUMPeyVIr8KdFTMUlby1p0PBYMk9spIxEUQYqrJQ==",
"dev": true,
"dependencies": {
"@hapi/hoek": "^9.0.0",
"@hapi/topo": "^5.0.0",
"@sideway/address": "^4.1.3",
"@sideway/formula": "^3.0.1",
"@sideway/pinpoint": "^2.0.0"
}
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true
},
"node_modules/merge2": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
"dev": true,
"engines": {
"node": ">= 8"
}
},
"node_modules/micromatch": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
"integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
"dev": true,
"dependencies": {
"braces": "^3.0.2",
"picomatch": "^2.3.1"
},
"engines": {
"node": ">=8.6"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/minimist": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"dev": true,
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/mute-stream": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz",
"integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==",
"dev": true
},
"node_modules/ncp": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz",
"integrity": "sha512-zIdGUrPRFTUELUvr3Gmc7KZ2Sw/h1PiVM0Af/oHB6zgnV1ikqSfRk+TOufi79aHYCW3NiOXmr1BP5nWbzojLaA==",
"dev": true,
"bin": {
"ncp": "bin/ncp"
}
},
"node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
@@ -690,88 +162,34 @@
}
}
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"dev": true,
"dependencies": {
"wrappy": "1"
}
},
"node_modules/p-map": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz",
"integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==",
"dev": true,
"dependencies": {
"aggregate-error": "^3.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/path-type": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
"integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true,
"engines": {
"node": ">=8.6"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/playwright": {
"version": "1.43.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.43.1.tgz",
"integrity": "sha512-V7SoH0ai2kNt1Md9E3Gwas5B9m8KR2GVvwZnAI6Pg0m3sh7UvgiYhRrhsziCmqMJNouPckiOhk8T+9bSAK0VIA==",
"version": "1.46.0",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.46.0.tgz",
"integrity": "sha512-XYJ5WvfefWONh1uPAUAi0H2xXV5S3vrtcnXe6uAOgdGi3aSpqOSXX08IAjXW34xitfuOJsvXU5anXZxPSEQiJw==",
"dev": true,
"dependencies": {
"playwright-core": "1.43.1"
"playwright-core": "1.46.0"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=16"
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.43.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.43.1.tgz",
"integrity": "sha512-EI36Mto2Vrx6VF7rm708qSnesVQKbxEWvPrfA1IPY6HgczBplDx7ENtx+K2n4kJ41sLLkuGfmb0ZLSSXlDhqPg==",
"version": "1.46.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.46.0.tgz",
"integrity": "sha512-9Y/d5UIwuJk8t3+lhmMSAJyNP1BUC/DqP3cQJDQQL/oWqAiuPTLgy7Q5dzglmTLwcBRdetzgNM/gni7ckfTr6A==",
"dev": true,
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=16"
"node": ">=18"
}
},
"node_modules/prompt": {
@@ -790,32 +208,6 @@
"node": ">= 6.0.0"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"dev": true
},
"node_modules/queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
]
},
"node_modules/read": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz",
@@ -828,16 +220,6 @@
"node": ">=0.8"
}
},
"node_modules/reusify": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
"integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
"dev": true,
"engines": {
"iojs": ">=1.0.0",
"node": ">=0.10.0"
}
},
"node_modules/revalidator": {
"version": "0.1.8",
"resolved": "https://registry.npmjs.org/revalidator/-/revalidator-0.1.8.tgz",
@@ -847,62 +229,6 @@
"node": ">= 0.4.0"
}
},
"node_modules/rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
"dev": true,
"dependencies": {
"glob": "^7.1.3"
},
"bin": {
"rimraf": "bin.js"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/run-parallel": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
"integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"dependencies": {
"queue-microtask": "^1.2.2"
}
},
"node_modules/rxjs": {
"version": "7.8.1",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
"integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
"dev": true,
"dependencies": {
"tslib": "^2.1.0"
}
},
"node_modules/slash": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
"integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/stack-trace": {
"version": "0.0.10",
"resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz",
@@ -912,27 +238,15 @@
"node": "*"
}
},
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true,
"dependencies": {
"is-number": "^7.0.0"
},
"engines": {
"node": ">=8.0"
}
},
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
},
"node_modules/tslib": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==",
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz",
"integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==",
"dev": true
},
"node_modules/typescript": {
@@ -954,25 +268,6 @@
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"dev": true
},
"node_modules/wait-on": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/wait-on/-/wait-on-7.2.0.tgz",
"integrity": "sha512-wCQcHkRazgjG5XoAq9jbTMLpNIjoSlZslrJ2+N9MxDsGEv1HnFoVjOCexL0ESva7Y9cu350j+DWADdk54s4AFQ==",
"dev": true,
"dependencies": {
"axios": "^1.6.1",
"joi": "^17.11.0",
"lodash": "^4.17.21",
"minimist": "^1.2.8",
"rxjs": "^7.8.1"
},
"bin": {
"wait-on": "bin/wait-on"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
@@ -1012,20 +307,6 @@
"dependencies": {
"lodash": "^4.17.14"
}
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"dev": true
},
"node_modules/xhr2": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/xhr2/-/xhr2-0.2.1.tgz",
"integrity": "sha512-sID0rrVCqkVNUn8t6xuv9+6FViXjUVXq8H5rWOH2rz9fDNQEd4g0EA2XlcEdJXRz5BMEn4O1pJFdT+z4YHhoWw==",
"engines": {
"node": ">= 6"
}
}
}
}

View File

@@ -13,21 +13,15 @@
"devDependencies": {
"@playwright/test": "^1.43",
"@types/node": "^20.9.0",
"del": "^6.0.0",
"ncp": "^2.0.0",
"prompt": "^1.2.0",
"tslib": "^2.4.0",
"typescript": "^4.8.3",
"wait-on": "^7.2.0"
"typescript": "^4.8.3"
},
"dependencies": {
"@umbraco/json-models-builders": "^2.0.14",
"@umbraco/playwright-testhelpers": "^2.0.0-beta.73",
"@umbraco/json-models-builders": "^2.0.20",
"@umbraco/playwright-testhelpers": "^2.0.0-beta.84",
"camelize": "^1.0.0",
"dotenv": "^16.3.1",
"faker": "^4.1.0",
"form-data": "^4.0.0",
"node-fetch": "^2.6.7",
"xhr2": "^0.2.1"
"node-fetch": "^2.6.7"
}
}

View File

@@ -9,6 +9,7 @@ test.describe('DataType tests', () => {
const dataTypeName = 'TestDataType';
const folderName = 'TestDataTypeFolder';
const editorAlias = 'Umbraco.DateTime';
const editorUiAlias = 'Umb.PropertyEditorUi.DatePicker';
const dataTypeData = [
{
"alias": "tester",
@@ -29,7 +30,7 @@ test.describe('DataType tests', () => {
test('can create dataType', async ({umbracoApi}) => {
// Act
dataTypeId = await umbracoApi.dataType.create(dataTypeName, editorAlias, dataTypeData);
dataTypeId = await umbracoApi.dataType.create(dataTypeName, editorAlias, editorUiAlias, dataTypeData);
// Assert
expect(umbracoApi.dataType.doesExist(dataTypeId)).toBeTruthy();
@@ -37,7 +38,7 @@ test.describe('DataType tests', () => {
test('can update dataType', async ({umbracoApi}) => {
// Arrange
dataTypeId = await umbracoApi.dataType.create(dataTypeName, editorAlias, []);
dataTypeId = await umbracoApi.dataType.create(dataTypeName, editorAlias, editorUiAlias, []);
const dataType = await umbracoApi.dataType.get(dataTypeId);
dataType.values = dataTypeData;
@@ -52,7 +53,7 @@ test.describe('DataType tests', () => {
test('can delete dataType', async ({umbracoApi}) => {
// Arrange
dataTypeId = await umbracoApi.dataType.create(dataTypeName, editorAlias, dataTypeData);
dataTypeId = await umbracoApi.dataType.create(dataTypeName, editorAlias, editorUiAlias, dataTypeData);
expect(await umbracoApi.dataType.doesExist(dataTypeId)).toBeTruthy();
// Act
@@ -65,7 +66,7 @@ test.describe('DataType tests', () => {
test('can move a dataType to a folder', async ({umbracoApi}) => {
// Arrange
await umbracoApi.dataType.ensureNameNotExists(folderName);
dataTypeId = await umbracoApi.dataType.create(dataTypeName, editorAlias, dataTypeData);
dataTypeId = await umbracoApi.dataType.create(dataTypeName, editorAlias, editorUiAlias, dataTypeData);
expect(await umbracoApi.dataType.doesNameExist(dataTypeName)).toBeTruthy();
dataTypeFolderId = await umbracoApi.dataType.createFolder(folderName);
expect(await umbracoApi.dataType.doesFolderExist(dataTypeFolderId)).toBeTruthy();
@@ -82,7 +83,7 @@ test.describe('DataType tests', () => {
test('can copy a dataType to a folder', async ({umbracoApi}) => {
// Arrange
await umbracoApi.dataType.ensureNameNotExists(folderName);
dataTypeId = await umbracoApi.dataType.create(dataTypeName, editorAlias, dataTypeData);
dataTypeId = await umbracoApi.dataType.create(dataTypeName, editorAlias, editorUiAlias, dataTypeData);
dataTypeFolderId = await umbracoApi.dataType.createFolder(folderName);
const dataType = await umbracoApi.dataType.get(dataTypeId);

View File

@@ -1,4 +1,4 @@
import { expect } from '@playwright/test';
import {expect} from '@playwright/test';
import {ConstantHelper, test} from '@umbraco/playwright-testhelpers';
let documentTypeId = '';
@@ -67,7 +67,7 @@ test('can open document type', async ({umbracoApi, umbracoUi}) => {
test('can open template', async ({umbracoApi, umbracoUi}) => {
// Arrange
const templateName = "TestTemplateForContent";
const templateName = 'TestTemplateForContent';
await umbracoApi.template.ensureNameNotExists(templateName);
const templateId = await umbracoApi.template.createDefaultTemplate(templateName);
documentTypeId = await umbracoApi.documentType.createDocumentTypeWithAllowedTemplate(documentTypeName, templateId, true);
@@ -88,8 +88,8 @@ test('can open template', async ({umbracoApi, umbracoUi}) => {
test('can change template', async ({umbracoApi, umbracoUi}) => {
// Arrange
const firstTemplateName = "TestTemplateOneForContent";
const secondTemplateName = "TestTemplateTwoForContent";
const firstTemplateName = 'TestTemplateOneForContent';
const secondTemplateName = 'TestTemplateTwoForContent';
await umbracoApi.template.ensureNameNotExists(firstTemplateName);
await umbracoApi.template.ensureNameNotExists(secondTemplateName);
const firstTemplateId = await umbracoApi.template.createDefaultTemplate(firstTemplateName);
@@ -115,8 +115,8 @@ test('can change template', async ({umbracoApi, umbracoUi}) => {
test('cannot change to a template that is not allowed in the document type', async ({umbracoApi, umbracoUi}) => {
// Arrange
const firstTemplateName = "TestTemplateOneForContent";
const secondTemplateName = "TestTemplateTwoForContent";
const firstTemplateName = 'TestTemplateOneForContent';
const secondTemplateName = 'TestTemplateTwoForContent';
await umbracoApi.template.ensureNameNotExists(firstTemplateName);
await umbracoApi.template.ensureNameNotExists(secondTemplateName);
const firstTemplateId = await umbracoApi.template.createDefaultTemplate(firstTemplateName);

View File

@@ -1,4 +1,4 @@
import { ConstantHelper, test, AliasHelper } from '@umbraco/playwright-testhelpers';
import {ConstantHelper, test, AliasHelper} from '@umbraco/playwright-testhelpers';
import {expect} from "@playwright/test";
const contentName = 'TestContent';
@@ -18,6 +18,7 @@ test.afterEach(async ({umbracoApi}) => {
test('can create content with the checkbox list data type', async ({umbracoApi, umbracoUi}) => {
// Arrange
const expectedState = 'Draft';
const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName);
await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id);
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
@@ -33,26 +34,27 @@ test('can create content with the checkbox list data type', async ({umbracoApi,
await umbracoUi.content.isSuccessNotificationVisible();
expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy();
const contentData = await umbracoApi.document.getByName(contentName);
expect(contentData.variants[0].state).toBe(expectedState);
expect(contentData.values).toEqual([]);
});
test('can publish content with the checkbox list data type', async ({umbracoApi, umbracoUi}) => {
// Arrange
const expectedState = 'Published';
const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName);
await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id);
const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id);
await umbracoApi.document.createDefaultDocument(contentName, documentTypeId);
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
// Act
await umbracoUi.content.clickActionsMenuAtRoot();
await umbracoUi.content.clickCreateButton();
await umbracoUi.content.chooseDocumentType(documentTypeName);
await umbracoUi.content.enterContentName(contentName);
await umbracoUi.content.goToContentWithName(contentName);
await umbracoUi.content.clickSaveAndPublishButton();
// Assert
await umbracoUi.content.doesSuccessNotificationsHaveCount(2);
expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy();
const contentData = await umbracoApi.document.getByName(contentName);
expect(contentData.variants[0].state).toBe(expectedState);
expect(contentData.values).toEqual([]);
});
@@ -61,14 +63,12 @@ test('can create content with the custom checkbox list data type', async ({umbra
const customDataTypeName = 'CustomCheckboxList';
const optionValues = ['testOption1', 'testOption2'];
const customDataTypeId = await umbracoApi.dataType.createCheckboxListDataType(customDataTypeName, optionValues);
await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId);
const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId);
await umbracoApi.document.createDefaultDocument(contentName, documentTypeId);
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
// Act
await umbracoUi.content.clickActionsMenuAtRoot();
await umbracoUi.content.clickCreateButton();
await umbracoUi.content.chooseDocumentType(documentTypeName);
await umbracoUi.content.enterContentName(contentName);
await umbracoUi.content.goToContentWithName(contentName);
await umbracoUi.content.chooseCheckboxListOption(optionValues[0]);
await umbracoUi.content.clickSaveAndPublishButton();

View File

@@ -8,11 +8,10 @@ const contentPickerDocumentTypeName = 'DocumentTypeForContentPicker';
const contentPickerName = 'TestContentPicker';
let contentPickerDocumentTypeId = '';
test.beforeEach(async ({umbracoApi, umbracoUi}) => {
test.beforeEach(async ({umbracoApi}) => {
await umbracoApi.documentType.ensureNameNotExists(documentTypeName);
await umbracoApi.document.ensureNameNotExists(contentName);
contentPickerDocumentTypeId = await umbracoApi.documentType.createDefaultDocumentTypeWithAllowAsRoot(contentPickerDocumentTypeName);
await umbracoUi.goToBackOffice();
});
test.afterEach(async ({umbracoApi}) => {
@@ -24,9 +23,11 @@ test.afterEach(async ({umbracoApi}) => {
test('can create content with the content picker datatype', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => {
// Arrange
const expectedState = 'Draft';
const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName);
await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id);
const contentPickerId = await umbracoApi.document.createDefaultDocument(contentPickerName, contentPickerDocumentTypeId);
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
// Act
@@ -38,24 +39,25 @@ test('can create content with the content picker datatype', {tag: '@smoke'}, asy
await umbracoUi.content.clickSaveButton();
// Assert
await umbracoUi.content.doesSuccessNotificationsHaveCount(1);
await umbracoUi.content.isSuccessNotificationVisible();
expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy();
const contentData = await umbracoApi.document.getByName(contentName);
expect(contentData.variants[0].state).toBe(expectedState);
expect(contentData.values[0].value).toEqual(contentPickerId);
});
test('can publish content with the content picker data type', async ({umbracoApi, umbracoUi}) => {
// Arrange
const expectedState = 'Published';
const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName);
await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id);
const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id);
await umbracoApi.document.createDefaultDocument(contentName, documentTypeId);
const contentPickerId = await umbracoApi.document.createDefaultDocument(contentPickerName, contentPickerDocumentTypeId);
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
// Act
await umbracoUi.content.clickActionsMenuAtRoot();
await umbracoUi.content.clickCreateButton();
await umbracoUi.content.chooseDocumentType(documentTypeName);
await umbracoUi.content.enterContentName(contentName);
await umbracoUi.content.goToContentWithName(contentName);
await umbracoUi.content.addContentPicker(contentPickerName);
await umbracoUi.content.clickSaveAndPublishButton();
@@ -63,6 +65,7 @@ test('can publish content with the content picker data type', async ({umbracoApi
await umbracoUi.content.doesSuccessNotificationsHaveCount(2);
expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy();
const contentData = await umbracoApi.document.getByName(contentName);
expect(contentData.variants[0].state).toBe(expectedState);
expect(contentData.values[0].value).toEqual(contentPickerId);
});
@@ -70,15 +73,15 @@ test('can open content picker in the content', async ({umbracoApi, umbracoUi}) =
// Arrange
const customDataTypeName = 'CustomContentPicker';
const customDataTypeId = await umbracoApi.dataType.createContentPickerDataTypeWithShowOpenButton(customDataTypeName);
await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId);
const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId);
await umbracoApi.document.createDefaultDocument(contentName, documentTypeId);
// Create content to pick
await umbracoApi.document.createDefaultDocument(contentPickerName, contentPickerDocumentTypeId);
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
// Act
await umbracoUi.content.clickActionsMenuAtRoot();
await umbracoUi.content.clickCreateButton();
await umbracoUi.content.chooseDocumentType(documentTypeName);
await umbracoUi.content.enterContentName(contentName);
await umbracoUi.content.goToContentWithName(contentName);
await umbracoUi.content.addContentPicker(contentPickerName);
// Assert
@@ -97,19 +100,18 @@ test('can choose start node for the content picker in the content', async ({umbr
const childContentPickerName = 'TestChildContentPicker';
await umbracoApi.documentType.ensureNameNotExists(childContentPickerDocumentTypeName);
const childContentPickerDocumentTypeId = await umbracoApi.documentType.createDefaultDocumentType(childContentPickerDocumentTypeName);
const contentPickerDocumentTypeId = await umbracoApi.documentType.createDocumentTypeWithAllowedChildNode(contentPickerName, childContentPickerDocumentTypeId);
contentPickerDocumentTypeId = await umbracoApi.documentType.createDocumentTypeWithAllowedChildNode(contentPickerName, childContentPickerDocumentTypeId);
const contentPickerId = await umbracoApi.document.createDefaultDocument(contentPickerName, contentPickerDocumentTypeId);
await umbracoApi.document.createDefaultDocumentWithParent(childContentPickerName, childContentPickerDocumentTypeId, contentPickerId);
// Create a custom content picker with start node
const customDataTypeId = await umbracoApi.dataType.createContentPickerDataTypeWithStartNode(customDataTypeName, contentPickerId);
await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId);
const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId);
await umbracoApi.document.createDefaultDocument(contentName, documentTypeId);
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
// Act
await umbracoUi.content.clickActionsMenuAtRoot();
await umbracoUi.content.clickCreateButton();
await umbracoUi.content.chooseDocumentType(documentTypeName);
await umbracoUi.content.enterContentName(contentName);
await umbracoUi.content.goToContentWithName(contentName);
await umbracoUi.content.clickChooseButton();
// Assert
@@ -128,19 +130,18 @@ test.skip('can ignore user start node for the content picker in the content', as
const childContentPickerName = 'TestChildContentPicker';
await umbracoApi.documentType.ensureNameNotExists(childContentPickerDocumentTypeName);
const childContentPickerDocumentTypeId = await umbracoApi.documentType.createDefaultDocumentType(childContentPickerDocumentTypeName);
const contentPickerDocumentTypeId = await umbracoApi.documentType.createDocumentTypeWithAllowedChildNode(contentPickerName, childContentPickerDocumentTypeId);
contentPickerDocumentTypeId = await umbracoApi.documentType.createDocumentTypeWithAllowedChildNode(contentPickerName, childContentPickerDocumentTypeId);
const contentPickerId = await umbracoApi.document.createDefaultDocument(contentPickerName, contentPickerDocumentTypeId);
await umbracoApi.document.createDefaultDocumentWithParent(childContentPickerName, childContentPickerDocumentTypeId, contentPickerId);
// Create a custom content picker with the setting "ignore user start node" is enable
const customDataTypeId = await umbracoApi.dataType.createContentPickerDataTypeWithIgnoreUserStartNodes(customDataTypeName, contentPickerId);
await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId);
const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId);
await umbracoApi.document.createDefaultDocument(contentName, documentTypeId);
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
// Act
await umbracoUi.content.clickActionsMenuAtRoot();
await umbracoUi.content.clickCreateButton();
await umbracoUi.content.chooseDocumentType(documentTypeName);
await umbracoUi.content.enterContentName(contentName);
await umbracoUi.content.goToContentWithName(contentName);
await umbracoUi.content.clickChooseButton();
// Assert
@@ -158,6 +159,7 @@ test('can remove content picker in the content', async ({umbracoApi, umbracoUi})
const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id);
const contentPickerId = await umbracoApi.document.createDefaultDocument(contentPickerName, contentPickerDocumentTypeId);
await umbracoApi.document.createDocumentWithContentPicker(contentName, documentTypeId, contentPickerId);
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
// Act

View File

@@ -1,4 +1,4 @@
import { ConstantHelper, test, AliasHelper } from '@umbraco/playwright-testhelpers';
import {ConstantHelper, test, AliasHelper} from '@umbraco/playwright-testhelpers';
import {expect} from "@playwright/test";
const contentName = 'TestContent';
@@ -20,6 +20,7 @@ for (const dataTypeName of dataTypeNames) {
test(`can create content with the ${dataTypeName} data type`, async ({umbracoApi, umbracoUi}) => {
// Arrange
const expectedState = 'Draft';
const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName);
await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id);
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
@@ -35,26 +36,27 @@ for (const dataTypeName of dataTypeNames) {
await umbracoUi.content.isSuccessNotificationVisible();
expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy();
const contentData = await umbracoApi.document.getByName(contentName);
expect(contentData.variants[0].state).toBe(expectedState);
expect(contentData.values).toEqual([]);
});
test(`can publish content with the ${dataTypeName} data type`, async ({umbracoApi, umbracoUi}) => {
// Arrange
const expectedState = 'Published';
const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName);
await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id);
const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id);
await umbracoApi.document.createDefaultDocument(contentName, documentTypeId);
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
// Act
await umbracoUi.content.clickActionsMenuAtRoot();
await umbracoUi.content.clickCreateButton();
await umbracoUi.content.chooseDocumentType(documentTypeName);
await umbracoUi.content.enterContentName(contentName);
await umbracoUi.content.goToContentWithName(contentName);
await umbracoUi.content.clickSaveAndPublishButton();
// Assert
await umbracoUi.content.doesSuccessNotificationsHaveCount(2);
expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy();
const contentData = await umbracoApi.document.getByName(contentName);
expect(contentData.variants[0].state).toBe(expectedState);
expect(contentData.values).toEqual([]);
});
@@ -65,14 +67,12 @@ for (const dataTypeName of dataTypeNames) {
const selectedOptions = dataTypeName === 'Dropdown' ? [optionValues[0]] : optionValues;
const isMultiple = dataTypeName === 'Dropdown' ? false : true;
const customDataTypeId = await umbracoApi.dataType.createDropdownDataType(customDataTypeName, isMultiple, optionValues);
await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId);
const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId);
await umbracoApi.document.createDefaultDocument(contentName, documentTypeId);
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
// Act
await umbracoUi.content.clickActionsMenuAtRoot();
await umbracoUi.content.clickCreateButton();
await umbracoUi.content.chooseDocumentType(documentTypeName);
await umbracoUi.content.enterContentName(contentName);
await umbracoUi.content.goToContentWithName(contentName);
await umbracoUi.content.chooseDropdownOption(selectedOptions);
await umbracoUi.content.clickSaveAndPublishButton();

View File

@@ -0,0 +1,105 @@
import {ConstantHelper, test, AliasHelper} from '@umbraco/playwright-testhelpers';
import {expect} from "@playwright/test";
const contentName = 'TestContent';
const documentTypeName = 'TestDocumentTypeForContent';
const dataTypeName = 'Image Cropper';
const imageFileName = 'Umbraco.png';
const imageFilePath = './fixtures/mediaLibrary/' + imageFileName;
const defaultFocalPoint = {
left: 0.5,
top: 0.5,
};
test.beforeEach(async ({umbracoApi, umbracoUi}) => {
await umbracoApi.documentType.ensureNameNotExists(documentTypeName);
await umbracoApi.document.ensureNameNotExists(contentName);
await umbracoUi.goToBackOffice();
});
test.afterEach(async ({umbracoApi}) => {
await umbracoApi.document.ensureNameNotExists(contentName);
await umbracoApi.documentType.ensureNameNotExists(documentTypeName);
});
test('can create content with the image cropper data type', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => {
// Arrange
const expectedState = 'Draft';
const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName);
await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id);
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
// Act
await umbracoUi.content.clickActionsMenuAtRoot();
await umbracoUi.content.clickCreateButton();
await umbracoUi.content.chooseDocumentType(documentTypeName);
await umbracoUi.content.enterContentName(contentName);
await umbracoUi.content.uploadFile(imageFilePath);
await umbracoUi.content.clickSaveButton();
// Assert
await umbracoUi.content.isSuccessNotificationVisible();
expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy();
const contentData = await umbracoApi.document.getByName(contentName);
expect(contentData.variants[0].state).toBe(expectedState);
expect(contentData.values[0].alias).toEqual(AliasHelper.toAlias(dataTypeName));
expect(contentData.values[0].value.src).toContain(AliasHelper.toAlias(imageFileName));
// TODO: is no longer null, we need to set an expected crops value
// expect(contentData.values[0].value.crops).toEqual([]);
expect(contentData.values[0].value.focalPoint).toEqual(defaultFocalPoint);
});
test('can publish content with the image cropper data type', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => {
// Arrange
const expectedState = 'Published';
const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName);
const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id);
await umbracoApi.document.createDefaultDocument(contentName, documentTypeId);
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
// Act
await umbracoUi.content.goToContentWithName(contentName);
await umbracoUi.content.uploadFile(imageFilePath);
await umbracoUi.content.clickSaveAndPublishButton();
// Assert
await umbracoUi.content.doesSuccessNotificationsHaveCount(2);
expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy();
const contentData = await umbracoApi.document.getByName(contentName);
expect(contentData.variants[0].state).toBe(expectedState);
expect(contentData.values[0].alias).toEqual(AliasHelper.toAlias(dataTypeName));
expect(contentData.values[0].value.src).toContain(AliasHelper.toAlias(imageFileName));
// TODO: is no longer null, we need to set an expected crops value
// expect(contentData.values[0].value.crops).toEqual([]);
expect(contentData.values[0].value.focalPoint).toEqual(defaultFocalPoint);
});
test('can create content with the custom image cropper data type', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => {
// Arrange
const customDataTypeName = 'CustomImageCropper';
const cropValue = ['TestCropLabel', 100, 50];
const customDataTypeId = await umbracoApi.dataType.createImageCropperDataTypeWithOneCrop(customDataTypeName, cropValue[0], cropValue[1], cropValue[2]);
const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId);
await umbracoApi.document.createDefaultDocument(contentName, documentTypeId);
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
// Act
await umbracoUi.content.goToContentWithName(contentName);
await umbracoUi.content.uploadFile(imageFilePath);
await umbracoUi.content.clickSaveAndPublishButton();
// Assert
await umbracoUi.content.doesSuccessNotificationsHaveCount(2);
expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy();
const contentData = await umbracoApi.document.getByName(contentName);
expect(contentData.values[0].alias).toEqual(AliasHelper.toAlias(customDataTypeName));
expect(contentData.values[0].value.src).toContain(AliasHelper.toAlias(imageFileName));
expect(contentData.values[0].value.focalPoint).toEqual(defaultFocalPoint);
expect(contentData.values[0].value.crops[0].alias).toEqual(AliasHelper.toAlias(cropValue[0]));
expect(contentData.values[0].value.crops[0].width).toEqual(cropValue[1]);
expect(contentData.values[0].value.crops[0].height).toEqual(cropValue[2]);
// Clean
await umbracoApi.dataType.ensureNameNotExists(customDataTypeName);
});

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