Compare commits
62
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c6ab394947 | ||
|
|
d8f68d2c40 | ||
|
|
ff88617db0 | ||
|
|
9f912aea0e | ||
|
|
ba95c12f09 | ||
|
|
14fbd20665 | ||
|
|
2d8b5e8786 | ||
|
|
747e095178 | ||
|
|
1cfa5a225e | ||
|
|
7888b9a4ce | ||
|
|
e31582b297 | ||
|
|
75cc017a18 | ||
|
|
d60137e6da | ||
|
|
9f9c88781a | ||
|
|
c7014e159b | ||
|
|
31e1acce67 | ||
|
|
35c51a029a | ||
|
|
8c1128c85b | ||
|
|
c9021ab2d2 | ||
|
|
67a71f8f82 | ||
|
|
edd0a4a4a9 | ||
|
|
11270eaaf5 | ||
|
|
2d71b5a63b | ||
|
|
9bab74d30e | ||
|
|
0ee0db8071 | ||
|
|
c17d4e1a60 | ||
|
|
fee222daff | ||
|
|
119fde2033 | ||
|
|
52c21b0fca | ||
|
|
99d5a7e609 | ||
|
|
7e1d1a1968 | ||
|
|
b743f6a2df | ||
|
|
a2511ff09b | ||
|
|
2c23e67c65 | ||
|
|
8e837d387d | ||
|
|
801fb5f885 | ||
|
|
ed517ecd86 | ||
|
|
b5e46ba880 | ||
|
|
2735f17ed8 | ||
|
|
3e28e10cdf | ||
|
|
9799c550f4 | ||
|
|
b7e43a8def | ||
|
|
cdbbd6a921 | ||
|
|
5198e7c52d | ||
|
|
49f5d2e2d4 | ||
|
|
57b3a196bf | ||
|
|
dff90c6ec0 | ||
|
|
040495f359 | ||
|
|
a2ad95d965 | ||
|
|
64f2447c0e | ||
|
|
b648126d19 | ||
|
|
812b414d96 | ||
|
|
ec91c47158 | ||
|
|
8915064780 | ||
|
|
83bcd37250 | ||
|
|
9e3f8c7d0c | ||
|
|
37e4d80ce8 | ||
|
|
8d9343b564 | ||
|
|
696b74cdb6 | ||
|
|
a17e398f28 | ||
|
|
9af9dd2c02 | ||
|
|
abfa07367f |
@@ -49,4 +49,5 @@
|
||||
<PropertyGroup>
|
||||
<GitVersionBaseDirectory>$(MSBuildThisFileDirectory)</GitVersionBaseDirectory>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
+38
-50
@@ -41,6 +41,10 @@ parameters:
|
||||
displayName: TestFilter used for release type builds on non windows agents
|
||||
type: string
|
||||
default: ' '
|
||||
- name: isNightly
|
||||
displayName: 'Is nightly build (used for MyGet feed)'
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
variables:
|
||||
nodeVersion: 20
|
||||
@@ -101,44 +105,12 @@ stages:
|
||||
command: restore
|
||||
projects: $(solution)
|
||||
- task: DotNetCoreCLI@2
|
||||
displayName: Run dotnet build
|
||||
name: build
|
||||
displayName: Run dotnet build and generate NuGet packages
|
||||
inputs:
|
||||
command: build
|
||||
projects: $(solution)
|
||||
arguments: '--configuration $(buildConfiguration) --no-restore -p:ContinuousIntegrationBuild=true'
|
||||
- script: |
|
||||
version="$(Build.BuildNumber)"
|
||||
echo "varsion: $version"
|
||||
|
||||
major="$(echo $version | cut -d '.' -f 1)"
|
||||
echo "major version: $major"
|
||||
|
||||
echo "##vso[task.setvariable variable=majorVersion;isOutput=true]$major"
|
||||
displayName: Set major version
|
||||
name: determineMajorVersion
|
||||
- task: PowerShell@2
|
||||
displayName: Prepare nupkg
|
||||
inputs:
|
||||
targetType: inline
|
||||
script: |
|
||||
$umbracoVersion = "$(Build.BuildNumber)" -replace "\+",".g"
|
||||
$templatePaths = Get-ChildItem 'templates/**/.template.config/template.json'
|
||||
|
||||
foreach ($templatePath in $templatePaths) {
|
||||
$a = Get-Content $templatePath -Raw | ConvertFrom-Json
|
||||
if ($a.symbols -and $a.symbols.UmbracoVersion) {
|
||||
$a.symbols.UmbracoVersion.defaultValue = $umbracoVersion
|
||||
$a | ConvertTo-Json -Depth 32 | Set-Content $templatePath
|
||||
}
|
||||
}
|
||||
|
||||
dotnet pack $(solution) --configuration $(buildConfiguration) -p:BuildProjectReferences=false --output $(Build.ArtifactStagingDirectory)/nupkg
|
||||
- script: |
|
||||
sha="$(Build.SourceVersion)"
|
||||
sha=${sha:0:7}
|
||||
buildnumber="$(Build.BuildNumber)_$(Build.BuildId)_$sha"
|
||||
echo "##vso[build.updatebuildnumber]$buildnumber"
|
||||
displayName: Update build number
|
||||
arguments: '--configuration $(buildConfiguration) --no-restore --property:ContinuousIntegrationBuild=true --property:GeneratePackageOnBuild=true --property:PackageOutputPath=$(Build.ArtifactStagingDirectory)/nupkg'
|
||||
- task: PublishPipelineArtifact@1
|
||||
displayName: Publish nupkg
|
||||
inputs:
|
||||
@@ -151,11 +123,11 @@ stages:
|
||||
artifactName: build_output
|
||||
|
||||
- stage: Build_Docs
|
||||
condition: and(succeeded(), or(startsWith(variables['Build.SourceBranch'], 'refs/heads/release/'), ${{parameters.buildApiDocs}}))
|
||||
condition: and(succeeded(), or(eq(dependencies.Build.outputs['A.build.NBGV_PublicRelease'], 'True'), ${{parameters.buildApiDocs}}))
|
||||
displayName: Prepare API Documentation
|
||||
dependsOn: Build
|
||||
variables:
|
||||
umbracoMajorVersion: $[ stageDependencies.Build.A.outputs['determineMajorVersion.majorVersion'] ]
|
||||
umbracoMajorVersion: $[ stageDependencies.Build.A.outputs['build.NBGV_VersionMajor'] ]
|
||||
jobs:
|
||||
# C# API Reference
|
||||
- job:
|
||||
@@ -212,7 +184,7 @@ stages:
|
||||
displayName: Use Node.js 10.15.x
|
||||
retryCountOnTaskFailure: 3
|
||||
inputs:
|
||||
versionSpec: 10.15.0 # Won't work with higher versions
|
||||
versionSpec: 10.15.x # Won't work with higher versions
|
||||
- script: |
|
||||
npm ci --no-fund --no-audit --prefer-offline
|
||||
npx gulp docs
|
||||
@@ -281,6 +253,8 @@ stages:
|
||||
- stage: Integration
|
||||
displayName: Integration Tests
|
||||
dependsOn: Build
|
||||
variables:
|
||||
releaseTestFilter: eq(dependencies.Build.outputs['A.build.NBGV_PublicRelease'], 'True')
|
||||
jobs:
|
||||
# Integration Tests (SQLite)
|
||||
- job:
|
||||
@@ -314,7 +288,7 @@ stages:
|
||||
command: test
|
||||
projects: '**/*.Tests.Integration.csproj'
|
||||
testRunTitle: Integration Tests SQLite - $(Agent.OS)
|
||||
${{ if or( parameters.forceReleaseTestFilter, startsWith(variables['Build.SourceBranch'], 'refs/heads/release/')) }}:
|
||||
${{ if or(variables.releaseTestFilter, parameters.forceReleaseTestFilter) }}:
|
||||
arguments: '--configuration $(buildConfiguration) --no-build ${{parameters.integrationReleaseTestFilter}}'
|
||||
${{ else }}:
|
||||
arguments: '--configuration $(buildConfiguration) ${{parameters.integrationNonReleaseTestFilter}}'
|
||||
@@ -328,7 +302,7 @@ stages:
|
||||
command: test
|
||||
projects: '**/*.Tests.Integration.csproj'
|
||||
testRunTitle: Integration Tests SQLite - $(Agent.OS)
|
||||
${{ if or( parameters.forceReleaseTestFilter, startsWith(variables['Build.SourceBranch'], 'refs/heads/release/')) }}:
|
||||
${{ if or(variables.releaseTestFilter, parameters.forceReleaseTestFilter) }}:
|
||||
arguments: '--configuration $(buildConfiguration) --no-build ${{parameters.nonWindowsIntegrationReleaseTestFilter}}'
|
||||
${{ else }}:
|
||||
arguments: '--configuration $(buildConfiguration) ${{parameters.nonWindowsIntegrationNonReleaseTestFilter}}'
|
||||
@@ -339,7 +313,7 @@ stages:
|
||||
# Integration Tests (SQL Server)
|
||||
- job:
|
||||
timeoutInMinutes: 120
|
||||
condition: or(startsWith(variables['Build.SourceBranch'], 'refs/heads/release/'), ${{parameters.sqlServerIntegrationTests}})
|
||||
condition: or(eq(stageDependencies.Build.A.outputs['build.NBGV_PublicRelease'], 'True'), ${{parameters.sqlServerIntegrationTests}})
|
||||
displayName: Integration Tests (SQL Server)
|
||||
strategy:
|
||||
matrix:
|
||||
@@ -350,7 +324,7 @@ stages:
|
||||
Linux:
|
||||
vmImage: 'ubuntu-latest'
|
||||
testDb: SqlServer
|
||||
connectionString: 'Server=localhost,1433;User Id=sa;Password=$(SA_PASSWORD);'
|
||||
connectionString: 'Server=localhost,1433;User Id=sa;Password=$(SA_PASSWORD);TrustServerCertificate=true'
|
||||
pool:
|
||||
vmImage: $(vmImage)
|
||||
variables:
|
||||
@@ -361,6 +335,11 @@ stages:
|
||||
inputs:
|
||||
artifact: build_output
|
||||
path: $(Build.SourcesDirectory)
|
||||
- task: UseDotNet@2
|
||||
displayName: Use .NET $(dotnetVersion)
|
||||
inputs:
|
||||
version: $(dotnetVersion)
|
||||
includePreviewVersions: $(dotnetIncludePreviewVersions)
|
||||
- powershell: sqllocaldb start mssqllocaldb
|
||||
displayName: Start localdb (Windows only)
|
||||
condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT'))
|
||||
@@ -374,7 +353,7 @@ stages:
|
||||
command: test
|
||||
projects: '**/*.Tests.Integration.csproj'
|
||||
testRunTitle: Integration Tests SQL Server - $(Agent.OS)
|
||||
${{ if or( parameters.forceReleaseTestFilter, startsWith(variables['Build.SourceBranch'], 'refs/heads/release/')) }}:
|
||||
${{ if or(variables.releaseTestFilter, parameters.forceReleaseTestFilter) }}:
|
||||
arguments: '--configuration $(buildConfiguration) --no-build ${{parameters.integrationReleaseTestFilter}}'
|
||||
${{ else }}:
|
||||
arguments: '--configuration $(buildConfiguration) --no-build ${{parameters.integrationNonReleaseTestFilter}}'
|
||||
@@ -389,7 +368,7 @@ stages:
|
||||
command: test
|
||||
projects: '**/*.Tests.Integration.csproj'
|
||||
testRunTitle: Integration Tests SQL Server - $(Agent.OS)
|
||||
${{ if or( parameters.forceReleaseTestFilter, startsWith(variables['Build.SourceBranch'], 'refs/heads/release/')) }}:
|
||||
${{ if or(variables.releaseTestFilter, parameters.forceReleaseTestFilter) }}:
|
||||
arguments: '--configuration $(buildConfiguration) --no-build ${{parameters.nonWindowsIntegrationReleaseTestFilter}}'
|
||||
${{ else }}:
|
||||
arguments: '--configuration $(buildConfiguration) --no-build ${{parameters.nonWindowsIntegrationNonReleaseTestFilter}}'
|
||||
@@ -420,6 +399,7 @@ stages:
|
||||
dockerImageName: umbraco-linux
|
||||
Windows:
|
||||
vmImage: 'windows-latest'
|
||||
DOTNET_GENERATE_ASPNET_CERTIFICATE: true # Automatically generate HTTPS development certificate on Windows
|
||||
# Enable console logging in Release mode
|
||||
Serilog__WriteTo__0__Name: Async
|
||||
Serilog__WriteTo__0__Args__configure__0__Name: Console
|
||||
@@ -510,6 +490,8 @@ stages:
|
||||
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
|
||||
env:
|
||||
CI: true
|
||||
CommitId: $(Build.SourceVersion)
|
||||
AgentOs: $(Agent.OS)
|
||||
- pwsh: |
|
||||
docker logs $(dockerImageName) > $(Build.ArtifactStagingDirectory)/playwright.log 2>&1
|
||||
docker stop $(dockerImageName)
|
||||
@@ -548,9 +530,11 @@ stages:
|
||||
- Unit
|
||||
- Integration
|
||||
# - E2E # TODO: Enable when stable.
|
||||
condition: and(succeeded(), or(startsWith(variables['Build.SourceBranch'], 'refs/heads/release/'), ${{parameters.myGetDeploy}}))
|
||||
condition: and(succeeded(), or(eq(dependencies.Build.outputs['A.build.NBGV_PublicRelease'], 'True'), ${{parameters.myGetDeploy}}))
|
||||
jobs:
|
||||
- job:
|
||||
pool:
|
||||
vmImage: "windows-latest" # NuGetCommand@2 is no longer supported on Ubuntu 24.04 so we'll use windows until an alternative is available.
|
||||
displayName: Push to pre-release feed
|
||||
steps:
|
||||
- checkout: none
|
||||
@@ -565,16 +549,20 @@ stages:
|
||||
command: 'push'
|
||||
packagesToPush: $(Build.ArtifactStagingDirectory)/**/*.nupkg
|
||||
nuGetFeedType: 'external'
|
||||
publishFeedCredentials: 'MyGet - Pre-releases'
|
||||
|
||||
${{ if eq(parameters.isNightly, true) }}:
|
||||
publishFeedCredentials: 'MyGet - Umbraco Nightly'
|
||||
${{ else }}:
|
||||
publishFeedCredentials: 'MyGet - Pre-releases'
|
||||
- stage: Deploy_NuGet
|
||||
displayName: NuGet release
|
||||
dependsOn:
|
||||
- Deploy_MyGet
|
||||
- Build_Docs
|
||||
condition: and(succeeded(), or(startsWith(variables['Build.SourceBranch'], 'refs/heads/release/'), ${{parameters.nuGetDeploy}}))
|
||||
condition: and(succeeded(), or(eq(dependencies.Build.outputs['A.build.NBGV_PublicRelease'], 'True'), ${{parameters.nuGetDeploy}}))
|
||||
jobs:
|
||||
- job:
|
||||
pool:
|
||||
vmImage: "windows-latest" # NuGetCommand@2 is no longer supported on Ubuntu 24.04 so we'll use windows until an alternative is available.
|
||||
displayName: Push to NuGet
|
||||
steps:
|
||||
- checkout: none
|
||||
@@ -595,12 +583,12 @@ stages:
|
||||
pool:
|
||||
vmImage: 'windows-latest' # Apparently AzureFileCopy is windows only :(
|
||||
variables:
|
||||
umbracoMajorVersion: $[ stageDependencies.Build.A.outputs['determineMajorVersion.majorVersion'] ]
|
||||
umbracoMajorVersion: $[ stageDependencies.Build.A.outputs['build.NBGV_VersionMajor'] ]
|
||||
displayName: Upload API Documention
|
||||
dependsOn:
|
||||
- Build
|
||||
- Deploy_NuGet
|
||||
condition: and(succeeded(), or(startsWith(variables['Build.SourceBranch'], 'refs/heads/release/'), ${{parameters.uploadApiDocs}}))
|
||||
condition: and(succeeded(), or(eq(dependencies.Build.outputs['A.build.NBGV_PublicRelease'], 'True'), ${{parameters.uploadApiDocs}}))
|
||||
jobs:
|
||||
- job:
|
||||
displayName: Upload C# Docs
|
||||
|
||||
@@ -8,13 +8,13 @@ schedules:
|
||||
displayName: Daily midnight build
|
||||
branches:
|
||||
include:
|
||||
- v9/dev
|
||||
- v10/dev
|
||||
- v12/dev
|
||||
- v13/dev
|
||||
- v14/dev
|
||||
|
||||
steps:
|
||||
- checkout: none
|
||||
- task: TriggerBuild@4
|
||||
inputs:
|
||||
definitionIsInCurrentTeamProject: true
|
||||
@@ -26,10 +26,10 @@ steps:
|
||||
useSameBranch: true
|
||||
waitForQueuedBuildsToFinish: false
|
||||
storeInEnvironmentVariable: false
|
||||
templateParameters: 'sqlServerIntegrationTests: true, forceReleaseTestFilter: true'
|
||||
templateParameters: 'sqlServerIntegrationTests: true, forceReleaseTestFilter: true, myGetDeploy: true, isNightly: true'
|
||||
authenticationMethod: 'OAuth Token'
|
||||
enableBuildInQueueCondition: false
|
||||
dependentOnSuccessfulBuildCondition: false
|
||||
dependentOnFailedBuildCondition: false
|
||||
checkbuildsoncurrentbranch: false
|
||||
failTaskIfConditionsAreNotFulfilled: false
|
||||
failTaskIfConditionsAreNotFulfilled: false
|
||||
|
||||
@@ -4,67 +4,64 @@
|
||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||
<NoWarn>NU1507</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="Dazinator.Extensions.FileProviders" Version="2.0.0" />
|
||||
<PackageVersion Include="Examine" Version="3.0.1"/>
|
||||
<PackageVersion Include="Examine.Core" Version="3.0.1"/>
|
||||
<PackageVersion Include="HtmlAgilityPack" Version="1.11.54"/>
|
||||
<PackageVersion Include="IPNetwork2" Version="2.6.618"/>
|
||||
<PackageVersion Include="K4os.Compression.LZ4" Version="1.3.6"/>
|
||||
<PackageVersion Include="MailKit" Version="3.2.0"/>
|
||||
<PackageVersion Include="Markdown" Version="2.2.1"/>
|
||||
<PackageVersion Include="MessagePack" Version="2.5.129"/>
|
||||
<PackageVersion Include="Examine" Version="3.0.1" />
|
||||
<PackageVersion Include="Examine.Core" Version="3.0.1" />
|
||||
<PackageVersion Include="HtmlAgilityPack" Version="1.11.54" />
|
||||
<PackageVersion Include="IPNetwork2" Version="2.6.618" />
|
||||
<PackageVersion Include="K4os.Compression.LZ4" Version="1.3.6" />
|
||||
<PackageVersion Include="MailKit" Version="3.2.0" />
|
||||
<PackageVersion Include="Markdown" Version="2.2.1" />
|
||||
<PackageVersion Include="MessagePack" Version="2.5.187" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="6.0.24" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="6.0.24" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="6.0.24"/>
|
||||
<PackageVersion Include="Microsoft.CSharp" Version="4.7.0"/>
|
||||
<PackageVersion Include="Microsoft.Extensions.FileProviders.Embedded" Version="6.0.24"/>
|
||||
<PackageVersion Include="Microsoft.Extensions.FileProviders.Physical" Version="6.0.0"/>
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="6.0.0"/>
|
||||
<PackageVersion Include="Microsoft.Extensions.Identity.Core" Version="6.0.24"/>
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="6.0.0"/>
|
||||
<PackageVersion Include="Microsoft.Extensions.Options" Version="6.0.0"/>
|
||||
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="6.0.0"/>
|
||||
<PackageVersion Include="Microsoft.Extensions.Options.DataAnnotations" Version="6.0.0"/>
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0"/>
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0"/>
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="6.0.1"/>
|
||||
<PackageVersion Include="Microsoft.Extensions.Http" Version="6.0.0"/>
|
||||
<PackageVersion Include="Microsoft.Extensions.Identity.Stores" Version="6.0.24"/>
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="6.0.24" />
|
||||
<PackageVersion Include="Microsoft.CSharp" Version="4.7.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.FileProviders.Embedded" Version="6.0.24" />
|
||||
<PackageVersion Include="Microsoft.Extensions.FileProviders.Physical" Version="6.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="6.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Identity.Core" Version="6.0.24" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="6.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options" Version="6.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="6.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options.DataAnnotations" Version="6.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="6.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http" Version="6.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Identity.Stores" Version="6.0.24" />
|
||||
<PackageVersion Include="MiniProfiler.AspNetCore.Mvc" Version="4.2.22" />
|
||||
<PackageVersion Include="MiniProfiler.Shared" Version="4.2.22"/>
|
||||
<PackageVersion Include="ncrontab" Version="3.3.3"/>
|
||||
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3"/>
|
||||
<PackageVersion Include="NPoco.SqlServer" Version="5.3.2"/>
|
||||
<PackageVersion Include="Serilog" Version="2.12.0"/>
|
||||
<PackageVersion Include="MiniProfiler.Shared" Version="4.2.22" />
|
||||
<PackageVersion Include="ncrontab" Version="3.3.3" />
|
||||
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageVersion Include="NPoco.SqlServer" Version="5.3.2" />
|
||||
<PackageVersion Include="Serilog" Version="2.12.0" />
|
||||
<PackageVersion Include="Serilog.AspNetCore" Version="5.0.0" />
|
||||
<PackageVersion Include="Serilog.Enrichers.Process" Version="2.0.2"/>
|
||||
<PackageVersion Include="Serilog.Enrichers.Thread" Version="3.1.0"/>
|
||||
<PackageVersion Include="Serilog.Expressions" Version="3.4.1"/>
|
||||
<PackageVersion Include="Serilog.Extensions.Hosting" Version="4.2.0"/>
|
||||
<PackageVersion Include="Serilog.Formatting.Compact" Version="1.1.0"/>
|
||||
<PackageVersion Include="Serilog.Formatting.Compact.Reader" Version="1.0.5"/>
|
||||
<PackageVersion Include="Serilog.Settings.Configuration" Version="3.4.0"/>
|
||||
<PackageVersion Include="Serilog.Sinks.Async" Version="1.5.0"/>
|
||||
<PackageVersion Include="Serilog.Sinks.File" Version="5.0.0"/>
|
||||
<PackageVersion Include="Serilog.Sinks.Map" Version="1.0.2"/>
|
||||
<PackageVersion Include="SixLabors.ImageSharp" Version="2.1.6"/>
|
||||
<PackageVersion Include="Serilog.Enrichers.Process" Version="2.0.2" />
|
||||
<PackageVersion Include="Serilog.Enrichers.Thread" Version="3.1.0" />
|
||||
<PackageVersion Include="Serilog.Expressions" Version="3.4.1" />
|
||||
<PackageVersion Include="Serilog.Extensions.Hosting" Version="4.2.0" />
|
||||
<PackageVersion Include="Serilog.Formatting.Compact" Version="1.1.0" />
|
||||
<PackageVersion Include="Serilog.Formatting.Compact.Reader" Version="1.0.5" />
|
||||
<PackageVersion Include="Serilog.Settings.Configuration" Version="3.4.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.Async" Version="1.5.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.File" Version="5.0.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.Map" Version="1.0.2" />
|
||||
<PackageVersion Include="SixLabors.ImageSharp" Version="2.1.10" />
|
||||
<PackageVersion Include="SixLabors.ImageSharp.Web" Version="2.0.2" />
|
||||
<PackageVersion Include="Smidge.InMemory" Version="4.3.0" />
|
||||
<PackageVersion Include="Smidge.Nuglify" Version="4.2.1" />
|
||||
<PackageVersion Include="System.IO.FileSystem.AccessControl" Version="5.0.0"/>
|
||||
<PackageVersion Include="System.Security.Cryptography.Pkcs" Version="6.0.4"/>
|
||||
<PackageVersion Include="System.Threading.Tasks.Dataflow" Version="6.0.0"/>
|
||||
<PackageVersion Include="System.ComponentModel.Annotations" Version="5.0.0"/>
|
||||
<PackageVersion Include="System.Reflection.Emit.Lightweight" Version="4.7.0"/>
|
||||
<PackageVersion Include="System.Runtime.Caching" Version="6.0.0"/>
|
||||
<PackageVersion Include="Umbraco.CSharpTest.Net.Collections" Version="14.906.1403.1085"/>
|
||||
|
||||
<PackageVersion Include="System.IO.FileSystem.AccessControl" Version="5.0.0" />
|
||||
<PackageVersion Include="System.Security.Cryptography.Pkcs" Version="6.0.4" />
|
||||
<PackageVersion Include="System.Threading.Tasks.Dataflow" Version="6.0.0" />
|
||||
<PackageVersion Include="System.ComponentModel.Annotations" Version="5.0.0" />
|
||||
<PackageVersion Include="System.Reflection.Emit.Lightweight" Version="4.7.0" />
|
||||
<PackageVersion Include="System.Runtime.Caching" Version="6.0.0" />
|
||||
<PackageVersion Include="Umbraco.CSharpTest.Net.Collections" Version="14.906.1403.1085" />
|
||||
<!-- Add dependencies that we force an update to, even that we do not use them explicitly and they seems to be taken from the framework instead of from Nuget -->
|
||||
<PackageVersion Include="System.Net.Http" Version="4.3.4"/>
|
||||
<PackageVersion Include="System.Security.Cryptography.Xml" Version="6.0.1"/>
|
||||
<PackageVersion Include="System.Text.RegularExpressions" Version="4.3.1"/>
|
||||
<PackageVersion Include="System.Net.Http" Version="4.3.4" />
|
||||
<PackageVersion Include="System.Security.Cryptography.Xml" Version="6.0.1" />
|
||||
<PackageVersion Include="System.Text.RegularExpressions" Version="4.3.1" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Umbraco.Core\Umbraco.Core.csproj" />
|
||||
<PackageReference Include="Umbraco.Deploy.Core" Version="10.2.4" />
|
||||
<PackageReference Include="Umbraco.Forms.Core" Version="10.5.1" />
|
||||
<PackageReference Include="Umbraco.Deploy.Core" Version="10.4.0" />
|
||||
<PackageReference Include="Umbraco.Forms.Core" Version="10.5.4" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
+6
-4
@@ -134,9 +134,10 @@ public class SqlServerDistributedLockingMechanism : IDistributedLockingMechanism
|
||||
|
||||
const string query = "SELECT value FROM umbracoLock WITH (REPEATABLEREAD) WHERE id=@id";
|
||||
|
||||
db.Execute("SET LOCK_TIMEOUT " + _timeout.TotalMilliseconds + ";");
|
||||
var lockTimeoutQuery = $"SET LOCK_TIMEOUT {_timeout.TotalMilliseconds}";
|
||||
|
||||
var i = db.ExecuteScalar<int?>(query, new { id = LockId });
|
||||
// execute the lock timeout query and the actual query in a single server roundtrip
|
||||
var i = db.ExecuteScalar<int?>($"{lockTimeoutQuery};{query}", new { id = LockId });
|
||||
|
||||
if (i == null)
|
||||
{
|
||||
@@ -169,9 +170,10 @@ public class SqlServerDistributedLockingMechanism : IDistributedLockingMechanism
|
||||
const string query =
|
||||
@"UPDATE umbracoLock WITH (REPEATABLEREAD) SET value = (CASE WHEN (value=1) THEN -1 ELSE 1 END) WHERE id=@id";
|
||||
|
||||
db.Execute("SET LOCK_TIMEOUT " + _timeout.TotalMilliseconds + ";");
|
||||
var lockTimeoutQuery = $"SET LOCK_TIMEOUT {_timeout.TotalMilliseconds}";
|
||||
|
||||
var i = db.Execute(query, new { id = LockId });
|
||||
// execute the lock timeout query and the actual query in a single server roundtrip
|
||||
var i = db.Execute($"{lockTimeoutQuery};{query}", new { id = LockId });
|
||||
|
||||
if (i == 0)
|
||||
{
|
||||
|
||||
@@ -154,7 +154,7 @@ public class SqliteDistributedLockingMechanism : IDistributedLockingMechanism
|
||||
|
||||
try
|
||||
{
|
||||
var i = command.ExecuteNonQuery();
|
||||
var i = db.ExecuteNonQuery(command);
|
||||
|
||||
if (i == 0)
|
||||
{
|
||||
|
||||
@@ -9,6 +9,9 @@ namespace Umbraco.Cms.Core.Cache;
|
||||
/// </summary>
|
||||
public class ObjectCacheAppCache : IAppPolicyCache, IDisposable
|
||||
{
|
||||
private static readonly TimeSpan _readLockTimeout = TimeSpan.FromSeconds(5);
|
||||
private static readonly TimeSpan _writeLockTimeout = TimeSpan.FromSeconds(5);
|
||||
|
||||
private readonly ReaderWriterLockSlim _locker = new(LockRecursionPolicy.SupportsRecursion);
|
||||
private bool _disposedValue;
|
||||
|
||||
@@ -33,7 +36,10 @@ public class ObjectCacheAppCache : IAppPolicyCache, IDisposable
|
||||
Lazy<object?>? result;
|
||||
try
|
||||
{
|
||||
_locker.EnterReadLock();
|
||||
if (_locker.TryEnterReadLock(_readLockTimeout) is false)
|
||||
{
|
||||
throw new TimeoutException("Timeout exceeded to the memory cache when getting item");
|
||||
}
|
||||
result = MemoryCache.Get(key) as Lazy<object?>; // null if key not found
|
||||
}
|
||||
finally
|
||||
@@ -195,7 +201,10 @@ public class ObjectCacheAppCache : IAppPolicyCache, IDisposable
|
||||
{
|
||||
try
|
||||
{
|
||||
_locker.EnterWriteLock();
|
||||
if (_locker.TryEnterWriteLock(_writeLockTimeout) is false)
|
||||
{
|
||||
throw new TimeoutException("Timeout exceeded to the memory cache when clearing item");
|
||||
}
|
||||
if (MemoryCache[key] == null)
|
||||
{
|
||||
return;
|
||||
@@ -223,8 +232,10 @@ public class ObjectCacheAppCache : IAppPolicyCache, IDisposable
|
||||
var isInterface = type.IsInterface;
|
||||
try
|
||||
{
|
||||
_locker.EnterWriteLock();
|
||||
|
||||
if (_locker.TryEnterWriteLock(_writeLockTimeout) is false)
|
||||
{
|
||||
throw new TimeoutException("Timeout exceeded to the memory cache when clearing by type");
|
||||
}
|
||||
// ToArray required to remove
|
||||
foreach (var key in MemoryCache
|
||||
.Where(x =>
|
||||
@@ -259,7 +270,10 @@ public class ObjectCacheAppCache : IAppPolicyCache, IDisposable
|
||||
{
|
||||
try
|
||||
{
|
||||
_locker.EnterWriteLock();
|
||||
if (_locker.TryEnterWriteLock(_writeLockTimeout) is false)
|
||||
{
|
||||
throw new TimeoutException("Timeout exceeded to the memory cache when clearing by generic type");
|
||||
}
|
||||
Type typeOfT = typeof(T);
|
||||
var isInterface = typeOfT.IsInterface;
|
||||
|
||||
@@ -296,7 +310,10 @@ public class ObjectCacheAppCache : IAppPolicyCache, IDisposable
|
||||
{
|
||||
try
|
||||
{
|
||||
_locker.EnterWriteLock();
|
||||
if (_locker.TryEnterWriteLock(_writeLockTimeout) is false)
|
||||
{
|
||||
throw new TimeoutException("Timeout exceeded to the memory cache when clearing generic type with predicate");
|
||||
}
|
||||
Type typeOfT = typeof(T);
|
||||
var isInterface = typeOfT.IsInterface;
|
||||
|
||||
@@ -338,7 +355,10 @@ public class ObjectCacheAppCache : IAppPolicyCache, IDisposable
|
||||
{
|
||||
try
|
||||
{
|
||||
_locker.EnterWriteLock();
|
||||
if (_locker.TryEnterWriteLock(_writeLockTimeout) is false)
|
||||
{
|
||||
throw new TimeoutException("Timeout exceeded to the memory cache when clearing with prefix");
|
||||
}
|
||||
|
||||
// ToArray required to remove
|
||||
foreach (var key in MemoryCache
|
||||
@@ -365,7 +385,10 @@ public class ObjectCacheAppCache : IAppPolicyCache, IDisposable
|
||||
|
||||
try
|
||||
{
|
||||
_locker.EnterWriteLock();
|
||||
if (_locker.TryEnterWriteLock(_writeLockTimeout) is false)
|
||||
{
|
||||
throw new TimeoutException("Timeout exceeded to the memory cach when clearing by regex");
|
||||
}
|
||||
|
||||
// ToArray required to remove
|
||||
foreach (var key in MemoryCache
|
||||
|
||||
@@ -3,30 +3,26 @@ using System.Collections;
|
||||
namespace Umbraco.Cms.Core.Composing;
|
||||
|
||||
/// <summary>
|
||||
/// Provides a base class for builder collections.
|
||||
/// Provides a base class for builder collections.
|
||||
/// </summary>
|
||||
/// <typeparam name="TItem">The type of the items.</typeparam>
|
||||
public abstract class BuilderCollectionBase<TItem> : IBuilderCollection<TItem>
|
||||
{
|
||||
private readonly LazyReadOnlyCollection<TItem> _items;
|
||||
|
||||
/// Initializes a new instance of the
|
||||
/// <see cref="BuilderCollectionBase{TItem}" />
|
||||
/// with items.
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BuilderCollectionBase{TItem}" /> with items.
|
||||
/// </summary>
|
||||
/// <param name="items">The items.</param>
|
||||
public BuilderCollectionBase(Func<IEnumerable<TItem>> items) => _items = new LazyReadOnlyCollection<TItem>(items);
|
||||
public BuilderCollectionBase(Func<IEnumerable<TItem>> items)
|
||||
=> _items = new LazyReadOnlyCollection<TItem>(items);
|
||||
|
||||
/// <inheritdoc />
|
||||
public int Count => _items.Count;
|
||||
|
||||
/// <summary>
|
||||
/// Gets an enumerator.
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
public IEnumerator<TItem> GetEnumerator() => _items.GetEnumerator();
|
||||
|
||||
/// <summary>
|
||||
/// Gets an enumerator.
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
}
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
namespace Umbraco.Cms.Core.Composing;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a builder collection, ie an immutable enumeration of items.
|
||||
/// Represents a builder collection, ie an immutable enumeration of items.
|
||||
/// </summary>
|
||||
/// <typeparam name="TItem">The type of the items.</typeparam>
|
||||
public interface IBuilderCollection<out TItem> : IEnumerable<TItem>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the number of items in the collection.
|
||||
/// Gets the number of items in the collection.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The count.
|
||||
/// </value>
|
||||
int Count { get; }
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// See LICENSE for more details.
|
||||
|
||||
using System.ComponentModel;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Umbraco.Cms.Core.Configuration.Models;
|
||||
|
||||
@@ -24,6 +25,8 @@ public class SecuritySettings
|
||||
|
||||
internal const int StaticMemberDefaultLockoutTimeInMinutes = 30 * 24 * 60;
|
||||
internal const int StaticUserDefaultLockoutTimeInMinutes = 30 * 24 * 60;
|
||||
private const long StaticUserDefaultFailedLoginDurationInMilliseconds = 1000;
|
||||
private const long StaticUserMinimumFailedLoginDurationInMilliseconds = 250;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to keep the user logged in.
|
||||
@@ -109,4 +112,26 @@ public class SecuritySettings
|
||||
[Obsolete("Use ContentSettings.AllowEditFromInvariant instead")]
|
||||
[DefaultValue(StaticAllowEditInvariantFromNonDefault)]
|
||||
public bool AllowEditInvariantFromNonDefault { get; set; } = StaticAllowEditInvariantFromNonDefault;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the default duration (in milliseconds) of failed login attempts.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The default duration (in milliseconds) of failed login attempts.
|
||||
/// </value>
|
||||
/// <remarks>
|
||||
/// The user login endpoint ensures that failed login attempts take at least as long as the average successful login.
|
||||
/// However, if no successful logins have occurred, this value is used as the default duration.
|
||||
/// </remarks>
|
||||
[DefaultValue(StaticUserDefaultFailedLoginDurationInMilliseconds)]
|
||||
public long UserDefaultFailedLoginDurationInMilliseconds { get; set; } = StaticUserDefaultFailedLoginDurationInMilliseconds;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the minimum duration (in milliseconds) of failed login attempts.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The minimum duration (in milliseconds) of failed login attempts.
|
||||
/// </value>
|
||||
[DefaultValue(StaticUserMinimumFailedLoginDurationInMilliseconds)]
|
||||
public long UserMinimumFailedLoginDurationInMilliseconds { get; set; } = StaticUserMinimumFailedLoginDurationInMilliseconds;
|
||||
}
|
||||
|
||||
@@ -358,7 +358,7 @@ namespace Umbraco.Cms.Core.IO
|
||||
|
||||
// nothing prevents us to reach the file, security-wise, yet it is outside
|
||||
// this filesystem's root - throw
|
||||
throw new UnauthorizedAccessException($"File original: [{originalPath}] full: [{path}] is outside this filesystem's root.");
|
||||
throw new UnauthorizedAccessException($"Requested path {originalPath} is outside this filesystem's root.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -7,19 +7,21 @@ namespace Umbraco.Cms.Core.IO;
|
||||
|
||||
internal class ShadowWrapper : IFileSystem, IFileProviderFactory
|
||||
{
|
||||
private static readonly string ShadowFsPath = Constants.SystemDirectories.TempData.EnsureEndsWith('/') + "ShadowFs";
|
||||
private readonly IHostingEnvironment _hostingEnvironment;
|
||||
private readonly IIOHelper _ioHelper;
|
||||
private const string ShadowFsPath = "ShadowFs";
|
||||
|
||||
private readonly Func<bool?>? _isScoped;
|
||||
private readonly IIOHelper _ioHelper;
|
||||
private readonly IHostingEnvironment _hostingEnvironment;
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
private readonly string _shadowPath;
|
||||
private readonly Func<bool?>? _isScoped;
|
||||
|
||||
private string? _shadowDir;
|
||||
private ShadowFileSystem? _shadowFileSystem;
|
||||
|
||||
public ShadowWrapper(IFileSystem innerFileSystem, IIOHelper ioHelper, IHostingEnvironment hostingEnvironment, ILoggerFactory loggerFactory, string shadowPath, Func<bool?>? isScoped = null)
|
||||
{
|
||||
InnerFileSystem = innerFileSystem;
|
||||
|
||||
_ioHelper = ioHelper ?? throw new ArgumentNullException(nameof(ioHelper));
|
||||
_hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment));
|
||||
_loggerFactory = loggerFactory;
|
||||
@@ -35,18 +37,19 @@ internal class ShadowWrapper : IFileSystem, IFileProviderFactory
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_isScoped is not null && _shadowFileSystem is not null)
|
||||
Func<bool?>? isScoped = _isScoped;
|
||||
if (isScoped is not null && _shadowFileSystem is not null)
|
||||
{
|
||||
var isScoped = _isScoped!();
|
||||
bool? scoped = isScoped();
|
||||
|
||||
// if the filesystem is created *after* shadowing starts, it won't be shadowing
|
||||
// better not ignore that situation and raised a meaningful (?) exception
|
||||
if (isScoped.HasValue && isScoped.Value && _shadowFileSystem == null)
|
||||
// better not ignore that situation and raise a meaningful (?) exception
|
||||
if (scoped.HasValue && scoped.Value && _shadowFileSystem == null)
|
||||
{
|
||||
throw new Exception("The filesystems are shadowing, but this filesystem is not.");
|
||||
}
|
||||
|
||||
return isScoped.HasValue && isScoped.Value
|
||||
return scoped.HasValue && scoped.Value
|
||||
? _shadowFileSystem
|
||||
: InnerFileSystem;
|
||||
}
|
||||
@@ -56,8 +59,7 @@ internal class ShadowWrapper : IFileSystem, IFileProviderFactory
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IFileProvider? Create() =>
|
||||
InnerFileSystem.TryCreateFileProvider(out IFileProvider? fileProvider) ? fileProvider : null;
|
||||
public IFileProvider? Create() => InnerFileSystem.TryCreateFileProvider(out IFileProvider? fileProvider) ? fileProvider : null;
|
||||
|
||||
public IEnumerable<string> GetDirectories(string path) => FileSystem.GetDirectories(path);
|
||||
|
||||
@@ -69,8 +71,7 @@ internal class ShadowWrapper : IFileSystem, IFileProviderFactory
|
||||
|
||||
public void AddFile(string path, Stream stream) => FileSystem.AddFile(path, stream);
|
||||
|
||||
public void AddFile(string path, Stream stream, bool overrideExisting) =>
|
||||
FileSystem.AddFile(path, stream, overrideExisting);
|
||||
public void AddFile(string path, Stream stream, bool overrideExisting) => FileSystem.AddFile(path, stream, overrideExisting);
|
||||
|
||||
public IEnumerable<string> GetFiles(string path) => FileSystem.GetFiles(path);
|
||||
|
||||
@@ -107,8 +108,7 @@ internal class ShadowWrapper : IFileSystem, IFileProviderFactory
|
||||
{
|
||||
var id = GuidUtils.ToBase32String(Guid.NewGuid(), idLength);
|
||||
|
||||
var virt = ShadowFsPath + "/" + id;
|
||||
var shadowDir = hostingEnvironment.MapPathContentRoot(virt);
|
||||
var shadowDir = Path.Combine(hostingEnvironment.LocalTempPath, ShadowFsPath, id);
|
||||
if (Directory.Exists(shadowDir))
|
||||
{
|
||||
continue;
|
||||
@@ -129,10 +129,10 @@ internal class ShadowWrapper : IFileSystem, IFileProviderFactory
|
||||
// note: no thread-safety here, because ShadowFs is thread-safe due to the check
|
||||
// on ShadowFileSystemsScope.None - and if None is false then we should be running
|
||||
// in a single thread anyways
|
||||
var virt = Path.Combine(ShadowFsPath, id, _shadowPath);
|
||||
_shadowDir = _hostingEnvironment.MapPathContentRoot(virt);
|
||||
var rootUrl = Path.Combine(ShadowFsPath, id, _shadowPath);
|
||||
_shadowDir = Path.Combine(_hostingEnvironment.LocalTempPath, rootUrl);
|
||||
Directory.CreateDirectory(_shadowDir);
|
||||
var tempfs = new PhysicalFileSystem(_ioHelper, _hostingEnvironment, _loggerFactory.CreateLogger<PhysicalFileSystem>(), _shadowDir, _hostingEnvironment.ToAbsolute(virt));
|
||||
var tempfs = new PhysicalFileSystem(_ioHelper, _hostingEnvironment, _loggerFactory.CreateLogger<PhysicalFileSystem>(), _shadowDir, rootUrl);
|
||||
_shadowFileSystem = new ShadowFileSystem(InnerFileSystem, tempfs);
|
||||
}
|
||||
|
||||
@@ -160,7 +160,7 @@ internal class ShadowWrapper : IFileSystem, IFileProviderFactory
|
||||
|
||||
// shadowPath make be path/to/dir, remove each
|
||||
dir = dir!.Replace('/', Path.DirectorySeparatorChar);
|
||||
var min = _hostingEnvironment.MapPathContentRoot(ShadowFsPath).Length;
|
||||
var min = Path.Combine(_hostingEnvironment.LocalTempPath, ShadowFsPath).Length;
|
||||
var pos = dir.LastIndexOf(Path.DirectorySeparatorChar);
|
||||
while (pos > min)
|
||||
{
|
||||
|
||||
@@ -1,49 +1,88 @@
|
||||
namespace Umbraco.Cms.Core.Models.Editors;
|
||||
|
||||
/// <summary>
|
||||
/// Used to track reference to other entities in a property value
|
||||
/// Used to track a reference to another entity in a property value.
|
||||
/// </summary>
|
||||
public struct UmbracoEntityReference : IEquatable<UmbracoEntityReference>
|
||||
{
|
||||
private static readonly UmbracoEntityReference _empty = new(UnknownTypeUdi.Instance, string.Empty);
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UmbracoEntityReference" /> struct.
|
||||
/// </summary>
|
||||
/// <param name="udi">The UDI.</param>
|
||||
/// <param name="relationTypeAlias">The relation type alias.</param>
|
||||
public UmbracoEntityReference(Udi udi, string relationTypeAlias)
|
||||
{
|
||||
Udi = udi ?? throw new ArgumentNullException(nameof(udi));
|
||||
RelationTypeAlias = relationTypeAlias ?? throw new ArgumentNullException(nameof(relationTypeAlias));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UmbracoEntityReference" /> struct for a document or media item.
|
||||
/// </summary>
|
||||
/// <param name="udi">The UDI.</param>
|
||||
public UmbracoEntityReference(Udi udi)
|
||||
{
|
||||
Udi = udi ?? throw new ArgumentNullException(nameof(udi));
|
||||
|
||||
switch (udi.EntityType)
|
||||
{
|
||||
case Constants.UdiEntityType.Document:
|
||||
RelationTypeAlias = Constants.Conventions.RelationTypes.RelatedDocumentAlias;
|
||||
break;
|
||||
case Constants.UdiEntityType.Media:
|
||||
RelationTypeAlias = Constants.Conventions.RelationTypes.RelatedMediaAlias;
|
||||
break;
|
||||
default:
|
||||
RelationTypeAlias = Constants.Conventions.RelationTypes.RelatedDocumentAlias;
|
||||
// No relation type alias convention for this entity type, so leave it empty
|
||||
RelationTypeAlias = string.Empty;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the UDI.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The UDI.
|
||||
/// </value>
|
||||
public Udi Udi { get; }
|
||||
|
||||
public static UmbracoEntityReference Empty() => _empty;
|
||||
|
||||
public static bool IsEmpty(UmbracoEntityReference reference) => reference == Empty();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the relation type alias.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The relation type alias.
|
||||
/// </value>
|
||||
public string RelationTypeAlias { get; }
|
||||
|
||||
public static bool operator ==(UmbracoEntityReference left, UmbracoEntityReference right) => left.Equals(right);
|
||||
/// <summary>
|
||||
/// Gets an empty reference.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// An empty reference.
|
||||
/// </returns>
|
||||
public static UmbracoEntityReference Empty() => _empty;
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified reference is empty.
|
||||
/// </summary>
|
||||
/// <param name="reference">The reference.</param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if the specified reference is empty; otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
public static bool IsEmpty(UmbracoEntityReference reference) => reference == Empty();
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool Equals(object? obj) => obj is UmbracoEntityReference reference && Equals(reference);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Equals(UmbracoEntityReference other) =>
|
||||
EqualityComparer<Udi>.Default.Equals(Udi, other.Udi) &&
|
||||
RelationTypeAlias == other.RelationTypeAlias;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override int GetHashCode()
|
||||
{
|
||||
var hashCode = -487348478;
|
||||
@@ -52,5 +91,9 @@ public struct UmbracoEntityReference : IEquatable<UmbracoEntityReference>
|
||||
return hashCode;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public static bool operator ==(UmbracoEntityReference left, UmbracoEntityReference right) => left.Equals(right);
|
||||
|
||||
/// <inheritdoc />
|
||||
public static bool operator !=(UmbracoEntityReference left, UmbracoEntityReference right) => !(left == right);
|
||||
}
|
||||
|
||||
@@ -4,64 +4,156 @@ using Umbraco.Cms.Core.Models.Editors;
|
||||
|
||||
namespace Umbraco.Cms.Core.PropertyEditors;
|
||||
|
||||
/// <summary>
|
||||
/// Provides a builder collection for <see cref="IDataValueReferenceFactory" /> items.
|
||||
/// </summary>
|
||||
public class DataValueReferenceFactoryCollection : BuilderCollectionBase<IDataValueReferenceFactory>
|
||||
{
|
||||
public DataValueReferenceFactoryCollection(Func<IEnumerable<IDataValueReferenceFactory>> items)
|
||||
: base(items)
|
||||
{
|
||||
}
|
||||
|
||||
// TODO: We could further reduce circular dependencies with PropertyEditorCollection by not having IDataValueReference implemented
|
||||
// by property editors and instead just use the already built in IDataValueReferenceFactory and/or refactor that into a more normal collection
|
||||
public IEnumerable<UmbracoEntityReference> GetAllReferences(
|
||||
IPropertyCollection properties,
|
||||
PropertyEditorCollection propertyEditors)
|
||||
{
|
||||
var trackedRelations = new HashSet<UmbracoEntityReference>();
|
||||
|
||||
foreach (IProperty p in properties)
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DataValueReferenceFactoryCollection" /> class.
|
||||
/// </summary>
|
||||
/// <param name="items">The items.</param>
|
||||
public DataValueReferenceFactoryCollection(Func<IEnumerable<IDataValueReferenceFactory>> items)
|
||||
: base(items)
|
||||
{ }
|
||||
|
||||
/// <summary>
|
||||
/// Gets all unique references from the specified properties.
|
||||
/// </summary>
|
||||
/// <param name="properties">The properties.</param>
|
||||
/// <param name="propertyEditors">The property editors.</param>
|
||||
/// <returns>
|
||||
/// The unique references from the specified properties.
|
||||
/// </returns>
|
||||
public ISet<UmbracoEntityReference> GetAllReferences(IPropertyCollection properties, PropertyEditorCollection propertyEditors)
|
||||
{
|
||||
var references = new HashSet<UmbracoEntityReference>();
|
||||
|
||||
// Group by property editor alias to avoid duplicate lookups and optimize value parsing
|
||||
foreach (var propertyValuesByPropertyEditorAlias in properties.GroupBy(x => x.PropertyType.PropertyEditorAlias, x => x.Values))
|
||||
{
|
||||
if (!propertyEditors.TryGet(p.PropertyType.PropertyEditorAlias, out IDataEditor? editor))
|
||||
if (!propertyEditors.TryGet(propertyValuesByPropertyEditorAlias.Key, out IDataEditor? dataEditor))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// TODO: We will need to change this once we support tracking via variants/segments
|
||||
// for now, we are tracking values from ALL variants
|
||||
foreach (IPropertyValue propertyVal in p.Values)
|
||||
// Use distinct values to avoid duplicate parsing of the same value
|
||||
var values = new HashSet<object?>(properties.Count);
|
||||
foreach (IPropertyValue propertyValue in propertyValuesByPropertyEditorAlias.SelectMany(x => x))
|
||||
{
|
||||
var val = propertyVal.EditedValue;
|
||||
values.Add(propertyValue.EditedValue);
|
||||
values.Add(propertyValue.PublishedValue);
|
||||
}
|
||||
|
||||
IDataValueEditor? valueEditor = editor?.GetValueEditor();
|
||||
if (valueEditor is IDataValueReference reference)
|
||||
{
|
||||
IEnumerable<UmbracoEntityReference> refs = reference.GetReferences(val);
|
||||
foreach (UmbracoEntityReference r in refs)
|
||||
{
|
||||
trackedRelations.Add(r);
|
||||
}
|
||||
}
|
||||
references.UnionWith(GetReferences(dataEditor, values));
|
||||
}
|
||||
|
||||
// Loop over collection that may be add to existing property editors
|
||||
// implementation of GetReferences in IDataValueReference.
|
||||
// Allows developers to add support for references by a
|
||||
// package /property editor that did not implement IDataValueReference themselves
|
||||
foreach (IDataValueReferenceFactory item in this)
|
||||
{
|
||||
// Check if this value reference is for this datatype/editor
|
||||
// Then call it's GetReferences method - to see if the value stored
|
||||
// in the dataeditor/property has referecnes to media/content items
|
||||
if (item.IsForEditor(editor))
|
||||
{
|
||||
foreach (UmbracoEntityReference r in item.GetDataValueReference().GetReferences(val))
|
||||
{
|
||||
trackedRelations.Add(r);
|
||||
}
|
||||
}
|
||||
}
|
||||
return references;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the references.
|
||||
/// </summary>
|
||||
/// <param name="dataEditor">The data editor.</param>
|
||||
/// <param name="values">The values.</param>
|
||||
/// <returns>
|
||||
/// The references.
|
||||
/// </returns>
|
||||
public IEnumerable<UmbracoEntityReference> GetReferences(IDataEditor dataEditor, params object?[] values)
|
||||
=> GetReferences(dataEditor, (IEnumerable<object?>)values);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the references.
|
||||
/// </summary>
|
||||
/// <param name="dataEditor">The data editor.</param>
|
||||
/// <param name="values">The values.</param>
|
||||
/// <returns>
|
||||
/// The references.
|
||||
/// </returns>
|
||||
public IEnumerable<UmbracoEntityReference> GetReferences(IDataEditor dataEditor, IEnumerable<object?> values)
|
||||
{
|
||||
// TODO: We will need to change this once we support tracking via variants/segments
|
||||
// for now, we are tracking values from ALL variants
|
||||
if (dataEditor.GetValueEditor() is IDataValueReference dataValueReference)
|
||||
{
|
||||
foreach (UmbracoEntityReference reference in values.SelectMany(dataValueReference.GetReferences))
|
||||
{
|
||||
yield return reference;
|
||||
}
|
||||
}
|
||||
|
||||
return trackedRelations;
|
||||
// Loop over collection that may be add to existing property editors
|
||||
// implementation of GetReferences in IDataValueReference.
|
||||
// Allows developers to add support for references by a
|
||||
// package /property editor that did not implement IDataValueReference themselves
|
||||
foreach (IDataValueReferenceFactory dataValueReferenceFactory in this)
|
||||
{
|
||||
// Check if this value reference is for this datatype/editor
|
||||
// Then call it's GetReferences method - to see if the value stored
|
||||
// in the dataeditor/property has references to media/content items
|
||||
if (dataValueReferenceFactory.IsForEditor(dataEditor))
|
||||
{
|
||||
IDataValueReference factoryDataValueReference = dataValueReferenceFactory.GetDataValueReference();
|
||||
foreach (UmbracoEntityReference reference in values.SelectMany(factoryDataValueReference.GetReferences))
|
||||
{
|
||||
yield return reference;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all relation type aliases that are automatically tracked.
|
||||
/// </summary>
|
||||
/// <param name="propertyEditors">The property editors.</param>
|
||||
/// <returns>
|
||||
/// All relation type aliases that are automatically tracked.
|
||||
/// </returns>
|
||||
public ISet<string> GetAllAutomaticRelationTypesAliases(PropertyEditorCollection propertyEditors)
|
||||
{
|
||||
// Always add default automatic relation types
|
||||
var automaticRelationTypeAliases = new HashSet<string>(Constants.Conventions.RelationTypes.AutomaticRelationTypes);
|
||||
|
||||
// Add relation types for all property editors
|
||||
foreach (IDataEditor dataEditor in propertyEditors)
|
||||
{
|
||||
automaticRelationTypeAliases.UnionWith(GetAutomaticRelationTypesAliases(dataEditor));
|
||||
}
|
||||
|
||||
return automaticRelationTypeAliases;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the automatic relation types aliases.
|
||||
/// </summary>
|
||||
/// <param name="dataEditor">The data editor.</param>
|
||||
/// <returns>
|
||||
/// The automatic relation types aliases.
|
||||
/// </returns>
|
||||
public IEnumerable<string> GetAutomaticRelationTypesAliases(IDataEditor dataEditor)
|
||||
{
|
||||
if (dataEditor.GetValueEditor() is IDataValueReference dataValueReference)
|
||||
{
|
||||
// Return custom relation types from value editor implementation
|
||||
foreach (var alias in dataValueReference.GetAutomaticRelationTypesAliases())
|
||||
{
|
||||
yield return alias;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (IDataValueReferenceFactory dataValueReferenceFactory in this)
|
||||
{
|
||||
if (dataValueReferenceFactory.IsForEditor(dataEditor))
|
||||
{
|
||||
// Return custom relation types from factory
|
||||
foreach (var alias in dataValueReferenceFactory.GetDataValueReference().GetAutomaticRelationTypesAliases())
|
||||
{
|
||||
yield return alias;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,4 +50,28 @@ public class WebPath
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the provided web path is well-formed according to the specified UriKind.
|
||||
/// </summary>
|
||||
/// <param name="webPath">The web path to check. This can be null.</param>
|
||||
/// <param name="uriKind">The kind of Uri (Absolute, Relative, or RelativeOrAbsolute).</param>
|
||||
/// <returns>
|
||||
/// true if <paramref name="webPath"/> is well-formed; otherwise, false.
|
||||
/// </returns>
|
||||
public static bool IsWellFormedWebPath(string? webPath, UriKind uriKind)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(webPath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (webPath.StartsWith("//"))
|
||||
{
|
||||
return uriKind is not UriKind.Relative;
|
||||
}
|
||||
|
||||
return Uri.IsWellFormedUriString(webPath, uriKind);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Umbraco.Cms.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Makes a code block timed (take at least a certain amount of time). This class cannot be inherited.
|
||||
/// </summary>
|
||||
public sealed class TimedScope : IDisposable, IAsyncDisposable
|
||||
{
|
||||
private readonly TimeSpan _duration;
|
||||
private readonly CancellationTokenSource _cancellationTokenSource;
|
||||
private readonly Stopwatch _stopwatch;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the elapsed time.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The elapsed time.
|
||||
/// </value>
|
||||
public TimeSpan Elapsed => _stopwatch.Elapsed;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the remaining time.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The remaining time.
|
||||
/// </value>
|
||||
public TimeSpan Remaining
|
||||
=> TryGetRemaining(out TimeSpan remaining) ? remaining : TimeSpan.Zero;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TimedScope" /> class.
|
||||
/// </summary>
|
||||
/// <param name="millisecondsDuration">The number of milliseconds the scope should at least take.</param>
|
||||
public TimedScope(long millisecondsDuration)
|
||||
: this(TimeSpan.FromMilliseconds(millisecondsDuration))
|
||||
{ }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TimedScope" /> class.
|
||||
/// </summary>
|
||||
/// <param name="millisecondsDuration">The number of milliseconds the scope should at least take.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
public TimedScope(long millisecondsDuration, CancellationToken cancellationToken)
|
||||
: this(TimeSpan.FromMilliseconds(millisecondsDuration), cancellationToken)
|
||||
{ }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TimedScope"/> class.
|
||||
/// </summary>
|
||||
/// <param name="duration">The duration the scope should at least take.</param>
|
||||
public TimedScope(TimeSpan duration)
|
||||
: this(duration, new CancellationTokenSource())
|
||||
{ }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TimedScope" /> class.
|
||||
/// </summary>
|
||||
/// <param name="duration">The duration the scope should at least take.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
public TimedScope(TimeSpan duration, CancellationToken cancellationToken)
|
||||
: this(duration, CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
|
||||
{ }
|
||||
|
||||
private TimedScope(TimeSpan duration, CancellationTokenSource cancellationTokenSource)
|
||||
{
|
||||
_duration = duration;
|
||||
_cancellationTokenSource = cancellationTokenSource;
|
||||
_stopwatch = new Stopwatch();
|
||||
_stopwatch.Start();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cancels the timed scope.
|
||||
/// </summary>
|
||||
public void Cancel()
|
||||
=> _cancellationTokenSource.Cancel();
|
||||
|
||||
/// <summary>
|
||||
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This will block using <see cref="Thread.Sleep(TimeSpan)" /> until the remaining time has elapsed, if not cancelled.
|
||||
/// </remarks>
|
||||
public void Dispose()
|
||||
{
|
||||
if (_cancellationTokenSource.IsCancellationRequested is false &&
|
||||
TryGetRemaining(out TimeSpan remaining))
|
||||
{
|
||||
Thread.Sleep(remaining);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources asynchronously.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// A task that represents the asynchronous dispose operation.
|
||||
/// </returns>
|
||||
/// <remarks>
|
||||
/// This will delay using <see cref="Task.Delay(TimeSpan, CancellationToken)" /> until the remaining time has elapsed, if not cancelled.
|
||||
/// </remarks>
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (_cancellationTokenSource.IsCancellationRequested is false &&
|
||||
TryGetRemaining(out TimeSpan remaining))
|
||||
{
|
||||
await Task.Delay(remaining, _cancellationTokenSource.Token).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryGetRemaining(out TimeSpan remaining)
|
||||
{
|
||||
remaining = _duration.Subtract(Elapsed);
|
||||
|
||||
return remaining > TimeSpan.Zero;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<PackageId>Umbraco.Cms.Core</PackageId>
|
||||
<Title>Umbraco CMS - Core</Title>
|
||||
|
||||
@@ -250,6 +250,11 @@ public class ManifestParser : IManifestParser
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
|
||||
return Directory.GetFiles(_path, "package.manifest", SearchOption.AllDirectories);
|
||||
var files = Directory.GetFiles(_path, "package.manifest", SearchOption.AllDirectories);
|
||||
|
||||
// Ensure a consistent, alphabetical sorting of paths, because this is not guaranteed to be the same between file systems or OSes
|
||||
Array.Sort(files);
|
||||
|
||||
return files;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Data.Common;
|
||||
using NPoco;
|
||||
using Umbraco.Cms.Infrastructure.Migrations.Install;
|
||||
|
||||
@@ -33,4 +34,7 @@ public interface IUmbracoDatabase : IDatabase
|
||||
bool IsUmbracoInstalled();
|
||||
|
||||
DatabaseSchemaResult ValidateSchema();
|
||||
|
||||
/// <returns>The number of rows affected.</returns>
|
||||
int ExecuteNonQuery(DbCommand command) => command.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
+60
-64
@@ -1082,82 +1082,78 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
|
||||
|
||||
protected void PersistRelations(TEntity entity)
|
||||
{
|
||||
// Get all references from our core built in DataEditors/Property Editors
|
||||
// Along with seeing if deverlopers want to collect additional references from the DataValueReferenceFactories collection
|
||||
var trackedRelations = new List<UmbracoEntityReference>();
|
||||
trackedRelations.AddRange(_dataValueReferenceFactories.GetAllReferences(entity.Properties, PropertyEditors));
|
||||
// Get all references and automatic relation type aliases
|
||||
ISet<UmbracoEntityReference> references = _dataValueReferenceFactories.GetAllReferences(entity.Properties, PropertyEditors);
|
||||
ISet<string> automaticRelationTypeAliases = _dataValueReferenceFactories.GetAllAutomaticRelationTypesAliases(PropertyEditors);
|
||||
|
||||
var relationTypeAliases = GetAutomaticRelationTypesAliases(entity.Properties, PropertyEditors).ToArray();
|
||||
|
||||
// First delete all auto-relations for this entity
|
||||
RelationRepository.DeleteByParent(entity.Id, relationTypeAliases);
|
||||
|
||||
if (trackedRelations.Count == 0)
|
||||
if (references.Count == 0)
|
||||
{
|
||||
// Delete all relations using the automatic relation type aliases
|
||||
RelationRepository.DeleteByParent(entity.Id, automaticRelationTypeAliases.ToArray());
|
||||
|
||||
// No need to add new references/relations
|
||||
return;
|
||||
}
|
||||
|
||||
trackedRelations = trackedRelations.Distinct().ToList();
|
||||
var udiToGuids = trackedRelations.Select(x => x.Udi as GuidUdi)
|
||||
.ToDictionary(x => (Udi)x!, x => x!.Guid);
|
||||
// Lookup all relation type IDs
|
||||
var relationTypeLookup = RelationTypeRepository.GetMany(Array.Empty<int>())
|
||||
.Where(x => automaticRelationTypeAliases.Contains(x.Alias))
|
||||
.ToDictionary(x => x.Alias, x => x.Id);
|
||||
|
||||
// lookup in the DB all INT ids for the GUIDs and chuck into a dictionary
|
||||
var keyToIds = Database.Fetch<NodeIdKey>(Sql()
|
||||
.Select<NodeDto>(x => x.NodeId, x => x.UniqueId)
|
||||
.From<NodeDto>()
|
||||
.WhereIn<NodeDto>(x => x.UniqueId, udiToGuids.Values))
|
||||
.ToDictionary(x => x.UniqueId, x => x.NodeId);
|
||||
|
||||
var allRelationTypes = RelationTypeRepository.GetMany(Array.Empty<int>())?
|
||||
.ToDictionary(x => x.Alias, x => x);
|
||||
|
||||
IEnumerable<ReadOnlyRelation> toSave = trackedRelations.Select(rel =>
|
||||
{
|
||||
if (allRelationTypes is null || !allRelationTypes.TryGetValue(rel.RelationTypeAlias, out IRelationType? relationType))
|
||||
{
|
||||
throw new InvalidOperationException($"The relation type {rel.RelationTypeAlias} does not exist");
|
||||
}
|
||||
|
||||
if (!udiToGuids.TryGetValue(rel.Udi, out Guid guid))
|
||||
{
|
||||
return null; // This shouldn't happen!
|
||||
}
|
||||
|
||||
if (!keyToIds.TryGetValue(guid, out var id))
|
||||
{
|
||||
return null; // This shouldn't happen!
|
||||
}
|
||||
|
||||
return new ReadOnlyRelation(entity.Id, id, relationType.Id);
|
||||
}).WhereNotNull();
|
||||
|
||||
// Save bulk relations
|
||||
RelationRepository.SaveBulk(toSave);
|
||||
}
|
||||
|
||||
private IEnumerable<string> GetAutomaticRelationTypesAliases(
|
||||
IPropertyCollection properties,
|
||||
PropertyEditorCollection propertyEditors)
|
||||
{
|
||||
var automaticRelationTypesAliases = new HashSet<string>(Constants.Conventions.RelationTypes.AutomaticRelationTypes);
|
||||
|
||||
foreach (IProperty property in properties)
|
||||
// Lookup node IDs for all GUID based UDIs
|
||||
IEnumerable<Guid> keys = references.Select(x => x.Udi).OfType<GuidUdi>().Select(x => x.Guid);
|
||||
var keysLookup = Database.FetchByGroups<NodeIdKey, Guid>(keys, Constants.Sql.MaxParameterCount, guids =>
|
||||
{
|
||||
if (propertyEditors.TryGet(property.PropertyType.PropertyEditorAlias, out IDataEditor? editor) is false )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
return Sql()
|
||||
.Select<NodeDto>(x => x.NodeId, x => x.UniqueId)
|
||||
.From<NodeDto>()
|
||||
.WhereIn<NodeDto>(x => x.UniqueId, guids);
|
||||
}).ToDictionary(x => x.UniqueId, x => x.NodeId);
|
||||
|
||||
if (editor.GetValueEditor() is IDataValueReference reference)
|
||||
// Get all valid relations
|
||||
var relations = new List<(int ChildId, int RelationTypeId)>(references.Count);
|
||||
foreach (UmbracoEntityReference reference in references)
|
||||
{
|
||||
if (string.IsNullOrEmpty(reference.RelationTypeAlias))
|
||||
{
|
||||
foreach (var alias in reference.GetAutomaticRelationTypesAliases())
|
||||
{
|
||||
automaticRelationTypesAliases.Add(alias);
|
||||
}
|
||||
// Reference does not specify a relation type alias, so skip adding a relation
|
||||
Logger.LogDebug("The reference to {Udi} does not specify a relation type alias, so it will not be saved as relation.", reference.Udi);
|
||||
}
|
||||
else if (!automaticRelationTypeAliases.Contains(reference.RelationTypeAlias))
|
||||
{
|
||||
// Returning a reference that doesn't use an automatic relation type is an issue that should be fixed in code
|
||||
Logger.LogError("The reference to {Udi} uses a relation type {RelationTypeAlias} that is not an automatic relation type.", reference.Udi, reference.RelationTypeAlias);
|
||||
}
|
||||
else if (!relationTypeLookup.TryGetValue(reference.RelationTypeAlias, out int relationTypeId))
|
||||
{
|
||||
// A non-existent relation type could be caused by an environment issue (e.g. it was manually removed)
|
||||
Logger.LogWarning("The reference to {Udi} uses a relation type {RelationTypeAlias} that does not exist.", reference.Udi, reference.RelationTypeAlias);
|
||||
}
|
||||
else if (reference.Udi is not GuidUdi udi || !keysLookup.TryGetValue(udi.Guid, out var id))
|
||||
{
|
||||
// Relations only support references to items that are stored in the NodeDto table (because of foreign key constraints)
|
||||
Logger.LogInformation("The reference to {Udi} can not be saved as relation, because doesn't have a node ID.", reference.Udi);
|
||||
}
|
||||
else
|
||||
{
|
||||
relations.Add((id, relationTypeId));
|
||||
}
|
||||
}
|
||||
|
||||
return automaticRelationTypesAliases;
|
||||
// Get all existing relations (optimize for adding new and keeping existing relations)
|
||||
var query = Query<IRelation>().Where(x => x.ParentId == entity.Id).WhereIn(x => x.RelationTypeId, relationTypeLookup.Values);
|
||||
var existingRelations = RelationRepository.GetPagedRelationsByQuery(query, 0, int.MaxValue, out _, null)
|
||||
.ToDictionary(x => (x.ChildId, x.RelationTypeId)); // Relations are unique by parent ID, child ID and relation type ID
|
||||
|
||||
// Add relations that don't exist yet
|
||||
var relationsToAdd = relations.Except(existingRelations.Keys).Select(x => new ReadOnlyRelation(entity.Id, x.ChildId, x.RelationTypeId));
|
||||
RelationRepository.SaveBulk(relationsToAdd);
|
||||
|
||||
// Delete relations that don't exist anymore
|
||||
foreach (IRelation relation in existingRelations.Where(x => !relations.Contains(x.Key)).Select(x => x.Value))
|
||||
{
|
||||
RelationRepository.Delete(relation);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
+13
-7
@@ -1223,8 +1223,11 @@ AND umbracoNode.id <> @id",
|
||||
/// If this is not done, then in some cases the "edited" value for a particular culture for a document will remain true
|
||||
/// when it should be false
|
||||
/// if the property was changed to invariant. In order to do this we need to recalculate this value based on the values
|
||||
/// stored for each
|
||||
/// property, culture and current/published version.
|
||||
/// stored for each property, culture and current/published version.
|
||||
///
|
||||
/// Some of the sql statements in this function have a tendency to take a lot of parameters (nodeIds)
|
||||
/// as the WhereIn Npoco method translates all the nodeIds being passed in as parameters when using the SqlClient provider.
|
||||
/// this results in to many parameters (>2100) error => We need to batch the calls
|
||||
/// </remarks>
|
||||
private void RenormalizeDocumentEditedFlags(
|
||||
IReadOnlyCollection<int> propertyTypeIds,
|
||||
@@ -1380,16 +1383,19 @@ AND umbracoNode.id <> @id",
|
||||
// Now bulk update the table DocumentCultureVariationDto, once for edited = true, another for edited = false
|
||||
foreach (IGrouping<bool, DocumentCultureVariationDto> editValue in toUpdate.GroupBy(x => x.Edited))
|
||||
{
|
||||
Database.Execute(Sql().Update<DocumentCultureVariationDto>(u => u.Set(x => x.Edited, editValue.Key))
|
||||
.WhereIn<DocumentCultureVariationDto>(x => x.Id, editValue.Select(x => x.Id)));
|
||||
// update in batches to account for maximum parameter count
|
||||
foreach (IEnumerable<DocumentCultureVariationDto> batchedValues in editValue.InGroupsOf(Constants.Sql.MaxParameterCount))
|
||||
{
|
||||
Database.Execute(Sql().Update<DocumentCultureVariationDto>(u => u.Set(x => x.Edited, editValue.Key))
|
||||
.WhereIn<DocumentCultureVariationDto>(x => x.Id, batchedValues.Select(x => x.Id)));
|
||||
}
|
||||
}
|
||||
|
||||
// Now bulk update the umbracoDocument table
|
||||
// we need to do this in batches as the WhereIn Npoco method translates to all the nodeIds being passed in as parameters when using the SqlClient provider
|
||||
// this results in to many parameters (>2100) being passed to the client when there are a lot of documents being normalized
|
||||
foreach (IGrouping<bool, KeyValuePair<int, bool>> groupByValue in editedDocument.GroupBy(x => x.Value))
|
||||
{
|
||||
foreach (IEnumerable<KeyValuePair<int, bool>> batch in groupByValue.InGroupsOf(2000))
|
||||
// update in batches to account for maximum parameter count
|
||||
foreach (IEnumerable<KeyValuePair<int, bool>> batch in groupByValue.InGroupsOf(Constants.Sql.MaxParameterCount))
|
||||
{
|
||||
Database.Execute(Sql().Update<DocumentDto>(u => u.Set(x => x.Edited, groupByValue.Key))
|
||||
.WhereIn<DocumentDto>(x => x.NodeId, batch.Select(x => x.Key)));
|
||||
|
||||
@@ -224,6 +224,14 @@ public class UmbracoDatabase : Database, IUmbracoDatabase
|
||||
return databaseSchemaValidationResult ?? new DatabaseSchemaResult();
|
||||
}
|
||||
|
||||
public int ExecuteNonQuery(DbCommand command)
|
||||
{
|
||||
OnExecutingCommand(command);
|
||||
var i = command.ExecuteNonQuery();
|
||||
OnExecutedCommand(command);
|
||||
return i;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if Umbraco database tables are detected to be installed
|
||||
/// </summary>
|
||||
|
||||
@@ -17,12 +17,14 @@ internal abstract class BlockEditorPropertyValueEditor : DataValueEditor, IDataV
|
||||
{
|
||||
private BlockEditorValues? _blockEditorValues;
|
||||
private readonly IDataTypeService _dataTypeService;
|
||||
private readonly ILogger<BlockEditorPropertyValueEditor> _logger;
|
||||
private readonly PropertyEditorCollection _propertyEditors;
|
||||
private readonly DataValueReferenceFactoryCollection _dataValueReferenceFactories;
|
||||
private readonly ILogger<BlockEditorPropertyValueEditor> _logger;
|
||||
|
||||
protected BlockEditorPropertyValueEditor(
|
||||
DataEditorAttribute attribute,
|
||||
PropertyEditorCollection propertyEditors,
|
||||
DataValueReferenceFactoryCollection dataValueReferenceFactories,
|
||||
IDataTypeService dataTypeService,
|
||||
ILocalizedTextService textService,
|
||||
ILogger<BlockEditorPropertyValueEditor> logger,
|
||||
@@ -32,6 +34,7 @@ internal abstract class BlockEditorPropertyValueEditor : DataValueEditor, IDataV
|
||||
: base(textService, shortStringHelper, jsonSerializer, ioHelper, attribute)
|
||||
{
|
||||
_propertyEditors = propertyEditors;
|
||||
_dataValueReferenceFactories = dataValueReferenceFactories;
|
||||
_dataTypeService = dataTypeService;
|
||||
_logger = logger;
|
||||
}
|
||||
@@ -42,73 +45,60 @@ internal abstract class BlockEditorPropertyValueEditor : DataValueEditor, IDataV
|
||||
set => _blockEditorValues = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<UmbracoEntityReference> GetReferences(object? value)
|
||||
{
|
||||
var rawJson = value == null ? string.Empty : value is string str ? str : value.ToString();
|
||||
|
||||
var result = new List<UmbracoEntityReference>();
|
||||
BlockEditorData? blockEditorData = BlockEditorValues.DeserializeAndClean(rawJson);
|
||||
if (blockEditorData == null)
|
||||
// Group by property editor alias to avoid duplicate lookups and optimize value parsing
|
||||
foreach (var valuesByPropertyEditorAlias in GetAllPropertyValues(value).GroupBy(x => x.PropertyType.PropertyEditorAlias, x => x.Value))
|
||||
{
|
||||
return Enumerable.Empty<UmbracoEntityReference>();
|
||||
}
|
||||
|
||||
// loop through all content and settings data
|
||||
foreach (BlockItemData row in blockEditorData.BlockValue.ContentData.Concat(blockEditorData.BlockValue.SettingsData))
|
||||
{
|
||||
foreach (KeyValuePair<string, BlockItemData.BlockPropertyValue> prop in row.PropertyValues)
|
||||
if (!_propertyEditors.TryGet(valuesByPropertyEditorAlias.Key, out IDataEditor? dataEditor))
|
||||
{
|
||||
IDataEditor? propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias];
|
||||
continue;
|
||||
}
|
||||
|
||||
IDataValueEditor? valueEditor = propEditor?.GetValueEditor();
|
||||
if (!(valueEditor is IDataValueReference reference))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var val = prop.Value.Value?.ToString();
|
||||
|
||||
IEnumerable<UmbracoEntityReference> refs = reference.GetReferences(val);
|
||||
|
||||
result.AddRange(refs);
|
||||
// Use distinct values to avoid duplicate parsing of the same value
|
||||
foreach (UmbracoEntityReference reference in _dataValueReferenceFactories.GetReferences(dataEditor, valuesByPropertyEditorAlias.Distinct()))
|
||||
{
|
||||
yield return reference;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<ITag> GetTags(object? value, object? dataTypeConfiguration, int? languageId)
|
||||
{
|
||||
foreach (BlockItemData.BlockPropertyValue propertyValue in GetAllPropertyValues(value))
|
||||
{
|
||||
if (!_propertyEditors.TryGet(propertyValue.PropertyType.PropertyEditorAlias, out IDataEditor? dataEditor) ||
|
||||
dataEditor.GetValueEditor() is not IDataValueTags dataValueTags)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
object? configuration = _dataTypeService.GetDataType(propertyValue.PropertyType.DataTypeKey)?.Configuration;
|
||||
foreach (ITag tag in dataValueTags.GetTags(propertyValue.Value, configuration, languageId))
|
||||
{
|
||||
yield return tag;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<BlockItemData.BlockPropertyValue> GetAllPropertyValues(object? value)
|
||||
{
|
||||
var rawJson = value == null ? string.Empty : value is string str ? str : value.ToString();
|
||||
|
||||
BlockEditorData? blockEditorData = BlockEditorValues.DeserializeAndClean(rawJson);
|
||||
if (blockEditorData == null)
|
||||
if (blockEditorData is null)
|
||||
{
|
||||
return Enumerable.Empty<ITag>();
|
||||
yield break;
|
||||
}
|
||||
|
||||
var result = new List<ITag>();
|
||||
// loop through all content and settings data
|
||||
foreach (BlockItemData row in blockEditorData.BlockValue.ContentData.Concat(blockEditorData.BlockValue.SettingsData))
|
||||
// Return all property values from the content and settings data
|
||||
IEnumerable<BlockItemData> data = blockEditorData.BlockValue.ContentData.Concat(blockEditorData.BlockValue.SettingsData);
|
||||
foreach (BlockItemData.BlockPropertyValue propertyValue in data.SelectMany(x => x.PropertyValues.Select(x => x.Value)))
|
||||
{
|
||||
foreach (KeyValuePair<string, BlockItemData.BlockPropertyValue> prop in row.PropertyValues)
|
||||
{
|
||||
IDataEditor? propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias];
|
||||
|
||||
IDataValueEditor? valueEditor = propEditor?.GetValueEditor();
|
||||
if (valueEditor is not IDataValueTags tagsProvider)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
object? configuration = _dataTypeService.GetDataType(prop.Value.PropertyType.DataTypeKey)?.Configuration;
|
||||
|
||||
result.AddRange(tagsProvider.GetTags(prop.Value.Value, configuration, languageId));
|
||||
}
|
||||
yield return propertyValue;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
#region Convert database // editor
|
||||
@@ -119,7 +109,6 @@ internal abstract class BlockEditorPropertyValueEditor : DataValueEditor, IDataV
|
||||
/// Ensure that sub-editor values are translated through their ToEditor methods
|
||||
/// </summary>
|
||||
/// <param name="property"></param>
|
||||
/// <param name="dataTypeService"></param>
|
||||
/// <param name="culture"></param>
|
||||
/// <param name="segment"></param>
|
||||
/// <returns></returns>
|
||||
|
||||
@@ -50,6 +50,7 @@ public abstract class BlockGridPropertyEditorBase : DataEditor
|
||||
public BlockGridEditorPropertyValueEditor(
|
||||
DataEditorAttribute attribute,
|
||||
PropertyEditorCollection propertyEditors,
|
||||
DataValueReferenceFactoryCollection dataValueReferenceFactories,
|
||||
IDataTypeService dataTypeService,
|
||||
ILocalizedTextService textService,
|
||||
ILogger<BlockEditorPropertyValueEditor> logger,
|
||||
@@ -58,7 +59,7 @@ public abstract class BlockGridPropertyEditorBase : DataEditor
|
||||
IIOHelper ioHelper,
|
||||
IContentTypeService contentTypeService,
|
||||
IPropertyValidationService propertyValidationService)
|
||||
: base(attribute, propertyEditors, dataTypeService, textService, logger, shortStringHelper, jsonSerializer, ioHelper)
|
||||
: base(attribute, propertyEditors, dataValueReferenceFactories, dataTypeService, textService, logger, shortStringHelper, jsonSerializer, ioHelper)
|
||||
{
|
||||
BlockEditorValues = new BlockEditorValues(new BlockGridEditorDataConverter(jsonSerializer), contentTypeService, logger);
|
||||
Validators.Add(new BlockEditorValidator(propertyValidationService, BlockEditorValues, contentTypeService));
|
||||
|
||||
@@ -46,6 +46,7 @@ public abstract class BlockListPropertyEditorBase : DataEditor
|
||||
public BlockListEditorPropertyValueEditor(
|
||||
DataEditorAttribute attribute,
|
||||
PropertyEditorCollection propertyEditors,
|
||||
DataValueReferenceFactoryCollection dataValueReferenceFactories,
|
||||
IDataTypeService dataTypeService,
|
||||
IContentTypeService contentTypeService,
|
||||
ILocalizedTextService textService,
|
||||
@@ -54,7 +55,7 @@ public abstract class BlockListPropertyEditorBase : DataEditor
|
||||
IJsonSerializer jsonSerializer,
|
||||
IIOHelper ioHelper,
|
||||
IPropertyValidationService propertyValidationService) :
|
||||
base(attribute, propertyEditors, dataTypeService, textService, logger, shortStringHelper, jsonSerializer, ioHelper)
|
||||
base(attribute, propertyEditors, dataValueReferenceFactories,dataTypeService, textService, logger, shortStringHelper, jsonSerializer, ioHelper)
|
||||
{
|
||||
BlockEditorValues = new BlockEditorValues(new BlockListEditorDataConverter(), contentTypeService, logger);
|
||||
Validators.Add(new BlockEditorValidator(propertyValidationService, BlockEditorValues, contentTypeService));
|
||||
|
||||
@@ -89,9 +89,10 @@ public class NestedContentPropertyEditor : DataEditor
|
||||
internal class NestedContentPropertyValueEditor : DataValueEditor, IDataValueReference, IDataValueTags
|
||||
{
|
||||
private readonly IDataTypeService _dataTypeService;
|
||||
private readonly PropertyEditorCollection _propertyEditors;
|
||||
private readonly DataValueReferenceFactoryCollection _dataValueReferenceFactories;
|
||||
private readonly ILogger<NestedContentPropertyEditor> _logger;
|
||||
private readonly NestedContentValues _nestedContentValues;
|
||||
private readonly PropertyEditorCollection _propertyEditors;
|
||||
|
||||
public NestedContentPropertyValueEditor(
|
||||
IDataTypeService dataTypeService,
|
||||
@@ -100,16 +101,19 @@ public class NestedContentPropertyEditor : DataEditor
|
||||
IShortStringHelper shortStringHelper,
|
||||
DataEditorAttribute attribute,
|
||||
PropertyEditorCollection propertyEditors,
|
||||
DataValueReferenceFactoryCollection dataValueReferenceFactories,
|
||||
ILogger<NestedContentPropertyEditor> logger,
|
||||
IJsonSerializer jsonSerializer,
|
||||
IIOHelper ioHelper,
|
||||
IPropertyValidationService propertyValidationService)
|
||||
: base(localizedTextService, shortStringHelper, jsonSerializer, ioHelper, attribute)
|
||||
{
|
||||
_propertyEditors = propertyEditors;
|
||||
_dataTypeService = dataTypeService;
|
||||
_propertyEditors = propertyEditors;
|
||||
_dataValueReferenceFactories = dataValueReferenceFactories;
|
||||
_logger = logger;
|
||||
_nestedContentValues = new NestedContentValues(contentTypeService);
|
||||
|
||||
Validators.Add(new NestedContentValidator(propertyValidationService, _nestedContentValues, contentTypeService));
|
||||
}
|
||||
|
||||
@@ -137,66 +141,47 @@ public class NestedContentPropertyEditor : DataEditor
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<UmbracoEntityReference> GetReferences(object? value)
|
||||
{
|
||||
var rawJson = value == null ? string.Empty : value is string str ? str : value.ToString();
|
||||
|
||||
var result = new List<UmbracoEntityReference>();
|
||||
|
||||
foreach (NestedContentValues.NestedContentRowValue row in _nestedContentValues.GetPropertyValues(rawJson))
|
||||
// Group by property editor alias to avoid duplicate lookups and optimize value parsing
|
||||
foreach (var valuesByPropertyEditorAlias in GetAllPropertyValues(value).GroupBy(x => x.PropertyType.PropertyEditorAlias, x => x.Value))
|
||||
{
|
||||
foreach (KeyValuePair<string, NestedContentValues.NestedContentPropertyValue> prop in
|
||||
row.PropertyValues)
|
||||
if (!_propertyEditors.TryGet(valuesByPropertyEditorAlias.Key, out IDataEditor? dataEditor))
|
||||
{
|
||||
IDataEditor? propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias];
|
||||
continue;
|
||||
}
|
||||
|
||||
IDataValueEditor? valueEditor = propEditor?.GetValueEditor();
|
||||
if (!(valueEditor is IDataValueReference reference))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var val = prop.Value.Value?.ToString();
|
||||
|
||||
IEnumerable<UmbracoEntityReference> refs = reference.GetReferences(val);
|
||||
|
||||
result.AddRange(refs);
|
||||
// Use distinct values to avoid duplicate parsing of the same value
|
||||
foreach (UmbracoEntityReference reference in _dataValueReferenceFactories.GetReferences(dataEditor, valuesByPropertyEditorAlias.Distinct()))
|
||||
{
|
||||
yield return reference;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<ITag> GetTags(object? value, object? dataTypeConfiguration, int? languageId)
|
||||
{
|
||||
IReadOnlyList<NestedContentValues.NestedContentRowValue> rows =
|
||||
_nestedContentValues.GetPropertyValues(value);
|
||||
|
||||
var result = new List<ITag>();
|
||||
|
||||
foreach (NestedContentValues.NestedContentRowValue row in rows.ToList())
|
||||
foreach (NestedContentValues.NestedContentPropertyValue propertyValue in GetAllPropertyValues(value))
|
||||
{
|
||||
foreach (KeyValuePair<string, NestedContentValues.NestedContentPropertyValue> prop in row.PropertyValues
|
||||
.ToList())
|
||||
if (!_propertyEditors.TryGet(propertyValue.PropertyType.PropertyEditorAlias, out IDataEditor? dataEditor) ||
|
||||
dataEditor.GetValueEditor() is not IDataValueTags dataValueTags)
|
||||
{
|
||||
IDataEditor? propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias];
|
||||
continue;
|
||||
}
|
||||
|
||||
IDataValueEditor? valueEditor = propEditor?.GetValueEditor();
|
||||
if (valueEditor is not IDataValueTags tagsProvider)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
object? configuration = _dataTypeService.GetDataType(prop.Value.PropertyType.DataTypeKey)?.Configuration;
|
||||
|
||||
result.AddRange(tagsProvider.GetTags(prop.Value.Value, configuration, languageId));
|
||||
object? configuration = _dataTypeService.GetDataType(propertyValue.PropertyType.DataTypeKey)?.Configuration;
|
||||
foreach (ITag tag in dataValueTags.GetTags(propertyValue.Value, configuration, languageId))
|
||||
{
|
||||
yield return tag;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private IEnumerable<NestedContentValues.NestedContentPropertyValue> GetAllPropertyValues(object? value)
|
||||
=> _nestedContentValues.GetPropertyValues(value).SelectMany(x => x.PropertyValues.Values);
|
||||
|
||||
#region DB to String
|
||||
|
||||
public override string ConvertDbToString(IPropertyType propertyType, object? propertyValue)
|
||||
@@ -422,7 +407,8 @@ public class NestedContentPropertyEditor : DataEditor
|
||||
// set values to null
|
||||
row.PropertyValues[elementTypeProp.Alias] = new NestedContentValues.NestedContentPropertyValue
|
||||
{
|
||||
PropertyType = elementTypeProp, Value = null,
|
||||
PropertyType = elementTypeProp,
|
||||
Value = null,
|
||||
};
|
||||
row.RawPropertyValues[elementTypeProp.Alias] = null;
|
||||
}
|
||||
|
||||
@@ -23,10 +23,9 @@ namespace Umbraco.Cms.Infrastructure.Scoping
|
||||
private readonly bool _autoComplete;
|
||||
private readonly CoreDebugSettings _coreDebugSettings;
|
||||
|
||||
private readonly object _dictionaryLocker;
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
private readonly IsolationLevel _isolationLevel;
|
||||
private readonly object _lockQueueLocker = new();
|
||||
private readonly object _locker = new();
|
||||
private readonly ILogger<Scope> _logger;
|
||||
private readonly MediaFileManager _mediaFileManager;
|
||||
private readonly RepositoryCacheMode _repositoryCacheMode;
|
||||
@@ -87,7 +86,6 @@ namespace Umbraco.Cms.Infrastructure.Scoping
|
||||
_scopeFileSystem = scopeFileSystems;
|
||||
_autoComplete = autoComplete;
|
||||
Detachable = detachable;
|
||||
_dictionaryLocker = new object();
|
||||
|
||||
#if DEBUG_SCOPES
|
||||
_scopeProvider.RegisterScope(this);
|
||||
@@ -562,7 +560,7 @@ namespace Umbraco.Cms.Infrastructure.Scoping
|
||||
DisposeLastScope();
|
||||
}
|
||||
|
||||
lock (_lockQueueLocker)
|
||||
lock (_locker)
|
||||
{
|
||||
_queuedLocks?.Clear();
|
||||
}
|
||||
@@ -573,24 +571,24 @@ namespace Umbraco.Cms.Infrastructure.Scoping
|
||||
public void EagerReadLock(params int[] lockIds) => EagerReadLockInner(InstanceId, null, lockIds);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ReadLock(params int[] lockIds) => LazyReadLockInner(InstanceId, lockIds);
|
||||
public void ReadLock(params int[] lockIds) => EagerReadLockInner(InstanceId, null, lockIds);
|
||||
|
||||
public void EagerReadLock(TimeSpan timeout, int lockId) =>
|
||||
EagerReadLockInner(InstanceId, timeout, lockId);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ReadLock(TimeSpan timeout, int lockId) => LazyReadLockInner(InstanceId, timeout, lockId);
|
||||
public void ReadLock(TimeSpan timeout, int lockId) => EagerReadLockInner(InstanceId, timeout, lockId);
|
||||
|
||||
public void EagerWriteLock(params int[] lockIds) => EagerWriteLockInner(InstanceId, null, lockIds);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void WriteLock(params int[] lockIds) => LazyWriteLockInner(InstanceId, lockIds);
|
||||
public void WriteLock(params int[] lockIds) => EagerWriteLockInner(InstanceId, null, lockIds);
|
||||
|
||||
public void EagerWriteLock(TimeSpan timeout, int lockId) =>
|
||||
EagerWriteLockInner(InstanceId, timeout, lockId);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void WriteLock(TimeSpan timeout, int lockId) => LazyWriteLockInner(InstanceId, timeout, lockId);
|
||||
public void WriteLock(TimeSpan timeout, int lockId) => EagerWriteLockInner(InstanceId, timeout, lockId);
|
||||
|
||||
/// <summary>
|
||||
/// Used for testing. Ensures and gets any queued read locks.
|
||||
@@ -659,7 +657,7 @@ namespace Umbraco.Cms.Infrastructure.Scoping
|
||||
}
|
||||
else
|
||||
{
|
||||
lock (_lockQueueLocker)
|
||||
lock (_locker)
|
||||
{
|
||||
if (_queuedLocks?.Count > 0)
|
||||
{
|
||||
@@ -970,7 +968,7 @@ namespace Umbraco.Cms.Infrastructure.Scoping
|
||||
}
|
||||
else
|
||||
{
|
||||
lock (_dictionaryLocker)
|
||||
lock (_locker)
|
||||
{
|
||||
_readLocksDictionary?.Remove(instanceId);
|
||||
_writeLocksDictionary?.Remove(instanceId);
|
||||
@@ -1045,7 +1043,7 @@ namespace Umbraco.Cms.Infrastructure.Scoping
|
||||
|
||||
private void LazyLockInner(DistributedLockType lockType, Guid instanceId, params int[] lockIds)
|
||||
{
|
||||
lock (_lockQueueLocker)
|
||||
lock (_locker)
|
||||
{
|
||||
if (_queuedLocks == null)
|
||||
{
|
||||
@@ -1061,7 +1059,7 @@ namespace Umbraco.Cms.Infrastructure.Scoping
|
||||
|
||||
private void LazyLockInner(DistributedLockType lockType, Guid instanceId, TimeSpan timeout, int lockId)
|
||||
{
|
||||
lock (_lockQueueLocker)
|
||||
lock (_locker)
|
||||
{
|
||||
if (_queuedLocks == null)
|
||||
{
|
||||
@@ -1088,7 +1086,7 @@ namespace Umbraco.Cms.Infrastructure.Scoping
|
||||
}
|
||||
else
|
||||
{
|
||||
lock (_dictionaryLocker)
|
||||
lock (_locker)
|
||||
{
|
||||
foreach (var lockId in lockIds)
|
||||
{
|
||||
@@ -1122,7 +1120,7 @@ namespace Umbraco.Cms.Infrastructure.Scoping
|
||||
}
|
||||
else
|
||||
{
|
||||
lock (_dictionaryLocker)
|
||||
lock (_locker)
|
||||
{
|
||||
foreach (var lockId in lockIds)
|
||||
{
|
||||
|
||||
@@ -134,8 +134,8 @@ public abstract class UmbracoUserManager<TUser, TPasswordConfig> : UserManager<T
|
||||
/// <inheritdoc />
|
||||
public override async Task<bool> CheckPasswordAsync(TUser user, string? password)
|
||||
{
|
||||
// we cannot proceed if the user passed in does not have an identity
|
||||
if (user.HasIdentity == false)
|
||||
// we cannot proceed if the user passed in does not have an identity, or if no password is provided.
|
||||
if (user.HasIdentity == false || password is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -252,7 +252,7 @@ public abstract class UmbracoUserManager<TUser, TPasswordConfig> : UserManager<T
|
||||
public async Task<bool> ValidateCredentialsAsync(string username, string password)
|
||||
{
|
||||
TUser user = await FindByNameAsync(username);
|
||||
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
return false;
|
||||
@@ -263,7 +263,7 @@ public abstract class UmbracoUserManager<TUser, TPasswordConfig> : UserManager<T
|
||||
throw new NotSupportedException("The current user store does not implement " +
|
||||
typeof(IUserPasswordStore<>));
|
||||
}
|
||||
|
||||
|
||||
var result = await VerifyPasswordAsync(userPasswordStore, user, password);
|
||||
|
||||
return result == PasswordVerificationResult.Success || result == PasswordVerificationResult.SuccessRehashNeeded;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
using Umbraco.Cms.Core;
|
||||
@@ -6,6 +7,7 @@ using Umbraco.Cms.Core.Models.Editors;
|
||||
using Umbraco.Cms.Core.PropertyEditors;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Infrastructure.Macros;
|
||||
using Umbraco.Cms.Web.Common.DependencyInjection;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Templates;
|
||||
@@ -15,12 +17,23 @@ public sealed class HtmlMacroParameterParser : IHtmlMacroParameterParser
|
||||
private readonly ILogger<HtmlMacroParameterParser> _logger;
|
||||
private readonly IMacroService _macroService;
|
||||
private readonly ParameterEditorCollection _parameterEditors;
|
||||
private readonly DataValueReferenceFactoryCollection _dataValueReferenceFactories;
|
||||
|
||||
[Obsolete("Use the non-obsolete overload instead, scheduled for removal in v14")]
|
||||
public HtmlMacroParameterParser(IMacroService macroService, ILogger<HtmlMacroParameterParser> logger, ParameterEditorCollection parameterEditors)
|
||||
: this(
|
||||
macroService,
|
||||
logger,
|
||||
parameterEditors,
|
||||
StaticServiceProvider.Instance.GetRequiredService<DataValueReferenceFactoryCollection>())
|
||||
{ }
|
||||
|
||||
public HtmlMacroParameterParser(IMacroService macroService, ILogger<HtmlMacroParameterParser> logger, ParameterEditorCollection parameterEditors, DataValueReferenceFactoryCollection dataValueReferenceFactories)
|
||||
{
|
||||
_macroService = macroService;
|
||||
_logger = logger;
|
||||
_parameterEditors = parameterEditors;
|
||||
_dataValueReferenceFactories = dataValueReferenceFactories;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -41,6 +54,7 @@ public sealed class HtmlMacroParameterParser : IHtmlMacroParameterParser
|
||||
(macroAlias, macroAttributes) => foundMacros.Add(new Tuple<string?, Dictionary<string, string>>(
|
||||
macroAlias,
|
||||
new Dictionary<string, string>(macroAttributes, StringComparer.OrdinalIgnoreCase))));
|
||||
|
||||
foreach (UmbracoEntityReference umbracoEntityReference in GetUmbracoEntityReferencesFromMacros(foundMacros))
|
||||
{
|
||||
yield return umbracoEntityReference;
|
||||
@@ -52,8 +66,7 @@ public sealed class HtmlMacroParameterParser : IHtmlMacroParameterParser
|
||||
/// </summary>
|
||||
/// <param name="macroGridControls"></param>
|
||||
/// <returns></returns>
|
||||
public IEnumerable<UmbracoEntityReference> FindUmbracoEntityReferencesFromGridControlMacros(
|
||||
IEnumerable<GridValue.GridControl> macroGridControls)
|
||||
public IEnumerable<UmbracoEntityReference> FindUmbracoEntityReferencesFromGridControlMacros(IEnumerable<GridValue.GridControl> macroGridControls)
|
||||
{
|
||||
var foundMacros = new List<Tuple<string?, Dictionary<string, string>>>();
|
||||
|
||||
@@ -65,8 +78,7 @@ public sealed class HtmlMacroParameterParser : IHtmlMacroParameterParser
|
||||
// Collect any macro parameters that contain the media udi format
|
||||
if (gridMacro is not null && gridMacro.MacroParameters is not null && gridMacro.MacroParameters.Any())
|
||||
{
|
||||
foundMacros.Add(
|
||||
new Tuple<string?, Dictionary<string, string>>(gridMacro.MacroAlias, gridMacro.MacroParameters));
|
||||
foundMacros.Add(new Tuple<string?, Dictionary<string, string>>(gridMacro.MacroAlias, gridMacro.MacroParameters));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,14 +113,12 @@ public sealed class HtmlMacroParameterParser : IHtmlMacroParameterParser
|
||||
continue;
|
||||
}
|
||||
|
||||
foundMacroUmbracoEntityReferences.Add(
|
||||
new UmbracoEntityReference(Udi.Create(Constants.UdiEntityType.Macro, macroConfig.Key)));
|
||||
foundMacroUmbracoEntityReferences.Add(new UmbracoEntityReference(Udi.Create(Constants.UdiEntityType.Macro, macroConfig.Key)));
|
||||
|
||||
// Only do this if the macros actually have parameters
|
||||
if (macroConfig.Properties.Keys.Any(f => f != "macroAlias"))
|
||||
{
|
||||
foreach (UmbracoEntityReference umbracoEntityReference in GetUmbracoEntityReferencesFromMacroParameters(
|
||||
macro.Item2, macroConfig, _parameterEditors))
|
||||
foreach (UmbracoEntityReference umbracoEntityReference in GetUmbracoEntityReferencesFromMacroParameters(macro.Item2, macroConfig, _parameterEditors))
|
||||
{
|
||||
yield return umbracoEntityReference;
|
||||
}
|
||||
@@ -130,41 +140,23 @@ public sealed class HtmlMacroParameterParser : IHtmlMacroParameterParser
|
||||
/// look up the corresponding property editor for a macro parameter
|
||||
/// </param>
|
||||
/// <returns></returns>
|
||||
private IEnumerable<UmbracoEntityReference> GetUmbracoEntityReferencesFromMacroParameters(
|
||||
Dictionary<string, string> macroParameters, IMacro macroConfig, ParameterEditorCollection parameterEditors)
|
||||
private IEnumerable<UmbracoEntityReference> GetUmbracoEntityReferencesFromMacroParameters(Dictionary<string, string> macroParameters, IMacro macroConfig, ParameterEditorCollection parameterEditors)
|
||||
{
|
||||
var foundUmbracoEntityReferences = new List<UmbracoEntityReference>();
|
||||
foreach (IMacroProperty parameter in macroConfig.Properties)
|
||||
{
|
||||
if (macroParameters.TryGetValue(parameter.Alias, out var parameterValue))
|
||||
{
|
||||
var parameterEditorAlias = parameter.EditorAlias;
|
||||
|
||||
// Lookup propertyEditor from the registered ParameterEditors with the implmementation to avoid looking up for each parameter
|
||||
IDataEditor? parameterEditor = parameterEditors.FirstOrDefault(f =>
|
||||
string.Equals(f.Alias, parameterEditorAlias, StringComparison.OrdinalIgnoreCase));
|
||||
IDataEditor? parameterEditor = parameterEditors.FirstOrDefault(f => string.Equals(f.Alias, parameterEditorAlias, StringComparison.OrdinalIgnoreCase));
|
||||
if (parameterEditor is not null)
|
||||
{
|
||||
// Get the ParameterValueEditor for this PropertyEditor (where the GetReferences method is implemented) - cast as IDataValueReference to determine if 'it is' implemented for the editor
|
||||
if (parameterEditor.GetValueEditor() is IDataValueReference parameterValueEditor)
|
||||
foreach (UmbracoEntityReference entityReference in _dataValueReferenceFactories.GetReferences(parameterEditor, parameterValue))
|
||||
{
|
||||
foreach (UmbracoEntityReference entityReference in parameterValueEditor.GetReferences(
|
||||
parameterValue))
|
||||
{
|
||||
foundUmbracoEntityReferences.Add(entityReference);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"{0} doesn't have a ValueEditor that implements IDataValueReference",
|
||||
parameterEditor.Alias);
|
||||
yield return entityReference;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return foundUmbracoEntityReferences;
|
||||
}
|
||||
|
||||
// Poco class to deserialise the Json for a Macro Control
|
||||
|
||||
@@ -27,6 +27,8 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache;
|
||||
/// </remarks>
|
||||
public class ContentStore
|
||||
{
|
||||
private static readonly TimeSpan _monitorTimeout = TimeSpan.FromSeconds(30);
|
||||
|
||||
// TODO: collection trigger (ok for now)
|
||||
// see SnapDictionary notes
|
||||
private const long CollectMinGenDelta = 8;
|
||||
@@ -330,7 +332,12 @@ public class ContentStore
|
||||
throw new InvalidOperationException("Recursive locks not allowed");
|
||||
}
|
||||
|
||||
Monitor.Enter(_wlocko, ref lockInfo.Taken);
|
||||
Monitor.TryEnter(_wlocko, _monitorTimeout, ref lockInfo.Taken);
|
||||
|
||||
if (Monitor.IsEntered(_wlocko) is false)
|
||||
{
|
||||
throw new TimeoutException("Could not enter monitor before timeout in content store");
|
||||
}
|
||||
|
||||
lock (_rlocko)
|
||||
{
|
||||
|
||||
@@ -127,9 +127,25 @@ public class NuCacheContentService : RepositoryService, INuCacheContentService
|
||||
{
|
||||
using (ICoreScope scope = ScopeProvider.CreateCoreScope(repositoryCacheMode: RepositoryCacheMode.Scoped))
|
||||
{
|
||||
scope.ReadLock(Constants.Locks.ContentTree);
|
||||
scope.ReadLock(Constants.Locks.MediaTree);
|
||||
scope.ReadLock(Constants.Locks.MemberTree);
|
||||
if (contentTypeIds is null && mediaTypeIds is null && memberTypeIds is null)
|
||||
{
|
||||
scope.ReadLock(Constants.Locks.ContentTree,Constants.Locks.MediaTree,Constants.Locks.MemberTree);
|
||||
}
|
||||
|
||||
if (contentTypeIds is not null && contentTypeIds.Any())
|
||||
{
|
||||
scope.ReadLock(Constants.Locks.ContentTree);
|
||||
}
|
||||
|
||||
if (mediaTypeIds is not null && mediaTypeIds.Any())
|
||||
{
|
||||
scope.ReadLock(Constants.Locks.MediaTree);
|
||||
}
|
||||
|
||||
if (memberTypeIds is not null && memberTypeIds.Any())
|
||||
{
|
||||
scope.ReadLock(Constants.Locks.MemberTree);
|
||||
}
|
||||
|
||||
_repository.Rebuild(contentTypeIds, mediaTypeIds, memberTypeIds);
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ public class SnapDictionary<TKey, TValue>
|
||||
where TValue : class
|
||||
where TKey : notnull
|
||||
{
|
||||
private static readonly TimeSpan _monitorTimeout = TimeSpan.FromSeconds(30);
|
||||
|
||||
// minGenDelta to be adjusted
|
||||
// we may want to throttle collects even if delta is reached
|
||||
// we may want to force collect if delta is not reached but very old
|
||||
@@ -198,7 +200,12 @@ public class SnapDictionary<TKey, TValue>
|
||||
throw new InvalidOperationException("Recursive locks not allowed");
|
||||
}
|
||||
|
||||
Monitor.Enter(_wlocko, ref lockInfo.Taken);
|
||||
Monitor.TryEnter(_wlocko, _monitorTimeout, ref lockInfo.Taken);
|
||||
|
||||
if (Monitor.IsEntered(_wlocko) is false)
|
||||
{
|
||||
throw new TimeoutException("Could not enter the monitor before timeout in SnapDictionary");
|
||||
}
|
||||
|
||||
lock (_rlocko)
|
||||
{
|
||||
|
||||
@@ -19,6 +19,8 @@ public class
|
||||
{
|
||||
private readonly ContentPermissions _contentPermissions;
|
||||
|
||||
protected override UmbracoObjectTypes KeyParsingFilterType => UmbracoObjectTypes.Document;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ContentPermissionsQueryStringHandler" /> class.
|
||||
/// </summary>
|
||||
@@ -47,7 +49,11 @@ public class
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
var argument = routeVal.ToString();
|
||||
// Handle case where the incoming querystring could contain more than one value (e.g. ?id=1000&id=1001).
|
||||
// It's the first one that'll be processed by the protected method so we should verify that.
|
||||
var argument = routeVal.Count == 1
|
||||
? routeVal.ToString()
|
||||
: routeVal.FirstOrDefault()?.ToString() ?? string.Empty;
|
||||
|
||||
if (!TryParseNodeId(argument, out nodeId))
|
||||
{
|
||||
|
||||
@@ -18,6 +18,8 @@ public class MediaPermissionsQueryStringHandler : PermissionsQueryStringHandler<
|
||||
{
|
||||
private readonly MediaPermissions _mediaPermissions;
|
||||
|
||||
protected override UmbracoObjectTypes KeyParsingFilterType => UmbracoObjectTypes.Media;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MediaPermissionsQueryStringHandler" /> class.
|
||||
/// </summary>
|
||||
@@ -44,7 +46,11 @@ public class MediaPermissionsQueryStringHandler : PermissionsQueryStringHandler<
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
var argument = routeVal.ToString();
|
||||
// Handle case where the incoming querystring could contain more than one value (e.g. ?id=1000&id=1001).
|
||||
// It's the first one that'll be processed by the protected method so we should verify that.
|
||||
var argument = routeVal.Count == 1
|
||||
? routeVal.ToString()
|
||||
: routeVal.FirstOrDefault()?.ToString() ?? string.Empty;
|
||||
|
||||
if (!TryParseNodeId(argument, out var nodeId))
|
||||
{
|
||||
|
||||
@@ -49,12 +49,18 @@ public abstract class PermissionsQueryStringHandler<T> : MustSatisfyRequirementA
|
||||
/// </summary>
|
||||
protected IEntityService EntityService { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Defaults to Unknown so all types are allowed, since Keys are unique across all node types this works,
|
||||
/// but it if you are certain you are looking for a specific type this should be overwritten for DB query performance.
|
||||
/// </summary>
|
||||
protected virtual UmbracoObjectTypes KeyParsingFilterType => UmbracoObjectTypes.Unknown;
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to parse a node ID from a string representation found in a querystring value.
|
||||
/// </summary>
|
||||
/// <param name="argument">Querystring value.</param>
|
||||
/// <param name="nodeId">Output parsed Id.</param>
|
||||
/// <returns>True of node ID could be parased, false it not.</returns>
|
||||
/// <returns>True of node ID could be parsed, false it not.</returns>
|
||||
protected bool TryParseNodeId(string argument, out int nodeId)
|
||||
{
|
||||
// If the argument is an int, it will parse and can be assigned to nodeId.
|
||||
@@ -75,7 +81,7 @@ public abstract class PermissionsQueryStringHandler<T> : MustSatisfyRequirementA
|
||||
|
||||
if (Guid.TryParse(argument, out Guid key))
|
||||
{
|
||||
nodeId = EntityService.GetId(key, UmbracoObjectTypes.Document).Result;
|
||||
nodeId = EntityService.GetId(key, KeyParsingFilterType).Result;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -74,6 +74,9 @@ public class AuthenticationController : UmbracoApiControllerBase
|
||||
private readonly IUserService _userService;
|
||||
private readonly WebRoutingSettings _webRoutingSettings;
|
||||
|
||||
private const int FailedLoginDurationRandomOffsetInMilliseconds = 100;
|
||||
private static long? _loginDurationAverage;
|
||||
|
||||
// TODO: We need to review all _userManager.Raise calls since many/most should be on the usermanager or signinmanager, very few should be here
|
||||
[ActivatorUtilitiesConstructor]
|
||||
public AuthenticationController(
|
||||
@@ -129,12 +132,17 @@ public class AuthenticationController : UmbracoApiControllerBase
|
||||
AuthorizationPolicies.BackOfficeAccess)] // Needed to enforce the principle set on the request, if one exists.
|
||||
public IDictionary<string, object> GetPasswordConfig(int userId)
|
||||
{
|
||||
if (HttpContext.HasActivePasswordResetFlowSession(userId))
|
||||
{
|
||||
return _passwordConfiguration.GetConfiguration();
|
||||
}
|
||||
|
||||
Attempt<int> currentUserId =
|
||||
_backofficeSecurityAccessor.BackOfficeSecurity?.GetUserId() ?? Attempt<int>.Fail();
|
||||
return _passwordConfiguration.GetConfiguration(
|
||||
currentUserId.Success
|
||||
? currentUserId.Result != userId
|
||||
: true);
|
||||
|
||||
return currentUserId.Success
|
||||
? _passwordConfiguration.GetConfiguration(currentUserId.Result != userId)
|
||||
: new Dictionary<string, object>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -342,47 +350,85 @@ public class AuthenticationController : UmbracoApiControllerBase
|
||||
[Authorize(Policy = AuthorizationPolicies.DenyLocalLoginIfConfigured)]
|
||||
public async Task<ActionResult<UserDetail?>> PostLogin(LoginModel loginModel)
|
||||
{
|
||||
HttpContext.EndPasswordResetFlowSession();
|
||||
|
||||
// Start a timed scope to ensure failed responses return is a consistent time
|
||||
await using var timedScope = new TimedScope(GetLoginDuration(), CancellationToken.None);
|
||||
|
||||
// Sign the user in with username/password, this also gives a chance for developers to
|
||||
// custom verify the credentials and auto-link user accounts with a custom IBackOfficePasswordChecker
|
||||
SignInResult result = await _signInManager.PasswordSignInAsync(
|
||||
loginModel.Username, loginModel.Password, true, true);
|
||||
|
||||
if (result.Succeeded)
|
||||
if (result.Succeeded is false)
|
||||
{
|
||||
// return the user detail
|
||||
return GetUserDetail(_userService.GetByUsername(loginModel.Username));
|
||||
}
|
||||
BackOfficeIdentityUser? user = await _userManager.FindByNameAsync(loginModel.Username.Trim());
|
||||
|
||||
if (result.RequiresTwoFactor)
|
||||
{
|
||||
var twofactorView = _backOfficeTwoFactorOptions.GetTwoFactorView(loginModel.Username);
|
||||
if (twofactorView.IsNullOrWhiteSpace())
|
||||
if (user is not null &&
|
||||
await _userManager.CheckPasswordAsync(user, loginModel.Password))
|
||||
{
|
||||
return new ValidationErrorResult(
|
||||
$"The registered {typeof(IBackOfficeTwoFactorOptions)} of type {_backOfficeTwoFactorOptions.GetType()} did not return a view for two factor auth ");
|
||||
// The credentials were correct, so cancel timed scope and provide a more detailed failure response
|
||||
timedScope.Cancel();
|
||||
|
||||
if (result.RequiresTwoFactor)
|
||||
{
|
||||
var twofactorView = _backOfficeTwoFactorOptions.GetTwoFactorView(loginModel.Username);
|
||||
if (twofactorView.IsNullOrWhiteSpace())
|
||||
{
|
||||
return new ValidationErrorResult(
|
||||
$"The registered {typeof(IBackOfficeTwoFactorOptions)} of type {_backOfficeTwoFactorOptions.GetType()} did not return a view for two factor auth ");
|
||||
}
|
||||
|
||||
IUser? attemptedUser = _userService.GetByUsername(loginModel.Username);
|
||||
|
||||
// create a with information to display a custom two factor send code view
|
||||
var verifyResponse =
|
||||
new ObjectResult(new { twoFactorView = twofactorView, userId = attemptedUser?.Id })
|
||||
{
|
||||
StatusCode = StatusCodes.Status402PaymentRequired
|
||||
};
|
||||
|
||||
return verifyResponse;
|
||||
}
|
||||
|
||||
// TODO: We can check for these and respond differently if we think it's important
|
||||
// result.IsLockedOut
|
||||
// result.IsNotAllowed
|
||||
}
|
||||
|
||||
IUser? attemptedUser = _userService.GetByUsername(loginModel.Username);
|
||||
|
||||
// create a with information to display a custom two factor send code view
|
||||
var verifyResponse =
|
||||
new ObjectResult(new { twoFactorView = twofactorView, userId = attemptedUser?.Id })
|
||||
{
|
||||
StatusCode = StatusCodes.Status402PaymentRequired
|
||||
};
|
||||
|
||||
return verifyResponse;
|
||||
// Return BadRequest (400), we don't want to return a 401 because that get's intercepted
|
||||
// by our angular helper because it thinks that we need to re-perform the request once we are
|
||||
// authorized and we don't want to return a 403 because angular will show a warning message indicating
|
||||
// that the user doesn't have access to perform this function, we just want to return a normal invalid message.
|
||||
return BadRequest();
|
||||
}
|
||||
|
||||
// TODO: We can check for these and respond differently if we think it's important
|
||||
// result.IsLockedOut
|
||||
// result.IsNotAllowed
|
||||
// Set initial or update average (successful) login duration
|
||||
_loginDurationAverage = _loginDurationAverage is long average
|
||||
? (average + (long)timedScope.Elapsed.TotalMilliseconds) / 2
|
||||
: (long)timedScope.Elapsed.TotalMilliseconds;
|
||||
|
||||
// return BadRequest (400), we don't want to return a 401 because that get's intercepted
|
||||
// by our angular helper because it thinks that we need to re-perform the request once we are
|
||||
// authorized and we don't want to return a 403 because angular will show a warning message indicating
|
||||
// that the user doesn't have access to perform this function, we just want to return a normal invalid message.
|
||||
return BadRequest();
|
||||
// Cancel the timed scope (we don't want to unnecessarily wait on a successful response)
|
||||
timedScope.Cancel();
|
||||
|
||||
// Return the user detail
|
||||
return GetUserDetail(_userService.GetByUsername(loginModel.Username));
|
||||
}
|
||||
|
||||
private long GetLoginDuration()
|
||||
{
|
||||
var loginDuration = Math.Max(_loginDurationAverage ?? _securitySettings.UserDefaultFailedLoginDurationInMilliseconds, _securitySettings.UserMinimumFailedLoginDurationInMilliseconds);
|
||||
var random = new Random();
|
||||
var randomDelay = random.Next(-FailedLoginDurationRandomOffsetInMilliseconds, FailedLoginDurationRandomOffsetInMilliseconds);
|
||||
loginDuration += randomDelay;
|
||||
|
||||
// Just be sure we don't get a negative number - possible if someone has configured a very low UserMinimumFailedLoginDurationInMilliseconds value.
|
||||
if (loginDuration < 0)
|
||||
{
|
||||
loginDuration = 0;
|
||||
}
|
||||
|
||||
return loginDuration;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -401,6 +447,8 @@ public class AuthenticationController : UmbracoApiControllerBase
|
||||
return BadRequest();
|
||||
}
|
||||
|
||||
HttpContext.EndPasswordResetFlowSession();
|
||||
|
||||
BackOfficeIdentityUser? identityUser = await _userManager.FindByEmailAsync(model.Email);
|
||||
|
||||
await Task.Delay(RandomNumberGenerator.GetInt32(400, 2500)); // To randomize response time preventing user enumeration
|
||||
@@ -554,6 +602,8 @@ public class AuthenticationController : UmbracoApiControllerBase
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> PostSetPassword(SetPasswordModel model)
|
||||
{
|
||||
HttpContext.EndPasswordResetFlowSession();
|
||||
|
||||
BackOfficeIdentityUser? identityUser =
|
||||
await _userManager.FindByIdAsync(model.UserId.ToString(CultureInfo.InvariantCulture));
|
||||
|
||||
|
||||
@@ -370,6 +370,8 @@ public class BackOfficeController : UmbracoController
|
||||
var result = await _userManager.VerifyUserTokenAsync(user, "Default", "ResetPassword", resetCode);
|
||||
if (result)
|
||||
{
|
||||
HttpContext.StartPasswordResetFlowSession(userId);
|
||||
|
||||
//Add a flag and redirect for it to be displayed
|
||||
TempData[ViewDataExtensions.TokenPasswordResetCode] =
|
||||
_jsonSerializer.Serialize(
|
||||
|
||||
@@ -196,6 +196,7 @@ public class ContentController : ContentControllerBase
|
||||
/// Permission check is done for letter 'R' which is for <see cref="ActionRights" /> which the user must have access to
|
||||
/// update
|
||||
/// </remarks>
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<IEnumerable<AssignedUserGroupPermissions?>?>> PostSaveUserGroupPermissions(
|
||||
UserGroupPermissionsSave saveModel)
|
||||
{
|
||||
@@ -842,6 +843,7 @@ public class ContentController : ContentControllerBase
|
||||
[Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)]
|
||||
[FileUploadCleanupFilter]
|
||||
[ContentSaveValidation(skipUserAccessValidation:true)] // skip user access validation because we "only" require Settings access to create new blueprints from scratch
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<ContentItemDisplay<ContentVariantDisplay>?>?> PostSaveBlueprint(
|
||||
[ModelBinder(typeof(BlueprintItemBinder))] ContentItemSave contentItem)
|
||||
{
|
||||
@@ -879,6 +881,7 @@ public class ContentController : ContentControllerBase
|
||||
[FileUploadCleanupFilter]
|
||||
[ContentSaveValidation]
|
||||
[OutgoingEditorModelEvent]
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<ContentItemDisplay<ContentVariantScheduleDisplay>?>> PostSave(
|
||||
[ModelBinder(typeof(ContentItemBinder))] ContentItemSave contentItem)
|
||||
{
|
||||
@@ -1960,6 +1963,7 @@ public class ContentController : ContentControllerBase
|
||||
/// does not have Publish access to this node.
|
||||
/// </remarks>
|
||||
[Authorize(Policy = AuthorizationPolicies.ContentPermissionPublishById)]
|
||||
[HttpPost]
|
||||
public IActionResult PostPublishById(int id)
|
||||
{
|
||||
IContent? foundContent = GetObjectFromRequest(() => _contentService.GetById(id));
|
||||
@@ -1991,6 +1995,7 @@ public class ContentController : ContentControllerBase
|
||||
/// does not have Publish access to this node.
|
||||
/// </remarks>
|
||||
[Authorize(Policy = AuthorizationPolicies.ContentPermissionPublishById)]
|
||||
[HttpPost]
|
||||
public IActionResult PostPublishByIdAndCulture(PublishContent model)
|
||||
{
|
||||
var languageCount = _allLangs.Value.Count();
|
||||
@@ -2114,6 +2119,7 @@ public class ContentController : ContentControllerBase
|
||||
/// </summary>
|
||||
/// <param name="sorted"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> PostSort(ContentSortOrder sorted)
|
||||
{
|
||||
if (sorted == null)
|
||||
@@ -2165,6 +2171,7 @@ public class ContentController : ContentControllerBase
|
||||
/// </summary>
|
||||
/// <param name="move"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost]
|
||||
public async Task<IActionResult?> PostMove(MoveOrCopy move)
|
||||
{
|
||||
// Authorize...
|
||||
@@ -2199,6 +2206,7 @@ public class ContentController : ContentControllerBase
|
||||
/// </summary>
|
||||
/// <param name="copy"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<IContent>?> PostCopy(MoveOrCopy copy)
|
||||
{
|
||||
// Authorize...
|
||||
@@ -2238,6 +2246,7 @@ public class ContentController : ContentControllerBase
|
||||
/// <param name="model">The content and variants to unpublish</param>
|
||||
/// <returns></returns>
|
||||
[OutgoingEditorModelEvent]
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<ContentItemDisplayWithSchedule?>> PostUnpublish(UnpublishContent model)
|
||||
{
|
||||
IContent? foundContent = _contentService.GetById(model.Id);
|
||||
@@ -2960,6 +2969,7 @@ public class ContentController : ContentControllerBase
|
||||
return notifications;
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public IActionResult PostNotificationOptions(
|
||||
int contentId,
|
||||
[FromQuery(Name = "notifyOptions[]")] string[] notifyOptions)
|
||||
|
||||
@@ -7,6 +7,7 @@ using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.IO;
|
||||
using Umbraco.Cms.Core.Media;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Routing;
|
||||
using Umbraco.Cms.Web.Common.Attributes;
|
||||
using Umbraco.Cms.Web.Common.DependencyInjection;
|
||||
using Umbraco.Extensions;
|
||||
@@ -123,7 +124,7 @@ public class ImagesController : UmbracoAuthorizedApiController
|
||||
|
||||
private bool IsAllowed(string encodedImagePath)
|
||||
{
|
||||
if(Uri.IsWellFormedUriString(encodedImagePath, UriKind.Relative))
|
||||
if(WebPath.IsWellFormedWebPath(encodedImagePath, UriKind.Relative))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -189,7 +189,7 @@ public class MediaController : ContentControllerBase
|
||||
|
||||
if (mapped is not null)
|
||||
{
|
||||
//remove the listview app if it exists
|
||||
// remove the listview app if it exists
|
||||
mapped.ContentApps = mapped.ContentApps.Where(x => x.Alias != "umbListView").ToList();
|
||||
}
|
||||
|
||||
@@ -205,7 +205,7 @@ public class MediaController : ContentControllerBase
|
||||
var apps = new List<ContentApp>
|
||||
{
|
||||
ListViewContentAppFactory.CreateContentApp(_dataTypeService, _propertyEditors, "recycleBin", "media",
|
||||
Constants.DataTypes.DefaultMediaListView)
|
||||
Constants.DataTypes.DefaultMediaListView)
|
||||
};
|
||||
apps[0].Active = true;
|
||||
var display = new MediaItemDisplay
|
||||
@@ -238,7 +238,8 @@ public class MediaController : ContentControllerBase
|
||||
if (foundMedia == null)
|
||||
{
|
||||
HandleContentNotFound(id);
|
||||
//HandleContentNotFound will throw an exception
|
||||
|
||||
// HandleContentNotFound will throw an exception
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -306,8 +307,8 @@ public class MediaController : ContentControllerBase
|
||||
public PagedResult<ContentItemBasic<ContentPropertyBasic>> GetChildFolders(int id, int pageNumber = 1,
|
||||
int pageSize = 1000)
|
||||
{
|
||||
//Suggested convention for folder mediatypes - we can make this more or less complicated as long as we document it...
|
||||
//if you create a media type, which has an alias that ends with ...Folder then its a folder: ex: "secureFolder", "bannerFolder", "Folder"
|
||||
// Suggested convention for folder mediatypes - we can make this more or less complicated as long as we document it...
|
||||
// if you create a media type, which has an alias that ends with ...Folder then its a folder: ex: "secureFolder", "bannerFolder", "Folder"
|
||||
var folderTypes = _mediaTypeService
|
||||
.GetAll()
|
||||
.Where(x => x.Alias.EndsWith("Folder"))
|
||||
@@ -320,7 +321,8 @@ public class MediaController : ContentControllerBase
|
||||
}
|
||||
|
||||
IEnumerable<IMedia> children = _mediaService.GetPagedChildren(id, pageNumber - 1, pageSize, out long total,
|
||||
//lookup these content types
|
||||
|
||||
// lookup these content types
|
||||
_sqlContext.Query<IMedia>().Where(x => folderTypes.Contains(x.ContentTypeId)),
|
||||
Ordering.By("Name"));
|
||||
|
||||
@@ -336,6 +338,7 @@ public class MediaController : ContentControllerBase
|
||||
/// </summary>
|
||||
[FilterAllowedOutgoingMedia(typeof(IEnumerable<ContentItemBasic<ContentPropertyBasic>>))]
|
||||
public IEnumerable<ContentItemBasic<ContentPropertyBasic>> GetRootMedia() =>
|
||||
|
||||
// TODO: Add permissions check!
|
||||
_mediaService.GetRootMedia()?
|
||||
.Select(_umbracoMapper.Map<IMedia, ContentItemBasic<ContentPropertyBasic>>).WhereNotNull() ??
|
||||
@@ -357,7 +360,7 @@ public class MediaController : ContentControllerBase
|
||||
return HandleContentNotFound(id);
|
||||
}
|
||||
|
||||
//if the current item is in the recycle bin
|
||||
// if the current item is in the recycle bin
|
||||
if (foundMedia.Trashed == false)
|
||||
{
|
||||
Attempt<OperationResult?> moveResult = _mediaService.MoveToRecycleBin(foundMedia,
|
||||
@@ -385,12 +388,15 @@ public class MediaController : ContentControllerBase
|
||||
/// </summary>
|
||||
/// <param name="move"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> PostMove(MoveOrCopy move)
|
||||
{
|
||||
// Authorize...
|
||||
var requirement = new MediaPermissionsResourceRequirement();
|
||||
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeAsync(User,
|
||||
new MediaPermissionsResource(_mediaService.GetById(move.Id)), requirement);
|
||||
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeAsync(
|
||||
User,
|
||||
new MediaPermissionsResource(_mediaService.GetById(move.Id)),
|
||||
requirement);
|
||||
if (!authorizationResult.Succeeded)
|
||||
{
|
||||
return Forbid();
|
||||
@@ -403,18 +409,20 @@ public class MediaController : ContentControllerBase
|
||||
return convertToActionResult.Convert();
|
||||
}
|
||||
|
||||
var destinationParentID = move.ParentId;
|
||||
var sourceParentID = toMove?.ParentId;
|
||||
var destinationParentId = move.ParentId;
|
||||
var sourceParentId = toMove?.ParentId;
|
||||
|
||||
var moveResult = toMove is null
|
||||
? false
|
||||
: _mediaService.Move(toMove, move.ParentId,
|
||||
_backofficeSecurityAccessor.BackOfficeSecurity?.GetUserId().Result ?? -1);
|
||||
|
||||
if (sourceParentID == destinationParentID)
|
||||
if (sourceParentId == destinationParentId)
|
||||
{
|
||||
return ValidationProblem(new SimpleNotificationModel(new BackOfficeNotification("",
|
||||
_localizedTextService.Localize("media", "moveToSameFolderFailed"), NotificationStyle.Error)));
|
||||
return ValidationProblem(new SimpleNotificationModel(new BackOfficeNotification(
|
||||
string.Empty,
|
||||
_localizedTextService.Localize("media", "moveToSameFolderFailed"),
|
||||
NotificationStyle.Error)));
|
||||
}
|
||||
|
||||
if (moveResult == false)
|
||||
@@ -432,12 +440,13 @@ public class MediaController : ContentControllerBase
|
||||
[FileUploadCleanupFilter]
|
||||
[MediaItemSaveValidation]
|
||||
[OutgoingEditorModelEvent]
|
||||
[HttpPost]
|
||||
public ActionResult<MediaItemDisplay?>? PostSave(
|
||||
[ModelBinder(typeof(MediaItemBinder))] MediaItemSave contentItem)
|
||||
{
|
||||
//Recent versions of IE/Edge may send in the full client side file path instead of just the file name.
|
||||
//To ensure similar behavior across all browsers no matter what they do - we strip the FileName property of all
|
||||
//uploaded files to being *only* the actual file name (as it should be).
|
||||
// Recent versions of IE/Edge may send in the full client side file path instead of just the file name.
|
||||
// To ensure similar behavior across all browsers no matter what they do - we strip the FileName property of all
|
||||
// uploaded files to being *only* the actual file name (as it should be).
|
||||
if (contentItem.UploadedFiles != null && contentItem.UploadedFiles.Any())
|
||||
{
|
||||
foreach (ContentPropertyFile file in contentItem.UploadedFiles)
|
||||
@@ -446,14 +455,14 @@ public class MediaController : ContentControllerBase
|
||||
}
|
||||
}
|
||||
|
||||
//If we've reached here it means:
|
||||
// If we've reached here it means:
|
||||
// * Our model has been bound
|
||||
// * and validated
|
||||
// * any file attachments have been saved to their temporary location for us to use
|
||||
// * we have a reference to the DTO object and the persisted object
|
||||
// * Permissions are valid
|
||||
|
||||
//Don't update the name if it is empty
|
||||
// Don't update the name if it is empty
|
||||
if (contentItem.Name.IsNullOrWhiteSpace() == false && contentItem.PersistedContent is not null)
|
||||
{
|
||||
contentItem.PersistedContent.Name = contentItem.Name;
|
||||
@@ -466,14 +475,14 @@ public class MediaController : ContentControllerBase
|
||||
(save, property, v) => property?.SetValue(v), //set prop val
|
||||
null); // media are all invariant
|
||||
|
||||
//we will continue to save if model state is invalid, however we cannot save if critical data is missing.
|
||||
//TODO: Allowing media to be saved when it is invalid is odd - media doesn't have a publish phase so suddenly invalid data is allowed to be 'live'
|
||||
// we will continue to save if model state is invalid, however we cannot save if critical data is missing.
|
||||
// TODO: Allowing media to be saved when it is invalid is odd - media doesn't have a publish phase so suddenly invalid data is allowed to be 'live'
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
//check for critical data validation issues, we can't continue saving if this data is invalid
|
||||
// check for critical data validation issues, we can't continue saving if this data is invalid
|
||||
if (!RequiredForPersistenceAttribute.HasRequiredValuesForPersistence(contentItem))
|
||||
{
|
||||
//ok, so the absolute mandatory data is invalid and it's new, we cannot actually continue!
|
||||
// ok, so the absolute mandatory data is invalid and it's new, we cannot actually continue!
|
||||
// add the model state to the outgoing object and throw validation response
|
||||
MediaItemDisplay? forDisplay = _umbracoMapper.Map<MediaItemDisplay>(contentItem.PersistedContent);
|
||||
return ValidationProblem(forDisplay, ModelState);
|
||||
@@ -485,20 +494,20 @@ public class MediaController : ContentControllerBase
|
||||
return null;
|
||||
}
|
||||
|
||||
//save the item
|
||||
// save the item
|
||||
Attempt<OperationResult?> saveStatus = _mediaService.Save(contentItem.PersistedContent,
|
||||
_backofficeSecurityAccessor.BackOfficeSecurity?.GetUserId().Result ?? -1);
|
||||
|
||||
//return the updated model
|
||||
// return the updated model
|
||||
MediaItemDisplay? display = _umbracoMapper.Map<MediaItemDisplay>(contentItem.PersistedContent);
|
||||
|
||||
//lastly, if it is not valid, add the model state to the outgoing object and throw a 403
|
||||
// lastly, if it is not valid, add the model state to the outgoing object and throw a 403
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return ValidationProblem(display, ModelState, StatusCodes.Status403Forbidden);
|
||||
}
|
||||
|
||||
//put the correct msgs in
|
||||
// put the correct msgs in
|
||||
switch (contentItem.Action)
|
||||
{
|
||||
case ContentSaveAction.Save:
|
||||
@@ -513,7 +522,7 @@ public class MediaController : ContentControllerBase
|
||||
{
|
||||
AddCancelMessage(display);
|
||||
|
||||
//If the item is new and the operation was cancelled, we need to return a different
|
||||
// If the item is new and the operation was cancelled, we need to return a different
|
||||
// status code so the UI can handle it since it won't be able to redirect since there
|
||||
// is no Id to redirect to!
|
||||
if (saveStatus.Result?.Result == OperationResultType.FailedCancelledByEvent &&
|
||||
@@ -547,6 +556,7 @@ public class MediaController : ContentControllerBase
|
||||
/// </summary>
|
||||
/// <param name="sorted"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> PostSort(ContentSortOrder sorted)
|
||||
{
|
||||
if (sorted == null)
|
||||
@@ -554,7 +564,7 @@ public class MediaController : ContentControllerBase
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
//if there's nothing to sort just return ok
|
||||
// if there's nothing to sort just return ok
|
||||
if (sorted.IdSortOrder?.Length == 0)
|
||||
{
|
||||
return Ok();
|
||||
@@ -592,10 +602,11 @@ public class MediaController : ContentControllerBase
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<MediaItemDisplay?>> PostAddFolder(PostedFolder folder)
|
||||
{
|
||||
ActionResult<int?>? parentIdResult = await GetParentIdAsIntAsync(folder.ParentId, true);
|
||||
if (!(parentIdResult?.Result is null))
|
||||
if (parentIdResult?.Result is not null)
|
||||
{
|
||||
return new ActionResult<MediaItemDisplay?>(parentIdResult.Result);
|
||||
}
|
||||
@@ -625,6 +636,7 @@ public class MediaController : ContentControllerBase
|
||||
/// <remarks>
|
||||
/// We cannot validate this request with attributes (nicely) due to the nature of the multi-part for data.
|
||||
/// </remarks>
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> PostAddFile([FromForm] string path, [FromForm] string currentFolder,
|
||||
[FromForm] string contentTypeAlias, List<IFormFile> file)
|
||||
{
|
||||
@@ -632,15 +644,15 @@ public class MediaController : ContentControllerBase
|
||||
//ensure it exists
|
||||
Directory.CreateDirectory(root);
|
||||
|
||||
//must have a file
|
||||
// must have a file
|
||||
if (file is null || file.Count == 0)
|
||||
{
|
||||
return NotFound("No file was uploaded");
|
||||
}
|
||||
|
||||
//get the string json from the request
|
||||
// get the string json from the request
|
||||
ActionResult<int?>? parentIdResult = await GetParentIdAsIntAsync(currentFolder, true);
|
||||
if (!(parentIdResult?.Result is null))
|
||||
if (parentIdResult?.Result is not null)
|
||||
{
|
||||
return parentIdResult.Result;
|
||||
}
|
||||
@@ -653,7 +665,7 @@ public class MediaController : ContentControllerBase
|
||||
|
||||
var tempFiles = new PostedFiles();
|
||||
|
||||
//in case we pass a path with a folder in it, we will create it and upload media to it.
|
||||
// in case we pass a path with a folder in it, we will create it and upload media to it.
|
||||
if (!string.IsNullOrEmpty(path))
|
||||
{
|
||||
if (!IsFolderCreationAllowedHere(parentId.Value))
|
||||
@@ -669,16 +681,16 @@ public class MediaController : ContentControllerBase
|
||||
var folderName = folders[i];
|
||||
IMedia? folderMediaItem;
|
||||
|
||||
//if uploading directly to media root and not a subfolder
|
||||
// if uploading directly to media root and not a subfolder
|
||||
if (parentId == Constants.System.Root)
|
||||
{
|
||||
//look for matching folder
|
||||
// look for matching folder
|
||||
folderMediaItem =
|
||||
_mediaService.GetRootMedia()?.FirstOrDefault(x =>
|
||||
x.Name == folderName && x.ContentType.Alias == Constants.Conventions.MediaTypes.Folder);
|
||||
if (folderMediaItem == null)
|
||||
{
|
||||
//if null, create a folder
|
||||
// if null, create a folder
|
||||
folderMediaItem =
|
||||
_mediaService.CreateMedia(folderName, -1, Constants.Conventions.MediaTypes.Folder);
|
||||
_mediaService.Save(folderMediaItem);
|
||||
@@ -686,10 +698,10 @@ public class MediaController : ContentControllerBase
|
||||
}
|
||||
else
|
||||
{
|
||||
//get current parent
|
||||
// get current parent
|
||||
IMedia? mediaRoot = _mediaService.GetById(parentId.Value);
|
||||
|
||||
//if the media root is null, something went wrong, we'll abort
|
||||
// if the media root is null, something went wrong, we'll abort
|
||||
if (mediaRoot == null)
|
||||
{
|
||||
return Problem(
|
||||
@@ -697,7 +709,7 @@ public class MediaController : ContentControllerBase
|
||||
" returned null");
|
||||
}
|
||||
|
||||
//look for matching folder
|
||||
// look for matching folder
|
||||
folderMediaItem = FindInChildren(mediaRoot.Id, folderName, Constants.Conventions.MediaTypes.Folder);
|
||||
|
||||
if (folderMediaItem == null)
|
||||
@@ -709,7 +721,7 @@ public class MediaController : ContentControllerBase
|
||||
}
|
||||
}
|
||||
|
||||
//set the media root to the folder id so uploaded files will end there.
|
||||
// set the media root to the folder id so uploaded files will end there.
|
||||
parentId = folderMediaItem.Id;
|
||||
}
|
||||
}
|
||||
@@ -749,7 +761,7 @@ public class MediaController : ContentControllerBase
|
||||
}
|
||||
}
|
||||
|
||||
//Only set the permission-based mediaType if we only allow 1 specific file under this parent.
|
||||
// Only set the permission-based mediaType if we only allow 1 specific file under this parent.
|
||||
if (allowedContentTypes.Count == 1 && mediaTypeItem != null)
|
||||
{
|
||||
mediaTypeAlias = mediaTypeItem.Alias;
|
||||
@@ -762,7 +774,7 @@ public class MediaController : ContentControllerBase
|
||||
allowedContentTypes.UnionWith(typesAllowedAtRoot);
|
||||
}
|
||||
|
||||
//get the files
|
||||
// get the files
|
||||
foreach (IFormFile formFile in file)
|
||||
{
|
||||
var fileName = formFile.FileName.Trim(Constants.CharArrays.DoubleQuote).TrimEnd();
|
||||
@@ -821,6 +833,11 @@ public class MediaController : ContentControllerBase
|
||||
continue;
|
||||
}
|
||||
|
||||
if (allowedContentTypes.Any(x => x.Alias == mediaTypeItem.Alias) == false)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
mediaTypeAlias = mediaTypeItem.Alias;
|
||||
break;
|
||||
}
|
||||
@@ -866,8 +883,8 @@ public class MediaController : ContentControllerBase
|
||||
IMedia createdMediaItem = _mediaService.CreateMedia(mediaItemName, parentId.Value, mediaTypeAlias,
|
||||
_backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.Id ?? -1);
|
||||
|
||||
createdMediaItem.SetValue(_mediaFileManager, _mediaUrlGenerators, _shortStringHelper,
|
||||
_contentTypeBaseServiceProvider, Constants.Conventions.Media.File, fileName, stream);
|
||||
createdMediaItem.SetValue(_mediaFileManager, _mediaUrlGenerators, _shortStringHelper,
|
||||
_contentTypeBaseServiceProvider, Constants.Conventions.Media.File, fileName, stream);
|
||||
|
||||
Attempt<OperationResult?> saveResult = _mediaService.Save(createdMediaItem,
|
||||
_backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.Id ?? -1);
|
||||
@@ -878,13 +895,13 @@ public class MediaController : ContentControllerBase
|
||||
}
|
||||
}
|
||||
|
||||
//Different response if this is a 'blueimp' request
|
||||
// Different response if this is a 'blueimp' request
|
||||
if (HttpContext.Request.Query.Any(x => x.Key == "origin"))
|
||||
{
|
||||
KeyValuePair<string, StringValues> origin = HttpContext.Request.Query.First(x => x.Key == "origin");
|
||||
if (origin.Value == "blueimp")
|
||||
{
|
||||
return new JsonResult(tempFiles); //Don't output the angular xsrf stuff, blue imp doesn't like that
|
||||
return new JsonResult(tempFiles); // Don't output the angular xsrf stuff, blue imp doesn't like that
|
||||
}
|
||||
}
|
||||
|
||||
@@ -923,7 +940,11 @@ public class MediaController : ContentControllerBase
|
||||
var total = long.MaxValue;
|
||||
while (page * pageSize < total)
|
||||
{
|
||||
IEnumerable<IMedia> children = _mediaService.GetPagedChildren(mediaId, page++, pageSize, out total,
|
||||
IEnumerable<IMedia> children = _mediaService.GetPagedChildren(
|
||||
mediaId,
|
||||
page++,
|
||||
pageSize,
|
||||
out total,
|
||||
_sqlContext.Query<IMedia>().Where(x => x.Name == nameToFind));
|
||||
IMedia? match = children.FirstOrDefault(c => c.ContentType.Alias == contentTypeAlias);
|
||||
if (match != null)
|
||||
@@ -946,14 +967,13 @@ public class MediaController : ContentControllerBase
|
||||
/// <returns></returns>
|
||||
private async Task<ActionResult<int?>?> GetParentIdAsIntAsync(string? parentId, bool validatePermissions)
|
||||
{
|
||||
|
||||
// test for udi
|
||||
if (UdiParser.TryParse(parentId, out GuidUdi? parentUdi))
|
||||
{
|
||||
parentId = parentUdi?.Guid.ToString();
|
||||
}
|
||||
|
||||
//if it's not an INT then we'll check for GUID
|
||||
// if it's not an INT then we'll check for GUID
|
||||
if (int.TryParse(parentId, NumberStyles.Integer, CultureInfo.InvariantCulture, out int intParentId) == false)
|
||||
{
|
||||
// if a guid then try to look up the entity
|
||||
@@ -977,7 +997,7 @@ public class MediaController : ContentControllerBase
|
||||
}
|
||||
|
||||
// Authorize...
|
||||
//ensure the user has access to this folder by parent id!
|
||||
// ensure the user has access to this folder by parent id!
|
||||
if (validatePermissions)
|
||||
{
|
||||
var requirement = new MediaPermissionsResourceRequirement();
|
||||
@@ -1018,14 +1038,14 @@ public class MediaController : ContentControllerBase
|
||||
|
||||
if (model.ParentId < 0)
|
||||
{
|
||||
//cannot move if the content item is not allowed at the root unless there are
|
||||
//none allowed at root (in which case all should be allowed at root)
|
||||
// cannot move if the content item is not allowed at the root unless there are
|
||||
// none allowed at root (in which case all should be allowed at root)
|
||||
IMediaTypeService mediaTypeService = _mediaTypeService;
|
||||
if (toMove.ContentType.AllowedAsRoot == false && mediaTypeService.GetAll().Any(ct => ct.AllowedAsRoot))
|
||||
{
|
||||
var notificationModel = new SimpleNotificationModel();
|
||||
notificationModel.AddErrorNotification(_localizedTextService.Localize("moveOrCopy", "notAllowedAtRoot"),
|
||||
"");
|
||||
string.Empty);
|
||||
return ValidationProblem(notificationModel);
|
||||
}
|
||||
}
|
||||
@@ -1037,7 +1057,7 @@ public class MediaController : ContentControllerBase
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
//check if the item is allowed under this one
|
||||
// check if the item is allowed under this one
|
||||
IMediaType? parentContentType = _mediaTypeService.Get(parent.ContentTypeId);
|
||||
if (parentContentType?.AllowedContentTypes?.Select(x => x.Id).ToArray()
|
||||
.Any(x => x.Value == toMove.ContentType.Id) == false)
|
||||
@@ -1049,12 +1069,12 @@ public class MediaController : ContentControllerBase
|
||||
}
|
||||
|
||||
// Check on paths
|
||||
if (string.Format(",{0},", parent.Path)
|
||||
.IndexOf(string.Format(",{0},", toMove.Id), StringComparison.Ordinal) > -1)
|
||||
if ($",{parent.Path},"
|
||||
.IndexOf($",{toMove.Id},", StringComparison.Ordinal) > -1)
|
||||
{
|
||||
var notificationModel = new SimpleNotificationModel();
|
||||
notificationModel.AddErrorNotification(_localizedTextService.Localize("moveOrCopy", "notAllowedByPath"),
|
||||
"");
|
||||
string.Empty);
|
||||
return ValidationProblem(notificationModel);
|
||||
}
|
||||
}
|
||||
@@ -1110,7 +1130,8 @@ public class MediaController : ContentControllerBase
|
||||
/// Returns the child media objects - using the entity INT id
|
||||
/// </summary>
|
||||
[FilterAllowedOutgoingMedia(typeof(IEnumerable<ContentItemBasic<ContentPropertyBasic>>), "Items")]
|
||||
public PagedResult<ContentItemBasic<ContentPropertyBasic>> GetChildren(int id,
|
||||
public PagedResult<ContentItemBasic<ContentPropertyBasic>> GetChildren(
|
||||
int id,
|
||||
int pageNumber = 0,
|
||||
int pageSize = 0,
|
||||
string orderBy = "SortOrder",
|
||||
@@ -1118,7 +1139,7 @@ public class MediaController : ContentControllerBase
|
||||
bool orderBySystemField = true,
|
||||
string filter = "")
|
||||
{
|
||||
//if a request is made for the root node data but the user's start node is not the default, then
|
||||
// if a request is made for the root node data but the user's start node is not the default, then
|
||||
// we need to return their start nodes
|
||||
if (id == Constants.System.Root && UserStartNodes.Length > 0 &&
|
||||
UserStartNodes.Contains(Constants.System.Root) == false)
|
||||
@@ -1148,7 +1169,6 @@ public class MediaController : ContentControllerBase
|
||||
}
|
||||
|
||||
// else proceed as usual
|
||||
|
||||
long totalChildren;
|
||||
List<IMedia> children;
|
||||
if (pageNumber > 0 && pageSize > 0)
|
||||
@@ -1156,7 +1176,7 @@ public class MediaController : ContentControllerBase
|
||||
IQuery<IMedia>? queryFilter = null;
|
||||
if (filter.IsNullOrWhiteSpace() == false)
|
||||
{
|
||||
//add the default text filter
|
||||
// add the default text filter
|
||||
queryFilter = _sqlContext.Query<IMedia>()
|
||||
.Where(x => x.Name != null)
|
||||
.Where(x => x.Name!.Contains(filter));
|
||||
@@ -1164,14 +1184,16 @@ public class MediaController : ContentControllerBase
|
||||
|
||||
children = _mediaService
|
||||
.GetPagedChildren(
|
||||
id, pageNumber - 1, pageSize,
|
||||
id,
|
||||
pageNumber - 1,
|
||||
pageSize,
|
||||
out totalChildren,
|
||||
queryFilter,
|
||||
Ordering.By(orderBy, orderDirection, isCustomField: !orderBySystemField)).ToList();
|
||||
}
|
||||
else
|
||||
{
|
||||
//better to not use this without paging where possible, currently only the sort dialog does
|
||||
// better to not use this without paging where possible, currently only the sort dialog does
|
||||
children = _mediaService.GetPagedChildren(id, 0, int.MaxValue, out var total).ToList();
|
||||
totalChildren = children.Count;
|
||||
}
|
||||
@@ -1184,7 +1206,7 @@ public class MediaController : ContentControllerBase
|
||||
var pagedResult = new PagedResult<ContentItemBasic<ContentPropertyBasic>>(totalChildren, pageNumber, pageSize)
|
||||
{
|
||||
Items = children
|
||||
.Select(_umbracoMapper.Map<IMedia, ContentItemBasic<ContentPropertyBasic>>).WhereNotNull()
|
||||
.Select(_umbracoMapper.Map<IMedia, ContentItemBasic<ContentPropertyBasic>>).WhereNotNull()
|
||||
};
|
||||
|
||||
return pagedResult;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Globalization;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.ViewEngines;
|
||||
@@ -11,6 +12,7 @@ using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.Membership;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
using Umbraco.Cms.Core.Routing;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Web;
|
||||
@@ -129,6 +131,11 @@ public class PreviewController : Controller
|
||||
[Authorize(Policy = AuthorizationPolicies.BackOfficeAccess)]
|
||||
public ActionResult Frame(int id, string culture)
|
||||
{
|
||||
if (ValidateProvidedCulture(culture) is false)
|
||||
{
|
||||
throw new InvalidOperationException($"Could not recognise the provided culture: {culture}");
|
||||
}
|
||||
|
||||
EnterPreview(id);
|
||||
|
||||
// use a numeric URL because content may not be in cache and so .Url would fail
|
||||
@@ -137,6 +144,28 @@ public class PreviewController : Controller
|
||||
return RedirectPermanent($"../../{id}{query}");
|
||||
}
|
||||
|
||||
private static bool ValidateProvidedCulture(string culture)
|
||||
{
|
||||
if (string.IsNullOrEmpty(culture))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// We can be confident the backoffice will have provided a valid culture in linking to the
|
||||
// preview, so we don't need to check that the culture matches an Umbraco language.
|
||||
// We are only concerned here with protecting against XSS attacks from a fiddled preview
|
||||
// URL, so we can just confirm we have a valid culture.
|
||||
try
|
||||
{
|
||||
CultureInfo.GetCultureInfo(culture, true);
|
||||
return true;
|
||||
}
|
||||
catch (CultureNotFoundException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public ActionResult? EnterPreview(int id)
|
||||
{
|
||||
IUser? user = _backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser;
|
||||
@@ -152,8 +181,7 @@ public class PreviewController : Controller
|
||||
// Expire Client-side cookie that determines whether the user has accepted to be in Preview Mode when visiting the website.
|
||||
_cookieManager.ExpireCookie(Constants.Web.AcceptPreviewCookieName);
|
||||
|
||||
if (Uri.IsWellFormedUriString(redir, UriKind.Relative)
|
||||
&& redir.StartsWith("//") == false
|
||||
if (WebPath.IsWellFormedWebPath(redir, UriKind.Relative)
|
||||
&& Uri.TryCreate(redir, UriKind.Relative, out Uri? url))
|
||||
{
|
||||
return Redirect(url.ToString());
|
||||
|
||||
@@ -5,9 +5,20 @@ namespace Umbraco.Extensions;
|
||||
|
||||
public static class HttpContextExtensions
|
||||
{
|
||||
private const string PasswordResetFlowSessionKey = nameof(PasswordResetFlowSessionKey);
|
||||
|
||||
public static void SetExternalLoginProviderErrors(this HttpContext httpContext, BackOfficeExternalLoginProviderErrors errors)
|
||||
=> httpContext.Items[nameof(BackOfficeExternalLoginProviderErrors)] = errors;
|
||||
|
||||
public static BackOfficeExternalLoginProviderErrors? GetExternalLoginProviderErrors(this HttpContext httpContext)
|
||||
=> httpContext.Items[nameof(BackOfficeExternalLoginProviderErrors)] as BackOfficeExternalLoginProviderErrors;
|
||||
|
||||
internal static void StartPasswordResetFlowSession(this HttpContext httpContext, int userId)
|
||||
=> httpContext.Session.SetInt32(PasswordResetFlowSessionKey, userId);
|
||||
|
||||
internal static void EndPasswordResetFlowSession(this HttpContext httpContext)
|
||||
=> httpContext.Session.Remove(PasswordResetFlowSessionKey);
|
||||
|
||||
internal static bool HasActivePasswordResetFlowSession(this HttpContext httpContext, int userId)
|
||||
=> httpContext.Session.GetInt32(PasswordResetFlowSessionKey) == userId;
|
||||
}
|
||||
|
||||
@@ -59,6 +59,14 @@ public static class HttpContextExtensions
|
||||
await httpContext.AuthenticateAsync(Constants.Security.BackOfficeExternalAuthenticationType);
|
||||
}
|
||||
|
||||
// Update the HttpContext's user with the authenticated user's principal to ensure
|
||||
// that subsequent requests within the same context will recognize the user
|
||||
// as authenticated.
|
||||
if (result.Succeeded)
|
||||
{
|
||||
httpContext.User = result.Principal;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -141,7 +141,10 @@ public abstract class UmbracoViewPage<TModel> : RazorPage<TModel>
|
||||
string.Format(
|
||||
ContentSettings.PreviewBadge,
|
||||
HostingEnvironment.ToAbsolute(GlobalSettings.UmbracoPath),
|
||||
Context.Request.GetEncodedUrl(),
|
||||
System.Web.HttpUtility.HtmlEncode(Context.Request.GetEncodedUrl()), // Belt and braces - via a browser at least it doesn't seem possible to have anything other than
|
||||
// a valid culture code provided in the querystring of this URL.
|
||||
// But just to be sure of prevention of an XSS vulnterablity we'll HTML encode here too.
|
||||
// An expected URL is untouched by this encoding.
|
||||
UmbracoContext.PublishedRequest?.PublishedContent?.Id);
|
||||
}
|
||||
else
|
||||
|
||||
+1
-9
@@ -69,15 +69,7 @@
|
||||
editorService.mediaTypeEditor(editor);
|
||||
};
|
||||
|
||||
scope.openSVG = () => {
|
||||
var popup = window.open('', '_blank');
|
||||
var html = '<!DOCTYPE html><body><img src="' + scope.nodeUrl + '"/>' +
|
||||
'<script>history.pushState(null, null,"' + $location.$$absUrl + '");</script></body>';
|
||||
|
||||
popup.document.open();
|
||||
popup.document.write(html);
|
||||
popup.document.close();
|
||||
}
|
||||
scope.openSVG = () => mediaHelper.openSVG(scope.nodeUrl);
|
||||
|
||||
// watch for content updates - reload content when node is saved, published etc.
|
||||
scope.$watch('node.updateDate', function(newValue, oldValue){
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* @name umbraco.services.mediaHelper
|
||||
* @description A helper object used for dealing with media items
|
||||
**/
|
||||
function mediaHelper(umbRequestHelper, $http, $log) {
|
||||
function mediaHelper(umbRequestHelper, $http, $log, $location) {
|
||||
|
||||
//container of fileresolvers
|
||||
var _mediaFileResolvers = {};
|
||||
@@ -449,7 +449,29 @@ function mediaHelper(umbRequestHelper, $http, $log) {
|
||||
cropY2: options.crop ? options.crop.y2 : null
|
||||
})),
|
||||
"Failed to retrieve processed image URL for image: " + imagePath);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @ngdoc function
|
||||
* @name umbraco.services.mediaHelper#openSVG
|
||||
* @methodOf umbraco.services.mediaHelper
|
||||
* @function
|
||||
*
|
||||
* @description
|
||||
* Opens an SVG file in a new window as an image file, to prevent any potential XSS exploits.
|
||||
*
|
||||
* @param {string} imagePath File path, ex /media/1234/my-image.svg
|
||||
*/
|
||||
openSVG: function (imagePath) {
|
||||
var popup = window.open('', '_blank');
|
||||
var html = '<!DOCTYPE html><body style="background-image: linear-gradient(45deg, #ccc 25%, transparent 25%), linear-gradient(135deg, #ccc 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #ccc 75%), linear-gradient(135deg, transparent 75%, #ccc 75%); background-size:30px 30px; background-position:0 0, 15px 0, 15px -15px, 0px 15px;">'
|
||||
+ '<img src="' + imagePath + '"/>'
|
||||
+ '<script>history.pushState(null, null,"' + $location.$$absUrl + '");</script></body>';
|
||||
|
||||
popup.document.open();
|
||||
popup.document.write(html);
|
||||
popup.document.close();
|
||||
}
|
||||
|
||||
};
|
||||
} angular.module('umbraco.services').factory('mediaHelper', mediaHelper);
|
||||
|
||||
@@ -3,6 +3,7 @@ angular.module('umbraco.services')
|
||||
|
||||
var currentUser = null;
|
||||
var lastUserId = null;
|
||||
var countdownCounter = null;
|
||||
|
||||
//this tracks the last date/time that the user's remainingAuthSeconds was updated from the server
|
||||
// this is used so that we know when to go and get the user's remaining seconds directly.
|
||||
@@ -43,6 +44,10 @@ angular.module('umbraco.services')
|
||||
}
|
||||
currentUser = usr;
|
||||
lastServerTimeoutSet = new Date();
|
||||
//don't start the timer if it is already going
|
||||
if (countdownCounter) {
|
||||
return;
|
||||
}
|
||||
//start the timer
|
||||
countdownUserTimeout();
|
||||
}
|
||||
@@ -54,23 +59,23 @@ angular.module('umbraco.services')
|
||||
*/
|
||||
function countdownUserTimeout() {
|
||||
|
||||
$timeout(function () {
|
||||
countdownCounter = $timeout(function () {
|
||||
|
||||
if (currentUser) {
|
||||
//countdown by 5 seconds since that is how long our timer is for.
|
||||
currentUser.remainingAuthSeconds -= 5;
|
||||
|
||||
//if there are more than 30 remaining seconds, recurse!
|
||||
if (currentUser.remainingAuthSeconds > 30) {
|
||||
//if there are more than 20 remaining seconds, recurse!
|
||||
if (currentUser.remainingAuthSeconds > 20) {
|
||||
|
||||
//we need to check when the last time the timeout was set from the server, if
|
||||
// it has been more than 30 seconds then we'll manually go and retrieve it from the
|
||||
// it has been more than 20 seconds then we'll manually go and retrieve it from the
|
||||
// server - this helps to keep our local countdown in check with the true timeout.
|
||||
if (lastServerTimeoutSet != null) {
|
||||
var now = new Date();
|
||||
var seconds = (now.getTime() - lastServerTimeoutSet.getTime()) / 1000;
|
||||
|
||||
if (seconds > 30) {
|
||||
if (seconds > 20) {
|
||||
|
||||
//first we'll set the lastServerTimeoutSet to null - this is so we don't get back in to this loop while we
|
||||
// wait for a response from the server otherwise we'll be making double/triple/etc... calls while we wait.
|
||||
@@ -95,18 +100,23 @@ angular.module('umbraco.services')
|
||||
if (Umbraco.Sys.ServerVariables.umbracoSettings.keepUserLoggedIn !== true) {
|
||||
//NOTE: the safeApply because our timeout is set to not run digests (performance reasons)
|
||||
angularHelper.safeApply($rootScope, function () {
|
||||
try {
|
||||
//NOTE: We are calling this again so that the server can create a log that the timeout has expired, we
|
||||
// don't actually care about this result.
|
||||
authResource.getRemainingTimeoutSeconds();
|
||||
}
|
||||
finally {
|
||||
userAuthExpired();
|
||||
}
|
||||
//NOTE: We are calling this again so that the server can create a log that the timeout has expired
|
||||
//and we will show the login screen as close to the server's timout time as possible
|
||||
authResource.getRemainingTimeoutSeconds().then(function (result) {
|
||||
setUserTimeoutInternal(result);
|
||||
|
||||
//the client auth can expire a second earlier as the client internal clock is behind
|
||||
if (result < 1) {
|
||||
userAuthExpired();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
//recurse the countdown!
|
||||
countdownUserTimeout();
|
||||
}
|
||||
else {
|
||||
//we've got less than 30 seconds remaining so let's check the server
|
||||
//we've got less than 20 seconds remaining so let's check the server
|
||||
|
||||
if (lastServerTimeoutSet != null) {
|
||||
//first we'll set the lastServerTimeoutSet to null - this is so we don't get back in to this loop while we
|
||||
@@ -155,6 +165,7 @@ angular.module('umbraco.services')
|
||||
|
||||
lastServerTimeoutSet = null;
|
||||
currentUser = null;
|
||||
countdownCounter = null;
|
||||
|
||||
openLoginDialog(isLogout === undefined ? true : !isLogout);
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
<div class="umb-image-preview" ng-controller="umbImagePreviewController as controller">
|
||||
<img class="umb-image-preview--image" ng-if="vm.clientSide" ng-init="previewUrl = controller.getClientSideUrl(vm.clientSideData)" ng-src="{{previewUrl}}" alt="{{vm.name}}" />
|
||||
<a ng-if="!vm.clientSide" href="#" ng-href="{{vm.source}}" target="_blank" rel="noopener">
|
||||
<a ng-if="!vm.clientSide" href="" ng-attr-href="{{vm.extension !== 'svg' ? vm.source : undefined}}" ng-click="vm.extension === 'svg' && controller.openSVG(vm.source)" target="_blank" rel="noopener">
|
||||
<img class="umb-image-preview--image" ng-init="previewUrl = controller.getThumbnail(vm.source)" ng-src="{{previewUrl}}" alt="{{vm.name}}" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
+12
-14
@@ -1,18 +1,16 @@
|
||||
|
||||
|
||||
|
||||
|
||||
angular.module("umbraco")
|
||||
.controller("umbImagePreviewController",
|
||||
function (mediaHelper) {
|
||||
.controller("umbImagePreviewController",
|
||||
function (mediaHelper) {
|
||||
|
||||
var vm = this;
|
||||
var vm = this;
|
||||
|
||||
vm.getThumbnail = function(source) {
|
||||
return mediaHelper.getThumbnailFromPath(source) || source;
|
||||
}
|
||||
vm.getClientSideUrl = function(sourceData) {
|
||||
return URL.createObjectURL(sourceData);
|
||||
}
|
||||
vm.getThumbnail = function (source) {
|
||||
return mediaHelper.getThumbnailFromPath(source) || source;
|
||||
}
|
||||
|
||||
});
|
||||
vm.getClientSideUrl = function (sourceData) {
|
||||
return URL.createObjectURL(sourceData);
|
||||
}
|
||||
|
||||
vm.openSVG = (source) => mediaHelper.openSVG(source);
|
||||
});
|
||||
|
||||
@@ -43,4 +43,28 @@
|
||||
<PackagePath>UmbracoProject\wwwroot</PackagePath>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Update template.json files with the default UmbracoVersion value set to the current build version -->
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Umbraco.JsonSchema.Extensions" Version="0.3.0" PrivateAssets="all" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Update="**\.template.config\template.json" Pack="false" />
|
||||
</ItemGroup>
|
||||
<Target Name="GetUpdatedTemplateJsonPackageFiles" BeforeTargets="GenerateNuspec" AfterTargets="GetUmbracoBuildVersion">
|
||||
<ItemGroup>
|
||||
<_TemplateJsonFiles Include="**\.template.config\template.json" Exclude="bin\**;obj\**" />
|
||||
<_TemplateJsonFiles>
|
||||
<DestinationFile>$(IntermediateOutputPath)%(RelativeDir)%(Filename)%(Extension)</DestinationFile>
|
||||
</_TemplateJsonFiles>
|
||||
</ItemGroup>
|
||||
<Copy SourceFiles="@(_TemplateJsonFiles)" DestinationFiles="%(DestinationFile)" />
|
||||
<JsonPathUpdateValue JsonFile="%(_TemplateJsonFiles.DestinationFile)" Path="$.symbols.UmbracoVersion.defaultValue" Value=""$(PackageVersion)"" />
|
||||
<ItemGroup>
|
||||
<_PackageFiles Include="%(_TemplateJsonFiles.DestinationFile)">
|
||||
<PackagePath>%(_TemplateJsonFiles.RelativeDir)</PackagePath>
|
||||
</_PackageFiles>
|
||||
</ItemGroup>
|
||||
</Target>
|
||||
</Project>
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
"description": "The version of Umbraco.Cms to add as PackageReference.",
|
||||
"type": "parameter",
|
||||
"datatype": "string",
|
||||
"defaultValue": "10.0.0-rc1",
|
||||
"defaultValue": "*",
|
||||
"replaces": "UMBRACO_VERSION_FROM_TEMPLATE"
|
||||
},
|
||||
"Namespace": {
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
"description": "The version of Umbraco.Cms to add as PackageReference.",
|
||||
"type": "parameter",
|
||||
"datatype": "string",
|
||||
"defaultValue": "10.0.0-rc1",
|
||||
"defaultValue": "*",
|
||||
"replaces": "UMBRACO_VERSION_FROM_TEMPLATE"
|
||||
},
|
||||
"UseHttpsRedirect": {
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NPoco;
|
||||
@@ -15,7 +10,6 @@ using Umbraco.Cms.Persistence.Sqlite.Interceptors;
|
||||
using Umbraco.Cms.Tests.Common.Attributes;
|
||||
using Umbraco.Cms.Tests.Common.Testing;
|
||||
using Umbraco.Cms.Tests.Integration.Testing;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence;
|
||||
|
||||
@@ -126,6 +120,7 @@ public class LocksTests : UmbracoIntegrationTest
|
||||
}
|
||||
}
|
||||
|
||||
[NUnit.Framework.Ignore("We currently do not have a way to force lazy locks")]
|
||||
[Test]
|
||||
public void GivenNonEagerLocking_WhenNoDbIsAccessed_ThenNoSqlIsExecuted()
|
||||
{
|
||||
@@ -155,6 +150,37 @@ public class LocksTests : UmbracoIntegrationTest
|
||||
Assert.AreEqual(0, sqlCount);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GivenNonEagerLocking_WhenDbIsAccessed_ThenSqlIsExecuted()
|
||||
{
|
||||
var sqlCount = 0;
|
||||
|
||||
using (var scope = ScopeProvider.CreateScope())
|
||||
{
|
||||
var db = ScopeAccessor.AmbientScope.Database;
|
||||
try
|
||||
{
|
||||
db.EnableSqlCount = true;
|
||||
|
||||
// Issue a lock request, but we are using non-eager
|
||||
// locks so this only queues the request.
|
||||
// The lock will not be issued unless we resolve
|
||||
// scope.Database
|
||||
scope.WriteLock(Constants.Locks.Servers);
|
||||
|
||||
scope.Database.ExecuteScalar<int>("SELECT 1");
|
||||
|
||||
sqlCount = db.SqlCount;
|
||||
}
|
||||
finally
|
||||
{
|
||||
db.EnableSqlCount = false;
|
||||
}
|
||||
}
|
||||
|
||||
Assert.AreEqual(2,sqlCount);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[LongRunning]
|
||||
public void ConcurrentWritersTest()
|
||||
|
||||
+5
-3
@@ -1,4 +1,3 @@
|
||||
using System.Linq;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
@@ -15,6 +14,7 @@ public class DataValueEditorReuseTests
|
||||
{
|
||||
private Mock<IDataValueEditorFactory> _dataValueEditorFactoryMock;
|
||||
private PropertyEditorCollection _propertyEditorCollection;
|
||||
private DataValueReferenceFactoryCollection _dataValueReferenceFactories;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
@@ -31,6 +31,7 @@ public class DataValueEditorReuseTests
|
||||
Mock.Of<IIOHelper>()));
|
||||
|
||||
_propertyEditorCollection = new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty<IDataEditor>));
|
||||
_dataValueReferenceFactories = new DataValueReferenceFactoryCollection(Enumerable.Empty<IDataValueReferenceFactory>);
|
||||
|
||||
_dataValueEditorFactoryMock
|
||||
.Setup(m =>
|
||||
@@ -38,6 +39,7 @@ public class DataValueEditorReuseTests
|
||||
.Returns(() => new BlockListPropertyEditorBase.BlockListEditorPropertyValueEditor(
|
||||
new DataEditorAttribute("a", "b", "c"),
|
||||
_propertyEditorCollection,
|
||||
_dataValueReferenceFactories,
|
||||
Mock.Of<IDataTypeService>(),
|
||||
Mock.Of<IContentTypeService>(),
|
||||
Mock.Of<ILocalizedTextService>(),
|
||||
@@ -93,7 +95,7 @@ public class DataValueEditorReuseTests
|
||||
{
|
||||
var blockListPropertyEditor = new BlockListPropertyEditor(
|
||||
_dataValueEditorFactoryMock.Object,
|
||||
new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty<IDataEditor>)),
|
||||
_propertyEditorCollection,
|
||||
Mock.Of<IIOHelper>(),
|
||||
Mock.Of<IEditorConfigurationParser>(),
|
||||
Mock.Of<IBlockValuePropertyIndexValueFactory>());
|
||||
@@ -114,7 +116,7 @@ public class DataValueEditorReuseTests
|
||||
{
|
||||
var blockListPropertyEditor = new BlockListPropertyEditor(
|
||||
_dataValueEditorFactoryMock.Object,
|
||||
new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty<IDataEditor>)),
|
||||
_propertyEditorCollection,
|
||||
Mock.Of<IIOHelper>(),
|
||||
Mock.Of<IEditorConfigurationParser>(),
|
||||
Mock.Of<IBlockValuePropertyIndexValueFactory>());
|
||||
|
||||
+33
-3
@@ -1,9 +1,6 @@
|
||||
// Copyright (c) Umbraco.
|
||||
// See LICENSE for more details.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Cms.Core;
|
||||
@@ -174,6 +171,33 @@ public class DataValueReferenceFactoryCollectionTests
|
||||
Assert.AreEqual(trackedUdi4, result.ElementAt(1).Udi.ToString());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetAutomaticRelationTypesAliases_ContainsDefault()
|
||||
{
|
||||
var collection = new DataValueReferenceFactoryCollection(Enumerable.Empty<IDataValueReferenceFactory>);
|
||||
var propertyEditors = new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty<IDataEditor>));
|
||||
|
||||
var result = collection.GetAllAutomaticRelationTypesAliases(propertyEditors).ToArray();
|
||||
|
||||
var expected = Constants.Conventions.RelationTypes.AutomaticRelationTypes;
|
||||
CollectionAssert.AreEquivalent(expected, result, "Result does not contain the expected relation type aliases.");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetAutomaticRelationTypesAliases_ContainsCustom()
|
||||
{
|
||||
var collection = new DataValueReferenceFactoryCollection(() => new TestDataValueReferenceFactory().Yield());
|
||||
|
||||
var labelPropertyEditor = new LabelPropertyEditor(DataValueEditorFactory, IOHelper, EditorConfigurationParser);
|
||||
var propertyEditors = new PropertyEditorCollection(new DataEditorCollection(() => labelPropertyEditor.Yield()));
|
||||
var serializer = new ConfigurationEditorJsonSerializer();
|
||||
|
||||
var result = collection.GetAllAutomaticRelationTypesAliases(propertyEditors).ToArray();
|
||||
|
||||
var expected = Constants.Conventions.RelationTypes.AutomaticRelationTypes.Append("umbTest");
|
||||
CollectionAssert.AreEquivalent(expected, result, "Result does not contain the expected relation type aliases.");
|
||||
}
|
||||
|
||||
private class TestDataValueReferenceFactory : IDataValueReferenceFactory
|
||||
{
|
||||
public IDataValueReference GetDataValueReference() => new TestMediaDataValueReference();
|
||||
@@ -197,6 +221,12 @@ public class DataValueReferenceFactoryCollectionTests
|
||||
yield return new UmbracoEntityReference(udi);
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<string> GetAutomaticRelationTypesAliases() => new[]
|
||||
{
|
||||
"umbTest",
|
||||
"umbTest", // Duplicate on purpose to test distinct aliases
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,4 +31,87 @@ public class WebPathTests
|
||||
|
||||
[Test]
|
||||
public void Combine_must_handle_null() => Assert.Throws<ArgumentNullException>(() => WebPath.Combine(null));
|
||||
|
||||
|
||||
[Test]
|
||||
[TestCase("ftp://hello.com/", UriKind.Absolute, ExpectedResult = true)]
|
||||
[TestCase("file:///hello.com/", UriKind.Absolute, ExpectedResult = true)]
|
||||
[TestCase("ws://hello.com/", UriKind.Absolute, ExpectedResult = true)]
|
||||
[TestCase("wss://hello.com/", UriKind.Absolute, ExpectedResult = true)]
|
||||
[TestCase("https://hello.com:8080/", UriKind.Absolute, ExpectedResult = true)]
|
||||
[TestCase("http://hello.com:8080/", UriKind.Absolute, ExpectedResult = true)]
|
||||
[TestCase("https://hello.com/path", UriKind.Absolute, ExpectedResult = true)]
|
||||
[TestCase("http://hello.com/path", UriKind.Absolute, ExpectedResult = true)]
|
||||
[TestCase("https://hello.com/path?query=param", UriKind.Absolute, ExpectedResult = true)]
|
||||
[TestCase("http://hello.com/path?query=param", UriKind.Absolute, ExpectedResult = true)]
|
||||
[TestCase("https://hello.com/path#fragment", UriKind.Absolute, ExpectedResult = true)]
|
||||
[TestCase("http://hello.com/path#fragment", UriKind.Absolute, ExpectedResult = true)]
|
||||
[TestCase("https://hello.com/path?query=param#fragment", UriKind.Absolute, ExpectedResult = true)]
|
||||
[TestCase("http://hello.com/path?query=param#fragment", UriKind.Absolute, ExpectedResult = true)]
|
||||
[TestCase("https://hello.com:8080/path?query=param#fragment", UriKind.Absolute, ExpectedResult = true)]
|
||||
[TestCase("http://hello.com:8080/path?query=param#fragment", UriKind.Absolute, ExpectedResult = true)]
|
||||
[TestCase("//hello.com:8080/path?query=param#fragment", UriKind.Absolute, ExpectedResult = true)]
|
||||
[TestCase("//hello.com:8080/path", UriKind.Absolute, ExpectedResult = true)]
|
||||
[TestCase("//hello.com:8080", UriKind.Absolute, ExpectedResult = true)]
|
||||
[TestCase("//hello.com", UriKind.Absolute, ExpectedResult = true)]
|
||||
[TestCase("/test/test.jpg", UriKind.Absolute, ExpectedResult = false)]
|
||||
[TestCase("/test", UriKind.Absolute, ExpectedResult = false)]
|
||||
[TestCase("test", UriKind.Absolute, ExpectedResult = false)]
|
||||
[TestCase("", UriKind.Absolute, ExpectedResult = false)]
|
||||
[TestCase(null, UriKind.Absolute, ExpectedResult = false)]
|
||||
[TestCase("this is not welformed", UriKind.Absolute, ExpectedResult = false)]
|
||||
[TestCase("ftp://hello.com/", UriKind.Relative, ExpectedResult = false)]
|
||||
[TestCase("file:///hello.com/", UriKind.Relative, ExpectedResult = false)]
|
||||
[TestCase("ws://hello.com/", UriKind.Relative, ExpectedResult = false)]
|
||||
[TestCase("wss://hello.com/", UriKind.Relative, ExpectedResult = false)]
|
||||
[TestCase("https://hello.com:8080/", UriKind.Relative, ExpectedResult = false)]
|
||||
[TestCase("http://hello.com:8080/", UriKind.Relative, ExpectedResult = false)]
|
||||
[TestCase("https://hello.com/path", UriKind.Relative, ExpectedResult = false)]
|
||||
[TestCase("http://hello.com/path", UriKind.Relative, ExpectedResult = false)]
|
||||
[TestCase("https://hello.com/path?query=param", UriKind.Relative, ExpectedResult = false)]
|
||||
[TestCase("http://hello.com/path?query=param", UriKind.Relative, ExpectedResult = false)]
|
||||
[TestCase("https://hello.com/path#fragment", UriKind.Relative, ExpectedResult = false)]
|
||||
[TestCase("http://hello.com/path#fragment", UriKind.Relative, ExpectedResult = false)]
|
||||
[TestCase("https://hello.com/path?query=param#fragment", UriKind.Relative, ExpectedResult = false)]
|
||||
[TestCase("http://hello.com/path?query=param#fragment", UriKind.Relative, ExpectedResult = false)]
|
||||
[TestCase("https://hello.com:8080/path?query=param#fragment", UriKind.Relative, ExpectedResult = false)]
|
||||
[TestCase("http://hello.com:8080/path?query=param#fragment", UriKind.Relative, ExpectedResult = false)]
|
||||
[TestCase("//hello.com:8080/path?query=param#fragment", UriKind.Relative, ExpectedResult = false)]
|
||||
[TestCase("//hello.com:8080/path", UriKind.Relative, ExpectedResult = false)]
|
||||
[TestCase("//hello.com:8080", UriKind.Relative, ExpectedResult = false)]
|
||||
[TestCase("//hello.com", UriKind.Relative, ExpectedResult = false)]
|
||||
[TestCase("/test/test.jpg", UriKind.Relative, ExpectedResult = true)]
|
||||
[TestCase("/test", UriKind.Relative, ExpectedResult = true)]
|
||||
[TestCase("test", UriKind.Relative, ExpectedResult = true)]
|
||||
[TestCase("", UriKind.Relative, ExpectedResult = false)]
|
||||
[TestCase(null, UriKind.Relative, ExpectedResult = false)]
|
||||
[TestCase("this is not welformed", UriKind.Relative, ExpectedResult = false)]
|
||||
[TestCase("ftp://hello.com/", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
|
||||
[TestCase("file:///hello.com/", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
|
||||
[TestCase("ws://hello.com/", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
|
||||
[TestCase("wss://hello.com/", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
|
||||
[TestCase("https://hello.com:8080/", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
|
||||
[TestCase("http://hello.com:8080/", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
|
||||
[TestCase("https://hello.com/path", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
|
||||
[TestCase("http://hello.com/path", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
|
||||
[TestCase("https://hello.com/path?query=param", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
|
||||
[TestCase("http://hello.com/path?query=param", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
|
||||
[TestCase("https://hello.com/path#fragment", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
|
||||
[TestCase("http://hello.com/path#fragment", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
|
||||
[TestCase("https://hello.com/path?query=param#fragment", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
|
||||
[TestCase("http://hello.com/path?query=param#fragment", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
|
||||
[TestCase("https://hello.com:8080/path?query=param#fragment", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
|
||||
[TestCase("http://hello.com:8080/path?query=param#fragment", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
|
||||
[TestCase("//hello.com:8080/path?query=param#fragment", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
|
||||
[TestCase("//hello.com:8080/path", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
|
||||
[TestCase("//hello.com:8080", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
|
||||
[TestCase("//hello.com", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
|
||||
[TestCase("/test/test.jpg", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
|
||||
[TestCase("/test", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
|
||||
[TestCase("test", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
|
||||
[TestCase("", UriKind.RelativeOrAbsolute, ExpectedResult = false)]
|
||||
[TestCase(null, UriKind.RelativeOrAbsolute, ExpectedResult = false)]
|
||||
[TestCase("this is not welformed", UriKind.RelativeOrAbsolute, ExpectedResult = false)]
|
||||
public bool IsWellFormedWebPath(string? webPath, UriKind uriKind) => WebPath.IsWellFormedWebPath(webPath, uriKind);
|
||||
|
||||
}
|
||||
|
||||
+32
-14
@@ -2,9 +2,7 @@
|
||||
// See LICENSE for more details.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
@@ -35,7 +33,7 @@ public class ContentPermissionsQueryStringHandlerTests
|
||||
public async Task Node_Id_From_Requirement_With_Permission_Is_Authorized()
|
||||
{
|
||||
var authHandlerContext = CreateAuthorizationHandlerContext(NodeId);
|
||||
var mockHttpContextAccessor = CreateMockHttpContextAccessor();
|
||||
var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue();
|
||||
var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, new[] { "A" });
|
||||
|
||||
await sut.HandleAsync(authHandlerContext);
|
||||
@@ -47,7 +45,7 @@ public class ContentPermissionsQueryStringHandlerTests
|
||||
public async Task Node_Id_From_Requirement_Without_Permission_Is_Not_Authorized()
|
||||
{
|
||||
var authHandlerContext = CreateAuthorizationHandlerContext(NodeId);
|
||||
var mockHttpContextAccessor = CreateMockHttpContextAccessor();
|
||||
var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue();
|
||||
var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, new[] { "B" });
|
||||
|
||||
await sut.HandleAsync(authHandlerContext);
|
||||
@@ -60,7 +58,7 @@ public class ContentPermissionsQueryStringHandlerTests
|
||||
public async Task Node_Id_Missing_From_Requirement_And_QueryString_Is_Authorized()
|
||||
{
|
||||
var authHandlerContext = CreateAuthorizationHandlerContext();
|
||||
var mockHttpContextAccessor = CreateMockHttpContextAccessor("xxx");
|
||||
var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue("xxx");
|
||||
var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, new[] { "A" });
|
||||
|
||||
await sut.HandleAsync(authHandlerContext);
|
||||
@@ -72,7 +70,7 @@ public class ContentPermissionsQueryStringHandlerTests
|
||||
public async Task Node_Integer_Id_From_QueryString_With_Permission_Is_Authorized()
|
||||
{
|
||||
var authHandlerContext = CreateAuthorizationHandlerContext();
|
||||
var mockHttpContextAccessor = CreateMockHttpContextAccessor(queryStringValue: NodeId.ToString());
|
||||
var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(queryStringValue: NodeId.ToString());
|
||||
var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, new[] { "A" });
|
||||
|
||||
await sut.HandleAsync(authHandlerContext);
|
||||
@@ -85,7 +83,21 @@ public class ContentPermissionsQueryStringHandlerTests
|
||||
public async Task Node_Integer_Id_From_QueryString_Without_Permission_Is_Not_Authorized()
|
||||
{
|
||||
var authHandlerContext = CreateAuthorizationHandlerContext();
|
||||
var mockHttpContextAccessor = CreateMockHttpContextAccessor(queryStringValue: NodeId.ToString());
|
||||
var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(queryStringValue: NodeId.ToString());
|
||||
var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, new[] { "B" });
|
||||
|
||||
await sut.HandleAsync(authHandlerContext);
|
||||
|
||||
Assert.IsFalse(authHandlerContext.HasSucceeded);
|
||||
AssertContentCached(mockHttpContextAccessor);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Node_Integer_Id_From_QueryString_Without_Permission_Is_Not_Authorized_Even_When_Additional_Parameter_For_Id_With_Permission_Is_Provided()
|
||||
{
|
||||
// Provides initially failing test and verifies fix for advisory https://github.com/umbraco/Umbraco-CMS/security/advisories/GHSA-wx5h-wqfq-v698
|
||||
var authHandlerContext = CreateAuthorizationHandlerContext();
|
||||
var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValues(queryStringValues: new[] { NodeId.ToString(), 1001.ToString() });
|
||||
var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, new[] { "B" });
|
||||
|
||||
await sut.HandleAsync(authHandlerContext);
|
||||
@@ -98,7 +110,7 @@ public class ContentPermissionsQueryStringHandlerTests
|
||||
public async Task Node_Udi_Id_From_QueryString_With_Permission_Is_Authorized()
|
||||
{
|
||||
var authHandlerContext = CreateAuthorizationHandlerContext();
|
||||
var mockHttpContextAccessor = CreateMockHttpContextAccessor(queryStringValue: s_nodeUdi.ToString());
|
||||
var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(queryStringValue: s_nodeUdi.ToString());
|
||||
var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, new[] { "A" });
|
||||
|
||||
await sut.HandleAsync(authHandlerContext);
|
||||
@@ -111,7 +123,7 @@ public class ContentPermissionsQueryStringHandlerTests
|
||||
public async Task Node_Udi_Id_From_QueryString_Without_Permission_Is_Not_Authorized()
|
||||
{
|
||||
var authHandlerContext = CreateAuthorizationHandlerContext();
|
||||
var mockHttpContextAccessor = CreateMockHttpContextAccessor(queryStringValue: s_nodeUdi.ToString());
|
||||
var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(queryStringValue: s_nodeUdi.ToString());
|
||||
var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, new[] { "B" });
|
||||
|
||||
await sut.HandleAsync(authHandlerContext);
|
||||
@@ -124,7 +136,7 @@ public class ContentPermissionsQueryStringHandlerTests
|
||||
public async Task Node_Guid_Id_From_QueryString_With_Permission_Is_Authorized()
|
||||
{
|
||||
var authHandlerContext = CreateAuthorizationHandlerContext();
|
||||
var mockHttpContextAccessor = CreateMockHttpContextAccessor(queryStringValue: s_nodeGuid.ToString());
|
||||
var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(queryStringValue: s_nodeGuid.ToString());
|
||||
var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, new[] { "A" });
|
||||
|
||||
await sut.HandleAsync(authHandlerContext);
|
||||
@@ -137,7 +149,7 @@ public class ContentPermissionsQueryStringHandlerTests
|
||||
public async Task Node_Guid_Id_From_QueryString_Without_Permission_Is_Not_Authorized()
|
||||
{
|
||||
var authHandlerContext = CreateAuthorizationHandlerContext();
|
||||
var mockHttpContextAccessor = CreateMockHttpContextAccessor(queryStringValue: s_nodeGuid.ToString());
|
||||
var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(queryStringValue: s_nodeGuid.ToString());
|
||||
var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, new[] { "B" });
|
||||
|
||||
await sut.HandleAsync(authHandlerContext);
|
||||
@@ -150,7 +162,7 @@ public class ContentPermissionsQueryStringHandlerTests
|
||||
public async Task Node_Invalid_Id_From_QueryString_Is_Authorized()
|
||||
{
|
||||
var authHandlerContext = CreateAuthorizationHandlerContext();
|
||||
var mockHttpContextAccessor = CreateMockHttpContextAccessor(queryStringValue: "invalid");
|
||||
var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(queryStringValue: "invalid");
|
||||
var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, new[] { "A" });
|
||||
|
||||
await sut.HandleAsync(authHandlerContext);
|
||||
@@ -169,14 +181,20 @@ public class ContentPermissionsQueryStringHandlerTests
|
||||
return new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement }, user, resource);
|
||||
}
|
||||
|
||||
private static Mock<IHttpContextAccessor> CreateMockHttpContextAccessor(
|
||||
private static Mock<IHttpContextAccessor> CreateMockHttpContextAccessorWithQueryStringValue(
|
||||
string queryStringName = QueryStringName,
|
||||
string queryStringValue = "")
|
||||
=> CreateMockHttpContextAccessorWithQueryStringValues(queryStringName, new[] { queryStringValue });
|
||||
|
||||
private static Mock<IHttpContextAccessor> CreateMockHttpContextAccessorWithQueryStringValues(
|
||||
string queryStringName = QueryStringName,
|
||||
string[]? queryStringValues = null)
|
||||
{
|
||||
queryStringValues ??= Array.Empty<string>();
|
||||
var mockHttpContextAccessor = new Mock<IHttpContextAccessor>();
|
||||
var mockHttpContext = new Mock<HttpContext>();
|
||||
var mockHttpRequest = new Mock<HttpRequest>();
|
||||
var queryParams = new Dictionary<string, StringValues> { { queryStringName, queryStringValue } };
|
||||
var queryParams = new Dictionary<string, StringValues> { { queryStringName, new StringValues(queryStringValues) } };
|
||||
mockHttpRequest.SetupGet(x => x.Query).Returns(new QueryCollection(queryParams));
|
||||
mockHttpContext.SetupGet(x => x.Request).Returns(mockHttpRequest.Object);
|
||||
mockHttpContext.SetupGet(x => x.Items).Returns(new Dictionary<object, object>());
|
||||
|
||||
+39
-13
@@ -2,9 +2,7 @@
|
||||
// See LICENSE for more details.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
@@ -34,7 +32,7 @@ public class MediaPermissionsQueryStringHandlerTests
|
||||
public async Task Node_Id_Missing_From_QueryString_Is_Authorized()
|
||||
{
|
||||
var authHandlerContext = CreateAuthorizationHandlerContext();
|
||||
var mockHttpContextAccessor = CreateMockHttpContextAccessor("xxx");
|
||||
var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue("xxx");
|
||||
var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId);
|
||||
|
||||
await sut.HandleAsync(authHandlerContext);
|
||||
@@ -46,7 +44,7 @@ public class MediaPermissionsQueryStringHandlerTests
|
||||
public async Task Node_Integer_Id_From_QueryString_With_Permission_Is_Authorized()
|
||||
{
|
||||
var authHandlerContext = CreateAuthorizationHandlerContext();
|
||||
var mockHttpContextAccessor = CreateMockHttpContextAccessor(queryStringValue: NodeId.ToString());
|
||||
var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(queryStringValue: NodeId.ToString());
|
||||
var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId);
|
||||
|
||||
await sut.HandleAsync(authHandlerContext);
|
||||
@@ -59,7 +57,21 @@ public class MediaPermissionsQueryStringHandlerTests
|
||||
public async Task Node_Integer_Id_From_QueryString_Without_Permission_Is_Not_Authorized()
|
||||
{
|
||||
var authHandlerContext = CreateAuthorizationHandlerContext();
|
||||
var mockHttpContextAccessor = CreateMockHttpContextAccessor(queryStringValue: NodeId.ToString());
|
||||
var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(queryStringValue: NodeId.ToString());
|
||||
var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, 1001);
|
||||
|
||||
await sut.HandleAsync(authHandlerContext);
|
||||
|
||||
Assert.IsFalse(authHandlerContext.HasSucceeded);
|
||||
AssertMediaCached(mockHttpContextAccessor);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Node_Integer_Id_From_QueryString_Without_Permission_Is_Not_Authorized_Even_When_Additional_Parameter_For_Id_With_Permission_Is_Provided()
|
||||
{
|
||||
// Provides initially failing test and verifies fix for advisory https://github.com/umbraco/Umbraco-CMS/security/advisories/GHSA-wx5h-wqfq-v698
|
||||
var authHandlerContext = CreateAuthorizationHandlerContext();
|
||||
var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValues(queryStringValues: new[] { NodeId.ToString(), 1001.ToString() });
|
||||
var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, 1001);
|
||||
|
||||
await sut.HandleAsync(authHandlerContext);
|
||||
@@ -72,7 +84,7 @@ public class MediaPermissionsQueryStringHandlerTests
|
||||
public async Task Node_Udi_Id_From_QueryString_With_Permission_Is_Authorized()
|
||||
{
|
||||
var authHandlerContext = CreateAuthorizationHandlerContext();
|
||||
var mockHttpContextAccessor = CreateMockHttpContextAccessor(queryStringValue: s_nodeUdi.ToString());
|
||||
var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(queryStringValue: s_nodeUdi.ToString());
|
||||
var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId);
|
||||
|
||||
await sut.HandleAsync(authHandlerContext);
|
||||
@@ -85,7 +97,7 @@ public class MediaPermissionsQueryStringHandlerTests
|
||||
public async Task Node_Udi_Id_From_QueryString_Without_Permission_Is_Not_Authorized()
|
||||
{
|
||||
var authHandlerContext = CreateAuthorizationHandlerContext();
|
||||
var mockHttpContextAccessor = CreateMockHttpContextAccessor(queryStringValue: s_nodeUdi.ToString());
|
||||
var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(queryStringValue: s_nodeUdi.ToString());
|
||||
var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, 1001);
|
||||
|
||||
await sut.HandleAsync(authHandlerContext);
|
||||
@@ -98,7 +110,7 @@ public class MediaPermissionsQueryStringHandlerTests
|
||||
public async Task Node_Guid_Id_From_QueryString_With_Permission_Is_Authorized()
|
||||
{
|
||||
var authHandlerContext = CreateAuthorizationHandlerContext();
|
||||
var mockHttpContextAccessor = CreateMockHttpContextAccessor(queryStringValue: s_nodeGuid.ToString());
|
||||
var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(queryStringValue: s_nodeGuid.ToString());
|
||||
var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId);
|
||||
|
||||
await sut.HandleAsync(authHandlerContext);
|
||||
@@ -111,7 +123,7 @@ public class MediaPermissionsQueryStringHandlerTests
|
||||
public async Task Node_Guid_Id_From_QueryString_Without_Permission_Is_Not_Authorized()
|
||||
{
|
||||
var authHandlerContext = CreateAuthorizationHandlerContext();
|
||||
var mockHttpContextAccessor = CreateMockHttpContextAccessor(queryStringValue: s_nodeGuid.ToString());
|
||||
var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(queryStringValue: s_nodeGuid.ToString());
|
||||
var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, 1001);
|
||||
|
||||
await sut.HandleAsync(authHandlerContext);
|
||||
@@ -124,7 +136,7 @@ public class MediaPermissionsQueryStringHandlerTests
|
||||
public async Task Node_Invalid_Id_From_QueryString_Is_Authorized()
|
||||
{
|
||||
var authHandlerContext = CreateAuthorizationHandlerContext();
|
||||
var mockHttpContextAccessor = CreateMockHttpContextAccessor(queryStringValue: "invalid");
|
||||
var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(queryStringValue: "invalid");
|
||||
var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId);
|
||||
|
||||
await sut.HandleAsync(authHandlerContext);
|
||||
@@ -140,14 +152,21 @@ public class MediaPermissionsQueryStringHandlerTests
|
||||
return new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement }, user, resource);
|
||||
}
|
||||
|
||||
private static Mock<IHttpContextAccessor> CreateMockHttpContextAccessor(
|
||||
private static Mock<IHttpContextAccessor> CreateMockHttpContextAccessorWithQueryStringValue(
|
||||
string queryStringName = QueryStringName,
|
||||
string queryStringValue = "")
|
||||
=> CreateMockHttpContextAccessorWithQueryStringValues(queryStringName, new[] { queryStringValue });
|
||||
|
||||
private static Mock<IHttpContextAccessor> CreateMockHttpContextAccessorWithQueryStringValues(
|
||||
string queryStringName = QueryStringName,
|
||||
string[]? queryStringValues = null)
|
||||
{
|
||||
queryStringValues ??= Array.Empty<string>();
|
||||
|
||||
var mockHttpContextAccessor = new Mock<IHttpContextAccessor>();
|
||||
var mockHttpContext = new Mock<HttpContext>();
|
||||
var mockHttpRequest = new Mock<HttpRequest>();
|
||||
var queryParams = new Dictionary<string, StringValues> { { queryStringName, queryStringValue } };
|
||||
var queryParams = new Dictionary<string, StringValues> { { queryStringName, new StringValues(queryStringValues) } };
|
||||
mockHttpRequest.SetupGet(x => x.Query).Returns(new QueryCollection(queryParams));
|
||||
mockHttpContext.SetupGet(x => x.Request).Returns(mockHttpRequest.Object);
|
||||
mockHttpContext.SetupGet(x => x.Items).Returns(new Dictionary<object, object>());
|
||||
@@ -155,6 +174,13 @@ public class MediaPermissionsQueryStringHandlerTests
|
||||
return mockHttpContextAccessor;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="httpContextAccessor"></param>
|
||||
/// <param name="nodeId"></param>
|
||||
/// <param name="startMediaId">the startMediaId of the user being setup</param>
|
||||
/// <returns></returns>
|
||||
private MediaPermissionsQueryStringHandler CreateHandler(
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
int nodeId,
|
||||
@@ -179,7 +205,7 @@ public class MediaPermissionsQueryStringHandlerTests
|
||||
mockEntityService
|
||||
.Setup(x => x.GetId(
|
||||
It.Is<Guid>(y => y == s_nodeGuid),
|
||||
It.Is<UmbracoObjectTypes>(y => y == UmbracoObjectTypes.Document)))
|
||||
It.Is<UmbracoObjectTypes>(y => y == UmbracoObjectTypes.Media)))
|
||||
.Returns(Attempt<int>.Succeed(NodeId));
|
||||
return mockEntityService;
|
||||
}
|
||||
|
||||
+1
-1
@@ -44,7 +44,7 @@ public class MediaPermissionsResourceHandlerTests
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Resource_With_Node_Id_Withou_Permission_Is_Not_Authorized()
|
||||
public async Task Resource_With_Node_Id_Without_Permission_Is_Not_Authorized()
|
||||
{
|
||||
var authHandlerContext = CreateAuthorizationHandlerContext(NodeId, true);
|
||||
var sut = CreateHandler(NodeId, 1001);
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json",
|
||||
"version": "10.8.4",
|
||||
"version": "10.9.0-rc",
|
||||
"assemblyVersion": {
|
||||
"precision": "build"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user