Compare commits
257
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ab0abca99 | ||
|
|
009a999e22 | ||
|
|
d3c1443b14 | ||
|
|
8a9db11422 | ||
|
|
d9fb6df16e | ||
|
|
14ed3348bf | ||
|
|
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 | ||
|
|
abc312c9b4 | ||
|
|
2161edb871 | ||
|
|
e77e9c5691 | ||
|
|
e934a943b5 | ||
|
|
65bb2801b0 | ||
|
|
a0a4af6a0c | ||
|
|
e8d6cded2b | ||
|
|
c65204a146 | ||
|
|
95eb58587b | ||
|
|
86f3033334 | ||
|
|
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 | ||
|
|
0166727eee | ||
|
|
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 |
+25
-23
@@ -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.5" />
|
||||
<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>
|
||||
|
||||
@@ -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.DataType;
|
||||
@@ -7,10 +8,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.DataType;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize(Policy = AuthorizationPolicies.TreeAccessDataTypes)]
|
||||
public class CopyDataTypeController : DataTypeControllerBase
|
||||
{
|
||||
private readonly IDataTypeService _dataTypeService;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Asp.Versioning;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.Factories;
|
||||
@@ -8,10 +9,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.DataType;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize(Policy = AuthorizationPolicies.TreeAccessDataTypes)]
|
||||
public class CreateDataTypeController : DataTypeControllerBase
|
||||
{
|
||||
private readonly IDataTypeService _dataTypeService;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Asp.Versioning;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Core;
|
||||
@@ -6,10 +7,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.DataType;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize(Policy = AuthorizationPolicies.TreeAccessDataTypes)]
|
||||
public class DeleteDataTypeController : DataTypeControllerBase
|
||||
{
|
||||
private readonly IDataTypeService _dataTypeService;
|
||||
|
||||
@@ -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.DataType;
|
||||
@@ -7,10 +8,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.DataType;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize(Policy = AuthorizationPolicies.TreeAccessDataTypes)]
|
||||
public class MoveDataTypeController : DataTypeControllerBase
|
||||
{
|
||||
private readonly IDataTypeService _dataTypeService;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Asp.Versioning;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.Factories;
|
||||
@@ -8,10 +9,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.DataType;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize(Policy = AuthorizationPolicies.TreeAccessDataTypes)]
|
||||
public class UpdateDataTypeController : DataTypeControllerBase
|
||||
{
|
||||
private readonly IDataTypeService _dataTypeService;
|
||||
|
||||
+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.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.DocumentType;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)]
|
||||
public class ExportDocumentTypeController : DocumentTypeControllerBase
|
||||
{
|
||||
private readonly IContentTypeService _contentTypeService;
|
||||
|
||||
+3
-1
@@ -1,17 +1,19 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.DocumentType;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.Membership;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Services.ImportExport;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.Cms.Web.Common.Authorization;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.DocumentType;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)]
|
||||
public class ImportExistingDocumentTypeController : DocumentTypeControllerBase
|
||||
{
|
||||
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
|
||||
|
||||
+3
-1
@@ -1,17 +1,19 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.DocumentType;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.Membership;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Services.ImportExport;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.Cms.Web.Common.Authorization;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.DocumentType;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)]
|
||||
public class ImportNewDocumentTypeController : DocumentTypeControllerBase
|
||||
{
|
||||
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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.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.MediaType;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize(Policy = AuthorizationPolicies.TreeAccessMediaTypes)]
|
||||
public class ExportMediaTypeController : MediaTypeControllerBase
|
||||
{
|
||||
private readonly IMediaTypeService _mediaTypeService;
|
||||
|
||||
+3
-1
@@ -1,17 +1,19 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.MediaType;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.Membership;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Services.ImportExport;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.Cms.Web.Common.Authorization;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.MediaType;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize(Policy = AuthorizationPolicies.TreeAccessMediaTypes)]
|
||||
public class ImportExistingMediaTypeController : MediaTypeControllerBase
|
||||
{
|
||||
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
|
||||
|
||||
+3
-2
@@ -1,18 +1,19 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.Controllers.DocumentType;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.MediaType;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.Membership;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Services.ImportExport;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.Cms.Web.Common.Authorization;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.MediaType;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize(Policy = AuthorizationPolicies.TreeAccessMediaTypes)]
|
||||
public class ImportNewMediaTypeController : MediaTypeControllerBase
|
||||
{
|
||||
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.Controllers.DocumentType;
|
||||
using Umbraco.Cms.Api.Management.Routing;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.MediaType;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.Cms.Web.Common.Authorization;
|
||||
|
||||
+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)]
|
||||
|
||||
@@ -34,6 +34,8 @@ namespace Umbraco.Cms.Api.Management.Controllers.Security;
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
public class BackOfficeController : SecurityControllerBase
|
||||
{
|
||||
private static long? _loginDurationAverage;
|
||||
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
private readonly IBackOfficeSignInManager _backOfficeSignInManager;
|
||||
private readonly IBackOfficeUserManager _backOfficeUserManager;
|
||||
@@ -72,45 +74,65 @@ public class BackOfficeController : SecurityControllerBase
|
||||
[Authorize(Policy = AuthorizationPolicies.DenyLocalLoginIfConfigured)]
|
||||
public async Task<IActionResult> Login(CancellationToken cancellationToken, LoginRequestModel model)
|
||||
{
|
||||
IdentitySignInResult result = await _backOfficeSignInManager.PasswordSignInAsync(
|
||||
model.Username, model.Password, true, true);
|
||||
// Start a timed scope to ensure failed responses return is a consistent time
|
||||
var loginDuration = Math.Max(_loginDurationAverage ?? _securitySettings.Value.UserDefaultFailedLoginDurationInMilliseconds, _securitySettings.Value.UserMinimumFailedLoginDurationInMilliseconds);
|
||||
await using var timedScope = new TimedScope(loginDuration, cancellationToken);
|
||||
|
||||
if (result.IsNotAllowed)
|
||||
IdentitySignInResult result = await _backOfficeSignInManager.PasswordSignInAsync(model.Username, model.Password, true, true);
|
||||
if (result.Succeeded is false)
|
||||
{
|
||||
return StatusCode(StatusCodes.Status403Forbidden, new ProblemDetailsBuilder()
|
||||
.WithTitle("User is not allowed")
|
||||
.WithDetail("The operation is not allowed on the user")
|
||||
.Build());
|
||||
}
|
||||
|
||||
if (result.IsLockedOut)
|
||||
{
|
||||
return StatusCode(StatusCodes.Status403Forbidden, new ProblemDetailsBuilder()
|
||||
.WithTitle("User is locked")
|
||||
.WithDetail("The user is locked, and need to be unlocked before more login attempts can be executed.")
|
||||
.Build());
|
||||
}
|
||||
|
||||
if(result.RequiresTwoFactor)
|
||||
{
|
||||
string? twofactorView = _backOfficeTwoFactorOptions.GetTwoFactorView(model.Username);
|
||||
BackOfficeIdentityUser? attemptingUser = await _backOfficeUserManager.FindByNameAsync(model.Username);
|
||||
IEnumerable<string> enabledProviders = (await _userTwoFactorLoginService.GetProviderNamesAsync(attemptingUser!.Key)).Result.Where(x=>x.IsEnabledOnUser).Select(x=>x.ProviderName);
|
||||
return StatusCode(StatusCodes.Status402PaymentRequired, new RequiresTwoFactorResponseModel()
|
||||
// TODO: The result should include the user and whether the credentials were valid to avoid these additional checks
|
||||
BackOfficeIdentityUser? user = await _backOfficeUserManager.FindByNameAsync(model.Username.Trim()); // Align with UmbracoSignInManager and trim username!
|
||||
if (user is not null &&
|
||||
await _backOfficeUserManager.CheckPasswordAsync(user, model.Password))
|
||||
{
|
||||
TwoFactorLoginView = twofactorView,
|
||||
EnabledTwoFactorProviderNames = enabledProviders
|
||||
});
|
||||
// The credentials were correct, so cancel timed scope and provide a more detailed failure response
|
||||
await timedScope.CancelAsync();
|
||||
|
||||
if (result.IsNotAllowed)
|
||||
{
|
||||
return StatusCode(StatusCodes.Status403Forbidden, new ProblemDetailsBuilder()
|
||||
.WithTitle("User is not allowed")
|
||||
.WithDetail("The operation is not allowed on the user")
|
||||
.Build());
|
||||
}
|
||||
|
||||
if (result.IsLockedOut)
|
||||
{
|
||||
return StatusCode(StatusCodes.Status403Forbidden, new ProblemDetailsBuilder()
|
||||
.WithTitle("User is locked")
|
||||
.WithDetail("The user is locked, and need to be unlocked before more login attempts can be executed.")
|
||||
.Build());
|
||||
}
|
||||
|
||||
if (result.RequiresTwoFactor)
|
||||
{
|
||||
string? twofactorView = _backOfficeTwoFactorOptions.GetTwoFactorView(model.Username);
|
||||
IEnumerable<string> enabledProviders = (await _userTwoFactorLoginService.GetProviderNamesAsync(user.Key)).Result.Where(x => x.IsEnabledOnUser).Select(x => x.ProviderName);
|
||||
|
||||
return StatusCode(StatusCodes.Status402PaymentRequired, new RequiresTwoFactorResponseModel()
|
||||
{
|
||||
TwoFactorLoginView = twofactorView,
|
||||
EnabledTwoFactorProviderNames = enabledProviders
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return StatusCode(StatusCodes.Status401Unauthorized, new ProblemDetailsBuilder()
|
||||
.WithTitle("Invalid credentials")
|
||||
.WithDetail("The provided credentials are invalid. User has not been signed in.")
|
||||
.Build());
|
||||
}
|
||||
|
||||
if (result.Succeeded)
|
||||
{
|
||||
return Ok();
|
||||
}
|
||||
return StatusCode(StatusCodes.Status401Unauthorized, new ProblemDetailsBuilder()
|
||||
.WithTitle("Invalid credentials")
|
||||
.WithDetail("The provided credentials are invalid. User has not been signed in.")
|
||||
.Build());
|
||||
// Set initial or update average (successful) login duration
|
||||
_loginDurationAverage = _loginDurationAverage is long average
|
||||
? (average + (long)timedScope.Elapsed.TotalMilliseconds) / 2
|
||||
: (long)timedScope.Elapsed.TotalMilliseconds;
|
||||
|
||||
// Cancel the timed scope (we don't want to unnecessarily wait on a successful response)
|
||||
await timedScope.CancelAsync();
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
@@ -185,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)]
|
||||
|
||||
+4
-1
@@ -1,4 +1,4 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Common.Builders;
|
||||
using Umbraco.Cms.Api.Management.Routing;
|
||||
@@ -17,6 +17,9 @@ public abstract class TemporaryFileControllerBase : ManagementApiControllerBase
|
||||
.WithTitle("File extension not allowed")
|
||||
.WithDetail("The file extension is not allowed.")
|
||||
.Build()),
|
||||
TemporaryFileOperationStatus.InvalidFileName => BadRequest(problemDetailsBuilder
|
||||
.WithTitle("The provided file name is not valid")
|
||||
.Build()),
|
||||
TemporaryFileOperationStatus.KeyAlreadyUsed => BadRequest(problemDetailsBuilder
|
||||
.WithTitle("Key already used")
|
||||
.WithDetail("The specified key is already used.")
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<Description>Adds imaging support using ImageSharp/ImageSharp.Web version 2 to Umbraco CMS.</Description>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="SixLabors.ImageSharp" VersionOverride="[2.1.9, 3)" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" VersionOverride="[2.1.10, 3)" />
|
||||
<PackageReference Include="SixLabors.ImageSharp.Web" VersionOverride="[2.0.2, 3)" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
+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>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// See LICENSE for more details.
|
||||
|
||||
using System.ComponentModel;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Umbraco.Cms.Core.Configuration.Models;
|
||||
|
||||
@@ -19,12 +20,16 @@ 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-._@+\\";
|
||||
|
||||
internal const int StaticMemberDefaultLockoutTimeInMinutes = 30 * 24 * 60;
|
||||
internal const int StaticUserDefaultLockoutTimeInMinutes = 30 * 24 * 60;
|
||||
private const long StaticUserDefaultFailedLoginDurationInMilliseconds = 1000;
|
||||
private const long StaticUserMinimumFailedLoginDurationInMilliseconds = 250;
|
||||
internal const string StaticAuthorizeCallbackPathName = "/umbraco/oauth_complete";
|
||||
internal const string StaticAuthorizeCallbackLogoutPathName = "/umbraco/logout";
|
||||
internal const string StaticAuthorizeCallbackErrorPathName = "/umbraco/error";
|
||||
@@ -61,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
|
||||
@@ -108,6 +120,30 @@ public class SecuritySettings
|
||||
[DefaultValue(StaticAllowConcurrentLogins)]
|
||||
public bool AllowConcurrentLogins { get; set; } = StaticAllowConcurrentLogins;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the default duration (in milliseconds) of failed login attempts.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The default duration (in milliseconds) of failed login attempts.
|
||||
/// </value>
|
||||
/// <remarks>
|
||||
/// The user login endpoint ensures that failed login attempts take at least as long as the average successful login.
|
||||
/// However, if no successful logins have occurred, this value is used as the default duration.
|
||||
/// </remarks>
|
||||
[Range(0, long.MaxValue)]
|
||||
[DefaultValue(StaticUserDefaultFailedLoginDurationInMilliseconds)]
|
||||
public long UserDefaultFailedLoginDurationInMilliseconds { get; set; } = StaticUserDefaultFailedLoginDurationInMilliseconds;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the minimum duration (in milliseconds) of failed login attempts.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The minimum duration (in milliseconds) of failed login attempts.
|
||||
/// </value>
|
||||
[Range(0, long.MaxValue)]
|
||||
[DefaultValue(StaticUserMinimumFailedLoginDurationInMilliseconds)]
|
||||
public long UserMinimumFailedLoginDurationInMilliseconds { get; set; } = StaticUserMinimumFailedLoginDurationInMilliseconds;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value of the back-office host URI. Use this when running the back-office client and the Management API on different hosts. Leave empty when running both on the same host.
|
||||
/// </summary>
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user