Compare commits
247
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ab0abca99 | ||
|
|
009a999e22 | ||
|
|
8a9db11422 | ||
|
|
9e7a36865a | ||
|
|
db1d999721 | ||
|
|
18047a7cfb | ||
|
|
def7ebd48c | ||
|
|
8c2b1ebdc5 | ||
|
|
9227517a50 | ||
|
|
5322d0f7b5 | ||
|
|
a282cc5691 | ||
|
|
048f8bcdf9 | ||
|
|
7bcbc748d4 | ||
|
|
f30e6cfe7f | ||
|
|
026e80e026 | ||
|
|
a3b77cff63 | ||
|
|
4ca68d6995 | ||
|
|
280cb7f2b1 | ||
|
|
095a73132c | ||
|
|
cfb0fc23ac | ||
|
|
6620aca9fe | ||
|
|
2422a02b23 | ||
|
|
b4a9dc0770 | ||
|
|
e7411244fd | ||
|
|
9f357173c3 | ||
|
|
7dc6e3ed5b | ||
|
|
5dfff212d0 | ||
|
|
17615f966b | ||
|
|
7a2d6b6c63 | ||
|
|
343867f6ee | ||
|
|
b9837ac77c | ||
|
|
bb73ec6c68 | ||
|
|
ee2d7bbb1b | ||
|
|
5d48bc7371 | ||
|
|
59a4649528 | ||
|
|
f54b6033ab | ||
|
|
bf340cd7d4 | ||
|
|
313417cb91 | ||
|
|
7850078623 | ||
|
|
44bf3b77b3 | ||
|
|
8485458896 | ||
|
|
3d253f5f06 | ||
|
|
64287d7e1a | ||
|
|
edc78a5a4c | ||
|
|
8056a59a10 | ||
|
|
1a18d6c035 | ||
|
|
3e877182b5 | ||
|
|
0e4f883bc1 | ||
|
|
265cc62ab8 | ||
|
|
800c16970e | ||
|
|
6bd11bf233 | ||
|
|
a2fd82a3f3 | ||
|
|
56c0aebd76 | ||
|
|
7567990da1 | ||
|
|
c7d157bbfd | ||
|
|
4b9771c0eb | ||
|
|
2161edb871 | ||
|
|
e77e9c5691 | ||
|
|
e934a943b5 | ||
|
|
95eb58587b | ||
|
|
25628a8b76 | ||
|
|
7552e315fb | ||
|
|
d15b5b221e | ||
|
|
60e3fa97a9 | ||
|
|
fd1bd51aff | ||
|
|
b12620af9c | ||
|
|
d1af59fdf0 | ||
|
|
1b050ebc47 | ||
|
|
ff3e457c58 | ||
|
|
2ffaede07b | ||
|
|
2a83f57262 | ||
|
|
5be9193978 | ||
|
|
d3f264fc54 | ||
|
|
04ea002bf3 | ||
|
|
8f42a3a0ea | ||
|
|
4dd8fbe25d | ||
|
|
ee727c43e1 | ||
|
|
395213946b | ||
|
|
ed0b236208 | ||
|
|
8d56e75257 | ||
|
|
b3c27415d1 | ||
|
|
9839b0ee66 | ||
|
|
f3f57f9748 | ||
|
|
85d16a4354 | ||
|
|
c0517c3562 | ||
|
|
92e535a1c9 | ||
|
|
7411c2552b | ||
|
|
f2410f376e | ||
|
|
a9313c1367 | ||
|
|
58d6404ec8 | ||
|
|
53a5813189 | ||
|
|
5b3b9d770a | ||
|
|
4a54215fa1 | ||
|
|
52fbb34ddd | ||
|
|
c96dc7eaa1 | ||
|
|
f19409ba5b | ||
|
|
679c42ef90 | ||
|
|
77b6e6e6d9 | ||
|
|
32d0cb477e | ||
|
|
d451390d78 | ||
|
|
3304c6f472 | ||
|
|
34f09fa260 | ||
|
|
fd4efd38cd | ||
|
|
e4e9bfc3e9 | ||
|
|
3774dbe38c | ||
|
|
4ef50642ad | ||
|
|
cb88cbba43 | ||
|
|
f95fb78022 | ||
|
|
0f9a8bde61 | ||
|
|
129d369c1a | ||
|
|
dd2c591a5b | ||
|
|
e9758321ce | ||
|
|
82e58eae3e | ||
|
|
de78560343 | ||
|
|
f569553d79 | ||
|
|
16749a724d | ||
|
|
a627930b58 | ||
|
|
404a62aa0b | ||
|
|
2d9cfc880b | ||
|
|
ee8bdfc4ac | ||
|
|
f048cfe62d | ||
|
|
9cab0e8bb1 | ||
|
|
b55d484863 | ||
|
|
ed0058a503 | ||
|
|
21e3518a69 | ||
|
|
b0aed393d6 | ||
|
|
28756d449b | ||
|
|
38a96db369 | ||
|
|
a1ea203281 | ||
|
|
4590739fa5 | ||
|
|
7c617f2976 | ||
|
|
9141f61708 | ||
|
|
ba4120050f | ||
|
|
772c523a23 | ||
|
|
cbd4dc6e0d | ||
|
|
c7014e159b | ||
|
|
9febbc7db1 | ||
|
|
ec8e10f406 | ||
|
|
3233b24a4e | ||
|
|
5775e9cb18 | ||
|
|
570005f5e1 | ||
|
|
1e9182cfa4 | ||
|
|
fbaa3440f2 | ||
|
|
e662468ecc | ||
|
|
33eb4dd5a4 | ||
|
|
c8efc30654 | ||
|
|
d7b98a2f00 | ||
|
|
9edd21a3b0 | ||
|
|
7010ff1d26 | ||
|
|
6acdf21eab | ||
|
|
a5479bc96e | ||
|
|
79d117026f | ||
|
|
2709054bdb | ||
|
|
0acdd26850 | ||
|
|
2d4230c001 | ||
|
|
2d027ce9a1 | ||
|
|
b1d9085c83 | ||
|
|
41da2e2cf3 | ||
|
|
76fcf19b15 | ||
|
|
11ccafeb97 | ||
|
|
aa9f194d76 | ||
|
|
73d70ba9fc | ||
|
|
6435ff0bb3 | ||
|
|
f7729508cf | ||
|
|
31e1acce67 | ||
|
|
aa1f3df76b | ||
|
|
728dc89909 | ||
|
|
e36dc1f554 | ||
|
|
3431f76320 | ||
|
|
1bc5466a8d | ||
|
|
d83daf44d3 | ||
|
|
2ad5ecd5d5 | ||
|
|
35c51a029a | ||
|
|
8c1128c85b | ||
|
|
c9021ab2d2 | ||
|
|
67a71f8f82 | ||
|
|
97d74f39e7 | ||
|
|
edd0a4a4a9 | ||
|
|
6399f235f2 | ||
|
|
8a22672c7f | ||
|
|
11270eaaf5 | ||
|
|
1e32d59ecb | ||
|
|
eab27123dd | ||
|
|
a6253957c8 | ||
|
|
a8f5631144 | ||
|
|
ba1080541b | ||
|
|
1581eb61d3 | ||
|
|
6d98162e19 | ||
|
|
ee37ad0f4b | ||
|
|
2b3a91757d | ||
|
|
00563013b6 | ||
|
|
2d71b5a63b | ||
|
|
f4f83bccbe | ||
|
|
5183391a86 | ||
|
|
0551976126 | ||
|
|
da2a4d1713 | ||
|
|
9f5867bdf8 | ||
|
|
30b114d538 | ||
|
|
c3db3457e7 | ||
|
|
7787af2df1 | ||
|
|
3730109cce | ||
|
|
b814608c06 | ||
|
|
ed63b51e46 | ||
|
|
42912dd5c9 | ||
|
|
9bab74d30e | ||
|
|
c58a7b7b23 | ||
|
|
4fae91d55c | ||
|
|
6c08f69801 | ||
|
|
910fb4062b | ||
|
|
f2e4e44db1 | ||
|
|
9a12eea495 | ||
|
|
45f43a6b7a | ||
|
|
6bd558c016 | ||
|
|
fdb9cfa3e7 | ||
|
|
009394982c | ||
|
|
8c4780380d | ||
|
|
910d70302e | ||
|
|
63a487bc0b | ||
|
|
a40eadcfce | ||
|
|
8f26263178 | ||
|
|
14a0e62278 | ||
|
|
609b5f76d4 | ||
|
|
9b19d63a6a | ||
|
|
e4dacf5c8c | ||
|
|
99125ef5cc | ||
|
|
264fbb6987 | ||
|
|
ac575662ac | ||
|
|
6939472f37 | ||
|
|
0387307301 | ||
|
|
e92b4f3172 | ||
|
|
e733ec1717 | ||
|
|
c5243e562e | ||
|
|
5c857aa97d | ||
|
|
25a5dde8e9 | ||
|
|
2e068bb73e | ||
|
|
842cacde19 | ||
|
|
6a453a0910 | ||
|
|
ff9903b14d | ||
|
|
d64bf5de22 | ||
|
|
9300d867fb | ||
|
|
eb0f8b5c24 | ||
|
|
d58768f26d | ||
|
|
3667217053 | ||
|
|
3e6116fcba | ||
|
|
bff293213d | ||
|
|
a47a1775f2 | ||
|
|
b2a2df7a8a |
+24
-22
@@ -12,25 +12,25 @@
|
||||
</ItemGroup>
|
||||
<!-- Microsoft packages -->
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.8" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="8.0.8" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.11" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="8.0.11" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.10.0" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="8.0.8" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.8" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.8" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.8" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="8.0.11" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.11" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.11" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.11" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.FileProviders.Embedded" Version="8.0.8" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.FileProviders.Embedded" Version="8.0.11" />
|
||||
<PackageVersion Include="Microsoft.Extensions.FileProviders.Physical" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Identity.Core" Version="8.0.8" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Identity.Stores" Version="8.0.8" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Identity.Core" Version="8.0.11" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Identity.Stores" Version="8.0.11" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options" Version="8.0.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options.DataAnnotations" Version="8.0.0" />
|
||||
@@ -45,14 +45,14 @@
|
||||
<PackageVersion Include="Asp.Versioning.Mvc" Version="8.1.0" />
|
||||
<PackageVersion Include="Asp.Versioning.Mvc.ApiExplorer" Version="8.1.0" />
|
||||
<PackageVersion Include="Dazinator.Extensions.FileProviders" Version="2.0.0" />
|
||||
<PackageVersion Include="Examine" Version="3.3.0" />
|
||||
<PackageVersion Include="Examine.Core" Version="3.3.0" />
|
||||
<PackageVersion Include="HtmlAgilityPack" Version="1.11.65" />
|
||||
<PackageVersion Include="Examine" Version="3.5.0" />
|
||||
<PackageVersion Include="Examine.Core" Version="3.5.0" />
|
||||
<PackageVersion Include="HtmlAgilityPack" Version="1.11.71" />
|
||||
<PackageVersion Include="JsonPatch.Net" Version="3.1.1" />
|
||||
<PackageVersion Include="K4os.Compression.LZ4" Version="1.3.8" />
|
||||
<PackageVersion Include="MailKit" Version="4.7.1.1" />
|
||||
<PackageVersion Include="MailKit" Version="4.8.0" />
|
||||
<PackageVersion Include="Markdown" Version="2.2.1" />
|
||||
<PackageVersion Include="MessagePack" Version="2.5.187" />
|
||||
<PackageVersion Include="MessagePack" Version="2.5.192" />
|
||||
<PackageVersion Include="MiniProfiler.AspNetCore.Mvc" Version="4.3.8" />
|
||||
<PackageVersion Include="MiniProfiler.Shared" Version="4.3.8" />
|
||||
<PackageVersion Include="ncrontab" Version="3.3.3" />
|
||||
@@ -62,32 +62,34 @@
|
||||
<PackageVersion Include="OpenIddict.AspNetCore" Version="5.7.0" />
|
||||
<PackageVersion Include="OpenIddict.EntityFrameworkCore" Version="5.7.0" />
|
||||
<PackageVersion Include="Serilog" Version="3.1.1" />
|
||||
<PackageVersion Include="Serilog.AspNetCore" Version="8.0.2" />
|
||||
<PackageVersion Include="Serilog.AspNetCore" Version="8.0.3" />
|
||||
<PackageVersion Include="Serilog.Enrichers.Process" Version="2.0.2" />
|
||||
<PackageVersion Include="Serilog.Enrichers.Thread" Version="3.1.0" />
|
||||
<PackageVersion Include="Serilog.Expressions" Version="4.0.0" />
|
||||
<PackageVersion Include="Serilog.Extensions.Hosting" Version="8.0.0" />
|
||||
<PackageVersion Include="Serilog.Formatting.Compact" Version="2.0.0" />
|
||||
<PackageVersion Include="Serilog.Formatting.Compact.Reader" Version="3.0.0" />
|
||||
<PackageVersion Include="Serilog.Settings.Configuration" Version="8.0.2" />
|
||||
<PackageVersion Include="Serilog.Settings.Configuration" Version="8.0.4" />
|
||||
<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="3.1.7" />
|
||||
<PackageVersion Include="SixLabors.ImageSharp.Web" Version="3.1.3" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.7.3" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.9.0" />
|
||||
</ItemGroup>
|
||||
<!-- Transitive pinned versions (only required because our direct dependencies have vulnerable versions of transitive dependencies) -->
|
||||
<ItemGroup>
|
||||
<!-- Both Microsoft.EntityFrameworkCore.SqlServer and NPoco.SqlServer bring in a vulnerable version of Azure.Identity -->
|
||||
<PackageVersion Include="Azure.Identity" Version="1.12.0" />
|
||||
<PackageVersion Include="Azure.Identity" Version="1.13.1" />
|
||||
<!-- Dazinator.Extensions.FileProviders brings in a vulnerable version of System.Net.Http -->
|
||||
<PackageVersion Include="System.Net.Http" Version="4.3.4" />
|
||||
<!-- Examine brings in a vulnerable version of System.Security.Cryptography.Xml -->
|
||||
<PackageVersion Include="System.Security.Cryptography.Xml" Version="8.0.1" />
|
||||
<PackageVersion Include="System.Security.Cryptography.Xml" Version="8.0.2" />
|
||||
<!-- Both Dazinator.Extensions.FileProviders and MiniProfiler.AspNetCore.Mvc bring in a vulnerable version of System.Text.RegularExpressions -->
|
||||
<PackageVersion Include="System.Text.RegularExpressions" Version="4.3.1" />
|
||||
<!-- Both OpenIddict.AspNetCore, Npoco.SqlServer and Microsoft.EntityFrameworkCore.SqlServer bring in a vulnerable version of Microsoft.IdentityModel.JsonWebTokens -->
|
||||
<PackageVersion Include="Microsoft.IdentityModel.JsonWebTokens" Version="7.7.1" />
|
||||
<!-- Examine.Lucene bring in a vulnerable version of Lucene.Net.Replicator -->
|
||||
<PackageVersion Include="Lucene.Net.Replicator" Version="4.8.0-beta00017" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
+12
-12
@@ -5,8 +5,8 @@ parameters:
|
||||
displayName: Run SQL Server Integration Tests
|
||||
type: boolean
|
||||
default: false
|
||||
- name: sqlServerAcceptanceTests
|
||||
displayName: Run SQL Server Acceptance Tests
|
||||
- name: sqlServerLinuxAcceptanceTests
|
||||
displayName: Run SQL Server Linux Acceptance Tests
|
||||
type: boolean
|
||||
default: false
|
||||
- name: myGetDeploy
|
||||
@@ -75,7 +75,7 @@ stages:
|
||||
- job: A
|
||||
displayName: Build Umbraco CMS
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
vmImage: 'windows-latest'
|
||||
steps:
|
||||
- checkout: self
|
||||
submodules: true
|
||||
@@ -521,7 +521,7 @@ stages:
|
||||
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
|
||||
|
||||
# Test
|
||||
- pwsh: npm run smokeTest --ignore-certificate-errors
|
||||
- pwsh: npm run smokeTestSqlite --ignore-certificate-errors
|
||||
displayName: Run Playwright tests
|
||||
continueOnError: true
|
||||
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
|
||||
@@ -557,18 +557,17 @@ stages:
|
||||
|
||||
- job:
|
||||
displayName: E2E Tests (SQL Server)
|
||||
# condition: or(eq(stageDependencies.Build.A.outputs['build.NBGV_PublicRelease'], 'True'), ${{parameters.sqlServerAcceptanceTests}}) # Outcommented due to timeouts
|
||||
condition: eq(${{parameters.sqlServerAcceptanceTests}}, True)
|
||||
variables:
|
||||
# Connection string
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN: Data Source=(localdb)\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\Umbraco.mdf;Integrated Security=True
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN_PROVIDERNAME: Microsoft.Data.SqlClient
|
||||
strategy:
|
||||
matrix:
|
||||
Linux:
|
||||
vmImage: 'ubuntu-latest'
|
||||
SA_PASSWORD: $(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD)
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN: 'Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);TrustServerCertificate=True'
|
||||
${{ if eq(parameters.sqlServerLinuxAcceptanceTests, True) }} :
|
||||
Linux:
|
||||
vmImage: 'ubuntu-latest'
|
||||
SA_PASSWORD: $(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD)
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN: 'Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);TrustServerCertificate=True'
|
||||
Windows:
|
||||
vmImage: 'windows-latest'
|
||||
pool:
|
||||
@@ -594,7 +593,8 @@ stages:
|
||||
- pwsh: |
|
||||
"UMBRACO_USER_LOGIN=$(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSEREMAIL)
|
||||
UMBRACO_USER_PASSWORD=$(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD)
|
||||
URL=$(ASPNETCORE_URLS)" | Out-File .env
|
||||
URL=$(ASPNETCORE_URLS)
|
||||
STORAGE_STAGE_PATH=$(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/playwright/.auth/user.json" | Out-File .env
|
||||
displayName: Generate .env
|
||||
workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest
|
||||
|
||||
@@ -712,7 +712,7 @@ stages:
|
||||
dependsOn:
|
||||
- Unit
|
||||
- Integration
|
||||
# - E2E
|
||||
# - E2E
|
||||
condition: and(succeeded(), or(eq(dependencies.Build.outputs['A.build.NBGV_PublicRelease'], 'True'), ${{parameters.myGetDeploy}}))
|
||||
jobs:
|
||||
- job:
|
||||
|
||||
@@ -3,14 +3,12 @@ name: Nightly_E2E_Test_$(TeamProject)_$(Build.DefinitionName)_$(SourceBranchName
|
||||
pr: none
|
||||
trigger: none
|
||||
|
||||
schedules:
|
||||
- cron: '0 0 * * *'
|
||||
displayName: Daily midnight build
|
||||
branches:
|
||||
include:
|
||||
- v14/dev
|
||||
## Uncomment after merged to v15/dev
|
||||
## - v15/dev
|
||||
# schedules:
|
||||
# - cron: '0 0 * * *'
|
||||
# displayName: Daily midnight build
|
||||
# branches:
|
||||
# include:
|
||||
# - v14/dev
|
||||
|
||||
variables:
|
||||
nodeVersion: 20
|
||||
@@ -25,8 +23,8 @@ variables:
|
||||
NODE_OPTIONS: --max_old_space_size=16384
|
||||
|
||||
parameters:
|
||||
- name: runSqlServerE2ETests
|
||||
displayName: Run the SQL Server E2E Tests
|
||||
- name: runSmokeTests
|
||||
displayName: Run the smoke tests
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
@@ -109,7 +107,7 @@ stages:
|
||||
# E2E Tests
|
||||
- job:
|
||||
displayName: E2E Tests (SQLite)
|
||||
timeoutInMinutes: 120
|
||||
timeoutInMinutes: 180
|
||||
variables:
|
||||
# Connection string
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN: Data Source=Umbraco;Mode=Memory;Cache=Shared;Foreign Keys=True;Pooling=True
|
||||
@@ -207,7 +205,10 @@ stages:
|
||||
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
|
||||
|
||||
# Test
|
||||
- pwsh: npm run test --ignore-certificate-errors
|
||||
- ${{ if eq(parameters.runSmokeTests, true) }}:
|
||||
pwsh: npm run smokeTestSqlite --ignore-certificate-errors
|
||||
${{ else }}:
|
||||
pwsh: npm run testSqlite --ignore-certificate-errors
|
||||
displayName: Run Playwright tests
|
||||
continueOnError: true
|
||||
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
|
||||
@@ -243,8 +244,7 @@ stages:
|
||||
|
||||
- job:
|
||||
displayName: E2E Tests (SQL Server)
|
||||
condition: and(succeeded(), ${{ eq(parameters.runSqlServerE2ETests, true) }})
|
||||
timeoutInMinutes: 120
|
||||
timeoutInMinutes: 180
|
||||
variables:
|
||||
# Connection string
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN: Data Source=(localdb)\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\Umbraco.mdf;Integrated Security=True
|
||||
@@ -280,7 +280,8 @@ stages:
|
||||
- pwsh: |
|
||||
"UMBRACO_USER_LOGIN=$(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSEREMAIL)
|
||||
UMBRACO_USER_PASSWORD=$(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD)
|
||||
URL=$(ASPNETCORE_URLS)" | Out-File .env
|
||||
URL=$(ASPNETCORE_URLS)
|
||||
STORAGE_STAGE_PATH=$(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/playwright/.auth/user.json" | Out-File .env
|
||||
displayName: Generate .env
|
||||
workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest
|
||||
|
||||
@@ -351,7 +352,10 @@ stages:
|
||||
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
|
||||
|
||||
# Test
|
||||
- pwsh: npm run test --ignore-certificate-errors
|
||||
- ${{ if eq(parameters.runSmokeTests, true) }}:
|
||||
pwsh: npm run smokeTest --ignore-certificate-errors
|
||||
${{ else }}:
|
||||
pwsh: npm run test --ignore-certificate-errors
|
||||
displayName: Run Playwright tests
|
||||
continueOnError: true
|
||||
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
|
||||
|
||||
@@ -26,7 +26,7 @@ steps:
|
||||
useSameBranch: true
|
||||
waitForQueuedBuildsToFinish: false
|
||||
storeInEnvironmentVariable: false
|
||||
templateParameters: 'sqlServerIntegrationTests: true, sqlServerAcceptanceTests: true, forceReleaseTestFilter: true, myGetDeploy: true, isNightly: true'
|
||||
templateParameters: 'sqlServerIntegrationTests: true, forceReleaseTestFilter: true, myGetDeploy: true, isNightly: true'
|
||||
authenticationMethod: 'OAuth Token'
|
||||
enableBuildInQueueCondition: false
|
||||
dependentOnSuccessfulBuildCondition: false
|
||||
|
||||
@@ -46,7 +46,9 @@ public static class UmbracoBuilderAuthExtensions
|
||||
Paths.BackOfficeApi.LogoutEndpoint.TrimStart(Constants.CharArrays.ForwardSlash))
|
||||
.SetRevocationEndpointUris(
|
||||
Paths.MemberApi.RevokeEndpoint.TrimStart(Constants.CharArrays.ForwardSlash),
|
||||
Paths.BackOfficeApi.RevokeEndpoint.TrimStart(Constants.CharArrays.ForwardSlash));
|
||||
Paths.BackOfficeApi.RevokeEndpoint.TrimStart(Constants.CharArrays.ForwardSlash))
|
||||
.SetUserinfoEndpointUris(
|
||||
Paths.MemberApi.UserinfoEndpoint.TrimStart(Constants.CharArrays.ForwardSlash));
|
||||
|
||||
// Enable authorization code flow with PKCE
|
||||
options
|
||||
@@ -58,7 +60,8 @@ public static class UmbracoBuilderAuthExtensions
|
||||
options
|
||||
.UseAspNetCore()
|
||||
.EnableAuthorizationEndpointPassthrough()
|
||||
.EnableLogoutEndpointPassthrough();
|
||||
.EnableLogoutEndpointPassthrough()
|
||||
.EnableUserinfoEndpointPassthrough();
|
||||
|
||||
// Enable reference tokens
|
||||
// - see https://documentation.openiddict.com/configuration/token-storage.html
|
||||
|
||||
@@ -31,6 +31,8 @@ public static class Paths
|
||||
|
||||
public static readonly string RevokeEndpoint = EndpointPath($"{EndpointTemplate}/revoke");
|
||||
|
||||
public static readonly string UserinfoEndpoint = EndpointPath($"{EndpointTemplate}/userinfo");
|
||||
|
||||
// NOTE: we're NOT using /api/v1.0/ here because it will clash with the Delivery API docs
|
||||
private static string EndpointPath(string relativePath) => $"/umbraco/delivery/api/v1/{relativePath}";
|
||||
}
|
||||
|
||||
@@ -16,6 +16,9 @@
|
||||
|
||||
<!-- Both OpenIddict.AspNetCore, Npoco.SqlServer and Microsoft.EntityFrameworkCore.SqlServer bring in a vulnerable version of Microsoft.IdentityModel.JsonWebTokens -->
|
||||
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens"/>
|
||||
|
||||
<!-- Take top-level depedendency on OpenIddict.AspNetCore depends on a vulnerable version -->
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -15,6 +15,7 @@ namespace Umbraco.Cms.Api.Delivery.Controllers.Content;
|
||||
[ApiExplorerSettings(GroupName = "Content")]
|
||||
[LocalizeFromAcceptLanguageHeader]
|
||||
[ValidateStartItem]
|
||||
[AddVaryHeader]
|
||||
[OutputCache(PolicyName = Constants.DeliveryApi.OutputCache.ContentCachePolicy)]
|
||||
public abstract class ContentApiControllerBase : DeliveryApiControllerBase
|
||||
{
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using OpenIddict.Server.AspNetCore;
|
||||
using Umbraco.Cms.Api.Delivery.Routing;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Controllers.Security;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
[ApiController]
|
||||
[VersionedDeliveryApiRoute(Common.Security.Paths.MemberApi.EndpointTemplate)]
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
[Authorize(AuthenticationSchemes = OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)]
|
||||
public class CurrentMemberController : DeliveryApiControllerBase
|
||||
{
|
||||
private readonly ICurrentMemberClaimsProvider _currentMemberClaimsProvider;
|
||||
|
||||
public CurrentMemberController(ICurrentMemberClaimsProvider currentMemberClaimsProvider)
|
||||
=> _currentMemberClaimsProvider = currentMemberClaimsProvider;
|
||||
|
||||
[HttpGet("userinfo")]
|
||||
public async Task<IActionResult> Userinfo()
|
||||
{
|
||||
Dictionary<string, object> claims = await _currentMemberClaimsProvider.GetClaimsAsync();
|
||||
return Ok(claims);
|
||||
}
|
||||
}
|
||||
@@ -60,6 +60,7 @@ public static class UmbracoBuilderExtensions
|
||||
builder.Services.AddSingleton<IApiMediaQueryService, ApiMediaQueryService>();
|
||||
builder.Services.AddTransient<IMemberApplicationManager, MemberApplicationManager>();
|
||||
builder.Services.AddTransient<IRequestMemberAccessService, RequestMemberAccessService>();
|
||||
builder.Services.AddTransient<ICurrentMemberClaimsProvider, CurrentMemberClaimsProvider>();
|
||||
|
||||
builder.Services.ConfigureOptions<ConfigureUmbracoDeliveryApiSwaggerGenOptions>();
|
||||
builder.AddUmbracoApiOpenApiUI();
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Filters;
|
||||
|
||||
public sealed class AddVaryHeaderAttribute : ActionFilterAttribute
|
||||
{
|
||||
private const string Vary = "Accept-Language, Preview, Start-Item";
|
||||
|
||||
public override void OnResultExecuting(ResultExecutingContext context)
|
||||
=> context.HttpContext.Response.Headers.Vary = context.HttpContext.Response.Headers.Vary.Count > 0
|
||||
? $"{context.HttpContext.Response.Headers.Vary}, {Vary}"
|
||||
: Vary;
|
||||
}
|
||||
@@ -1,56 +1,7 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.Json.Serialization.Metadata;
|
||||
using Umbraco.Cms.Core.Models.DeliveryApi;
|
||||
using Umbraco.Cms.Infrastructure.Serialization;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Json;
|
||||
|
||||
// see https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/polymorphism?pivots=dotnet-7-0
|
||||
public class DeliveryApiJsonTypeResolver : DefaultJsonTypeInfoResolver
|
||||
{
|
||||
public override JsonTypeInfo GetTypeInfo(Type type, JsonSerializerOptions options)
|
||||
{
|
||||
JsonTypeInfo jsonTypeInfo = base.GetTypeInfo(type, options);
|
||||
|
||||
Type[] derivedTypes = GetDerivedTypes(jsonTypeInfo);
|
||||
if (derivedTypes.Length > 0)
|
||||
{
|
||||
ConfigureJsonPolymorphismOptions(jsonTypeInfo, derivedTypes);
|
||||
}
|
||||
|
||||
return jsonTypeInfo;
|
||||
}
|
||||
|
||||
protected virtual Type[] GetDerivedTypes(JsonTypeInfo jsonTypeInfo)
|
||||
{
|
||||
if (jsonTypeInfo.Type == typeof(IApiContent))
|
||||
{
|
||||
return new[] { typeof(ApiContent) };
|
||||
}
|
||||
|
||||
if (jsonTypeInfo.Type == typeof(IApiContentResponse))
|
||||
{
|
||||
return new[] { typeof(ApiContentResponse) };
|
||||
}
|
||||
|
||||
if (jsonTypeInfo.Type == typeof(IRichTextElement))
|
||||
{
|
||||
return new[] { typeof(RichTextRootElement), typeof(RichTextGenericElement), typeof(RichTextTextElement) };
|
||||
}
|
||||
|
||||
return Array.Empty<Type>();
|
||||
}
|
||||
|
||||
protected void ConfigureJsonPolymorphismOptions(JsonTypeInfo jsonTypeInfo, params Type[] derivedTypes)
|
||||
{
|
||||
jsonTypeInfo.PolymorphismOptions = new JsonPolymorphismOptions
|
||||
{
|
||||
UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization,
|
||||
};
|
||||
|
||||
foreach (Type derivedType in derivedTypes)
|
||||
{
|
||||
jsonTypeInfo.PolymorphismOptions.DerivedTypes.Add(new JsonDerivedType(derivedType));
|
||||
}
|
||||
}
|
||||
}
|
||||
public class DeliveryApiJsonTypeResolver : ContentJsonTypeResolverBase
|
||||
{ }
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Json;
|
||||
|
||||
public abstract class DeliveryApiVersionAwareJsonConverterBase<T> : JsonConverter<T>
|
||||
{
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
private readonly JsonConverter<T> _defaultConverter = (JsonConverter<T>)JsonSerializerOptions.Default.GetConverter(typeof(T));
|
||||
|
||||
public DeliveryApiVersionAwareJsonConverterBase(IHttpContextAccessor httpContextAccessor)
|
||||
=> _httpContextAccessor = httpContextAccessor;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
=> _defaultConverter.Read(ref reader, typeToConvert, options);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
|
||||
{
|
||||
Type type = typeof(T);
|
||||
var apiVersion = GetApiVersion();
|
||||
|
||||
// Get the properties in the specified order
|
||||
PropertyInfo[] properties = type.GetProperties().OrderBy(GetPropertyOrder).ToArray();
|
||||
|
||||
writer.WriteStartObject();
|
||||
|
||||
foreach (PropertyInfo property in properties)
|
||||
{
|
||||
// Filter out properties based on the API version
|
||||
var include = apiVersion is null || ShouldIncludeProperty(property, apiVersion.Value);
|
||||
|
||||
if (include is false)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var propertyName = property.Name;
|
||||
writer.WritePropertyName(options.PropertyNamingPolicy?.ConvertName(propertyName) ?? propertyName);
|
||||
JsonSerializer.Serialize(writer, property.GetValue(value), options);
|
||||
}
|
||||
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
|
||||
private int? GetApiVersion()
|
||||
{
|
||||
HttpContext? httpContext = _httpContextAccessor.HttpContext;
|
||||
ApiVersion? apiVersion = httpContext?.GetRequestedApiVersion();
|
||||
|
||||
return apiVersion?.MajorVersion;
|
||||
}
|
||||
|
||||
private int GetPropertyOrder(PropertyInfo prop)
|
||||
{
|
||||
var attribute = prop.GetCustomAttribute<JsonPropertyOrderAttribute>();
|
||||
return attribute?.Order ?? 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether a property should be included based on version bounds.
|
||||
/// </summary>
|
||||
/// <param name="propertyInfo">The property info.</param>
|
||||
/// <param name="version">An integer representing an API version.</param>
|
||||
/// <returns><c>true</c> if the property should be included; otherwise, <c>false</c>.</returns>
|
||||
private bool ShouldIncludeProperty(PropertyInfo propertyInfo, int version)
|
||||
{
|
||||
var attribute = propertyInfo
|
||||
.GetCustomAttributes(typeof(IncludeInApiVersionAttribute), false)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (attribute is not IncludeInApiVersionAttribute apiVersionAttribute)
|
||||
{
|
||||
return true; // No attribute means include the property
|
||||
}
|
||||
|
||||
// Check if the version is within the specified bounds
|
||||
var isWithinMinVersion = apiVersionAttribute.MinVersion.HasValue is false || version >= apiVersionAttribute.MinVersion.Value;
|
||||
var isWithinMaxVersion = apiVersionAttribute.MaxVersion.HasValue is false || version <= apiVersionAttribute.MaxVersion.Value;
|
||||
|
||||
return isWithinMinVersion && isWithinMaxVersion;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
using Umbraco.Cms.Api.Delivery.Indexing.Filters;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Querying.Filters;
|
||||
|
||||
@@ -15,15 +14,15 @@ public sealed class ContentTypeFilter : IFilterHandler
|
||||
/// <inheritdoc/>
|
||||
public FilterOption BuildFilterOption(string filter)
|
||||
{
|
||||
var alias = filter.Substring(ContentTypeSpecifier.Length);
|
||||
var filterValue = filter.Substring(ContentTypeSpecifier.Length);
|
||||
var negate = filterValue.StartsWith('!');
|
||||
var aliases = filterValue.TrimStart('!').Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
return new FilterOption
|
||||
{
|
||||
FieldName = ContentTypeFilterIndexer.FieldName,
|
||||
Values = alias.IsNullOrWhiteSpace() == false
|
||||
? new[] { alias.TrimStart('!') }
|
||||
: Array.Empty<string>(),
|
||||
Operator = alias.StartsWith('!')
|
||||
Values = aliases,
|
||||
Operator = negate
|
||||
? FilterOperation.IsNot
|
||||
: FilterOperation.Is
|
||||
};
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Api.Delivery.Indexing.Selectors;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
using Umbraco.Extensions;
|
||||
@@ -10,10 +12,22 @@ public sealed class AncestorsSelector : QueryOptionBase, ISelectorHandler
|
||||
{
|
||||
private const string AncestorsSpecifier = "ancestors:";
|
||||
private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor;
|
||||
private readonly IRequestPreviewService _requestPreviewService;
|
||||
|
||||
public AncestorsSelector(IPublishedSnapshotAccessor publishedSnapshotAccessor, IRequestRoutingService requestRoutingService)
|
||||
: base(publishedSnapshotAccessor, requestRoutingService) =>
|
||||
[Obsolete("Please use the non-obsolete constructor. Will be removed in V17.")]
|
||||
public AncestorsSelector(
|
||||
IPublishedSnapshotAccessor publishedSnapshotAccessor,
|
||||
IRequestRoutingService requestRoutingService)
|
||||
: this(publishedSnapshotAccessor, requestRoutingService, StaticServiceProvider.Instance.GetRequiredService<IRequestPreviewService>())
|
||||
{
|
||||
}
|
||||
|
||||
public AncestorsSelector(IPublishedSnapshotAccessor publishedSnapshotAccessor, IRequestRoutingService requestRoutingService, IRequestPreviewService requestPreviewService)
|
||||
: base(publishedSnapshotAccessor, requestRoutingService)
|
||||
{
|
||||
_publishedSnapshotAccessor = publishedSnapshotAccessor;
|
||||
_requestPreviewService = requestPreviewService;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool CanHandle(string query)
|
||||
@@ -37,10 +51,20 @@ public sealed class AncestorsSelector : QueryOptionBase, ISelectorHandler
|
||||
};
|
||||
}
|
||||
|
||||
IPublishedSnapshot publishedSnapshot = _publishedSnapshotAccessor.GetRequiredPublishedSnapshot();
|
||||
IPublishedContentCache contentCache = _publishedSnapshotAccessor.GetRequiredPublishedSnapshot()?.Content
|
||||
?? throw new InvalidOperationException("Could not obtain the content cache");
|
||||
|
||||
IPublishedContent contentItem = publishedSnapshot.Content?.GetById((Guid)id)
|
||||
?? throw new InvalidOperationException("Could not obtain the content cache");
|
||||
IPublishedContent? contentItem = contentCache.GetById(_requestPreviewService.IsPreview(), id.Value);
|
||||
|
||||
if (contentItem is null)
|
||||
{
|
||||
// no such content item, make sure the selector does not yield any results
|
||||
return new SelectorOption
|
||||
{
|
||||
FieldName = AncestorsSelectorIndexer.FieldName,
|
||||
Values = Array.Empty<string>()
|
||||
};
|
||||
}
|
||||
|
||||
var ancestorKeys = contentItem.Ancestors().Select(a => a.Key.ToString("D")).ToArray();
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
using OpenIddict.Abstractions;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Services;
|
||||
|
||||
// NOTE: this is public and unsealed to allow overriding the default claims with minimal effort.
|
||||
public class CurrentMemberClaimsProvider : ICurrentMemberClaimsProvider
|
||||
{
|
||||
private readonly IMemberManager _memberManager;
|
||||
|
||||
public CurrentMemberClaimsProvider(IMemberManager memberManager)
|
||||
=> _memberManager = memberManager;
|
||||
|
||||
public virtual async Task<Dictionary<string, object>> GetClaimsAsync()
|
||||
{
|
||||
MemberIdentityUser? memberIdentityUser = await _memberManager.GetCurrentMemberAsync();
|
||||
return memberIdentityUser is not null
|
||||
? await GetClaimsForMemberIdentityAsync(memberIdentityUser)
|
||||
: throw new InvalidOperationException("Could not retrieve the current member. This method should only ever be invoked when a member has been authorized.");
|
||||
}
|
||||
|
||||
protected virtual async Task<Dictionary<string, object>> GetClaimsForMemberIdentityAsync(MemberIdentityUser memberIdentityUser)
|
||||
{
|
||||
var claims = new Dictionary<string, object>
|
||||
{
|
||||
[OpenIddictConstants.Claims.Subject] = memberIdentityUser.Key
|
||||
};
|
||||
|
||||
if (memberIdentityUser.Name is not null)
|
||||
{
|
||||
claims[OpenIddictConstants.Claims.Name] = memberIdentityUser.Name;
|
||||
}
|
||||
|
||||
if (memberIdentityUser.Email is not null)
|
||||
{
|
||||
claims[OpenIddictConstants.Claims.Email] = memberIdentityUser.Email;
|
||||
}
|
||||
|
||||
claims[OpenIddictConstants.Claims.Role] = await _memberManager.GetRolesAsync(memberIdentityUser);
|
||||
|
||||
return claims;
|
||||
}
|
||||
}
|
||||
@@ -42,31 +42,42 @@ internal sealed class RequestRedirectService : RoutingServiceBase, IRequestRedir
|
||||
{
|
||||
requestedPath = requestedPath.EnsureStartsWith("/");
|
||||
|
||||
IPublishedContent? startItem = GetStartItem();
|
||||
|
||||
// must append the root content url segment if it is not hidden by config, because
|
||||
// the URL tracking is based on the actual URL, including the root content url segment
|
||||
if (_globalSettings.HideTopLevelNodeFromPath == false)
|
||||
if (_globalSettings.HideTopLevelNodeFromPath == false && startItem?.UrlSegment != null)
|
||||
{
|
||||
IPublishedContent? startItem = GetStartItem();
|
||||
if (startItem?.UrlSegment != null)
|
||||
{
|
||||
requestedPath = $"{startItem.UrlSegment.EnsureStartsWith("/")}{requestedPath}";
|
||||
}
|
||||
requestedPath = $"{startItem.UrlSegment.EnsureStartsWith("/")}{requestedPath}";
|
||||
}
|
||||
|
||||
var culture = _requestCultureService.GetRequestedCulture();
|
||||
|
||||
// append the configured domain content ID to the path if we have a domain bound request,
|
||||
// because URL tracking registers the tracked url like "{domain content ID}/{content path}"
|
||||
Uri contentRoute = GetDefaultRequestUri(requestedPath);
|
||||
DomainAndUri? domainAndUri = GetDomainAndUriForRoute(contentRoute);
|
||||
if (domainAndUri != null)
|
||||
// important: redirect URLs are always tracked without trailing slashes
|
||||
requestedPath = requestedPath.TrimEnd("/");
|
||||
IRedirectUrl? redirectUrl = _redirectUrlService.GetMostRecentRedirectUrl(requestedPath, culture);
|
||||
|
||||
// if a redirect URL was not found, try by appending the start item ID because URL tracking might have tracked
|
||||
// a redirect with "{root content ID}/{content path}"
|
||||
if (redirectUrl is null && startItem is not null)
|
||||
{
|
||||
requestedPath = GetContentRoute(domainAndUri, contentRoute);
|
||||
culture ??= domainAndUri.Culture;
|
||||
redirectUrl = _redirectUrlService.GetMostRecentRedirectUrl($"{startItem.Id}{requestedPath}", culture);
|
||||
}
|
||||
|
||||
// still no redirect URL found - try looking for a configured domain if we have a domain bound request,
|
||||
// because URL tracking might have tracked a redirect with "{domain content ID}/{content path}"
|
||||
if (redirectUrl is null)
|
||||
{
|
||||
Uri contentRoute = GetDefaultRequestUri(requestedPath);
|
||||
DomainAndUri? domainAndUri = GetDomainAndUriForRoute(contentRoute);
|
||||
if (domainAndUri is not null)
|
||||
{
|
||||
requestedPath = GetContentRoute(domainAndUri, contentRoute);
|
||||
culture ??= domainAndUri.Culture;
|
||||
redirectUrl = _redirectUrlService.GetMostRecentRedirectUrl(requestedPath, culture);
|
||||
}
|
||||
}
|
||||
|
||||
// important: redirect URLs are always tracked without trailing slashes
|
||||
IRedirectUrl? redirectUrl = _redirectUrlService.GetMostRecentRedirectUrl(requestedPath.TrimEnd("/"), culture);
|
||||
IPublishedContent? content = redirectUrl != null
|
||||
? _apiPublishedContentCache.GetById(redirectUrl.ContentKey)
|
||||
: null;
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
<Description>Contains the presentation layer for the Umbraco CMS Delivery API.</Description>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<!-- TODO: [ASP0019] use IHeaderDictionary.Append or the indexer to append or set headers and remove this override -->
|
||||
<!-- TODO: [ASP0019] use IHeaderDictionary.Append or the indexer to append or set headers,
|
||||
and remove this override -->
|
||||
<WarningsNotAsErrors>ASP0019</WarningsNotAsErrors>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
|
||||
@@ -46,6 +46,6 @@ public class CreateMediaController : CreateMediaControllerBase
|
||||
|
||||
return result.Success
|
||||
? CreatedAtId<ByKeyMediaController>(controller => nameof(controller.ByKey), result.Result.Content!.Key)
|
||||
: ContentEditingOperationStatusResult(result.Status);
|
||||
: MediaEditingOperationStatusResult(result.Status, requestModel, result.Result.ValidationResult);
|
||||
});
|
||||
}
|
||||
|
||||
+3
@@ -1,14 +1,17 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.Factories;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.MemberType;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Services.ContentTypeEditing;
|
||||
using Umbraco.Cms.Web.Common.Authorization;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.MemberType;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize(Policy = AuthorizationPolicies.TreeAccessMemberTypes)]
|
||||
public class AvailableCompositionMemberTypeController : MemberTypeControllerBase
|
||||
{
|
||||
private readonly IMemberTypeEditingService _memberTypeEditingService;
|
||||
|
||||
+3
@@ -1,4 +1,5 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.MemberType;
|
||||
@@ -6,10 +7,12 @@ using Umbraco.Cms.Core.Mapping;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.Cms.Web.Common.Authorization;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.MemberType;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize(Policy = AuthorizationPolicies.TreeAccessMemberTypes)]
|
||||
public class CompositionReferenceMemberTypeController : MemberTypeControllerBase
|
||||
{
|
||||
private readonly IMemberTypeService _memberTypeService;
|
||||
|
||||
+1
@@ -9,6 +9,7 @@ using Umbraco.Cms.Web.Common.Authorization;
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.MemberType;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize(Policy = AuthorizationPolicies.TreeAccessMemberTypes)]
|
||||
public class ConfigurationMemberTypeController : MemberTypeControllerBase
|
||||
{
|
||||
private readonly IConfigurationPresentationFactory _configurationPresentationFactory;
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.Cms.Web.Common.Authorization;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.MemberType;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize(Policy = AuthorizationPolicies.TreeAccessMemberTypes)]
|
||||
public class CopyMemberTypeController : MemberTypeControllerBase
|
||||
{
|
||||
private readonly IMemberTypeService _memberTypeService;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.Factories;
|
||||
@@ -9,10 +10,12 @@ using Umbraco.Cms.Core.Models.ContentTypeEditing;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Services.ContentTypeEditing;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.Cms.Web.Common.Authorization;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.MemberType;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize(Policy = AuthorizationPolicies.TreeAccessMemberTypes)]
|
||||
public class CreateMemberTypeController : MemberTypeControllerBase
|
||||
{
|
||||
private readonly IMemberTypeEditingPresentationFactory _memberTypeEditingPresentationFactory;
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.Cms.Web.Common.Authorization;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.MemberType;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize(Policy = AuthorizationPolicies.TreeAccessMemberTypes)]
|
||||
public class DeleteMemberTypeController : MemberTypeControllerBase
|
||||
{
|
||||
private readonly IMemberTypeService _memberTypeService;
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace Umbraco.Cms.Api.Management.Controllers.MemberType;
|
||||
|
||||
[VersionedApiBackOfficeRoute(Constants.UdiEntityType.MemberType)]
|
||||
[ApiExplorerSettings(GroupName = "Member Type")]
|
||||
[Authorize(Policy = AuthorizationPolicies.TreeAccessMemberTypes)]
|
||||
[Authorize(Policy = AuthorizationPolicies.TreeAccessMembersOrMemberTypes)]
|
||||
public abstract class MemberTypeControllerBase : ManagementApiControllerBase
|
||||
{
|
||||
protected IActionResult OperationStatusResult(ContentTypeOperationStatus status)
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ namespace Umbraco.Cms.Api.Management.Controllers.MemberType.Tree;
|
||||
|
||||
[VersionedApiBackOfficeRoute($"{Constants.Web.RoutePath.Tree}/{Constants.UdiEntityType.MemberType}")]
|
||||
[ApiExplorerSettings(GroupName = "Member Type")]
|
||||
[Authorize(Policy = AuthorizationPolicies.TreeAccessMemberTypes)]
|
||||
[Authorize(Policy = AuthorizationPolicies.TreeAccessMembersOrMemberTypes)]
|
||||
public class MemberTypeTreeControllerBase : NamedEntityTreeControllerBase<MemberTypeTreeItemResponseModel>
|
||||
{
|
||||
private readonly IMemberTypeService _memberTypeService;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.Factories;
|
||||
@@ -10,10 +11,12 @@ using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.ContentTypeEditing;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.Cms.Web.Common.Authorization;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.MemberType;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize(Policy = AuthorizationPolicies.TreeAccessMemberTypes)]
|
||||
public class UpdateMemberTypeController : MemberTypeControllerBase
|
||||
{
|
||||
private readonly IMemberTypeEditingPresentationFactory _memberTypeEditingPresentationFactory;
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ public class ByPathPartialViewController : PartialViewControllerBase
|
||||
_mapper = mapper;
|
||||
}
|
||||
|
||||
[HttpGet("{path}")]
|
||||
[HttpGet("{*path}")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(PartialViewResponseModel), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ public class DeletePartialViewController : PartialViewControllerBase
|
||||
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
|
||||
}
|
||||
|
||||
[HttpDelete("{path}")]
|
||||
[HttpDelete("{*path}")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ public class ByPathPartialViewFolderController : PartialViewFolderControllerBase
|
||||
_mapper = mapper;
|
||||
}
|
||||
|
||||
[HttpGet("{path}")]
|
||||
[HttpGet("{*path}")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(PartialViewFolderResponseModel), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ public class DeletePartialViewFolderController : PartialViewFolderControllerBase
|
||||
public DeletePartialViewFolderController(IPartialViewFolderService partialViewFolderService)
|
||||
=> _partialViewFolderService = partialViewFolderService;
|
||||
|
||||
[HttpDelete("{path}")]
|
||||
[HttpDelete("{*path}")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ public class UpdatePartialViewController : PartialViewControllerBase
|
||||
_mapper = mapper;
|
||||
}
|
||||
|
||||
[HttpPut("{path}")]
|
||||
[HttpPut("{*path}")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
|
||||
@@ -23,7 +23,7 @@ public class ByPathScriptController : ScriptControllerBase
|
||||
_mapper = mapper;
|
||||
}
|
||||
|
||||
[HttpGet("{path}")]
|
||||
[HttpGet("{*path}")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(ScriptResponseModel), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
|
||||
@@ -22,7 +22,7 @@ public class DeleteScriptController : ScriptControllerBase
|
||||
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
|
||||
}
|
||||
|
||||
[HttpDelete("{path}")]
|
||||
[HttpDelete("{*path}")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ public class ByPathScriptFolderController : ScriptFolderControllerBase
|
||||
_mapper = mapper;
|
||||
}
|
||||
|
||||
[HttpGet("{path}")]
|
||||
[HttpGet("{*path}")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(ScriptFolderResponseModel), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ public class DeleteScriptFolderController : ScriptFolderControllerBase
|
||||
public DeleteScriptFolderController(IScriptFolderService scriptFolderService)
|
||||
=> _scriptFolderService = scriptFolderService;
|
||||
|
||||
[HttpDelete("{path}")]
|
||||
[HttpDelete("{*path}")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
|
||||
@@ -29,7 +29,7 @@ public class UpdateScriptController : ScriptControllerBase
|
||||
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
|
||||
}
|
||||
|
||||
[HttpPut("{path}")]
|
||||
[HttpPut("{*path}")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
|
||||
@@ -207,12 +207,17 @@ public class BackOfficeController : SecurityControllerBase
|
||||
[MapToApiVersion("1.0")]
|
||||
public async Task<IActionResult> Signout(CancellationToken cancellationToken)
|
||||
{
|
||||
var userName = await GetUserNameFromAuthCookie();
|
||||
AuthenticateResult cookieAuthResult = await HttpContext.AuthenticateAsync(Constants.Security.BackOfficeAuthenticationType);
|
||||
var userName = cookieAuthResult.Principal?.Identity?.Name;
|
||||
var userId = cookieAuthResult.Principal?.Identity?.GetUserId();
|
||||
|
||||
await _backOfficeSignInManager.SignOutAsync();
|
||||
_backOfficeUserManager.NotifyLogoutSuccess(cookieAuthResult.Principal ?? User, userId);
|
||||
|
||||
_logger.LogInformation("User {UserName} from IP address {RemoteIpAddress} has logged out",
|
||||
userName ?? "UNKNOWN", HttpContext.Connection.RemoteIpAddress);
|
||||
_logger.LogInformation(
|
||||
"User {UserName} from IP address {RemoteIpAddress} has logged out",
|
||||
userName ?? "UNKNOWN",
|
||||
HttpContext.Connection.RemoteIpAddress);
|
||||
|
||||
// Returning a SignOutResult will ask OpenIddict to redirect the user agent
|
||||
// to the post_logout_redirect_uri specified by the client application.
|
||||
|
||||
@@ -23,7 +23,7 @@ public class ByPathStylesheetController : StylesheetControllerBase
|
||||
_umbracoMapper = umbracoMapper;
|
||||
}
|
||||
|
||||
[HttpGet("{path}")]
|
||||
[HttpGet("{*path}")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(StylesheetResponseModel), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.Extensions;
|
||||
@@ -9,10 +10,12 @@ using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.Cms.Web.Common.Authorization;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.Stylesheet;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize(Policy = AuthorizationPolicies.TreeAccessStylesheets)]
|
||||
public class CreateStylesheetController : StylesheetControllerBase
|
||||
{
|
||||
private readonly IStylesheetService _stylesheetService;
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.Extensions;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.Cms.Web.Common.Authorization;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.Stylesheet;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize(Policy = AuthorizationPolicies.TreeAccessStylesheets)]
|
||||
public class DeleteStylesheetController : StylesheetControllerBase
|
||||
{
|
||||
private readonly IStylesheetService _stylesheetService;
|
||||
@@ -22,7 +25,7 @@ public class DeleteStylesheetController : StylesheetControllerBase
|
||||
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
|
||||
}
|
||||
|
||||
[HttpDelete("{path}")]
|
||||
[HttpDelete("{*path}")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@ public class ByPathStylesheetFolderController : StylesheetFolderControllerBase
|
||||
_mapper = mapper;
|
||||
}
|
||||
|
||||
[HttpGet("{path}")]
|
||||
[HttpGet("{*path}")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(StylesheetFolderResponseModel), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ public class DeleteStylesheetFolderController : StylesheetFolderControllerBase
|
||||
public DeleteStylesheetFolderController(IStylesheetFolderService stylesheetFolderService)
|
||||
=> _stylesheetFolderService = stylesheetFolderService;
|
||||
|
||||
[HttpDelete("{path}")]
|
||||
[HttpDelete("{*path}")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.Extensions;
|
||||
@@ -9,10 +10,12 @@ using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.Cms.Web.Common.Authorization;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.Stylesheet;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize(Policy = AuthorizationPolicies.TreeAccessStylesheets)]
|
||||
public class RenameStylesheetController : StylesheetControllerBase
|
||||
{
|
||||
private readonly IStylesheetService _stylesheetService;
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace Umbraco.Cms.Api.Management.Controllers.Stylesheet;
|
||||
|
||||
[VersionedApiBackOfficeRoute($"{Constants.UdiEntityType.Stylesheet}")]
|
||||
[ApiExplorerSettings(GroupName = "Stylesheet")]
|
||||
[Authorize(Policy = AuthorizationPolicies.TreeAccessStylesheets)]
|
||||
[Authorize(Policy = AuthorizationPolicies.TreeAccessStylesheetsOrDocumentOrMediaOrMember)]
|
||||
public class StylesheetControllerBase : FileSystemManagementControllerBase
|
||||
{
|
||||
protected IActionResult StylesheetOperationStatusResult(StylesheetOperationStatus status) =>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.Extensions;
|
||||
@@ -9,10 +10,12 @@ using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.Cms.Web.Common.Authorization;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.Stylesheet;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize(Policy = AuthorizationPolicies.TreeAccessStylesheets)]
|
||||
public class UpdateStylesheetController : StylesheetControllerBase
|
||||
{
|
||||
private readonly IStylesheetService _stylesheetService;
|
||||
@@ -29,7 +32,7 @@ public class UpdateStylesheetController : StylesheetControllerBase
|
||||
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
|
||||
}
|
||||
|
||||
[HttpPut("{path}")]
|
||||
[HttpPut("{*path}")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.Filters;
|
||||
@@ -21,7 +21,8 @@ public abstract class UpgradeControllerBase : ManagementApiControllerBase
|
||||
? Ok()
|
||||
: OperationStatusResult(status, problemDetailsBuilder => status switch
|
||||
{
|
||||
UpgradeOperationStatus.UpgradeFailed => StatusCode(StatusCodes.Status500InternalServerError,
|
||||
UpgradeOperationStatus.UpgradeFailed => StatusCode(
|
||||
StatusCodes.Status500InternalServerError,
|
||||
problemDetailsBuilder
|
||||
.WithTitle("Upgrade failed")
|
||||
.WithDetail(result?.ErrorMessage ?? "An unknown error occurred.")
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Api.Management.Factories;
|
||||
using Umbraco.Cms.Api.Management.Security;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Notifications;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.DependencyInjection;
|
||||
|
||||
@@ -9,6 +11,13 @@ internal static class AuditLogBuilderExtensions
|
||||
internal static IUmbracoBuilder AddAuditLogs(this IUmbracoBuilder builder)
|
||||
{
|
||||
builder.Services.AddTransient<IAuditLogPresentationFactory, AuditLogPresentationFactory>();
|
||||
builder.AddNotificationHandler<UserLoginSuccessNotification, BackOfficeUserManagerAuditer>();
|
||||
builder.AddNotificationHandler<UserLogoutSuccessNotification, BackOfficeUserManagerAuditer>();
|
||||
builder.AddNotificationHandler<UserLoginFailedNotification, BackOfficeUserManagerAuditer>();
|
||||
builder.AddNotificationHandler<UserForgotPasswordRequestedNotification, BackOfficeUserManagerAuditer>();
|
||||
builder.AddNotificationHandler<UserForgotPasswordChangedNotification, BackOfficeUserManagerAuditer>();
|
||||
builder.AddNotificationHandler<UserPasswordChangedNotification, BackOfficeUserManagerAuditer>();
|
||||
builder.AddNotificationHandler<UserPasswordResetNotification, BackOfficeUserManagerAuditer>();
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
+9
-4
@@ -58,13 +58,16 @@ internal static class BackOfficeAuthPolicyBuilderExtensions
|
||||
|
||||
AddAllowedApplicationsPolicy(AuthorizationPolicies.SectionAccessContent, Constants.Applications.Content);
|
||||
AddAllowedApplicationsPolicy(AuthorizationPolicies.SectionAccessContentOrMedia, Constants.Applications.Content, Constants.Applications.Media);
|
||||
AddAllowedApplicationsPolicy(AuthorizationPolicies.SectionAccessForContentTree,
|
||||
AddAllowedApplicationsPolicy(
|
||||
AuthorizationPolicies.SectionAccessForContentTree,
|
||||
Constants.Applications.Content, Constants.Applications.Media, Constants.Applications.Users,
|
||||
Constants.Applications.Settings, Constants.Applications.Packages, Constants.Applications.Members);
|
||||
AddAllowedApplicationsPolicy(AuthorizationPolicies.SectionAccessForMediaTree,
|
||||
AddAllowedApplicationsPolicy(
|
||||
AuthorizationPolicies.SectionAccessForMediaTree,
|
||||
Constants.Applications.Content, Constants.Applications.Media, Constants.Applications.Users,
|
||||
Constants.Applications.Settings, Constants.Applications.Packages, Constants.Applications.Members);
|
||||
AddAllowedApplicationsPolicy(AuthorizationPolicies.SectionAccessForMemberTree,
|
||||
AddAllowedApplicationsPolicy(
|
||||
AuthorizationPolicies.SectionAccessForMemberTree,
|
||||
Constants.Applications.Content, Constants.Applications.Media, Constants.Applications.Members);
|
||||
AddAllowedApplicationsPolicy(AuthorizationPolicies.SectionAccessMedia, Constants.Applications.Media);
|
||||
AddAllowedApplicationsPolicy(AuthorizationPolicies.SectionAccessMembers, Constants.Applications.Members);
|
||||
@@ -84,11 +87,13 @@ internal static class BackOfficeAuthPolicyBuilderExtensions
|
||||
AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessMediaOrMediaTypes, Constants.Applications.Media, Constants.Applications.Settings);
|
||||
AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessMemberGroups, Constants.Applications.Members);
|
||||
AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessMemberTypes, Constants.Applications.Settings);
|
||||
AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessMembersOrMemberTypes, Constants.Applications.Settings, Constants.Applications.Members);
|
||||
AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessPartialViews, Constants.Applications.Settings);
|
||||
AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessRelationTypes, Constants.Applications.Settings);
|
||||
AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessScripts, Constants.Applications.Settings);
|
||||
AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessStylesheets, Constants.Applications.Settings);
|
||||
AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessTemplates, Constants.Applications.Settings);
|
||||
AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessStylesheetsOrDocumentOrMediaOrMember, Constants.Applications.Settings, Constants.Applications.Content, Constants.Applications.Media, Constants.Applications.Members);
|
||||
AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessTemplates, Constants.Applications.Settings, Constants.Applications.Content);
|
||||
AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessWebhooks, Constants.Applications.Settings);
|
||||
|
||||
// Contextual permissions
|
||||
|
||||
@@ -52,8 +52,7 @@ public static partial class UmbracoBuilderExtensions
|
||||
hostingEnvironment,
|
||||
factory.GetRequiredService<ILogger<PhysicalFileSystem>>(),
|
||||
hostingEnvironment.MapPathContentRoot(path),
|
||||
hostingEnvironment.ToAbsolute(path)
|
||||
);
|
||||
hostingEnvironment.ToAbsolute(path));
|
||||
});
|
||||
|
||||
return builder;
|
||||
|
||||
+5
-4
@@ -52,8 +52,7 @@ public static partial class UmbracoBuilderExtensions
|
||||
factory.GetRequiredService<IUserRepository>(),
|
||||
factory.GetRequiredService<IRuntimeState>(),
|
||||
factory.GetRequiredService<IEventMessagesFactory>(),
|
||||
factory.GetRequiredService<ILogger<BackOfficeUserStore>>()
|
||||
))
|
||||
factory.GetRequiredService<ILogger<BackOfficeUserStore>>()))
|
||||
.AddUserManager<IBackOfficeUserManager, BackOfficeUserManager>()
|
||||
.AddSignInManager<IBackOfficeSignInManager, BackOfficeSignInManager>()
|
||||
.AddClaimsPrincipalFactory<BackOfficeClaimsPrincipalFactory>()
|
||||
@@ -94,14 +93,16 @@ public static partial class UmbracoBuilderExtensions
|
||||
/// <summary>
|
||||
/// Adds support for external login providers in Umbraco
|
||||
/// </summary>
|
||||
public static IUmbracoBuilder AddBackOfficeExternalLogins(this IUmbracoBuilder umbracoBuilder,
|
||||
public static IUmbracoBuilder AddBackOfficeExternalLogins(
|
||||
this IUmbracoBuilder umbracoBuilder,
|
||||
Action<BackOfficeExternalLoginsBuilder> builder)
|
||||
{
|
||||
builder(new BackOfficeExternalLoginsBuilder(umbracoBuilder.Services));
|
||||
return umbracoBuilder;
|
||||
}
|
||||
|
||||
public static BackOfficeIdentityBuilder AddTwoFactorProvider<T>(this BackOfficeIdentityBuilder identityBuilder,
|
||||
public static BackOfficeIdentityBuilder AddTwoFactorProvider<T>(
|
||||
this BackOfficeIdentityBuilder identityBuilder,
|
||||
string providerName) where T : class, ITwoFactorProvider
|
||||
{
|
||||
identityBuilder.Services.AddSingleton<ITwoFactorProvider, T>();
|
||||
|
||||
@@ -32906,6 +32906,9 @@
|
||||
},
|
||||
"401": {
|
||||
"description": "The resource is protected and requires an authentication token"
|
||||
},
|
||||
"403": {
|
||||
"description": "The authenticated user do not have access to this resource"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
@@ -33107,6 +33110,9 @@
|
||||
},
|
||||
"401": {
|
||||
"description": "The resource is protected and requires an authentication token"
|
||||
},
|
||||
"403": {
|
||||
"description": "The authenticated user do not have access to this resource"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
@@ -33408,6 +33414,9 @@
|
||||
},
|
||||
"401": {
|
||||
"description": "The resource is protected and requires an authentication token"
|
||||
},
|
||||
"403": {
|
||||
"description": "The authenticated user do not have access to this resource"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
@@ -45357,4 +45366,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
using Umbraco.Cms.Api.Management.ViewModels;
|
||||
@@ -32,7 +32,12 @@ internal class NotificationHeaderFilter : IOperationFilter
|
||||
response.Headers.TryAdd(Constants.Headers.Notifications, new OpenApiHeader
|
||||
{
|
||||
Description = "The list of notifications produced during the request.",
|
||||
Schema = new OpenApiSchema { Type = "array" , Nullable = true, Items = new OpenApiSchema(){
|
||||
Schema = new OpenApiSchema
|
||||
{
|
||||
Type = "array",
|
||||
Nullable = true,
|
||||
Items = new OpenApiSchema()
|
||||
{
|
||||
Reference = new OpenApiReference()
|
||||
{
|
||||
Type = ReferenceType.Schema,
|
||||
|
||||
@@ -32,8 +32,10 @@ public class BackOfficeAuthenticationBuilder : AuthenticationBuilder
|
||||
/// <param name="displayName"></param>
|
||||
/// <param name="configureOptions"></param>
|
||||
/// <returns></returns>
|
||||
public override AuthenticationBuilder AddRemoteScheme<TOptions, THandler>(string authenticationScheme,
|
||||
string? displayName, Action<TOptions>? configureOptions)
|
||||
public override AuthenticationBuilder AddRemoteScheme<TOptions, THandler>(
|
||||
string authenticationScheme,
|
||||
string? displayName,
|
||||
Action<TOptions>? configureOptions)
|
||||
{
|
||||
// Validate that the prefix is set
|
||||
if (!authenticationScheme.StartsWith(Constants.Security.BackOfficeExternalAuthenticationTypePrefix))
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
using System.Globalization;
|
||||
using Umbraco.Cms.Core.Events;
|
||||
using Umbraco.Cms.Core.Models.Membership;
|
||||
using Umbraco.Cms.Core.Notifications;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Web.Common.Security;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Security;
|
||||
|
||||
/// <summary>
|
||||
/// Binds to notifications to write audit logs for the <see cref="BackOfficeUserManager" />
|
||||
/// </summary>
|
||||
internal sealed class BackOfficeUserManagerAuditer :
|
||||
INotificationHandler<UserLoginSuccessNotification>,
|
||||
INotificationHandler<UserLogoutSuccessNotification>,
|
||||
INotificationHandler<UserLoginFailedNotification>,
|
||||
INotificationHandler<UserForgotPasswordRequestedNotification>,
|
||||
INotificationHandler<UserForgotPasswordChangedNotification>,
|
||||
INotificationHandler<UserPasswordChangedNotification>,
|
||||
INotificationHandler<UserPasswordResetNotification>
|
||||
{
|
||||
private readonly IAuditService _auditService;
|
||||
private readonly IUserService _userService;
|
||||
|
||||
public BackOfficeUserManagerAuditer(IAuditService auditService, IUserService userService)
|
||||
{
|
||||
_auditService = auditService;
|
||||
_userService = userService;
|
||||
}
|
||||
|
||||
public void Handle(UserForgotPasswordChangedNotification notification) =>
|
||||
WriteAudit(
|
||||
notification.PerformingUserId,
|
||||
notification.AffectedUserId,
|
||||
notification.IpAddress,
|
||||
"umbraco/user/password/forgot/change",
|
||||
"password forgot/change");
|
||||
|
||||
public void Handle(UserForgotPasswordRequestedNotification notification) =>
|
||||
WriteAudit(
|
||||
notification.PerformingUserId,
|
||||
notification.AffectedUserId,
|
||||
notification.IpAddress,
|
||||
"umbraco/user/password/forgot/request",
|
||||
"password forgot/request");
|
||||
|
||||
public void Handle(UserLoginFailedNotification notification) =>
|
||||
WriteAudit(
|
||||
notification.PerformingUserId,
|
||||
null,
|
||||
notification.IpAddress,
|
||||
"umbraco/user/sign-in/failed",
|
||||
"login failed");
|
||||
|
||||
public void Handle(UserLoginSuccessNotification notification)
|
||||
=> WriteAudit(
|
||||
notification.PerformingUserId,
|
||||
notification.AffectedUserId,
|
||||
notification.IpAddress,
|
||||
"umbraco/user/sign-in/login",
|
||||
"login success");
|
||||
|
||||
public void Handle(UserLogoutSuccessNotification notification)
|
||||
=> WriteAudit(
|
||||
notification.PerformingUserId,
|
||||
notification.AffectedUserId,
|
||||
notification.IpAddress,
|
||||
"umbraco/user/sign-in/logout",
|
||||
"logout success");
|
||||
|
||||
public void Handle(UserPasswordChangedNotification notification) =>
|
||||
WriteAudit(
|
||||
notification.PerformingUserId,
|
||||
notification.AffectedUserId,
|
||||
notification.IpAddress,
|
||||
"umbraco/user/password/change",
|
||||
"password change");
|
||||
|
||||
public void Handle(UserPasswordResetNotification notification) =>
|
||||
WriteAudit(
|
||||
notification.PerformingUserId,
|
||||
notification.AffectedUserId,
|
||||
notification.IpAddress,
|
||||
"umbraco/user/password/reset",
|
||||
"password reset");
|
||||
|
||||
private static string FormatEmail(IMembershipUser? user) =>
|
||||
user is null ? string.Empty : user.Email.IsNullOrWhiteSpace() ? string.Empty : $"<{user.Email}>";
|
||||
|
||||
private void WriteAudit(
|
||||
string performingId,
|
||||
string? affectedId,
|
||||
string ipAddress,
|
||||
string eventType,
|
||||
string eventDetails)
|
||||
{
|
||||
int? performingIdAsInt = ParseUserId(performingId);
|
||||
int? affectedIdAsInt = ParseUserId(affectedId);
|
||||
|
||||
WriteAudit(performingIdAsInt, affectedIdAsInt, ipAddress, eventType, eventDetails);
|
||||
}
|
||||
|
||||
private static int? ParseUserId(string? id)
|
||||
=> int.TryParse(id, NumberStyles.Integer, CultureInfo.InvariantCulture, out var isAsInt) ? isAsInt : null;
|
||||
|
||||
private void WriteAudit(
|
||||
int? performingId,
|
||||
int? affectedId,
|
||||
string ipAddress,
|
||||
string eventType,
|
||||
string eventDetails)
|
||||
{
|
||||
var performingDetails = "User UNKNOWN:0";
|
||||
if (performingId.HasValue)
|
||||
{
|
||||
IUser? performingUser = _userService.GetUserById(performingId.Value);
|
||||
performingDetails = performingUser is null
|
||||
? $"User UNKNOWN:{performingId.Value}"
|
||||
: $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}";
|
||||
}
|
||||
|
||||
var affectedDetails = "User UNKNOWN:0";
|
||||
if (affectedId.HasValue)
|
||||
{
|
||||
IUser? affectedUser = _userService.GetUserById(affectedId.Value);
|
||||
affectedDetails = affectedUser is null
|
||||
? $"User UNKNOWN:{affectedId.Value}"
|
||||
: $"User \"{affectedUser.Name}\" {FormatEmail(affectedUser)}";
|
||||
}
|
||||
|
||||
_auditService.Write(
|
||||
performingId ?? 0,
|
||||
performingDetails,
|
||||
ipAddress,
|
||||
DateTime.UtcNow,
|
||||
affectedId ?? 0,
|
||||
affectedDetails,
|
||||
eventType,
|
||||
eventDetails);
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,8 @@ public interface IBackOfficeExternalLoginService
|
||||
|
||||
Task<Attempt<IEnumerable<IdentityError>, ExternalLoginOperationStatus>> HandleLoginCallbackAsync(HttpContext httpContext);
|
||||
|
||||
Task<Attempt<Guid?, ExternalLoginOperationStatus>> GenerateLoginProviderSecretAsync(ClaimsPrincipal claimsPrincipal,
|
||||
Task<Attempt<Guid?, ExternalLoginOperationStatus>> GenerateLoginProviderSecretAsync(
|
||||
ClaimsPrincipal claimsPrincipal,
|
||||
string loginProvider);
|
||||
|
||||
Task<Attempt<ClaimsPrincipal?, ExternalLoginOperationStatus>> ClaimsPrincipleFromLoginProviderLinkKeyAsync(
|
||||
|
||||
@@ -8,8 +8,15 @@
|
||||
<RootNamespace>Umbraco.Cms.Api.Management</RootNamespace>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<!-- TODO: Fix all warnings and remove this override -->
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
<!-- TODO: Fix [SA1117] params all on same line, [SA1401] make fields private,
|
||||
[SA1134] own line attributes, [CS0108] hidden inherited member, [CS0618]/[CS9042] update
|
||||
obsolete references, [CS1998] remove async or make method synchronous, [CS8524] switch statement,
|
||||
[IDE0060] removed unused parameter, [SA1649] file name match type, [CS0419] ambiguous reference,
|
||||
[CS1573] param tag for all parameters, [CS1574] unresolveable cref, and remove overrides -->
|
||||
<WarningsNotAsErrors>
|
||||
SA1117,SA1401,SA1134,CS0108,CS0618,CS1998,CS8524,CS9042,IDE0060,SA1649,CS0419,
|
||||
CS1573,CS1574
|
||||
</WarningsNotAsErrors>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="JsonPatch.Net" />
|
||||
|
||||
+3
-1
@@ -170,7 +170,9 @@ internal class SqlServerEFCoreDistributedLockingMechanism<T> : IDistributedLocki
|
||||
"A transaction with minimum ReadCommitted isolation level is required.");
|
||||
}
|
||||
|
||||
var rowsAffected = await dbContext.Database.ExecuteSqlAsync(@$"SET LOCK_TIMEOUT {(int)_timeout.TotalMilliseconds};UPDATE umbracoLock WITH (REPEATABLEREAD) SET value = (CASE WHEN (value=1) THEN -1 ELSE 1 END) WHERE id={LockId}");
|
||||
#pragma warning disable EF1002
|
||||
var rowsAffected = await dbContext.Database.ExecuteSqlRawAsync(@$"SET LOCK_TIMEOUT {(int)_timeout.TotalMilliseconds};UPDATE umbracoLock WITH (REPEATABLEREAD) SET value = (CASE WHEN (value=1) THEN -1 ELSE 1 END) WHERE id={LockId}");
|
||||
#pragma warning restore EF1002
|
||||
|
||||
if (rowsAffected == 0)
|
||||
{
|
||||
|
||||
@@ -4,13 +4,17 @@
|
||||
<Description>Adds support for Entity Framework Core to Umbraco CMS.</Description>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<!-- TODO: [IDE0270] Simplify null checks, [CS0108] resolve hiding inherited members, [CS1998] remove async or make method synchronous,
|
||||
and remove this override -->
|
||||
<!-- TODO: [IDE0270] Simplify null checks, [CS0108] resolve hiding inherited members, [CS1998]
|
||||
remove async or make method synchronous, and remove these overrides -->
|
||||
<WarningsNotAsErrors>IDE0270,CS0108,CS1998</WarningsNotAsErrors>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ItemGroup>
|
||||
<!-- Take top-level depedendency on Azure.Identity, because Microsoft.EntityFrameworkCore.SqlServer depends on a vulnerable version -->
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
|
||||
<!-- Take top-level depedendency on Microsoft.Extensions.Caching.Memory, because Microsoft.EntityFrameworkCore.* depends on a vulnerable version -->
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" />
|
||||
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" />
|
||||
<PackageReference Include="OpenIddict.EntityFrameworkCore" />
|
||||
|
||||
@@ -4,10 +4,15 @@
|
||||
<Description>Adds support for SQL Server to Umbraco CMS.</Description>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<!-- TODO: [SA1405] Simplify null checks, [SA1121] resolve hiding inherited members, [SA1117] remove async or make method synchronous,
|
||||
[IDE1006] fix naming rule violation, [CS0618] handle member obsolete appropriately, [IDE0270] simplify null check, [IDE0057] simplify substring,
|
||||
[IDE0054] use compound assignment, [CSO618] use NVARCARMAX, [IDE0048] add parenthesis for clarity, [CS1574] resolve ML comment cref attribute and remove this override -->
|
||||
<WarningsNotAsErrors>SA1405,SA1121,SA1117,SA1116,IDE1006,CS0618,IDE0270,IDE0057,IDE0054,CSO618,IDE0048,CS1574</WarningsNotAsErrors>
|
||||
<!-- TODO: [SA1405] Debug assret message text, [SA1121] resolve hiding inherited members, [SA1117] remove
|
||||
async or make method synchronous, [IDE1006] fix naming rule violation, [CS0618] handle member
|
||||
obsolete appropriately, [IDE0270] simplify null check, [IDE0057] simplify substring, [IDE0054]
|
||||
use compound assignment, [CSO618] use NVARCARMAX, [IDE0048] add parenthesis for clarity,
|
||||
[CS1574] resolve ML comment cref attribute, and remove these overrides -->
|
||||
<WarningsNotAsErrors>
|
||||
SA1405,SA1121,SA1117,SA1116,IDE1006,CS0618,IDE0270,IDE0057,IDE0054,CSO618,IDE0048,
|
||||
CS1574
|
||||
</WarningsNotAsErrors>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<!-- Take top-level depedendency on Azure.Identity, because NPoco.SqlServer depends on a vulnerable version -->
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<!-- TODO: [NU5123] Rename files so path is shorter -->
|
||||
<!-- TODO: [NU5123] Rename files so path is shorter and remove this override -->
|
||||
<WarningsNotAsErrors>NU5123</WarningsNotAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
<IncludeSymbols>false</IncludeSymbols>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<!-- TODO: [IDE0270] Simplify null checks, [CS0108] resolve hiding inherited members, [CS1998] remove async or make method synchronous,
|
||||
and remove this override -->
|
||||
<!-- TODO: [IDE0270] Simplify null checks, [CS0108] resolve hiding inherited members, [CS1998] remove
|
||||
async or make method synchronous, and remove these overrides -->
|
||||
<WarningsNotAsErrors>IDE0270,CS0108,CS1998</WarningsNotAsErrors>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
|
||||
@@ -140,7 +140,9 @@ public static class DistributedCacheExtensions
|
||||
Id = x.Item.Id,
|
||||
Key = x.Item.Key,
|
||||
ChangeTypes = x.ChangeTypes,
|
||||
Blueprint = x.Item.Blueprint
|
||||
Blueprint = x.Item.Blueprint,
|
||||
PublishedCultures = x.PublishedCultures?.ToArray(),
|
||||
UnpublishedCultures = x.UnpublishedCultures?.ToArray()
|
||||
});
|
||||
|
||||
dc.RefreshByPayload(ContentCacheRefresher.UniqueId, payloads);
|
||||
|
||||
@@ -182,6 +182,10 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase<ContentCac
|
||||
public TreeChangeTypes ChangeTypes { get; init; }
|
||||
|
||||
public bool Blueprint { get; init; }
|
||||
|
||||
public string[]? PublishedCultures { get; init; }
|
||||
|
||||
public string[]? UnpublishedCultures { get; init; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -11,6 +11,7 @@ public class RepositoryCachePolicyOptions
|
||||
public RepositoryCachePolicyOptions(Func<int> performCount)
|
||||
{
|
||||
PerformCount = performCount;
|
||||
CacheNullValues = false;
|
||||
GetAllCacheValidateCount = true;
|
||||
GetAllCacheAllowZeroCount = false;
|
||||
}
|
||||
@@ -21,6 +22,7 @@ public class RepositoryCachePolicyOptions
|
||||
public RepositoryCachePolicyOptions()
|
||||
{
|
||||
PerformCount = null;
|
||||
CacheNullValues = false;
|
||||
GetAllCacheValidateCount = false;
|
||||
GetAllCacheAllowZeroCount = false;
|
||||
}
|
||||
@@ -30,6 +32,11 @@ public class RepositoryCachePolicyOptions
|
||||
/// </summary>
|
||||
public Func<int>? PerformCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// True if the Get method will cache null results so that the db is not hit for repeated lookups
|
||||
/// </summary>
|
||||
public bool CacheNullValues { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// True/false as to validate the total item count when all items are returned from cache, the default is true but this
|
||||
/// means that a db lookup will occur - though that lookup will probably be significantly less expensive than the
|
||||
|
||||
@@ -34,7 +34,7 @@ public class TypeFinder : ITypeFinder
|
||||
"ServiceStack.", "SqlCE4Umbraco,", "Superpower,", // used by Serilog
|
||||
"System.", "TidyNet,", "TidyNet.", "WebDriver,", "itextsharp,", "mscorlib,", "NUnit,", "NUnit.", "NUnit3.",
|
||||
"Selenium.", "ImageProcessor", "MiniProfiler.", "Owin,", "SQLite",
|
||||
"ReSharperTestRunner", "ReSharperTestRunner32", "ReSharperTestRunner64", // These are used by the Jetbrains Rider IDE and Visual Studio ReSharper Extension
|
||||
"ReSharperTestRunner", "ReSharperTestRunner32", "ReSharperTestRunner64", "ReSharperTestRunnerArm32", "ReSharperTestRunnerArm64", // These are used by the Jetbrains Rider IDE and Visual Studio ReSharper Extension
|
||||
};
|
||||
|
||||
private static readonly ConcurrentDictionary<string, Type?> TypeNamesCache = new();
|
||||
|
||||
@@ -127,6 +127,7 @@ public class GlobalSettings
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to install the database when it is missing.
|
||||
/// </summary>
|
||||
[Obsolete("This option will be removed in V16.")]
|
||||
[DefaultValue(StaticInstallMissingDatabase)]
|
||||
public bool InstallMissingDatabase { get; set; } = StaticInstallMissingDatabase;
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ public class ModelsBuilderSettings
|
||||
internal const string StaticModelsDirectory = "~/umbraco/models";
|
||||
internal const bool StaticAcceptUnsafeModelsDirectory = false;
|
||||
internal const int StaticDebugLevel = 0;
|
||||
internal const bool StaticIncludeVersionNumberInGeneratedModels = true;
|
||||
private bool _flagOutOfDateModels = true;
|
||||
|
||||
/// <summary>
|
||||
@@ -78,4 +79,16 @@ public class ModelsBuilderSettings
|
||||
/// <remarks>0 means minimal (safe on live site), anything else means more and more details (maybe not safe).</remarks>
|
||||
[DefaultValue(StaticDebugLevel)]
|
||||
public int DebugLevel { get; set; } = StaticDebugLevel;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the version number should be included in generated models.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// By default this is written to the <see cref="System.CodeDom.Compiler.GeneratedCodeAttribute"/> output in
|
||||
/// generated code for each property of the model. This can be useful for debugging purposes but isn't essential,
|
||||
/// and it has the causes the generated code to change every time Umbraco is upgraded. In turn, this leads
|
||||
/// to unnecessary code file changes that need to be checked into source control. Default is <c>true</c>.
|
||||
/// </remarks>
|
||||
[DefaultValue(StaticIncludeVersionNumberInGeneratedModels)]
|
||||
public bool IncludeVersionNumberInGeneratedModels { get; set; } = StaticIncludeVersionNumberInGeneratedModels;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ public class RequestHandlerSettings
|
||||
{
|
||||
internal const bool StaticAddTrailingSlash = true;
|
||||
internal const string StaticConvertUrlsToAscii = "try";
|
||||
internal const string StaticConvertFileNamesToAscii = "false";
|
||||
internal const bool StaticEnableDefaultCharReplacements = true;
|
||||
|
||||
internal static readonly CharItem[] DefaultCharCollection =
|
||||
@@ -73,6 +74,22 @@ public class RequestHandlerSettings
|
||||
/// </summary>
|
||||
public bool ShouldTryConvertUrlsToAscii => ConvertUrlsToAscii.InvariantEquals("try");
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to convert file names to ASCII (valid values: "true", "try" or "false").
|
||||
/// </summary>
|
||||
[DefaultValue(StaticConvertFileNamesToAscii)]
|
||||
public string ConvertFileNamesToAscii { get; set; } = StaticConvertFileNamesToAscii;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether URLs should be converted to ASCII.
|
||||
/// </summary>
|
||||
public bool ShouldConvertFileNamesToAscii => ConvertFileNamesToAscii.InvariantEquals("true");
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether URLs should be tried to be converted to ASCII.
|
||||
/// </summary>
|
||||
public bool ShouldTryConvertFileNamesToAscii => ConvertFileNamesToAscii.InvariantEquals("try");
|
||||
|
||||
/// <summary>
|
||||
/// Disable all default character replacements
|
||||
/// </summary>
|
||||
|
||||
@@ -20,6 +20,8 @@ public class SecuritySettings
|
||||
internal const bool StaticAllowEditInvariantFromNonDefault = false;
|
||||
internal const bool StaticAllowConcurrentLogins = false;
|
||||
internal const string StaticAuthCookieName = "UMB_UCONTEXT";
|
||||
internal const bool StaticUsernameIsEmail = true;
|
||||
internal const bool StaticMemberRequireUniqueEmail = true;
|
||||
|
||||
internal const string StaticAllowedUserNameCharacters =
|
||||
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+\\";
|
||||
@@ -64,7 +66,14 @@ public class SecuritySettings
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the user's email address is to be considered as their username.
|
||||
/// </summary>
|
||||
public bool UsernameIsEmail { get; set; } = true;
|
||||
[DefaultValue(StaticUsernameIsEmail)]
|
||||
public bool UsernameIsEmail { get; set; } = StaticUsernameIsEmail;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the member's email address must be unique.
|
||||
/// </summary>
|
||||
[DefaultValue(StaticMemberRequireUniqueEmail)]
|
||||
public bool MemberRequireUniqueEmail { get; set; } = StaticMemberRequireUniqueEmail;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the set of allowed characters for a username
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
public interface ICurrentMemberClaimsProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Retrieves the claims for the currently logged in member.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is used by the OIDC user info endpoint to supply "current user" info.
|
||||
/// </remarks>
|
||||
Task<Dictionary<string, object>> GetClaimsAsync();
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
namespace Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public class IncludeInApiVersionAttribute : Attribute
|
||||
{
|
||||
public int? MinVersion { get; }
|
||||
|
||||
public int? MaxVersion { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="IncludeInApiVersionAttribute"/> class.
|
||||
/// Specifies that the property should be included in the API response if the API version falls within the specified bounds.
|
||||
/// </summary>
|
||||
/// <param name="minVersion">The minimum API version (inclusive) for which the property should be included.</param>
|
||||
/// <param name="maxVersion">The maximum API version (inclusive) for which the property should be included.</param>
|
||||
public IncludeInApiVersionAttribute(int minVersion = -1, int maxVersion = -1)
|
||||
{
|
||||
MinVersion = minVersion >= 0 ? minVersion : null;
|
||||
MaxVersion = maxVersion >= 0 ? maxVersion : null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
public class NoopCurrentMemberClaimsProvider : ICurrentMemberClaimsProvider
|
||||
{
|
||||
public Task<Dictionary<string, object>> GetClaimsAsync() => Task.FromResult(new Dictionary<string, object>());
|
||||
}
|
||||
@@ -26,8 +26,6 @@ using Umbraco.Cms.Core.Notifications;
|
||||
using Umbraco.Cms.Core.Packaging;
|
||||
using Umbraco.Cms.Core.Persistence.Repositories;
|
||||
using Umbraco.Cms.Core.PropertyEditors;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
using Umbraco.Cms.Core.PublishedCache.Internal;
|
||||
using Umbraco.Cms.Core.Routing;
|
||||
using Umbraco.Cms.Core.Runtime;
|
||||
using Umbraco.Cms.Core.Scoping;
|
||||
@@ -36,7 +34,10 @@ using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.ContentTypeEditing;
|
||||
using Umbraco.Cms.Core.DynamicRoot;
|
||||
using Umbraco.Cms.Core.Preview;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
using Umbraco.Cms.Core.PublishedCache.Internal;
|
||||
using Umbraco.Cms.Core.Security.Authorization;
|
||||
using Umbraco.Cms.Core.Serialization;
|
||||
using Umbraco.Cms.Core.Services.FileSystem;
|
||||
using Umbraco.Cms.Core.Services.ImportExport;
|
||||
using Umbraco.Cms.Core.Services.Querying.RecycleBin;
|
||||
@@ -373,7 +374,10 @@ namespace Umbraco.Cms.Core.DependencyInjection
|
||||
Services.AddUnique<IWebhookService, WebhookService>();
|
||||
Services.AddUnique<IWebhookLogService, WebhookLogService>();
|
||||
Services.AddUnique<IWebhookLogFactory, WebhookLogFactory>();
|
||||
Services.AddUnique<IWebhookRequestService, WebhookRequestService>();
|
||||
Services.AddUnique<IWebhookRequestService>(factory => new WebhookRequestService(
|
||||
factory.GetRequiredService<ICoreScopeProvider>(),
|
||||
factory.GetRequiredService<IWebhookRequestRepository>(),
|
||||
factory.GetRequiredService<IWebhookJsonSerializer>()));
|
||||
|
||||
// Data type configuration cache
|
||||
Services.AddUnique<IDataTypeConfigurationCache, DataTypeConfigurationCache>();
|
||||
|
||||
@@ -27,7 +27,9 @@ public class RootDynamicRootOriginFinder : IDynamicRootOriginFinder
|
||||
return null;
|
||||
}
|
||||
|
||||
var entity = _entityService.Get(query.Context.ParentKey);
|
||||
// when creating new content, CurrentKey will be null - fallback to using ParentKey
|
||||
Guid entityKey = query.Context.CurrentKey ?? query.Context.ParentKey;
|
||||
var entity = _entityService.Get(entityKey);
|
||||
|
||||
if (entity is null || _allowedObjectTypes.Contains(entity.NodeObjectType) is false)
|
||||
{
|
||||
|
||||
@@ -20,12 +20,14 @@ public class SiteDynamicRootOriginFinder : RootDynamicRootOriginFinder
|
||||
|
||||
public override Guid? FindOriginKey(DynamicRootNodeQuery query)
|
||||
{
|
||||
if (query.OriginAlias != SupportedOriginType || query.Context.CurrentKey.HasValue is false)
|
||||
if (query.OriginAlias != SupportedOriginType)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
IEntitySlim? entity = _entityService.Get(query.Context.CurrentKey.Value);
|
||||
// when creating new content, CurrentKey will be null - fallback to using ParentKey
|
||||
Guid entityKey = query.Context.CurrentKey ?? query.Context.ParentKey;
|
||||
IEntitySlim? entity = _entityService.Get(entityKey);
|
||||
if (entity is null || entity.NodeObjectType != Constants.ObjectTypes.Document)
|
||||
{
|
||||
return null;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,7 @@
|
||||
@using Umbraco.Extensions
|
||||
|
||||
@{
|
||||
var isLoggedIn = Context.User?.Identity?.IsAuthenticated ?? false;
|
||||
var isLoggedIn = Context.User.GetMemberIdentity()?.IsAuthenticated ?? false;
|
||||
var logoutModel = new PostRedirectModel();
|
||||
// You can modify this to redirect to a different URL instead of the current one
|
||||
logoutModel.RedirectUrl = null;
|
||||
@@ -15,7 +15,7 @@
|
||||
{
|
||||
<div class="login-status">
|
||||
|
||||
<p>Welcome back <strong>@Context?.User?.Identity?.Name</strong>!</p>
|
||||
<p>Welcome back <strong>@Context.User?.GetMemberIdentity()?.Name</strong>!</p>
|
||||
|
||||
@using (Html.BeginUmbracoForm<UmbLoginStatusController>("HandleLogout", new { RedirectUrl = logoutModel.RedirectUrl }))
|
||||
{
|
||||
|
||||
@@ -384,7 +384,10 @@ public static class ClaimsIdentityExtensions
|
||||
var firstValue = identity.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
if (firstValue is not null)
|
||||
{
|
||||
return int.Parse(firstValue, CultureInfo.InvariantCulture);
|
||||
if (int.TryParse(firstValue, CultureInfo.InvariantCulture, out var id))
|
||||
{
|
||||
return id;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// See LICENSE for more details.
|
||||
|
||||
using System.ComponentModel;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.Security.Cryptography;
|
||||
@@ -83,11 +84,11 @@ public static class StringExtensions
|
||||
return fileName;
|
||||
}
|
||||
|
||||
var spanFileName = fileName.AsSpan();
|
||||
ReadOnlySpan<char> spanFileName = fileName.AsSpan();
|
||||
var lastIndex = spanFileName.LastIndexOf('.');
|
||||
if (lastIndex > 0)
|
||||
{
|
||||
var ext = spanFileName[lastIndex..];
|
||||
ReadOnlySpan<char> ext = spanFileName[lastIndex..];
|
||||
|
||||
// file extensions cannot contain whitespace
|
||||
if (ext.Contains(' '))
|
||||
@@ -377,7 +378,7 @@ public static class StringExtensions
|
||||
|
||||
while (value.StartsWith(forRemoving, StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
value = value.Substring(forRemoving.Length);
|
||||
value = value[forRemoving.Length..];
|
||||
}
|
||||
|
||||
return value;
|
||||
@@ -497,7 +498,7 @@ public static class StringExtensions
|
||||
|
||||
var convertToHex = input.ConvertToHex();
|
||||
var hexLength = convertToHex.Length < 32 ? convertToHex.Length : 32;
|
||||
var hex = convertToHex.Substring(0, hexLength).PadLeft(32, '0');
|
||||
var hex = convertToHex[..hexLength].PadLeft(32, '0');
|
||||
Guid output = Guid.Empty;
|
||||
return Guid.TryParse(hex, out output) ? output : Guid.Empty;
|
||||
}
|
||||
@@ -528,8 +529,8 @@ public static class StringExtensions
|
||||
var strValue = string.Empty;
|
||||
while (hexValue.Length > 0)
|
||||
{
|
||||
strValue += Convert.ToChar(Convert.ToUInt32(hexValue.Substring(0, 2), 16)).ToString();
|
||||
hexValue = hexValue.Substring(2, hexValue.Length - 2);
|
||||
strValue += Convert.ToChar(Convert.ToUInt32(hexValue[..2], 16)).ToString();
|
||||
hexValue = hexValue[2..];
|
||||
}
|
||||
|
||||
return strValue;
|
||||
@@ -871,7 +872,7 @@ public static class StringExtensions
|
||||
return truncatedString;
|
||||
}
|
||||
|
||||
truncatedString = text.Substring(0, strLength);
|
||||
truncatedString = text[..strLength];
|
||||
truncatedString = truncatedString.TrimEnd();
|
||||
truncatedString += suffix;
|
||||
|
||||
@@ -914,7 +915,7 @@ public static class StringExtensions
|
||||
public static string ToFirstUpper(this string input) =>
|
||||
string.IsNullOrWhiteSpace(input)
|
||||
? input
|
||||
: input.Substring(0, 1).ToUpper() + input.Substring(1);
|
||||
: input[..1].ToUpper() + input[1..];
|
||||
|
||||
/// <summary>
|
||||
/// Returns a copy of the string with the first character converted to lowercase.
|
||||
@@ -924,7 +925,7 @@ public static class StringExtensions
|
||||
public static string ToFirstLower(this string input) =>
|
||||
string.IsNullOrWhiteSpace(input)
|
||||
? input
|
||||
: input.Substring(0, 1).ToLower() + input.Substring(1);
|
||||
: input[..1].ToLower() + input[1..];
|
||||
|
||||
/// <summary>
|
||||
/// Returns a copy of the string with the first character converted to uppercase using the casing rules of the
|
||||
@@ -936,7 +937,7 @@ public static class StringExtensions
|
||||
public static string ToFirstUpper(this string input, CultureInfo culture) =>
|
||||
string.IsNullOrWhiteSpace(input)
|
||||
? input
|
||||
: input.Substring(0, 1).ToUpper(culture) + input.Substring(1);
|
||||
: input[..1].ToUpper(culture) + input[1..];
|
||||
|
||||
/// <summary>
|
||||
/// Returns a copy of the string with the first character converted to lowercase using the casing rules of the
|
||||
@@ -948,7 +949,7 @@ public static class StringExtensions
|
||||
public static string ToFirstLower(this string input, CultureInfo culture) =>
|
||||
string.IsNullOrWhiteSpace(input)
|
||||
? input
|
||||
: input.Substring(0, 1).ToLower(culture) + input.Substring(1);
|
||||
: input[..1].ToLower(culture) + input[1..];
|
||||
|
||||
/// <summary>
|
||||
/// Returns a copy of the string with the first character converted to uppercase using the casing rules of the
|
||||
@@ -959,7 +960,7 @@ public static class StringExtensions
|
||||
public static string ToFirstUpperInvariant(this string input) =>
|
||||
string.IsNullOrWhiteSpace(input)
|
||||
? input
|
||||
: input.Substring(0, 1).ToUpperInvariant() + input.Substring(1);
|
||||
: input[..1].ToUpperInvariant() + input[1..];
|
||||
|
||||
/// <summary>
|
||||
/// Returns a copy of the string with the first character converted to lowercase using the casing rules of the
|
||||
@@ -970,7 +971,7 @@ public static class StringExtensions
|
||||
public static string ToFirstLowerInvariant(this string input) =>
|
||||
string.IsNullOrWhiteSpace(input)
|
||||
? input
|
||||
: input.Substring(0, 1).ToLowerInvariant() + input.Substring(1);
|
||||
: input[..1].ToLowerInvariant() + input[1..];
|
||||
|
||||
/// <summary>
|
||||
/// Returns a new string in which all occurrences of specified strings are replaced by other specified strings.
|
||||
@@ -1355,7 +1356,7 @@ public static class StringExtensions
|
||||
return a;
|
||||
}
|
||||
|
||||
return char.ToLowerInvariant(a[0]) + a.Substring(1);
|
||||
return char.ToLowerInvariant(a[0]) + a[1..];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -1558,6 +1559,14 @@ public static class StringExtensions
|
||||
yield return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether a string is a valid email address.
|
||||
/// </summary>
|
||||
/// <param name="email">The string check</param>
|
||||
/// <returns>Returns a bool indicating whether the string is an email address.</returns>
|
||||
public static bool IsEmail(this string? email) =>
|
||||
string.IsNullOrWhiteSpace(email) is false && new EmailAddressAttribute().IsValid(email);
|
||||
|
||||
// having benchmarked various solutions (incl. for/foreach, split and LINQ based ones),
|
||||
// this is by far the fastest way to find string needles in a string haystack
|
||||
public static int CountOccurrences(this string haystack, string needle)
|
||||
|
||||
@@ -99,11 +99,7 @@ public class HashGenerator : DisposableObjectSlim
|
||||
var hashType = CryptoConfig.AllowOnlyFipsAlgorithms ? "SHA1" : "MD5";
|
||||
|
||||
// create an instance of the correct hashing provider based on the type passed in
|
||||
var hasher = HashAlgorithm.Create(hashType);
|
||||
if (hasher == null)
|
||||
{
|
||||
throw new InvalidOperationException("No hashing type found by name " + hashType);
|
||||
}
|
||||
HashAlgorithm hasher = HashAlgorithm.Create(hashType) ?? throw new InvalidOperationException("No hashing type found by name " + hashType);
|
||||
|
||||
using (hasher)
|
||||
{
|
||||
|
||||
@@ -15,7 +15,9 @@ public class ViewHelper : IViewHelper
|
||||
{
|
||||
_viewFileSystem = fileSystems.MvcViewsFileSystem ?? throw new ArgumentNullException(nameof(fileSystems));
|
||||
_defaultViewContentProvider = defaultViewContentProvider ?? throw new ArgumentNullException(nameof(defaultViewContentProvider));
|
||||
}[Obsolete("Inject IDefaultViewContentProvider instead")]
|
||||
}
|
||||
|
||||
[Obsolete("Inject IDefaultViewContentProvider instead")]
|
||||
public static string GetDefaultFileContent(string? layoutPageAlias = null, string? modelClassName = null, string? modelNamespace = null, string? modelNamespaceAlias = null)
|
||||
{
|
||||
IDefaultViewContentProvider viewContentProvider =
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
namespace Umbraco.Cms.Core.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Describes the options available with publishing a content branch for force publishing.
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum PublishBranchFilter
|
||||
{
|
||||
/// <summary>
|
||||
/// The default behavior is to publish only the published content that has changed.
|
||||
/// </summary>
|
||||
Default = 0,
|
||||
|
||||
/// <summary>
|
||||
/// For publishing a branch, publish all changed content, including content that is not published.
|
||||
/// </summary>
|
||||
IncludeUnpublished = 1,
|
||||
|
||||
/// <summary>
|
||||
/// For publishing a branch, force republishing of all published content, including content that has not changed.
|
||||
/// </summary>
|
||||
ForceRepublish = 2,
|
||||
|
||||
/// <summary>
|
||||
/// For publishing a branch, publish all content, including content that is not published and content that has not changed.
|
||||
/// </summary>
|
||||
All = IncludeUnpublished | ForceRepublish,
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
namespace Umbraco.Cms.Core.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies options for publishing notifcations when saving.
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum PublishNotificationSaveOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Do not publish any notifications.
|
||||
/// </summary>
|
||||
None = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Only publish the saving notification.
|
||||
/// </summary>
|
||||
Saving = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Only publish the saved notification.
|
||||
/// </summary>
|
||||
Saved = 2,
|
||||
|
||||
/// <summary>
|
||||
/// Publish all the notifications.
|
||||
/// </summary>
|
||||
All = Saving | Saved,
|
||||
}
|
||||
@@ -32,4 +32,14 @@ public class ContentTreeChangeNotification : TreeChangeNotification<IContent>
|
||||
: base(new TreeChange<IContent>(target, changeTypes), messages)
|
||||
{
|
||||
}
|
||||
|
||||
public ContentTreeChangeNotification(
|
||||
IContent target,
|
||||
TreeChangeTypes changeTypes,
|
||||
IEnumerable<string>? publishedCultures,
|
||||
IEnumerable<string>? unpublishedCultures,
|
||||
EventMessages messages)
|
||||
: base(new TreeChange<IContent>(target, changeTypes, publishedCultures, unpublishedCultures), messages)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,8 @@ public abstract class ConfigurationEditor<TConfiguration> : ConfigurationEditor
|
||||
}
|
||||
}
|
||||
|
||||
protected TConfiguration? AsConfigurationObject(IDictionary<string, object> configuration,
|
||||
protected TConfiguration? AsConfigurationObject(
|
||||
IDictionary<string, object> configuration,
|
||||
IConfigurationEditorJsonSerializer configurationEditorJsonSerializer) =>
|
||||
ToConfigurationObject(configuration, configurationEditorJsonSerializer) is TConfiguration configurationObject
|
||||
? configurationObject
|
||||
|
||||
@@ -37,6 +37,12 @@ public class DecimalValueConverter : PropertyValueConverterBase
|
||||
return Convert.ToDecimal(sourceDouble);
|
||||
}
|
||||
|
||||
// is it an integer?
|
||||
if (source is int sourceInteger)
|
||||
{
|
||||
return Convert.ToDecimal(sourceInteger);
|
||||
}
|
||||
|
||||
// is it a string?
|
||||
if (source is string sourceString)
|
||||
{
|
||||
|
||||
@@ -33,7 +33,7 @@ public interface ITagQuery
|
||||
/// <summary>
|
||||
/// Gets all document tags.
|
||||
/// </summary>
|
||||
/// /// <remarks>
|
||||
/// <remarks>
|
||||
/// If no culture is specified, it retrieves tags with an invariant culture.
|
||||
/// If a culture is specified, it only retrieves tags for that culture.
|
||||
/// Use "*" to retrieve tags for all cultures.
|
||||
|
||||
@@ -111,6 +111,12 @@ public sealed class UriUtility
|
||||
if (path != "/")
|
||||
{
|
||||
path = path.TrimEnd(Constants.CharArrays.ForwardSlash);
|
||||
|
||||
// perform fallback to root if the path was all slashes (i.e. https://some.where//////)
|
||||
if (path == string.Empty)
|
||||
{
|
||||
path = "/";
|
||||
}
|
||||
}
|
||||
|
||||
return uri.Rewrite(path);
|
||||
|
||||
@@ -40,7 +40,7 @@ namespace Umbraco.Cms.Core.Semver
|
||||
public sealed class SemVersion : IComparable<SemVersion>, IComparable, ISerializable
|
||||
#endif
|
||||
{
|
||||
private static Regex parseEx =
|
||||
private static readonly Regex _parseEx =
|
||||
new(
|
||||
@"^(?<major>\d+)" +
|
||||
@"(\.(?<minor>\d+))?" +
|
||||
@@ -137,7 +137,7 @@ namespace Umbraco.Cms.Core.Semver
|
||||
/// <exception cref="System.InvalidOperationException">When a invalid version string is passed.</exception>
|
||||
public static SemVersion Parse(string version, bool strict = false)
|
||||
{
|
||||
Match match = parseEx.Match(version);
|
||||
Match match = _parseEx.Match(version);
|
||||
if (!match.Success)
|
||||
{
|
||||
throw new ArgumentException("Invalid version.", "version");
|
||||
@@ -218,9 +218,9 @@ namespace Umbraco.Cms.Core.Semver
|
||||
/// <returns>If versionA is equal to versionB <c>true</c>, else <c>false</c>.</returns>
|
||||
public static bool Equals(SemVersion versionA, SemVersion versionB)
|
||||
{
|
||||
if (ReferenceEquals(versionA, null))
|
||||
if (versionA is null)
|
||||
{
|
||||
return ReferenceEquals(versionB, null);
|
||||
return versionB is null;
|
||||
}
|
||||
|
||||
return versionA.Equals(versionB);
|
||||
@@ -237,9 +237,9 @@ namespace Umbraco.Cms.Core.Semver
|
||||
/// </returns>
|
||||
public static int Compare(SemVersion versionA, SemVersion versionB)
|
||||
{
|
||||
if (ReferenceEquals(versionA, null))
|
||||
if (versionA is null)
|
||||
{
|
||||
return ReferenceEquals(versionB, null) ? 0 : -1;
|
||||
return versionB is null ? 0 : -1;
|
||||
}
|
||||
|
||||
return versionA.CompareTo(versionB);
|
||||
@@ -303,10 +303,10 @@ namespace Umbraco.Cms.Core.Semver
|
||||
public string Build { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns a <see cref="System.String" /> that represents this instance.
|
||||
/// Returns a <see cref="string" /> that represents this instance.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// A <see cref="System.String" /> that represents this instance.
|
||||
/// A <see cref="string" /> that represents this instance.
|
||||
/// </returns>
|
||||
public override string ToString()
|
||||
{
|
||||
@@ -354,7 +354,7 @@ namespace Umbraco.Cms.Core.Semver
|
||||
/// </returns>
|
||||
public int CompareTo(SemVersion? other)
|
||||
{
|
||||
if (ReferenceEquals(other, null))
|
||||
if (other is null)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
@@ -389,7 +389,7 @@ namespace Umbraco.Cms.Core.Semver
|
||||
/// </returns>
|
||||
public int CompareByPrecedence(SemVersion other)
|
||||
{
|
||||
if (ReferenceEquals(other, null))
|
||||
if (other is null)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
@@ -443,9 +443,8 @@ namespace Umbraco.Cms.Core.Semver
|
||||
{
|
||||
var ac = aComps[i];
|
||||
var bc = bComps[i];
|
||||
int anum, bnum;
|
||||
var isanum = int.TryParse(ac, out anum);
|
||||
var isbnum = int.TryParse(bc, out bnum);
|
||||
var isanum = int.TryParse(ac, out int anum);
|
||||
var isbnum = int.TryParse(bc, out int bnum);
|
||||
int r;
|
||||
if (isanum && isbnum)
|
||||
{
|
||||
@@ -479,15 +478,15 @@ namespace Umbraco.Cms.Core.Semver
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified <see cref="System.Object" /> is equal to this instance.
|
||||
/// Determines whether the specified <see cref="object" /> is equal to this instance.
|
||||
/// </summary>
|
||||
/// <param name="obj">The <see cref="System.Object" /> to compare with this instance.</param>
|
||||
/// <param name="obj">The <see cref="object" /> to compare with this instance.</param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if the specified <see cref="System.Object" /> is equal to this instance; otherwise, <c>false</c>.
|
||||
/// <c>true</c> if the specified <see cref="object" /> is equal to this instance; otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
if (ReferenceEquals(obj, null))
|
||||
if (obj is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
namespace Umbraco.Cms.Core.Serialization;
|
||||
|
||||
public interface IWebhookJsonSerializer : IJsonSerializer
|
||||
{ }
|
||||
@@ -8,10 +8,22 @@ public class TreeChange<TItem>
|
||||
ChangeTypes = changeTypes;
|
||||
}
|
||||
|
||||
public TreeChange(TItem changedItem, TreeChangeTypes changeTypes, IEnumerable<string>? publishedCultures, IEnumerable<string>? unpublishedCultures)
|
||||
{
|
||||
Item = changedItem;
|
||||
ChangeTypes = changeTypes;
|
||||
PublishedCultures = publishedCultures;
|
||||
UnpublishedCultures = unpublishedCultures;
|
||||
}
|
||||
|
||||
public TItem Item { get; }
|
||||
|
||||
public TreeChangeTypes ChangeTypes { get; }
|
||||
|
||||
public IEnumerable<string>? PublishedCultures { get; }
|
||||
|
||||
public IEnumerable<string>? UnpublishedCultures { get; }
|
||||
|
||||
public EventArgs ToEventArgs() => new EventArgs(this);
|
||||
|
||||
public class EventArgs : System.EventArgs
|
||||
|
||||
@@ -354,11 +354,9 @@ public class ContentService : RepositoryService, IContentService
|
||||
throw new ArgumentNullException(nameof(parent));
|
||||
}
|
||||
|
||||
IContentType contentType = GetContentType(contentTypeAlias);
|
||||
if (contentType == null)
|
||||
{
|
||||
throw new ArgumentException("No content type with that alias.", nameof(contentTypeAlias)); // causes rollback
|
||||
}
|
||||
IContentType contentType = GetContentType(contentTypeAlias)
|
||||
// causes rollback
|
||||
?? throw new ArgumentException("No content type with that alias.", nameof(contentTypeAlias));
|
||||
|
||||
var content = new Content(name, parent, contentType, userId);
|
||||
|
||||
@@ -382,11 +380,11 @@ public class ContentService : RepositoryService, IContentService
|
||||
// locking the content tree secures content types too
|
||||
scope.WriteLock(Constants.Locks.ContentTree);
|
||||
|
||||
IContentType contentType = GetContentType(contentTypeAlias); // + locks
|
||||
if (contentType == null)
|
||||
{
|
||||
throw new ArgumentException("No content type with that alias.", nameof(contentTypeAlias)); // causes rollback
|
||||
}
|
||||
IContentType contentType = GetContentType(contentTypeAlias)
|
||||
// + locks
|
||||
??
|
||||
// causes rollback
|
||||
throw new ArgumentException("No content type with that alias.", nameof(contentTypeAlias));
|
||||
|
||||
IContent? parent = parentId > 0 ? GetById(parentId) : null; // + locks
|
||||
if (parentId > 0 && parent == null)
|
||||
@@ -428,11 +426,11 @@ public class ContentService : RepositoryService, IContentService
|
||||
// locking the content tree secures content types too
|
||||
scope.WriteLock(Constants.Locks.ContentTree);
|
||||
|
||||
IContentType contentType = GetContentType(contentTypeAlias); // + locks
|
||||
if (contentType == null)
|
||||
{
|
||||
throw new ArgumentException("No content type with that alias.", nameof(contentTypeAlias)); // causes rollback
|
||||
}
|
||||
IContentType contentType = GetContentType(contentTypeAlias)
|
||||
// + locks
|
||||
??
|
||||
// causes rollback
|
||||
throw new ArgumentException("No content type with that alias.", nameof(contentTypeAlias));
|
||||
|
||||
var content = new Content(name, parent, contentType, userId);
|
||||
|
||||
@@ -1554,7 +1552,12 @@ public class ContentService : RepositoryService, IContentService
|
||||
// events and audit
|
||||
scope.Notifications.Publish(
|
||||
new ContentUnpublishedNotification(content, eventMessages).WithState(notificationState));
|
||||
scope.Notifications.Publish(new ContentTreeChangeNotification(content, TreeChangeTypes.RefreshBranch, eventMessages));
|
||||
scope.Notifications.Publish(new ContentTreeChangeNotification(
|
||||
content,
|
||||
TreeChangeTypes.RefreshBranch,
|
||||
variesByCulture ? culturesPublishing.IsCollectionEmpty() ? null : culturesPublishing : null,
|
||||
variesByCulture ? culturesUnpublishing.IsCollectionEmpty() ? null : culturesUnpublishing : ["*"],
|
||||
eventMessages));
|
||||
|
||||
if (culturesUnpublishing != null)
|
||||
{
|
||||
@@ -1613,7 +1616,12 @@ public class ContentService : RepositoryService, IContentService
|
||||
if (!branchOne)
|
||||
{
|
||||
scope.Notifications.Publish(
|
||||
new ContentTreeChangeNotification(content, changeType, eventMessages));
|
||||
new ContentTreeChangeNotification(
|
||||
content,
|
||||
changeType,
|
||||
variesByCulture ? culturesPublishing.IsCollectionEmpty() ? null : culturesPublishing : ["*"],
|
||||
variesByCulture ? culturesUnpublishing.IsCollectionEmpty() ? null : culturesUnpublishing : null,
|
||||
eventMessages));
|
||||
scope.Notifications.Publish(
|
||||
new ContentPublishedNotification(content, eventMessages).WithState(notificationState));
|
||||
}
|
||||
@@ -1997,7 +2005,7 @@ public class ContentService : RepositoryService, IContentService
|
||||
{
|
||||
// note: EditedValue and PublishedValue are objects here, so it is important to .Equals()
|
||||
// and not to == them, else we would be comparing references, and that is a bad thing
|
||||
cultures = cultures ?? Array.Empty<string>();
|
||||
cultures ??= Array.Empty<string>();
|
||||
|
||||
if (content.ContentType.VariesByCulture() is false && cultures.Length == 0)
|
||||
{
|
||||
@@ -2060,7 +2068,6 @@ public class ContentService : RepositoryService, IContentService
|
||||
var results = new List<PublishResult>();
|
||||
var publishedDocuments = new List<IContent>();
|
||||
|
||||
IDictionary<string, object?>? initialNotificationState = null;
|
||||
using (ICoreScope scope = ScopeProvider.CreateCoreScope())
|
||||
{
|
||||
scope.WriteLock(Constants.Locks.ContentTree);
|
||||
@@ -2079,7 +2086,8 @@ public class ContentService : RepositoryService, IContentService
|
||||
}
|
||||
|
||||
// deal with the branch root - if it fails, abort
|
||||
PublishResult? result = PublishBranchItem(scope, document, shouldPublish, publishCultures, true, publishedDocuments, eventMessages, userId, allLangs, out initialNotificationState);
|
||||
HashSet<string>? culturesToPublish = shouldPublish(document);
|
||||
PublishResult? result = PublishBranchItem(scope, document, culturesToPublish, publishCultures, true, publishedDocuments, eventMessages, userId, allLangs, out IDictionary<string, object?>? notificationState);
|
||||
if (result != null)
|
||||
{
|
||||
results.Add(result);
|
||||
@@ -2089,6 +2097,8 @@ public class ContentService : RepositoryService, IContentService
|
||||
}
|
||||
}
|
||||
|
||||
HashSet<string> culturesPublished = culturesToPublish ?? [];
|
||||
|
||||
// deal with descendants
|
||||
// if one fails, abort its branch
|
||||
var exclude = new HashSet<int>();
|
||||
@@ -2114,12 +2124,14 @@ public class ContentService : RepositoryService, IContentService
|
||||
}
|
||||
|
||||
// no need to check path here, parent has to be published here
|
||||
result = PublishBranchItem(scope, d, shouldPublish, publishCultures, false, publishedDocuments, eventMessages, userId, allLangs, out _);
|
||||
culturesToPublish = shouldPublish(d);
|
||||
result = PublishBranchItem(scope, d, culturesToPublish, publishCultures, false, publishedDocuments, eventMessages, userId, allLangs, out _);
|
||||
if (result != null)
|
||||
{
|
||||
results.Add(result);
|
||||
if (result.Success)
|
||||
{
|
||||
culturesPublished.UnionWith(culturesToPublish ?? []);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -2136,9 +2148,15 @@ public class ContentService : RepositoryService, IContentService
|
||||
|
||||
// trigger events for the entire branch
|
||||
// (SaveAndPublishBranchOne does *not* do it)
|
||||
var variesByCulture = document.ContentType.VariesByCulture();
|
||||
scope.Notifications.Publish(
|
||||
new ContentTreeChangeNotification(document, TreeChangeTypes.RefreshBranch, eventMessages));
|
||||
scope.Notifications.Publish(new ContentPublishedNotification(publishedDocuments, eventMessages, true).WithState(initialNotificationState));
|
||||
new ContentTreeChangeNotification(
|
||||
document,
|
||||
TreeChangeTypes.RefreshBranch,
|
||||
variesByCulture ? culturesPublished.IsCollectionEmpty() ? null : culturesPublished : ["*"],
|
||||
null,
|
||||
eventMessages));
|
||||
scope.Notifications.Publish(new ContentPublishedNotification(publishedDocuments, eventMessages).WithState(notificationState));
|
||||
|
||||
scope.Complete();
|
||||
}
|
||||
@@ -2152,7 +2170,7 @@ public class ContentService : RepositoryService, IContentService
|
||||
private PublishResult? PublishBranchItem(
|
||||
ICoreScope scope,
|
||||
IContent document,
|
||||
Func<IContent, HashSet<string>?> shouldPublish,
|
||||
HashSet<string>? culturesToPublish,
|
||||
Func<IContent, HashSet<string>, IReadOnlyCollection<ILanguage>,
|
||||
bool> publishCultures,
|
||||
bool isRoot,
|
||||
@@ -2162,9 +2180,7 @@ public class ContentService : RepositoryService, IContentService
|
||||
IReadOnlyCollection<ILanguage> allLangs,
|
||||
out IDictionary<string, object?>? initialNotificationState)
|
||||
{
|
||||
HashSet<string>? culturesToPublish = shouldPublish(document);
|
||||
|
||||
initialNotificationState = null;
|
||||
initialNotificationState = new Dictionary<string, object?>();
|
||||
|
||||
// we need to guard against unsaved changes before proceeding; the document will be saved, but we're not firing any saved notifications
|
||||
if (HasUnsavedChanges(document))
|
||||
@@ -2701,6 +2717,13 @@ public class ContentService : RepositoryService, IContentService
|
||||
GetPagedDescendants(content.Id, page++, pageSize, out total);
|
||||
foreach (IContent descendant in descendants)
|
||||
{
|
||||
// when copying a branch into itself, the copy of a root would be seen as a descendant
|
||||
// and would be copied again => filter it out.
|
||||
if (descendant.Id == copy.Id)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// if parent has not been copied, skip, else gets its copy id
|
||||
if (idmap.TryGetValue(descendant.ParentId, out parentId) == false)
|
||||
{
|
||||
@@ -2726,6 +2749,9 @@ public class ContentService : RepositoryService, IContentService
|
||||
descendantCopy.CreatorId = userId;
|
||||
descendantCopy.WriterId = userId;
|
||||
|
||||
// since the repository relies on the dirty state to figure out whether it needs to update the sort order, we mark it dirty here
|
||||
descendantCopy.SortOrder = descendantCopy.SortOrder;
|
||||
|
||||
// save and flush (see above)
|
||||
_documentRepository.Save(descendantCopy);
|
||||
|
||||
@@ -3036,6 +3062,7 @@ public class ContentService : RepositoryService, IContentService
|
||||
/// <param name="evtMsgs"></param>
|
||||
/// <param name="culturesPublishing"></param>
|
||||
/// <param name="allLangs"></param>
|
||||
/// <param name="notificationState"></param>
|
||||
/// <returns></returns>
|
||||
private PublishResult StrategyCanPublish(
|
||||
ICoreScope scope,
|
||||
@@ -3466,13 +3493,11 @@ public class ContentService : RepositoryService, IContentService
|
||||
scope.ReadLock(Constants.Locks.ContentTypes);
|
||||
|
||||
IQuery<IContentType> query = Query<IContentType>().Where(x => x.Alias == contentTypeAlias);
|
||||
IContentType? contentType = _contentTypeRepository.Get(query).FirstOrDefault();
|
||||
|
||||
if (contentType == null)
|
||||
{
|
||||
throw new Exception(
|
||||
$"No ContentType matching the passed in Alias: '{contentTypeAlias}' was found"); // causes rollback
|
||||
}
|
||||
IContentType? contentType = _contentTypeRepository.Get(query).FirstOrDefault()
|
||||
??
|
||||
// causes rollback
|
||||
throw new Exception($"No ContentType matching the passed in Alias: '{contentTypeAlias}'" +
|
||||
$" was found");
|
||||
|
||||
return contentType;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
@@ -135,7 +135,10 @@ public class CultureImpactFactory : ICultureImpactFactory
|
||||
if (isDefault)
|
||||
{
|
||||
if (throwOnFail)
|
||||
{
|
||||
throw new InvalidOperationException("The 'all' culture can not be the default culture.");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Umbraco.Cms.Core.Events;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.Membership;
|
||||
using Umbraco.Cms.Core.Persistence.Querying;
|
||||
@@ -315,6 +316,21 @@ public interface IContentService : IContentServiceBase<IContent>
|
||||
/// </summary>
|
||||
OperationResult Move(IContent content, int parentId, int userId = Constants.Security.SuperUserId);
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to move the <see cref="IContent"/> <paramref name="content"/> to under the node with id <paramref name="parentId"/>.
|
||||
/// </summary>
|
||||
/// <param name="content">The <see cref="IContent"/> that shall be moved.</param>
|
||||
/// <param name="parentId">The id of the new parent node.</param>
|
||||
/// <param name="userId">Id of the user attempting to move <paramref name="content"/>.</param>
|
||||
/// <returns>Success if moving succeeded, otherwise Failed.</returns>
|
||||
[Obsolete("Adds return type to Move method. Will be removed in V14, as the original method will be adjusted.")]
|
||||
OperationResult
|
||||
AttemptMove(IContent content, int parentId, int userId = Constants.Security.SuperUserId)
|
||||
{
|
||||
Move(content, parentId, userId);
|
||||
return OperationResult.Succeed(new EventMessages());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies a document.
|
||||
/// </summary>
|
||||
|
||||
@@ -216,6 +216,15 @@ public interface IMemberService : IMembershipMemberService, IContentServiceBase<
|
||||
/// </returns>
|
||||
IMember CreateMemberWithIdentity(string username, string email, string name, IMemberType memberType);
|
||||
|
||||
/// <summary>
|
||||
/// Saves an <see cref="IMembershipUser" />
|
||||
/// </summary>
|
||||
/// <remarks>An <see cref="IMembershipUser" /> can be of type <see cref="IMember" /> or <see cref="IUser" /></remarks>
|
||||
/// <param name="member"><see cref="IMember" /> or <see cref="IUser" /> to Save</param>
|
||||
/// <param name="publishNotificationSaveOptions"> Enum for deciding which notifications to publish.</param>
|
||||
/// <param name="userId">Id of the User saving the Member</param>
|
||||
Attempt<OperationResult?> Save(IMember member, PublishNotificationSaveOptions publishNotificationSaveOptions, int userId = Constants.Security.SuperUserId) => Save(member, userId);
|
||||
|
||||
/// <summary>
|
||||
/// Saves a single <see cref="IMember" /> object
|
||||
/// </summary>
|
||||
@@ -267,6 +276,21 @@ public interface IMemberService : IMembershipMemberService, IContentServiceBase<
|
||||
/// </returns>
|
||||
IMember? GetById(int id);
|
||||
|
||||
/// <summary>
|
||||
/// Get an list of <see cref="IMember"/> for all members with the specified email.
|
||||
/// </summary>
|
||||
/// <param name="email">Email to use for retrieval</param>
|
||||
/// <returns>
|
||||
/// <see cref="IEnumerable{IMember}" />
|
||||
/// </returns>
|
||||
IEnumerable<IMember> GetMembersByEmail(string email)
|
||||
=>
|
||||
// TODO (V16): Remove this default implementation.
|
||||
// The following is very inefficient, but will return the correct data, so probably better than throwing a NotImplementedException
|
||||
// in the default implentation here, for, presumably rare, cases where a custom IMemberService implementation has been registered and
|
||||
// does not override this method.
|
||||
GetAllMembers().Where(x => x.Email.Equals(email));
|
||||
|
||||
/// <summary>
|
||||
/// Gets all Members for the specified MemberType alias
|
||||
/// </summary>
|
||||
|
||||
@@ -51,7 +51,8 @@ public interface IPackageDataInstallation
|
||||
/// <param name="dictionaryItemElementList">Xml to import</param>
|
||||
/// <param name="userId"></param>
|
||||
/// <returns>An enumerable list of dictionary items</returns>
|
||||
IReadOnlyList<IDictionaryItem> ImportDictionaryItems(IEnumerable<XElement> dictionaryItemElementList,
|
||||
IReadOnlyList<IDictionaryItem> ImportDictionaryItems(
|
||||
IEnumerable<XElement> dictionaryItemElementList,
|
||||
int userId);
|
||||
|
||||
IEnumerable<IDictionaryItem> ImportDictionaryItem(XElement dictionaryItemElement, int userId, Guid? parentId);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user