Compare commits
129
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9505011d71 | ||
|
|
2579aaf2db | ||
|
|
2b7784a226 | ||
|
|
8555a97b39 | ||
|
|
c2dd685a4b | ||
|
|
66fc819379 | ||
|
|
4f1f7e15c4 | ||
|
|
a826c52e2e | ||
|
|
8b2c22aaf1 | ||
|
|
aecfee4469 | ||
|
|
9c785a9c5b | ||
|
|
2fe10387ee | ||
|
|
8642b9e615 | ||
|
|
2a604c8719 | ||
|
|
9c0a0a1086 | ||
|
|
1a4256f997 | ||
|
|
80ae0380a2 | ||
|
|
fd01282798 | ||
|
|
f7ba2eaa62 | ||
|
|
9485a95c0e | ||
|
|
f1ab605bb9 | ||
|
|
3472ff9ba3 | ||
|
|
577dc06d55 | ||
|
|
f4771d1495 | ||
|
|
4b3ce53acf | ||
|
|
ca267047d3 | ||
|
|
0543163817 | ||
|
|
72f43a5821 | ||
|
|
aea9034adf | ||
|
|
6e6f822761 | ||
|
|
137aa20a10 | ||
|
|
d7231c5435 | ||
|
|
be116436d9 | ||
|
|
aed7505e4b | ||
|
|
590a020303 | ||
|
|
15c6ca7628 | ||
|
|
49ba89c22a | ||
|
|
c295271757 | ||
|
|
12b483ff05 | ||
|
|
7502a38033 | ||
|
|
4e74dbf218 | ||
|
|
fa5c53b571 | ||
|
|
76fed82e91 | ||
|
|
43ac32282c | ||
|
|
96ecef0a92 | ||
|
|
5e87dead44 | ||
|
|
7af67d2944 | ||
|
|
ce59537006 | ||
|
|
f87e15b941 | ||
|
|
1f82bdde3d | ||
|
|
0d2393d866 | ||
|
|
bea21d7b99 | ||
|
|
3dc65c48b3 | ||
|
|
18ab333afc | ||
|
|
fd91f88a7e | ||
|
|
13c164d81f | ||
|
|
f33eb3f678 | ||
|
|
e893682723 | ||
|
|
d9c201e3d1 | ||
|
|
a5fcfc231d | ||
|
|
6ba03a48c8 | ||
|
|
8434c7d0cb | ||
|
|
3854b2bd53 | ||
|
|
08d217360e | ||
|
|
b762135554 | ||
|
|
644334c63b | ||
|
|
a09e1777c4 | ||
|
|
9cb59fe1b4 | ||
|
|
6bc498ad41 | ||
|
|
f88e28d642 | ||
|
|
194fee7c91 | ||
|
|
4a65f56d9d | ||
|
|
62c1d44a5d | ||
|
|
c2eea5d6cc | ||
|
|
21bf23b67d | ||
|
|
48759b9852 | ||
|
|
79639c0571 | ||
|
|
0792e4358b | ||
|
|
caeb3454e1 | ||
|
|
942ccc82d9 | ||
|
|
8aa9dc8f19 | ||
|
|
5488c77e0e | ||
|
|
daace4b4a0 | ||
|
|
1ceec183a3 | ||
|
|
81a8a0c191 | ||
|
|
ae41438a36 | ||
|
|
5337c38f2c | ||
|
|
7751e40ba8 | ||
|
|
d5a2f0572e | ||
|
|
5a65eb1758 | ||
|
|
105cb9da41 | ||
|
|
a3a8be4717 | ||
|
|
d17ba805b2 | ||
|
|
96f597e440 | ||
|
|
6458bb40f9 | ||
|
|
31bcbc1147 | ||
|
|
62edad17a1 | ||
|
|
369b020d9d | ||
|
|
ec354cef92 | ||
|
|
a504fd1ef8 | ||
|
|
4c05a114c5 | ||
|
|
e71f36d816 | ||
|
|
fdf759d08d | ||
|
|
1ab13a970b | ||
|
|
e22b459d9c | ||
|
|
a19b9fb5fe | ||
|
|
cdf9ee4566 | ||
|
|
e53220c8f5 | ||
|
|
12adfd52bd | ||
|
|
494674d354 | ||
|
|
4ba186633c | ||
|
|
e336f9dfb0 | ||
|
|
3ac37f3686 | ||
|
|
0a027dd80d | ||
|
|
7b4684cd70 | ||
|
|
3df8b9e41a | ||
|
|
fd34ce5bd7 | ||
|
|
99c2aaf17a | ||
|
|
a4c373d3b5 | ||
|
|
bcedc8de2a | ||
|
|
767894b723 | ||
|
|
1fe7931d07 | ||
|
|
16132b0075 | ||
|
|
dab9df3f10 | ||
|
|
b036eb3a75 | ||
|
|
629e905187 | ||
|
|
bfd2594c7b | ||
|
|
d9592aa26d | ||
|
|
97e0c79d94 |
+10
-3
@@ -37,7 +37,7 @@ In order to work with the Umbraco source code locally, first make sure you have
|
||||
|
||||
### Familiarizing yourself with the code
|
||||
|
||||
Umbraco is a .NET application using C#. The solution is broken down into multiple projects. There are several class libraries. The `Umbraco.Web.UI` project is the main project that hosts the back office and login screen. This is the project you will want to run to see your changes.
|
||||
Umbraco is a .NET application using C#. The solution is broken down into multiple projects. There are several class libraries. The `Umbraco.Web.UI` project is the main project that hosts the back office and login screen. This is the project you will want to run to see your changes.
|
||||
|
||||
There are two web projects in the solution with client-side assets based on TypeScript, `Umbraco.Web.UI.Client` and `Umbraco.Web.UI.Login`.
|
||||
|
||||
@@ -73,13 +73,20 @@ Just be careful not to include this change in your PR.
|
||||
|
||||
Conversely, if you are working on front-end only, you want to build the back-end once and then run it. Before you do so, update the configuration in `appSettings.json` to add the following under `Umbraco:Cms:Security`:
|
||||
|
||||
```
|
||||
```json
|
||||
"BackOfficeHost": "http://localhost:5173",
|
||||
"AuthorizeCallbackPathName": "/oauth_complete",
|
||||
"AuthorizeCallbackLogoutPathName": "/logout",
|
||||
"AuthorizeCallbackErrorPathName": "/error"
|
||||
"AuthorizeCallbackErrorPathName": "/error",
|
||||
"BackOfficeTokenCookie": {
|
||||
"Enabled": true,
|
||||
"SameSite": "None"
|
||||
}
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> If you get stuck in a login loop, try clearing your browser cookies for localhost, and make sure that the `BackOfficeTokenCookie` settings are correct. Namely, that `SameSite` should be set to `None` when running the front-end server separately.
|
||||
|
||||
Then run Umbraco from the command line.
|
||||
|
||||
```
|
||||
|
||||
@@ -38,6 +38,14 @@ Some important documentation links to get you started:
|
||||
- [Getting to know Umbraco](https://docs.umbraco.com/umbraco-cms/fundamentals/get-to-know-umbraco)
|
||||
- [Tutorials for creating a basic website and customizing the editing experience](https://docs.umbraco.com/umbraco-cms/tutorials/overview)
|
||||
|
||||
## Backoffice Preview
|
||||
|
||||
Want to see the latest backoffice UI in action? Check out our live preview:
|
||||
|
||||
**[backofficepreview.umbraco.com](https://backofficepreview.umbraco.com/)**
|
||||
|
||||
This preview is automatically deployed from the main branch and showcases the latest backoffice features and improvements. It runs from mock data and persistent edits are not supported.
|
||||
|
||||
## Get help
|
||||
|
||||
If you need a bit of feedback while building your Umbraco projects, we are [chatty on Discord](https://discord.umbraco.com). Our Discord server serves as a social space for all Umbracians. If you have any questions or need some help with a problem, head over to our [dedicated forum](https://forum.umbraco.com/) where the Umbraco Community will be happy to help.
|
||||
|
||||
@@ -94,19 +94,34 @@ The solution contains 30 C# projects organized as follows:
|
||||
|
||||
## Common Tasks
|
||||
|
||||
### Frontend Development
|
||||
For frontend-only changes:
|
||||
1. Configure backend for frontend development:
|
||||
```json
|
||||
<!-- Add to src/Umbraco.Web.UI/appsettings.json under Umbraco:Cms:Security: -->
|
||||
### Running Umbraco in Different Modes
|
||||
|
||||
**Production Mode (Standard Development)**
|
||||
Use this for backend development, testing full builds, or when you don't need hot reloading:
|
||||
1. Build frontend assets: `cd src/Umbraco.Web.UI.Client && npm run build:for:cms`
|
||||
2. Run backend: `cd src/Umbraco.Web.UI && dotnet run --no-build`
|
||||
3. Access backoffice: `https://localhost:44339/umbraco`
|
||||
4. Application uses compiled frontend from `wwwroot/umbraco/backoffice/`
|
||||
|
||||
**Vite Dev Server Mode (Frontend Development with Hot Reload)**
|
||||
Use this for frontend-only development with hot module reloading:
|
||||
1. Configure backend for frontend development - Add to `src/Umbraco.Web.UI/appsettings.json` under `Umbraco:CMS:Security`:
|
||||
```json
|
||||
"BackOfficeHost": "http://localhost:5173",
|
||||
"AuthorizeCallbackPathName": "/oauth_complete",
|
||||
"AuthorizeCallbackLogoutPathName": "/logout",
|
||||
"AuthorizeCallbackErrorPathName": "/error"
|
||||
"AuthorizeCallbackErrorPathName": "/error",
|
||||
"BackOfficeTokenCookie": {
|
||||
"Enabled": true,
|
||||
"SameSite": "None"
|
||||
}
|
||||
```
|
||||
2. Run backend: `cd src/Umbraco.Web.UI && dotnet run --no-build`
|
||||
3. Run frontend dev server: `cd src/Umbraco.Web.UI.Client && npm run dev:server`
|
||||
4. Access backoffice: `http://localhost:5173/` (no `/umbraco` prefix)
|
||||
5. Changes to TypeScript/Lit files hot reload automatically
|
||||
|
||||
**Important:** Remove the `BackOfficeHost` configuration before committing or switching back to production mode.
|
||||
|
||||
### Backend-Only Development
|
||||
For backend-only changes, disable frontend builds:
|
||||
|
||||
@@ -52,7 +52,7 @@ jobs:
|
||||
for (const item of items) {
|
||||
const releaseLabels = (item.labels || [])
|
||||
.map(l => (typeof l === "string" ? l : l.name)) // always get the name
|
||||
.filter(n => typeof n === "string" && n.startsWith("release/"));
|
||||
.filter(n => typeof n === "string" && n.startsWith("release/") && n !== "release/no-notes");
|
||||
if (releaseLabels.length === 0) continue;
|
||||
|
||||
core.info(`#${item.number}: ${releaseLabels.join(", ")}`);
|
||||
|
||||
Vendored
+5
-1
@@ -101,10 +101,14 @@
|
||||
"env": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"ASPNETCORE_URLS": "https://localhost:44339",
|
||||
"UMBRACO__CMS__WEBROUTING__UMBRACOAPPLICATIONURL": "https://localhost:44339",
|
||||
"UMBRACO__CMS__SECURITY__BACKOFFICEHOST": "http://localhost:5173",
|
||||
"UMBRACO__CMS__SECURITY__AUTHORIZECALLBACKPATHNAME": "/oauth_complete",
|
||||
"UMBRACO__CMS__SECURITY__AUTHORIZECALLBACKLOGOUTPATHNAME": "/logout",
|
||||
"UMBRACO__CMS__SECURITY__AUTHORIZECALLBACKERRORPATHNAME": "/error"
|
||||
"UMBRACO__CMS__SECURITY__AUTHORIZECALLBACKERRORPATHNAME": "/error",
|
||||
"UMBRACO__CMS__SECURITY__KEEPUSERLOGGEDIN": "true",
|
||||
"UMBRACO__CMS__SECURITY__BACKOFFICETOKENCOOKIE__ENABLED": "true",
|
||||
"UMBRACO__CMS__SECURITY__BACKOFFICETOKENCOOKIE__SAMESITE": "None"
|
||||
},
|
||||
"sourceFileMap": {
|
||||
"/Views": "${workspaceFolder}/Umbraco.Web.UI/Views"
|
||||
|
||||
@@ -14,14 +14,11 @@
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="9.0.4" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.13.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="4.13.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.13.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.Workspaces.Common" Version="4.13.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.Workspaces.MSBuild" Version="4.13.0" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.4" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.4" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.4" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.4" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.4" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="9.0.4" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.4" />
|
||||
|
||||
@@ -34,6 +34,10 @@ parameters:
|
||||
displayName: Upload API docs
|
||||
type: boolean
|
||||
default: false
|
||||
- name: uploadDependencyTrack
|
||||
displayName: Upload BOMs to Dependency Track
|
||||
type: boolean
|
||||
default: false
|
||||
- name: forceReleaseTestFilter
|
||||
displayName: Force to use the release test filters
|
||||
type: boolean
|
||||
@@ -103,6 +107,15 @@ stages:
|
||||
command: build
|
||||
projects: $(solution)
|
||||
arguments: "--configuration $(buildConfiguration) --no-restore --property:ContinuousIntegrationBuild=true --property:GeneratePackageOnBuild=true --property:PackageOutputPath=$(Build.ArtifactStagingDirectory)/nupkg"
|
||||
- powershell: |
|
||||
dotnet tool install --global CycloneDX
|
||||
dotnet-CycloneDX $(solution) --output $(Build.ArtifactStagingDirectory)/bom --filename bom-dotnet.xml
|
||||
displayName: 'Generate Backend BOM'
|
||||
- powershell: |
|
||||
npm install --global @cyclonedx/cyclonedx-npm
|
||||
cyclonedx-npm -o $(Build.ArtifactStagingDirectory)\bom\bom-login.xml --ignore-npm-errors --verbose
|
||||
displayName: Generate Login UI BOM
|
||||
workingDirectory: src/Umbraco.Web.UI.Login
|
||||
- task: PublishPipelineArtifact@1
|
||||
displayName: Publish nupkg
|
||||
inputs:
|
||||
@@ -113,6 +126,11 @@ stages:
|
||||
inputs:
|
||||
targetPath: $(Build.SourcesDirectory)
|
||||
artifactName: build_output
|
||||
- task: PublishPipelineArtifact@1
|
||||
displayName: Publish Backend BOM
|
||||
inputs:
|
||||
targetPath: $(Build.ArtifactStagingDirectory)/bom
|
||||
artifactName: bom-backend
|
||||
|
||||
- job: B
|
||||
displayName: Build Bellissima Package
|
||||
@@ -124,6 +142,11 @@ stages:
|
||||
lfs: false,
|
||||
fetchDepth: 500
|
||||
- template: templates/backoffice-install.yml
|
||||
- powershell: |
|
||||
npm install --global @cyclonedx/cyclonedx-npm
|
||||
cyclonedx-npm -o $(Build.ArtifactStagingDirectory)/bom/bom-backoffice.xml --ignore-npm-errors --verbose
|
||||
displayName: Generate Backoffice UI BOM
|
||||
workingDirectory: src/Umbraco.Web.UI.Client
|
||||
- script: npm run build:for:npm
|
||||
displayName: Run build:for:npm
|
||||
workingDirectory: src/Umbraco.Web.UI.Client
|
||||
@@ -140,6 +163,35 @@ stages:
|
||||
inputs:
|
||||
targetPath: $(Build.ArtifactStagingDirectory)/npm
|
||||
artifactName: npm
|
||||
- publish: $(Build.ArtifactStagingDirectory)/bom
|
||||
artifact: bom-frontend
|
||||
displayName: 'Publish Frontend BOM'
|
||||
|
||||
- stage: E2E_BOM
|
||||
displayName: E2E Tests BOM Generation
|
||||
dependsOn: []
|
||||
jobs:
|
||||
- job:
|
||||
displayName: E2E Generate BOM
|
||||
pool:
|
||||
vmImage: "ubuntu-latest"
|
||||
steps:
|
||||
- checkout: self
|
||||
submodules: false
|
||||
lfs: false,
|
||||
fetchDepth: 500
|
||||
- template: templates/e2e-install.yml
|
||||
parameters:
|
||||
nodeVersion: ${{ variables.nodeVersion }}
|
||||
npm_config_cache: ${{ variables.npm_config_cache }}
|
||||
- powershell: |
|
||||
npm install --global @cyclonedx/cyclonedx-npm
|
||||
cyclonedx-npm -o $(Build.ArtifactStagingDirectory)/bom/bom-e2e.xml --ignore-npm-errors --verbose
|
||||
displayName: Generate E2E Tests BOM
|
||||
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
|
||||
- publish: $(Build.ArtifactStagingDirectory)/bom
|
||||
artifact: bom-e2e
|
||||
displayName: 'Publish E2E BOM'
|
||||
|
||||
- stage: Build_Docs
|
||||
condition: and(succeeded(), or(eq(dependencies.Build.outputs['A.build.NBGV_PublicRelease'], 'True'), ${{parameters.buildApiDocs}}))
|
||||
@@ -668,6 +720,34 @@ stages:
|
||||
ASPNETCORE_URLS: ${{ variables.ASPNETCORE_URLS }}
|
||||
DatabaseType: ${{ variables.DatabaseType }}
|
||||
|
||||
- stage: Dependency_Track
|
||||
displayName: Dependency Track
|
||||
dependsOn:
|
||||
- Build
|
||||
- E2E_BOM
|
||||
condition: and(succeeded(), or(eq(dependencies.Build.outputs['A.build.NBGV_PublicRelease'], 'True'), ${{parameters.uploadDependencyTrack}}))
|
||||
variables:
|
||||
# Determine Umbraco version based on whether it's a public release or not. If public release, use major version, else use full NuGet package version.
|
||||
umbracoVersion: $[ iif(eq(stageDependencies.Build.A.outputs['build.NBGV_PublicRelease'], 'True'), stageDependencies.Build.A.outputs['build.NBGV_VersionMajor'], stageDependencies.Build.A.outputs['build.NBGV_NuGetPackageVersion']) ]
|
||||
jobs:
|
||||
- template: templates/dependency-track.yml
|
||||
parameters:
|
||||
projectName: "Umbraco-CMS"
|
||||
umbracoVersion: $(umbracoVersion)
|
||||
projects:
|
||||
- name: "Backend"
|
||||
artifact: "bom-backend"
|
||||
bomFilePath: "bom-dotnet.xml"
|
||||
- name: "Login"
|
||||
artifact: "bom-backend"
|
||||
bomFilePath: "bom-login.xml"
|
||||
- name: "Backoffice"
|
||||
artifact: "bom-frontend"
|
||||
bomFilePath: "bom-backoffice.xml"
|
||||
- name: "E2E"
|
||||
artifact: "bom-e2e"
|
||||
bomFilePath: "bom-e2e.xml"
|
||||
|
||||
###############################################
|
||||
## Release
|
||||
###############################################
|
||||
@@ -874,3 +954,4 @@ stages:
|
||||
ContainerName: "$web"
|
||||
BlobPrefix: v$(umbracoMajorVersion)/ui-api
|
||||
CleanTargetBeforeCopy: true
|
||||
|
||||
|
||||
@@ -26,38 +26,18 @@ steps:
|
||||
artifact: nupkg
|
||||
path: $(Agent.BuildDirectory)/app/nupkg
|
||||
|
||||
- task: NodeTool@0
|
||||
displayName: Use Node.js $(nodeVersion)
|
||||
inputs:
|
||||
versionSpec: $(nodeVersion)
|
||||
|
||||
- task: UseDotNet@2
|
||||
displayName: Use .NET SDK from global.json
|
||||
inputs:
|
||||
useGlobalJson: true
|
||||
|
||||
- pwsh: |
|
||||
"UMBRACO_USER_LOGIN=${{ parameters.PlaywrightUserEmail }}
|
||||
UMBRACO_USER_PASSWORD=${{ parameters.PlaywrightPassword }}
|
||||
URL=${{ parameters.ASPNETCORE_URLS }}
|
||||
STORAGE_STAGE_PATH=$(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/playwright/.auth/user.json
|
||||
CONSOLE_ERRORS_PATH=$(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/console-errors.json" | Out-File .env
|
||||
displayName: Generate .env
|
||||
workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest
|
||||
|
||||
# Cache and restore NPM packages
|
||||
- task: Cache@2
|
||||
displayName: Cache NPM packages
|
||||
inputs:
|
||||
key: 'npm_e2e | "$(Agent.OS)" | $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/package-lock.json'
|
||||
restoreKeys: |
|
||||
npm_e2e | "$(Agent.OS)"
|
||||
npm_e2e
|
||||
path: ${{ parameters.npm_config_cache }}
|
||||
|
||||
- script: npm ci --no-fund --no-audit --prefer-offline
|
||||
workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest
|
||||
displayName: Restore NPM packages
|
||||
- template: templates/e2e-install.yml
|
||||
parameters:
|
||||
nodeVersion: ${{ parameters.nodeVersion }}
|
||||
npm_config_cache: ${{ parameters.npm_config_cache }}
|
||||
PlaywrightUserEmail: ${{ parameters.PlaywrightUserEmail }}
|
||||
PlaywrightPassword: ${{ parameters.PlaywrightPassword }}
|
||||
ASPNETCORE_URLS: ${{ parameters.ASPNETCORE_URLS }}
|
||||
|
||||
# Install Template
|
||||
- pwsh: |
|
||||
|
||||
@@ -4,12 +4,11 @@ pr: none
|
||||
trigger: none
|
||||
|
||||
schedules:
|
||||
- cron: '0 0 * * *'
|
||||
displayName: Daily midnight build
|
||||
- cron: '0 6 * * *'
|
||||
displayName: Daily 6 AM build (v16/dev)
|
||||
branches:
|
||||
include:
|
||||
- v15/dev
|
||||
- main
|
||||
- v16/dev
|
||||
|
||||
parameters:
|
||||
- name: skipIntegrationTests
|
||||
@@ -294,7 +293,8 @@ stages:
|
||||
|
||||
- stage: DefaultConfigE2E
|
||||
displayName: Default Config E2E Tests
|
||||
dependsOn: Build
|
||||
dependsOn: Integration
|
||||
condition: always()
|
||||
variables:
|
||||
npm_config_cache: $(Pipeline.Workspace)/.npm_e2e
|
||||
# Enable console logging in Release mode
|
||||
@@ -475,7 +475,8 @@ stages:
|
||||
|
||||
- stage: AdditionalConfigE2E
|
||||
displayName: Additional Config E2E Tests
|
||||
dependsOn: Build
|
||||
dependsOn: DefaultConfigE2E
|
||||
condition: always()
|
||||
variables:
|
||||
npm_config_cache: $(Pipeline.Workspace)/.npm_e2e
|
||||
ASPNETCORE_URLS: https://localhost:44331
|
||||
@@ -674,4 +675,4 @@ stages:
|
||||
--data "$PAYLOAD" \
|
||||
"$SLACK_WEBHOOK_URL"
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: $(E2ESLACKWEBHOOKURL)
|
||||
SLACK_WEBHOOK_URL: $(E2ESLACKWEBHOOKURL)
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
parameters:
|
||||
- name: projectName
|
||||
type: string
|
||||
- name: umbracoVersion
|
||||
type: string
|
||||
- name: projects
|
||||
type: object
|
||||
|
||||
jobs:
|
||||
- job: Create_DT_Project
|
||||
displayName: Create Dependency Track Project
|
||||
steps:
|
||||
- checkout: none
|
||||
|
||||
- bash: |
|
||||
project_id=$(curl --no-progress-meter -H "X-Api-Key: $(DT_API_KEY)" "$(DT_API_URL)/v1/project/lookup?name=${{ parameters.projectName }}&version=${{ parameters.umbracoVersion }}" | jq -r '.uuid')
|
||||
if [ "$project_id" != "null" ] && [ -n "$project_id" ]; then
|
||||
echo "Project '${{ parameters.projectName }}' with version '${{ parameters.umbracoVersion }}' already exists (ID: $project_id)."
|
||||
else
|
||||
project_id=$(curl --no-progress-meter \
|
||||
-X PUT "$(DT_API_URL)/v1/project" \
|
||||
-H "X-Api-Key: $(DT_API_KEY)" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"name": "${{ parameters.projectName }}", "version": "${{ parameters.umbracoVersion }}", "collectionLogic": "AGGREGATE_DIRECT_CHILDREN"}' \
|
||||
| jq -r '.uuid')
|
||||
if [ -z "$project_id" ] || [ "$project_id" == "null" ]; then
|
||||
echo "Failed to create project '${{ parameters.projectName }}' version '${{ parameters.umbracoVersion }}'."
|
||||
exit 1
|
||||
fi
|
||||
echo "Created project '${{ parameters.projectName }}' with version '${{ parameters.umbracoVersion }}' (ID: $project_id)."
|
||||
fi
|
||||
displayName: Ensure main project exists in Dependency Track
|
||||
|
||||
- ${{ each project in parameters.projects }}:
|
||||
- job:
|
||||
displayName: Upload ${{ project.name }} BOM
|
||||
dependsOn: Create_DT_Project
|
||||
steps:
|
||||
- checkout: none
|
||||
|
||||
- download: current
|
||||
artifact: ${{ project.artifact }}
|
||||
displayName: Download ${{ project.artifact }} artifact
|
||||
|
||||
- script: |
|
||||
curl --no-progress-meter --fail-with-body \
|
||||
-X POST "$(DT_API_URL)/v1/bom" \
|
||||
-H "X-Api-Key: $(DT_API_KEY)" \
|
||||
-H "Content-Type: multipart/form-data" \
|
||||
-F "autoCreate=true" \
|
||||
-F "projectName=${{ parameters.projectName }}-${{ project.name }}" \
|
||||
-F "projectVersion=${{ parameters.umbracoVersion }}" \
|
||||
-F "parentName=${{ parameters.projectName }}" \
|
||||
-F "parentVersion=${{ parameters.umbracoVersion }}" \
|
||||
-F "bom=@$(Pipeline.Workspace)/${{ project.artifact }}/${{ project.bomFilePath }}"
|
||||
displayName: Upload ${{ project.name }} BOM to Dependency Track
|
||||
@@ -0,0 +1,49 @@
|
||||
parameters:
|
||||
- name: nodeVersion
|
||||
type: string
|
||||
default: ''
|
||||
|
||||
- name: npm_config_cache
|
||||
type: string
|
||||
default: ''
|
||||
|
||||
- name: PlaywrightUserEmail
|
||||
type: string
|
||||
default: ''
|
||||
|
||||
- name: PlaywrightPassword
|
||||
type: string
|
||||
default: ''
|
||||
|
||||
- name: ASPNETCORE_URLS
|
||||
type: string
|
||||
default: ''
|
||||
|
||||
steps:
|
||||
- task: NodeTool@0
|
||||
displayName: Use Node.js $(nodeVersion)
|
||||
inputs:
|
||||
versionSpec: $(nodeVersion)
|
||||
|
||||
- pwsh: |
|
||||
"UMBRACO_USER_LOGIN=${{ parameters.PlaywrightUserEmail }}
|
||||
UMBRACO_USER_PASSWORD=${{ parameters.PlaywrightPassword }}
|
||||
URL=${{ parameters.ASPNETCORE_URLS }}
|
||||
STORAGE_STAGE_PATH=$(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/playwright/.auth/user.json
|
||||
CONSOLE_ERRORS_PATH=$(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/console-errors.json" | Out-File .env
|
||||
displayName: Generate .env
|
||||
workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest
|
||||
|
||||
# Cache and restore NPM packages
|
||||
- task: Cache@2
|
||||
displayName: Cache NPM packages
|
||||
inputs:
|
||||
key: 'npm_e2e | "$(Agent.OS)" | $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/package-lock.json'
|
||||
restoreKeys: |
|
||||
npm_e2e | "$(Agent.OS)"
|
||||
npm_e2e
|
||||
path: ${{ parameters.npm_config_cache }}
|
||||
|
||||
- script: npm ci --no-fund --no-audit --prefer-offline
|
||||
workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest
|
||||
displayName: Restore NPM packages
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"sdk": {
|
||||
"version": "9.0.100",
|
||||
"version": "9.0.306",
|
||||
"rollForward": "latestFeature",
|
||||
"allowPrerelease": false
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.Accessors;
|
||||
|
||||
public sealed class RequestContextOutputExpansionStrategyAccessor : RequestContextServiceAccessorBase<IOutputExpansionStrategy>, IOutputExpansionStrategyAccessor
|
||||
{
|
||||
public RequestContextOutputExpansionStrategyAccessor(IHttpContextAccessor httpContextAccessor)
|
||||
: base(httpContextAccessor)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.Accessors;
|
||||
|
||||
public abstract class RequestContextServiceAccessorBase<T>
|
||||
where T : class
|
||||
{
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
|
||||
protected RequestContextServiceAccessorBase(IHttpContextAccessor httpContextAccessor)
|
||||
=> _httpContextAccessor = httpContextAccessor;
|
||||
|
||||
public bool TryGetValue([NotNullWhen(true)] out T? requestStartNodeService)
|
||||
{
|
||||
requestStartNodeService = _httpContextAccessor.HttpContext?.RequestServices.GetService<T>();
|
||||
return requestStartNodeService is not null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Options;
|
||||
using OpenIddict.Server;
|
||||
using OpenIddict.Validation;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.Events;
|
||||
using Umbraco.Cms.Core.Notifications;
|
||||
using Umbraco.Cms.Web.Common.Security;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.DependencyInjection;
|
||||
|
||||
internal sealed class HideBackOfficeTokensHandler
|
||||
: IOpenIddictServerHandler<OpenIddictServerEvents.ApplyTokenResponseContext>,
|
||||
IOpenIddictServerHandler<OpenIddictServerEvents.ApplyAuthorizationResponseContext>,
|
||||
IOpenIddictServerHandler<OpenIddictServerEvents.ExtractTokenRequestContext>,
|
||||
IOpenIddictValidationHandler<OpenIddictValidationEvents.ProcessAuthenticationContext>,
|
||||
INotificationHandler<UserLogoutSuccessNotification>
|
||||
{
|
||||
private const string RedactedTokenValue = "[redacted]";
|
||||
private const string AccessTokenCookieKey = "__Host-umbAccessToken";
|
||||
private const string RefreshTokenCookieKey = "__Host-umbRefreshToken";
|
||||
private const string PkceCodeCookieKey = "__Host-umbPkceCode";
|
||||
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
private readonly IDataProtectionProvider _dataProtectionProvider;
|
||||
private readonly BackOfficeTokenCookieSettings _backOfficeTokenCookieSettings;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
|
||||
public HideBackOfficeTokensHandler(
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
IDataProtectionProvider dataProtectionProvider,
|
||||
IOptions<BackOfficeTokenCookieSettings> backOfficeTokenCookieSettings,
|
||||
IOptions<GlobalSettings> globalSettings)
|
||||
{
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
_dataProtectionProvider = dataProtectionProvider;
|
||||
_backOfficeTokenCookieSettings = backOfficeTokenCookieSettings.Value;
|
||||
_globalSettings = globalSettings.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is invoked when tokens (access and refresh tokens) are issued to a client. For the back-office client,
|
||||
/// we will intercept the response, write the tokens from the response into HTTP-only cookies, and redact the
|
||||
/// tokens from the response, so they are not exposed to the client.
|
||||
/// </summary>
|
||||
public ValueTask HandleAsync(OpenIddictServerEvents.ApplyTokenResponseContext context)
|
||||
{
|
||||
if (context.Request?.ClientId is not Constants.OAuthClientIds.BackOffice)
|
||||
{
|
||||
// Only ever handle the back-office client.
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
HttpContext httpContext = GetHttpContext();
|
||||
|
||||
if (context.Response.AccessToken is not null)
|
||||
{
|
||||
SetCookie(httpContext, AccessTokenCookieKey, context.Response.AccessToken);
|
||||
context.Response.AccessToken = RedactedTokenValue;
|
||||
}
|
||||
|
||||
if (context.Response.RefreshToken is not null)
|
||||
{
|
||||
SetCookie(httpContext, RefreshTokenCookieKey, context.Response.RefreshToken);
|
||||
context.Response.RefreshToken = RedactedTokenValue;
|
||||
}
|
||||
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is invoked when a PKCE code is issued to the client. For the back-office client, we will intercept the
|
||||
/// response, write the PKCE code from the response into a HTTP-only cookie, and redact the code from the response,
|
||||
/// so it's not exposed to the client.
|
||||
/// </summary>
|
||||
public ValueTask HandleAsync(OpenIddictServerEvents.ApplyAuthorizationResponseContext context)
|
||||
{
|
||||
if (context.Request?.ClientId is not Constants.OAuthClientIds.BackOffice)
|
||||
{
|
||||
// Only ever handle the back-office client.
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
if (context.Response.Code is not null)
|
||||
{
|
||||
SetCookie(GetHttpContext(), PkceCodeCookieKey, context.Response.Code);
|
||||
context.Response.Code = RedactedTokenValue;
|
||||
}
|
||||
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is invoked when requesting new tokens.
|
||||
/// </summary>
|
||||
public ValueTask HandleAsync(OpenIddictServerEvents.ExtractTokenRequestContext context)
|
||||
{
|
||||
if (context.Request?.ClientId != Constants.OAuthClientIds.BackOffice)
|
||||
{
|
||||
// Only ever handle the back-office client.
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
// Handle when the PKCE code is being exchanged for an access token.
|
||||
if (context.Request.Code == RedactedTokenValue
|
||||
&& TryGetCookie(PkceCodeCookieKey, out var code))
|
||||
{
|
||||
context.Request.Code = code;
|
||||
|
||||
// We won't need the PKCE cookie after this, let's remove it.
|
||||
RemoveCookie(GetHttpContext(), PkceCodeCookieKey);
|
||||
}
|
||||
else
|
||||
{
|
||||
// PCKE codes should always be redacted. If we got here, someone might be trying to pass another PKCE
|
||||
// code. For security reasons, explicitly discard the code (if any) to be on the safe side.
|
||||
context.Request.Code = null;
|
||||
}
|
||||
|
||||
// Handle when a refresh token is being exchanged for a new access token.
|
||||
if (context.Request.RefreshToken == RedactedTokenValue
|
||||
&& TryGetCookie(RefreshTokenCookieKey, out var refreshToken))
|
||||
{
|
||||
context.Request.RefreshToken = refreshToken;
|
||||
}
|
||||
else
|
||||
{
|
||||
// If we got here, either the refresh token was not redacted, or nothing was found in the refresh token cookie.
|
||||
// If OpenIddict found a refresh token, it could be an old token that is potentially still valid. For security
|
||||
// reasons, we cannot accept that; at this point, we expect the refresh tokens to be explicitly redacted.
|
||||
context.Request.RefreshToken = null;
|
||||
}
|
||||
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is invoked when extracting the auth context for a client request.
|
||||
/// </summary>
|
||||
public ValueTask HandleAsync(OpenIddictValidationEvents.ProcessAuthenticationContext context)
|
||||
{
|
||||
// For the back-office client, this only happens when an access token is sent to the API.
|
||||
if (context.AccessToken != RedactedTokenValue)
|
||||
{
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
if (TryGetCookie(AccessTokenCookieKey, out var accessToken))
|
||||
{
|
||||
context.AccessToken = accessToken;
|
||||
}
|
||||
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
public void Handle(UserLogoutSuccessNotification notification)
|
||||
{
|
||||
HttpContext? context = _httpContextAccessor.HttpContext;
|
||||
if (context is null)
|
||||
{
|
||||
// For some reason there is no ambient HTTP context, so we can't clean up the cookies.
|
||||
// This is OK, because the tokens in the cookies have already been revoked at user sign-out,
|
||||
// so the cookie clean-up is mostly cosmetic.
|
||||
return;
|
||||
}
|
||||
|
||||
context.Response.Cookies.Delete(AccessTokenCookieKey);
|
||||
context.Response.Cookies.Delete(RefreshTokenCookieKey);
|
||||
}
|
||||
|
||||
private HttpContext GetHttpContext()
|
||||
=> _httpContextAccessor.GetRequiredHttpContext();
|
||||
|
||||
private void SetCookie(HttpContext httpContext, string key, string value)
|
||||
{
|
||||
var cookieValue = EncryptionHelper.Encrypt(value, _dataProtectionProvider);
|
||||
|
||||
RemoveCookie(httpContext, key);
|
||||
httpContext.Response.Cookies.Append(key, cookieValue, GetCookieOptions(httpContext));
|
||||
}
|
||||
|
||||
private void RemoveCookie(HttpContext httpContext, string key)
|
||||
=> httpContext.Response.Cookies.Delete(key, GetCookieOptions(httpContext));
|
||||
|
||||
private CookieOptions GetCookieOptions(HttpContext httpContext) =>
|
||||
new()
|
||||
{
|
||||
// Prevent the client-side scripts from accessing the cookie.
|
||||
HttpOnly = true,
|
||||
|
||||
// Mark the cookie as essential to the application, to enforce it despite any
|
||||
// data collection consent options. This aligns with how ASP.NET Core Identity
|
||||
// does when writing cookies for cookie authentication.
|
||||
IsEssential = true,
|
||||
|
||||
// Cookie path must be root for optimal security.
|
||||
Path = "/",
|
||||
|
||||
// For optimal security, the cooke must be secure. However, Umbraco allows for running development
|
||||
// environments over HTTP, so we need to take that into account here.
|
||||
// Thus, we will make the cookie secure if:
|
||||
// - HTTPS is explicitly enabled by config (default for production environments), or
|
||||
// - The current request is over HTTPS (meaning the environment supports it regardless of config).
|
||||
Secure = _globalSettings.UseHttps || httpContext.Request.IsHttps,
|
||||
|
||||
// SameSite is configurable (see BackOfficeTokenCookieSettings for defaults):
|
||||
SameSite = ParseSameSiteMode(_backOfficeTokenCookieSettings.SameSite),
|
||||
};
|
||||
|
||||
private bool TryGetCookie(string key, [NotNullWhen(true)] out string? value)
|
||||
{
|
||||
if (GetHttpContext().Request.Cookies.TryGetValue(key, out var cookieValue))
|
||||
{
|
||||
value = EncryptionHelper.Decrypt(cookieValue, _dataProtectionProvider);
|
||||
return true;
|
||||
}
|
||||
|
||||
value = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static SameSiteMode ParseSameSiteMode(string sameSiteMode) =>
|
||||
Enum.TryParse(sameSiteMode, ignoreCase: true, out SameSiteMode result)
|
||||
? result
|
||||
: throw new ArgumentException($"The provided {nameof(sameSiteMode)} value could not be parsed into as SameSiteMode value.", nameof(sameSiteMode));
|
||||
}
|
||||
@@ -9,6 +9,7 @@ using Umbraco.Cms.Api.Common.Security;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Notifications;
|
||||
using Umbraco.Cms.Infrastructure.BackgroundJobs.Jobs;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
@@ -28,6 +29,11 @@ public static class UmbracoBuilderAuthExtensions
|
||||
|
||||
private static void ConfigureOpenIddict(IUmbracoBuilder builder)
|
||||
{
|
||||
// Optionally hide tokens from the back-office.
|
||||
var hideBackOfficeTokens = (builder.Config
|
||||
.GetSection(Constants.Configuration.ConfigBackOfficeTokenCookie)
|
||||
.Get<BackOfficeTokenCookieSettings>() ?? new BackOfficeTokenCookieSettings()).Enabled;
|
||||
|
||||
builder.Services.AddOpenIddict()
|
||||
// Register the OpenIddict server components.
|
||||
.AddServer(options =>
|
||||
@@ -113,6 +119,28 @@ public static class UmbracoBuilderAuthExtensions
|
||||
{
|
||||
configuration.UseSingletonHandler<ProcessRequestContextHandler>().SetOrder(OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreHandlers.ResolveRequestUri.Descriptor.Order - 1);
|
||||
});
|
||||
|
||||
if (hideBackOfficeTokens)
|
||||
{
|
||||
options.AddEventHandler<OpenIddictServerEvents.ApplyTokenResponseContext>(configuration =>
|
||||
{
|
||||
configuration
|
||||
.UseSingletonHandler<HideBackOfficeTokensHandler>()
|
||||
.SetOrder(OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreHandlers.ProcessJsonResponse<OpenIddictServerEvents.ApplyTokenResponseContext>.Descriptor.Order - 1);
|
||||
});
|
||||
options.AddEventHandler<OpenIddictServerEvents.ApplyAuthorizationResponseContext>(configuration =>
|
||||
{
|
||||
configuration
|
||||
.UseSingletonHandler<HideBackOfficeTokensHandler>()
|
||||
.SetOrder(OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreHandlers.Authentication.ProcessQueryResponse.Descriptor.Order - 1);
|
||||
});
|
||||
options.AddEventHandler<OpenIddictServerEvents.ExtractTokenRequestContext>(configuration =>
|
||||
{
|
||||
configuration
|
||||
.UseSingletonHandler<HideBackOfficeTokensHandler>()
|
||||
.SetOrder(OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreHandlers.ExtractPostRequest<OpenIddictServerEvents.ExtractTokenRequestContext>.Descriptor.Order + 1);
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
// Register the OpenIddict validation components.
|
||||
@@ -137,9 +165,25 @@ public static class UmbracoBuilderAuthExtensions
|
||||
{
|
||||
configuration.UseSingletonHandler<ProcessRequestContextHandler>().SetOrder(OpenIddict.Validation.AspNetCore.OpenIddictValidationAspNetCoreHandlers.ResolveRequestUri.Descriptor.Order - 1);
|
||||
});
|
||||
|
||||
if (hideBackOfficeTokens)
|
||||
{
|
||||
options.AddEventHandler<OpenIddictValidationEvents.ProcessAuthenticationContext>(configuration =>
|
||||
{
|
||||
configuration
|
||||
.UseSingletonHandler<HideBackOfficeTokensHandler>()
|
||||
// IMPORTANT: the handler must be AFTER the built-in query string handler, because the client-side SignalR library sometimes appends access tokens to the query string.
|
||||
.SetOrder(OpenIddict.Validation.AspNetCore.OpenIddictValidationAspNetCoreHandlers.ExtractAccessTokenFromQueryString.Descriptor.Order + 1);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
builder.Services.AddRecurringBackgroundJob<OpenIddictCleanupJob>();
|
||||
builder.Services.ConfigureOptions<ConfigureOpenIddict>();
|
||||
|
||||
if (hideBackOfficeTokens)
|
||||
{
|
||||
builder.AddNotificationHandler<UserLogoutSuccessNotification, HideBackOfficeTokensHandler>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.Rendering;
|
||||
|
||||
public class ElementOnlyOutputExpansionStrategy : IOutputExpansionStrategy
|
||||
{
|
||||
protected const string All = "$all";
|
||||
protected const string None = "";
|
||||
protected const string ExpandParameterName = "expand";
|
||||
protected const string FieldsParameterName = "fields";
|
||||
|
||||
private readonly IApiPropertyRenderer _propertyRenderer;
|
||||
|
||||
protected Stack<Node?> ExpandProperties { get; } = new();
|
||||
|
||||
protected Stack<Node?> IncludeProperties { get; } = new();
|
||||
|
||||
public ElementOnlyOutputExpansionStrategy(
|
||||
IApiPropertyRenderer propertyRenderer)
|
||||
{
|
||||
_propertyRenderer = propertyRenderer;
|
||||
}
|
||||
|
||||
public virtual IDictionary<string, object?> MapContentProperties(IPublishedContent content)
|
||||
=> content.ItemType == PublishedItemType.Content
|
||||
? MapProperties(content.Properties)
|
||||
: throw new ArgumentException($"Invalid item type. This method can only be used with item type {nameof(PublishedItemType.Content)}, got: {content.ItemType}");
|
||||
|
||||
public virtual IDictionary<string, object?> MapMediaProperties(IPublishedContent media, bool skipUmbracoProperties = true)
|
||||
{
|
||||
if (media.ItemType != PublishedItemType.Media)
|
||||
{
|
||||
throw new ArgumentException($"Invalid item type. This method can only be used with item type {PublishedItemType.Media}, got: {media.ItemType}");
|
||||
}
|
||||
|
||||
IPublishedProperty[] properties = media
|
||||
.Properties
|
||||
.Where(p => skipUmbracoProperties is false || p.Alias.StartsWith("umbraco") is false)
|
||||
.ToArray();
|
||||
|
||||
return properties.Any()
|
||||
? MapProperties(properties)
|
||||
: new Dictionary<string, object?>();
|
||||
}
|
||||
|
||||
public virtual IDictionary<string, object?> MapElementProperties(IPublishedElement element)
|
||||
=> MapProperties(element.Properties, true);
|
||||
|
||||
private IDictionary<string, object?> MapProperties(IEnumerable<IPublishedProperty> properties, bool forceExpandProperties = false)
|
||||
{
|
||||
Node? currentExpandProperties = ExpandProperties.Count > 0 ? ExpandProperties.Peek() : null;
|
||||
if (ExpandProperties.Count > 1 && currentExpandProperties is null && forceExpandProperties is false)
|
||||
{
|
||||
return new Dictionary<string, object?>();
|
||||
}
|
||||
|
||||
Node? currentIncludeProperties = IncludeProperties.Count > 0 ? IncludeProperties.Peek() : null;
|
||||
var result = new Dictionary<string, object?>();
|
||||
foreach (IPublishedProperty property in properties)
|
||||
{
|
||||
Node? nextIncludeProperties = GetNextProperties(currentIncludeProperties, property.Alias);
|
||||
if (currentIncludeProperties is not null && currentIncludeProperties.Items.Any() && nextIncludeProperties is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Node? nextExpandProperties = GetNextProperties(currentExpandProperties, property.Alias);
|
||||
|
||||
IncludeProperties.Push(nextIncludeProperties);
|
||||
ExpandProperties.Push(nextExpandProperties);
|
||||
|
||||
result[property.Alias] = GetPropertyValue(property);
|
||||
|
||||
ExpandProperties.Pop();
|
||||
IncludeProperties.Pop();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private Node? GetNextProperties(Node? currentProperties, string propertyAlias)
|
||||
=> currentProperties?.Items.FirstOrDefault(i => i.Key == All)
|
||||
?? currentProperties?.Items.FirstOrDefault(i => i.Key == "properties")?.Items.FirstOrDefault(i => i.Key == All || i.Key == propertyAlias);
|
||||
|
||||
private object? GetPropertyValue(IPublishedProperty property)
|
||||
=> _propertyRenderer.GetPropertyValue(property, ExpandProperties.Peek() is not null);
|
||||
|
||||
protected sealed class Node
|
||||
{
|
||||
public string Key { get; private set; } = string.Empty;
|
||||
|
||||
public List<Node> Items { get; } = new();
|
||||
|
||||
public static Node Parse(string value)
|
||||
{
|
||||
// verify that there are as many start brackets as there are end brackets
|
||||
if (value.CountOccurrences("[") != value.CountOccurrences("]"))
|
||||
{
|
||||
throw new ArgumentException("Value did not contain an equal number of start and end brackets");
|
||||
}
|
||||
|
||||
// verify that the value does not start with a start bracket
|
||||
if (value.StartsWith("["))
|
||||
{
|
||||
throw new ArgumentException("Value cannot start with a bracket");
|
||||
}
|
||||
|
||||
// verify that there are no empty brackets
|
||||
if (value.Contains("[]"))
|
||||
{
|
||||
throw new ArgumentException("Value cannot contain empty brackets");
|
||||
}
|
||||
|
||||
var stack = new Stack<Node>();
|
||||
var root = new Node { Key = "root" };
|
||||
stack.Push(root);
|
||||
|
||||
var currentNode = new Node();
|
||||
root.Items.Add(currentNode);
|
||||
|
||||
foreach (char c in value)
|
||||
{
|
||||
switch (c)
|
||||
{
|
||||
case '[': // Start a new node, child of the current node
|
||||
stack.Push(currentNode);
|
||||
currentNode = new Node();
|
||||
stack.Peek().Items.Add(currentNode);
|
||||
break;
|
||||
case ',': // Start a new node, but at the same level of the current node
|
||||
currentNode = new Node();
|
||||
stack.Peek().Items.Add(currentNode);
|
||||
break;
|
||||
case ']': // Back to parent of the current node
|
||||
currentNode = stack.Pop();
|
||||
break;
|
||||
default: // Add char to current node key
|
||||
currentNode.Key += c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return root;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,28 +35,35 @@ public static class UmbracoBuilderExtensions
|
||||
builder.Services.AddScoped<IRequestStartItemProvider, RequestStartItemProvider>();
|
||||
builder.Services.AddScoped<RequestContextOutputExpansionStrategy>();
|
||||
builder.Services.AddScoped<RequestContextOutputExpansionStrategyV2>();
|
||||
builder.Services.AddScoped<IOutputExpansionStrategy>(provider =>
|
||||
{
|
||||
HttpContext? httpContext = provider.GetRequiredService<IHttpContextAccessor>().HttpContext;
|
||||
ApiVersion? apiVersion = httpContext?.GetRequestedApiVersion();
|
||||
if (apiVersion is null)
|
||||
{
|
||||
return provider.GetRequiredService<RequestContextOutputExpansionStrategyV2>();
|
||||
}
|
||||
|
||||
// V1 of the Delivery API uses a different expansion strategy than V2+
|
||||
return apiVersion.MajorVersion == 1
|
||||
? provider.GetRequiredService<RequestContextOutputExpansionStrategy>()
|
||||
: provider.GetRequiredService<RequestContextOutputExpansionStrategyV2>();
|
||||
});
|
||||
builder.Services.AddUnique<IOutputExpansionStrategy>(
|
||||
provider =>
|
||||
{
|
||||
HttpContext? httpContext = provider.GetRequiredService<IHttpContextAccessor>().HttpContext;
|
||||
ApiVersion? apiVersion = httpContext?.GetRequestedApiVersion();
|
||||
if (apiVersion is null)
|
||||
{
|
||||
return provider.GetRequiredService<RequestContextOutputExpansionStrategyV2>();
|
||||
}
|
||||
|
||||
// V1 of the Delivery API uses a different expansion strategy than V2+
|
||||
return apiVersion.MajorVersion == 1
|
||||
? provider.GetRequiredService<RequestContextOutputExpansionStrategy>()
|
||||
: provider.GetRequiredService<RequestContextOutputExpansionStrategyV2>();
|
||||
},
|
||||
ServiceLifetime.Scoped);
|
||||
|
||||
builder.Services.AddSingleton<IRequestCultureService, RequestCultureService>();
|
||||
builder.Services.AddSingleton<IRequestSegmmentService, RequestSegmentService>();
|
||||
builder.Services.AddSingleton<IRequestSegmentService, RequestSegmentService>();
|
||||
builder.Services.AddSingleton<IRequestRoutingService, RequestRoutingService>();
|
||||
builder.Services.AddSingleton<IRequestRedirectService, RequestRedirectService>();
|
||||
builder.Services.AddSingleton<IRequestPreviewService, RequestPreviewService>();
|
||||
builder.Services.AddSingleton<IOutputExpansionStrategyAccessor, RequestContextOutputExpansionStrategyAccessor>();
|
||||
|
||||
// Webooks register a more basic implementation, remove it.
|
||||
builder.Services.AddUnique<IOutputExpansionStrategyAccessor, RequestContextOutputExpansionStrategyAccessor>(ServiceLifetime.Singleton);
|
||||
builder.Services.AddSingleton<IRequestStartItemProviderAccessor, RequestContextRequestStartItemProviderAccessor>();
|
||||
|
||||
builder.Services.AddSingleton<IApiAccessService, ApiAccessService>();
|
||||
builder.Services.AddSingleton<IApiContentQueryService, ApiContentQueryService>();
|
||||
builder.Services.AddSingleton<IApiContentQueryProvider, ApiContentQueryProvider>();
|
||||
|
||||
@@ -1,62 +1,25 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Umbraco.Cms.Api.Common.Rendering;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Rendering;
|
||||
|
||||
internal sealed class RequestContextOutputExpansionStrategyV2 : IOutputExpansionStrategy
|
||||
internal sealed class RequestContextOutputExpansionStrategyV2 : ElementOnlyOutputExpansionStrategy, IOutputExpansionStrategy
|
||||
{
|
||||
private const string All = "$all";
|
||||
private const string None = "";
|
||||
private const string ExpandParameterName = "expand";
|
||||
private const string FieldsParameterName = "fields";
|
||||
|
||||
private readonly IApiPropertyRenderer _propertyRenderer;
|
||||
private readonly ILogger<RequestContextOutputExpansionStrategyV2> _logger;
|
||||
|
||||
private readonly Stack<Node?> _expandProperties;
|
||||
private readonly Stack<Node?> _includeProperties;
|
||||
|
||||
public RequestContextOutputExpansionStrategyV2(
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
IApiPropertyRenderer propertyRenderer,
|
||||
ILogger<RequestContextOutputExpansionStrategyV2> logger)
|
||||
: base(propertyRenderer)
|
||||
{
|
||||
_propertyRenderer = propertyRenderer;
|
||||
_logger = logger;
|
||||
_expandProperties = new Stack<Node?>();
|
||||
_includeProperties = new Stack<Node?>();
|
||||
|
||||
InitializeExpandAndInclude(httpContextAccessor);
|
||||
}
|
||||
|
||||
public IDictionary<string, object?> MapContentProperties(IPublishedContent content)
|
||||
=> content.ItemType == PublishedItemType.Content
|
||||
? MapProperties(content.Properties)
|
||||
: throw new ArgumentException($"Invalid item type. This method can only be used with item type {nameof(PublishedItemType.Content)}, got: {content.ItemType}");
|
||||
|
||||
public IDictionary<string, object?> MapMediaProperties(IPublishedContent media, bool skipUmbracoProperties = true)
|
||||
{
|
||||
if (media.ItemType != PublishedItemType.Media)
|
||||
{
|
||||
throw new ArgumentException($"Invalid item type. This method can only be used with item type {PublishedItemType.Media}, got: {media.ItemType}");
|
||||
}
|
||||
|
||||
IPublishedProperty[] properties = media
|
||||
.Properties
|
||||
.Where(p => skipUmbracoProperties is false || p.Alias.StartsWith("umbraco") is false)
|
||||
.ToArray();
|
||||
|
||||
return properties.Any()
|
||||
? MapProperties(properties)
|
||||
: new Dictionary<string, object?>();
|
||||
}
|
||||
|
||||
public IDictionary<string, object?> MapElementProperties(IPublishedElement element)
|
||||
=> MapProperties(element.Properties, true);
|
||||
|
||||
private void InitializeExpandAndInclude(IHttpContextAccessor httpContextAccessor)
|
||||
{
|
||||
string? QueryValue(string key) => httpContextAccessor.HttpContext?.Request.Query[key];
|
||||
@@ -66,7 +29,7 @@ internal sealed class RequestContextOutputExpansionStrategyV2 : IOutputExpansion
|
||||
|
||||
try
|
||||
{
|
||||
_expandProperties.Push(Node.Parse(toExpand));
|
||||
ExpandProperties.Push(Node.Parse(toExpand));
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
{
|
||||
@@ -76,7 +39,7 @@ internal sealed class RequestContextOutputExpansionStrategyV2 : IOutputExpansion
|
||||
|
||||
try
|
||||
{
|
||||
_includeProperties.Push(Node.Parse(toInclude));
|
||||
IncludeProperties.Push(Node.Parse(toInclude));
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
{
|
||||
@@ -84,102 +47,4 @@ internal sealed class RequestContextOutputExpansionStrategyV2 : IOutputExpansion
|
||||
throw new ArgumentException($"Could not parse the '{FieldsParameterName}' parameter: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private IDictionary<string, object?> MapProperties(IEnumerable<IPublishedProperty> properties, bool forceExpandProperties = false)
|
||||
{
|
||||
Node? currentExpandProperties = _expandProperties.Peek();
|
||||
if (_expandProperties.Count > 1 && currentExpandProperties is null && forceExpandProperties is false)
|
||||
{
|
||||
return new Dictionary<string, object?>();
|
||||
}
|
||||
|
||||
Node? currentIncludeProperties = _includeProperties.Peek();
|
||||
var result = new Dictionary<string, object?>();
|
||||
foreach (IPublishedProperty property in properties)
|
||||
{
|
||||
Node? nextIncludeProperties = GetNextProperties(currentIncludeProperties, property.Alias);
|
||||
if (currentIncludeProperties is not null && currentIncludeProperties.Items.Any() && nextIncludeProperties is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Node? nextExpandProperties = GetNextProperties(currentExpandProperties, property.Alias);
|
||||
|
||||
_includeProperties.Push(nextIncludeProperties);
|
||||
_expandProperties.Push(nextExpandProperties);
|
||||
|
||||
result[property.Alias] = GetPropertyValue(property);
|
||||
|
||||
_expandProperties.Pop();
|
||||
_includeProperties.Pop();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private Node? GetNextProperties(Node? currentProperties, string propertyAlias)
|
||||
=> currentProperties?.Items.FirstOrDefault(i => i.Key == All)
|
||||
?? currentProperties?.Items.FirstOrDefault(i => i.Key == "properties")?.Items.FirstOrDefault(i => i.Key == All || i.Key == propertyAlias);
|
||||
|
||||
private object? GetPropertyValue(IPublishedProperty property)
|
||||
=> _propertyRenderer.GetPropertyValue(property, _expandProperties.Peek() is not null);
|
||||
|
||||
private sealed class Node
|
||||
{
|
||||
public string Key { get; private set; } = string.Empty;
|
||||
|
||||
public List<Node> Items { get; } = new();
|
||||
|
||||
public static Node Parse(string value)
|
||||
{
|
||||
// verify that there are as many start brackets as there are end brackets
|
||||
if (value.CountOccurrences("[") != value.CountOccurrences("]"))
|
||||
{
|
||||
throw new ArgumentException("Value did not contain an equal number of start and end brackets");
|
||||
}
|
||||
|
||||
// verify that the value does not start with a start bracket
|
||||
if (value.StartsWith("["))
|
||||
{
|
||||
throw new ArgumentException("Value cannot start with a bracket");
|
||||
}
|
||||
|
||||
// verify that there are no empty brackets
|
||||
if (value.Contains("[]"))
|
||||
{
|
||||
throw new ArgumentException("Value cannot contain empty brackets");
|
||||
}
|
||||
|
||||
var stack = new Stack<Node>();
|
||||
var root = new Node { Key = "root" };
|
||||
stack.Push(root);
|
||||
|
||||
var currentNode = new Node();
|
||||
root.Items.Add(currentNode);
|
||||
|
||||
foreach (char c in value)
|
||||
{
|
||||
switch (c)
|
||||
{
|
||||
case '[': // Start a new node, child of the current node
|
||||
stack.Push(currentNode);
|
||||
currentNode = new Node();
|
||||
stack.Peek().Items.Add(currentNode);
|
||||
break;
|
||||
case ',': // Start a new node, but at the same level of the current node
|
||||
currentNode = new Node();
|
||||
stack.Peek().Items.Add(currentNode);
|
||||
break;
|
||||
case ']': // Back to parent of the current node
|
||||
currentNode = stack.Pop();
|
||||
break;
|
||||
default: // Add char to current node key
|
||||
currentNode.Key += c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return root;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,31 +1,60 @@
|
||||
using Asp.Versioning;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Document;
|
||||
using Umbraco.Cms.Core.Actions;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Mapping;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Security.Authorization;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Web.Common.Authorization;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.Document;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
public class DomainsController : DocumentControllerBase
|
||||
{
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
private readonly IDomainService _domainService;
|
||||
private readonly IUmbracoMapper _umbracoMapper;
|
||||
|
||||
public DomainsController(IDomainService domainService, IUmbracoMapper umbracoMapper)
|
||||
[ActivatorUtilitiesConstructor]
|
||||
public DomainsController(IAuthorizationService authorizationService, IDomainService domainService, IUmbracoMapper umbracoMapper)
|
||||
{
|
||||
_authorizationService = authorizationService;
|
||||
_domainService = domainService;
|
||||
_umbracoMapper = umbracoMapper;
|
||||
}
|
||||
|
||||
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")]
|
||||
public DomainsController(IDomainService domainService, IUmbracoMapper umbracoMapper)
|
||||
: this(
|
||||
StaticServiceProvider.Instance.GetRequiredService<IAuthorizationService>(),
|
||||
domainService,
|
||||
umbracoMapper)
|
||||
{
|
||||
}
|
||||
|
||||
[MapToApiVersion("1.0")]
|
||||
[HttpGet("{id:guid}/domains")]
|
||||
[ProducesResponseType(typeof(DomainsResponseModel), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> Domains(CancellationToken cancellationToken, Guid id)
|
||||
{
|
||||
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
|
||||
User,
|
||||
ContentPermissionResource.WithKeys(ActionBrowse.ActionLetter, id),
|
||||
AuthorizationPolicies.ContentPermissionByResource);
|
||||
|
||||
if (!authorizationResult.Succeeded)
|
||||
{
|
||||
return Forbidden();
|
||||
}
|
||||
|
||||
IDomain[] assignedDomains = (await _domainService.GetAssignedDomainsAsync(id, true))
|
||||
.OrderBy(d => d.SortOrder)
|
||||
.ToArray();
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
using Asp.Versioning;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Api.Management.Factories;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Document;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Actions;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Mapping;
|
||||
using Umbraco.Cms.Core.Models.ContentEditing;
|
||||
using Umbraco.Cms.Core.Security.Authorization;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.Cms.Web.Common.Authorization;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.Document;
|
||||
@@ -15,17 +21,30 @@ namespace Umbraco.Cms.Api.Management.Controllers.Document;
|
||||
[ApiVersion("1.0")]
|
||||
public class UpdateDomainsController : DocumentControllerBase
|
||||
{
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
private readonly IDomainService _domainService;
|
||||
private readonly IUmbracoMapper _umbracoMapper;
|
||||
private readonly IDomainPresentationFactory _domainPresentationFactory;
|
||||
|
||||
public UpdateDomainsController(IDomainService domainService, IUmbracoMapper umbracoMapper, IDomainPresentationFactory domainPresentationFactory)
|
||||
[ActivatorUtilitiesConstructor]
|
||||
public UpdateDomainsController(IAuthorizationService authorizationService, IDomainService domainService, IUmbracoMapper umbracoMapper, IDomainPresentationFactory domainPresentationFactory)
|
||||
{
|
||||
_authorizationService = authorizationService;
|
||||
_domainService = domainService;
|
||||
_umbracoMapper = umbracoMapper;
|
||||
_domainPresentationFactory = domainPresentationFactory;
|
||||
}
|
||||
|
||||
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")]
|
||||
public UpdateDomainsController(IDomainService domainService, IUmbracoMapper umbracoMapper, IDomainPresentationFactory domainPresentationFactory)
|
||||
: this(
|
||||
StaticServiceProvider.Instance.GetRequiredService<IAuthorizationService>(),
|
||||
domainService,
|
||||
umbracoMapper,
|
||||
domainPresentationFactory)
|
||||
{
|
||||
}
|
||||
|
||||
[MapToApiVersion("1.0")]
|
||||
[HttpPut("{id:guid}/domains")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
@@ -37,6 +56,16 @@ public class UpdateDomainsController : DocumentControllerBase
|
||||
Guid id,
|
||||
UpdateDomainsRequestModel updateModel)
|
||||
{
|
||||
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
|
||||
User,
|
||||
ContentPermissionResource.WithKeys(ActionAssignDomain.ActionLetter, id),
|
||||
AuthorizationPolicies.ContentPermissionByResource);
|
||||
|
||||
if (!authorizationResult.Succeeded)
|
||||
{
|
||||
return Forbidden();
|
||||
}
|
||||
|
||||
DomainsUpdateModel domainsUpdateModel = _umbracoMapper.Map<DomainsUpdateModel>(updateModel)!;
|
||||
|
||||
Attempt<DomainUpdateResult, DomainOperationStatus> result = await _domainService.UpdateDomainsAsync(id, domainsUpdateModel);
|
||||
|
||||
+31
-1
@@ -1,33 +1,63 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Document;
|
||||
using Umbraco.Cms.Core.Actions;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Security.Authorization;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Web.Common.Authorization;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.Document;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
public class UpdateNotificationsController : DocumentControllerBase
|
||||
{
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
private readonly IContentEditingService _contentEditingService;
|
||||
private readonly INotificationService _notificationService;
|
||||
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
|
||||
|
||||
public UpdateNotificationsController(IContentEditingService contentEditingService, INotificationService notificationService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
|
||||
[ActivatorUtilitiesConstructor]
|
||||
public UpdateNotificationsController(IAuthorizationService authorizationService, IContentEditingService contentEditingService, INotificationService notificationService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
|
||||
{
|
||||
_authorizationService = authorizationService;
|
||||
_contentEditingService = contentEditingService;
|
||||
_notificationService = notificationService;
|
||||
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
|
||||
}
|
||||
|
||||
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")]
|
||||
public UpdateNotificationsController(IContentEditingService contentEditingService, INotificationService notificationService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
|
||||
: this(
|
||||
StaticServiceProvider.Instance.GetRequiredService<IAuthorizationService>(),
|
||||
contentEditingService,
|
||||
notificationService,
|
||||
backOfficeSecurityAccessor)
|
||||
{
|
||||
}
|
||||
|
||||
[MapToApiVersion("1.0")]
|
||||
[HttpPut("{id:guid}/notifications")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> UpdateNotifications(CancellationToken cancellationToken, Guid id, UpdateDocumentNotificationsRequestModel updateModel)
|
||||
{
|
||||
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
|
||||
User,
|
||||
ContentPermissionResource.WithKeys(ActionBrowse.ActionLetter, id),
|
||||
AuthorizationPolicies.ContentPermissionByResource);
|
||||
|
||||
if (!authorizationResult.Succeeded)
|
||||
{
|
||||
return Forbidden();
|
||||
}
|
||||
|
||||
IContent? content = await _contentEditingService.GetAsync(id);
|
||||
if (content == null)
|
||||
{
|
||||
|
||||
+13
-15
@@ -1,34 +1,32 @@
|
||||
using Asp.Versioning;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Api.Management.Services.FileSystem;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Tree;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.IO;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.PartialView.Tree;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
public class AncestorsPartialViewTreeController : PartialViewTreeControllerBase
|
||||
{
|
||||
private readonly IPartialViewTreeService _partialViewTreeService;
|
||||
|
||||
// TODO Remove the static service provider, and replace with base when the other constructors are obsoleted.
|
||||
public AncestorsPartialViewTreeController(IPartialViewTreeService partialViewTreeService)
|
||||
: this(partialViewTreeService, StaticServiceProvider.Instance.GetRequiredService<FileSystems>())
|
||||
=> _partialViewTreeService = partialViewTreeService;
|
||||
|
||||
[ActivatorUtilitiesConstructor]
|
||||
[Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")]
|
||||
public AncestorsPartialViewTreeController(IPartialViewTreeService partialViewTreeService, FileSystems fileSystems)
|
||||
: base(partialViewTreeService, fileSystems) =>
|
||||
_partialViewTreeService = partialViewTreeService;
|
||||
public AncestorsPartialViewTreeController(IPartialViewTreeService partialViewTreeService)
|
||||
: base(partialViewTreeService)
|
||||
{
|
||||
}
|
||||
|
||||
[Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")]
|
||||
[Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")]
|
||||
public AncestorsPartialViewTreeController(IPartialViewTreeService partialViewTreeService, FileSystems fileSystems)
|
||||
: base(partialViewTreeService, fileSystems)
|
||||
{
|
||||
}
|
||||
|
||||
[Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")]
|
||||
public AncestorsPartialViewTreeController(FileSystems fileSystems)
|
||||
: this(StaticServiceProvider.Instance.GetRequiredService<IPartialViewTreeService>(), fileSystems)
|
||||
: base(fileSystems)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
+14
-15
@@ -1,34 +1,33 @@
|
||||
using Asp.Versioning;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Core.IO;
|
||||
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
|
||||
using Umbraco.Cms.Api.Management.Services.FileSystem;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Tree;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.IO;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.PartialView.Tree;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
public class ChildrenPartialViewTreeController : PartialViewTreeControllerBase
|
||||
{
|
||||
private readonly IPartialViewTreeService _partialViewTreeService;
|
||||
|
||||
// TODO Remove the static service provider, and replace with base when the other constructors are obsoleted.
|
||||
public ChildrenPartialViewTreeController(IPartialViewTreeService partialViewTreeService)
|
||||
: this(partialViewTreeService, StaticServiceProvider.Instance.GetRequiredService<FileSystems>())
|
||||
=> _partialViewTreeService = partialViewTreeService;
|
||||
|
||||
[ActivatorUtilitiesConstructor]
|
||||
[Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")]
|
||||
public ChildrenPartialViewTreeController(IPartialViewTreeService partialViewTreeService, FileSystems fileSystems)
|
||||
: base(partialViewTreeService, fileSystems) =>
|
||||
_partialViewTreeService = partialViewTreeService;
|
||||
public ChildrenPartialViewTreeController(IPartialViewTreeService partialViewTreeService)
|
||||
: base(partialViewTreeService)
|
||||
{
|
||||
}
|
||||
|
||||
[Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")]
|
||||
[Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")]
|
||||
public ChildrenPartialViewTreeController(IPartialViewTreeService partialViewTreeService, FileSystems fileSystems)
|
||||
: base(partialViewTreeService, fileSystems)
|
||||
{
|
||||
}
|
||||
|
||||
[Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")]
|
||||
public ChildrenPartialViewTreeController(FileSystems fileSystems)
|
||||
: this(StaticServiceProvider.Instance.GetRequiredService<IPartialViewTreeService>(), fileSystems)
|
||||
: base(fileSystems)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
+9
-13
@@ -1,11 +1,9 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Api.Management.Controllers.Tree;
|
||||
using Umbraco.Cms.Api.Management.Routing;
|
||||
using Umbraco.Cms.Api.Management.Services.FileSystem;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.IO;
|
||||
using Umbraco.Cms.Web.Common.Authorization;
|
||||
|
||||
@@ -16,30 +14,28 @@ namespace Umbraco.Cms.Api.Management.Controllers.PartialView.Tree;
|
||||
[Authorize(Policy = AuthorizationPolicies.TreeAccessPartialViews)]
|
||||
public class PartialViewTreeControllerBase : FileSystemTreeControllerBase
|
||||
{
|
||||
private readonly IPartialViewTreeService _partialViewTreeService;
|
||||
|
||||
// TODO Remove the static service provider, and replace with base when the other constructors are obsoleted.
|
||||
public PartialViewTreeControllerBase(IPartialViewTreeService partialViewTreeService)
|
||||
: this(partialViewTreeService, StaticServiceProvider.Instance.GetRequiredService<FileSystems>()) =>
|
||||
_partialViewTreeService = partialViewTreeService;
|
||||
: base(partialViewTreeService)
|
||||
{
|
||||
FileSystem = null!;
|
||||
}
|
||||
|
||||
// FileSystem is required therefore, we can't remove it without some wizadry. When obsoletion is due, remove this.
|
||||
[ActivatorUtilitiesConstructor]
|
||||
[Obsolete("Scheduled for removal in Umbraco 18.")]
|
||||
// FileSystem is required therefore, we can't remove it without some wizardry. When obsoletion is due, remove this.
|
||||
[Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")]
|
||||
public PartialViewTreeControllerBase(IPartialViewTreeService partialViewTreeService, FileSystems fileSystems)
|
||||
: base(partialViewTreeService)
|
||||
{
|
||||
_partialViewTreeService = partialViewTreeService;
|
||||
FileSystem = fileSystems.PartialViewsFileSystem ??
|
||||
throw new ArgumentException("Missing scripts file system", nameof(fileSystems));
|
||||
}
|
||||
|
||||
[Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 18.")]
|
||||
[Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")]
|
||||
public PartialViewTreeControllerBase(FileSystems fileSystems)
|
||||
: this(StaticServiceProvider.Instance.GetRequiredService<IPartialViewTreeService>())
|
||||
: base()
|
||||
=> FileSystem = fileSystems.PartialViewsFileSystem ??
|
||||
throw new ArgumentException("Missing scripts file system", nameof(fileSystems));
|
||||
|
||||
[Obsolete("Included in the service class. Scheduled to be removed in Umbraco 18.")]
|
||||
[Obsolete("Included in the service class. Scheduled to be removed in Umbraco 19.")]
|
||||
protected override IFileSystem FileSystem { get; }
|
||||
}
|
||||
|
||||
+14
-15
@@ -1,34 +1,33 @@
|
||||
using Asp.Versioning;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Core.IO;
|
||||
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
|
||||
using Umbraco.Cms.Api.Management.Services.FileSystem;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Tree;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.IO;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.PartialView.Tree;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
public class RootPartialViewTreeController : PartialViewTreeControllerBase
|
||||
{
|
||||
private readonly IPartialViewTreeService _partialViewTreeService;
|
||||
|
||||
// TODO Remove the static service provider, and replace with base when the other constructors are obsoleted.
|
||||
public RootPartialViewTreeController(IPartialViewTreeService partialViewTreeService)
|
||||
: this(partialViewTreeService, StaticServiceProvider.Instance.GetRequiredService<FileSystems>())
|
||||
=> _partialViewTreeService = partialViewTreeService;
|
||||
|
||||
[ActivatorUtilitiesConstructor]
|
||||
[Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")]
|
||||
public RootPartialViewTreeController(IPartialViewTreeService partialViewTreeService, FileSystems fileSystems)
|
||||
: base(partialViewTreeService, fileSystems) =>
|
||||
_partialViewTreeService = partialViewTreeService;
|
||||
public RootPartialViewTreeController(IPartialViewTreeService partialViewTreeService)
|
||||
: base(partialViewTreeService)
|
||||
{
|
||||
}
|
||||
|
||||
[Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")]
|
||||
[Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")]
|
||||
public RootPartialViewTreeController(IPartialViewTreeService partialViewTreeService, FileSystems fileSystems)
|
||||
: base(partialViewTreeService, fileSystems)
|
||||
{
|
||||
}
|
||||
|
||||
[Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")]
|
||||
public RootPartialViewTreeController(FileSystems fileSystems)
|
||||
: this(StaticServiceProvider.Instance.GetRequiredService<IPartialViewTreeService>(), fileSystems)
|
||||
: base(fileSystems)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
+13
-14
@@ -1,32 +1,31 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
|
||||
using Umbraco.Cms.Api.Management.Services.FileSystem;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Tree;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.IO;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.PartialView.Tree;
|
||||
|
||||
public class SiblingsPartialViewTreeController : PartialViewTreeControllerBase
|
||||
{
|
||||
private readonly IPartialViewTreeService _partialViewTreeService;
|
||||
|
||||
// TODO Remove the static service provider, and replace with base when the other constructors are obsoleted.
|
||||
public SiblingsPartialViewTreeController(IPartialViewTreeService partialViewTreeService)
|
||||
: this(partialViewTreeService, StaticServiceProvider.Instance.GetRequiredService<FileSystems>())
|
||||
=> _partialViewTreeService = partialViewTreeService;
|
||||
|
||||
[ActivatorUtilitiesConstructor]
|
||||
[Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")]
|
||||
public SiblingsPartialViewTreeController(IPartialViewTreeService partialViewTreeService, FileSystems fileSystems)
|
||||
: base(partialViewTreeService, fileSystems) =>
|
||||
_partialViewTreeService = partialViewTreeService;
|
||||
public SiblingsPartialViewTreeController(IPartialViewTreeService partialViewTreeService)
|
||||
: base(partialViewTreeService)
|
||||
{
|
||||
}
|
||||
|
||||
[Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")]
|
||||
[Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")]
|
||||
public SiblingsPartialViewTreeController(IPartialViewTreeService partialViewTreeService, FileSystems fileSystems)
|
||||
: base(partialViewTreeService, fileSystems)
|
||||
{
|
||||
}
|
||||
|
||||
[Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")]
|
||||
public SiblingsPartialViewTreeController(FileSystems fileSystems)
|
||||
: this(StaticServiceProvider.Instance.GetRequiredService<IPartialViewTreeService>(), fileSystems)
|
||||
: base(fileSystems)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
+13
-14
@@ -1,10 +1,9 @@
|
||||
using Asp.Versioning;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Api.Management.Services.FileSystem;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Tree;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.IO;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.Script.Tree;
|
||||
@@ -12,22 +11,22 @@ namespace Umbraco.Cms.Api.Management.Controllers.Script.Tree;
|
||||
[ApiVersion("1.0")]
|
||||
public class AncestorsScriptTreeController : ScriptTreeControllerBase
|
||||
{
|
||||
private readonly IScriptTreeService _scriptTreeService;
|
||||
|
||||
// TODO Remove the static service provider, and replace with base when the other constructors are obsoleted.
|
||||
public AncestorsScriptTreeController(IScriptTreeService scriptTreeService)
|
||||
: this(scriptTreeService, StaticServiceProvider.Instance.GetRequiredService<FileSystems>())
|
||||
=> _scriptTreeService = scriptTreeService;
|
||||
|
||||
[ActivatorUtilitiesConstructor]
|
||||
[Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")]
|
||||
public AncestorsScriptTreeController(IScriptTreeService scriptTreeService, FileSystems fileSystems)
|
||||
: base(scriptTreeService, fileSystems) =>
|
||||
_scriptTreeService = scriptTreeService;
|
||||
public AncestorsScriptTreeController(IScriptTreeService scriptTreeService)
|
||||
: base(scriptTreeService)
|
||||
{
|
||||
}
|
||||
|
||||
[Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")]
|
||||
[Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")]
|
||||
public AncestorsScriptTreeController(IScriptTreeService scriptTreeService, FileSystems fileSystems)
|
||||
: base(scriptTreeService, fileSystems)
|
||||
{
|
||||
}
|
||||
|
||||
[Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")]
|
||||
public AncestorsScriptTreeController(FileSystems fileSystems)
|
||||
: this(StaticServiceProvider.Instance.GetRequiredService<IScriptTreeService>(), fileSystems)
|
||||
: base(fileSystems)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
+14
-15
@@ -1,34 +1,33 @@
|
||||
using Asp.Versioning;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Core.IO;
|
||||
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
|
||||
using Umbraco.Cms.Api.Management.Services.FileSystem;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Tree;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.IO;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.Script.Tree;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
public class ChildrenScriptTreeController : ScriptTreeControllerBase
|
||||
{
|
||||
private readonly IScriptTreeService _scriptTreeService;
|
||||
|
||||
// TODO Remove the static service provider, and replace with base when the other constructors are obsoleted.
|
||||
public ChildrenScriptTreeController(IScriptTreeService scriptTreeService)
|
||||
: this(scriptTreeService, StaticServiceProvider.Instance.GetRequiredService<FileSystems>())
|
||||
=> _scriptTreeService = scriptTreeService;
|
||||
|
||||
[ActivatorUtilitiesConstructor]
|
||||
[Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")]
|
||||
public ChildrenScriptTreeController(IScriptTreeService scriptTreeService, FileSystems fileSystems)
|
||||
: base(scriptTreeService, fileSystems) =>
|
||||
_scriptTreeService = scriptTreeService;
|
||||
public ChildrenScriptTreeController(IScriptTreeService scriptTreeService)
|
||||
: base(scriptTreeService)
|
||||
{
|
||||
}
|
||||
|
||||
[Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")]
|
||||
[Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")]
|
||||
public ChildrenScriptTreeController(IScriptTreeService scriptTreeService, FileSystems fileSystems)
|
||||
: base(scriptTreeService, fileSystems)
|
||||
{
|
||||
}
|
||||
|
||||
[Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")]
|
||||
public ChildrenScriptTreeController(FileSystems fileSystems)
|
||||
: this(StaticServiceProvider.Instance.GetRequiredService<IScriptTreeService>(), fileSystems)
|
||||
: base(fileSystems)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
+14
-15
@@ -1,34 +1,33 @@
|
||||
using Asp.Versioning;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Core.IO;
|
||||
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
|
||||
using Umbraco.Cms.Api.Management.Services.FileSystem;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Tree;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.IO;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.Script.Tree;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
public class RootScriptTreeController : ScriptTreeControllerBase
|
||||
{
|
||||
private readonly IScriptTreeService _scriptTreeService;
|
||||
|
||||
// TODO Remove the static service provider, and replace with base when the other constructors are obsoleted.
|
||||
public RootScriptTreeController(IScriptTreeService scriptTreeService)
|
||||
: this(scriptTreeService, StaticServiceProvider.Instance.GetRequiredService<FileSystems>())
|
||||
=> _scriptTreeService = scriptTreeService;
|
||||
|
||||
[ActivatorUtilitiesConstructor]
|
||||
[Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")]
|
||||
public RootScriptTreeController(IScriptTreeService scriptTreeService, FileSystems fileSystems)
|
||||
: base(scriptTreeService, fileSystems) =>
|
||||
_scriptTreeService = scriptTreeService;
|
||||
public RootScriptTreeController(IScriptTreeService scriptTreeService)
|
||||
: base(scriptTreeService)
|
||||
{
|
||||
}
|
||||
|
||||
[Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")]
|
||||
[Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")]
|
||||
public RootScriptTreeController(IScriptTreeService scriptTreeService, FileSystems fileSystems)
|
||||
: base(scriptTreeService, fileSystems)
|
||||
{
|
||||
}
|
||||
|
||||
[Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")]
|
||||
public RootScriptTreeController(FileSystems fileSystems)
|
||||
: this(StaticServiceProvider.Instance.GetRequiredService<IScriptTreeService>(), fileSystems)
|
||||
: base(fileSystems)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Api.Management.Controllers.Tree;
|
||||
using Umbraco.Cms.Api.Management.Routing;
|
||||
using Umbraco.Cms.Api.Management.Services.FileSystem;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.IO;
|
||||
using Umbraco.Cms.Web.Common.Authorization;
|
||||
|
||||
@@ -16,30 +14,28 @@ namespace Umbraco.Cms.Api.Management.Controllers.Script.Tree;
|
||||
[Authorize(Policy = AuthorizationPolicies.TreeAccessScripts)]
|
||||
public class ScriptTreeControllerBase : FileSystemTreeControllerBase
|
||||
{
|
||||
private readonly IScriptTreeService _scriptTreeService;
|
||||
|
||||
// TODO Remove the static service provider, and replace with base when the other constructors are obsoleted.
|
||||
public ScriptTreeControllerBase(IScriptTreeService scriptTreeService)
|
||||
: this(scriptTreeService, StaticServiceProvider.Instance.GetRequiredService<FileSystems>()) =>
|
||||
_scriptTreeService = scriptTreeService;
|
||||
: base(scriptTreeService)
|
||||
{
|
||||
FileSystem = null!;
|
||||
}
|
||||
|
||||
// FileSystem is required therefore, we can't remove it without some wizadry. When obsoletion is due, remove this.
|
||||
[ActivatorUtilitiesConstructor]
|
||||
[Obsolete("Scheduled for removal in Umbraco 18.")]
|
||||
// FileSystem is required therefore, we can't remove it without some wizardry. When obsoletion is due, remove this.
|
||||
[Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")]
|
||||
public ScriptTreeControllerBase(IScriptTreeService scriptTreeService, FileSystems fileSystems)
|
||||
: base(scriptTreeService)
|
||||
{
|
||||
_scriptTreeService = scriptTreeService;
|
||||
FileSystem = fileSystems.ScriptsFileSystem ??
|
||||
throw new ArgumentException("Missing scripts file system", nameof(fileSystems));
|
||||
}
|
||||
|
||||
[Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 18.")]
|
||||
[Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")]
|
||||
public ScriptTreeControllerBase(FileSystems fileSystems)
|
||||
: this(StaticServiceProvider.Instance.GetRequiredService<IScriptTreeService>())
|
||||
: base()
|
||||
=> FileSystem = fileSystems.ScriptsFileSystem ??
|
||||
throw new ArgumentException("Missing scripts file system", nameof(fileSystems));
|
||||
|
||||
[Obsolete("Included in the service class. Scheduled to be removed in Umbraco 18.")]
|
||||
[Obsolete("Included in the service class. Scheduled to be removed in Umbraco 19.")]
|
||||
protected override IFileSystem FileSystem { get; }
|
||||
}
|
||||
|
||||
+13
-14
@@ -1,32 +1,31 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
|
||||
using Umbraco.Cms.Api.Management.Services.FileSystem;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Tree;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.IO;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.Script.Tree;
|
||||
|
||||
public class SiblingsScriptTreeController : ScriptTreeControllerBase
|
||||
{
|
||||
private readonly IScriptTreeService _scriptTreeService;
|
||||
|
||||
// TODO Remove the static service provider, and replace with base when the other constructors are obsoleted.
|
||||
public SiblingsScriptTreeController(IScriptTreeService scriptTreeService)
|
||||
: this(scriptTreeService, StaticServiceProvider.Instance.GetRequiredService<FileSystems>())
|
||||
=> _scriptTreeService = scriptTreeService;
|
||||
|
||||
[ActivatorUtilitiesConstructor]
|
||||
[Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")]
|
||||
public SiblingsScriptTreeController(IScriptTreeService scriptTreeService, FileSystems fileSystems)
|
||||
: base(scriptTreeService, fileSystems) =>
|
||||
_scriptTreeService = scriptTreeService;
|
||||
public SiblingsScriptTreeController(IScriptTreeService scriptTreeService)
|
||||
: base(scriptTreeService)
|
||||
{
|
||||
}
|
||||
|
||||
[Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")]
|
||||
[Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")]
|
||||
public SiblingsScriptTreeController(IScriptTreeService scriptTreeService, FileSystems fileSystems)
|
||||
: base(scriptTreeService, fileSystems)
|
||||
{
|
||||
}
|
||||
|
||||
[Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")]
|
||||
public SiblingsScriptTreeController(FileSystems fileSystems)
|
||||
: this(StaticServiceProvider.Instance.GetRequiredService<IScriptTreeService>(), fileSystems)
|
||||
: base(fileSystems)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
+13
-14
@@ -1,10 +1,9 @@
|
||||
using Asp.Versioning;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Api.Management.Services.FileSystem;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Tree;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.IO;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.Stylesheet.Tree;
|
||||
@@ -12,22 +11,22 @@ namespace Umbraco.Cms.Api.Management.Controllers.Stylesheet.Tree;
|
||||
[ApiVersion("1.0")]
|
||||
public class AncestorsStylesheetTreeController : StylesheetTreeControllerBase
|
||||
{
|
||||
private readonly IStyleSheetTreeService _styleSheetTreeService;
|
||||
|
||||
// TODO Remove the static service provider, and replace with base when the other constructors are obsoleted.
|
||||
public AncestorsStylesheetTreeController(IStyleSheetTreeService styleSheetTreeService)
|
||||
: this(styleSheetTreeService, StaticServiceProvider.Instance.GetRequiredService<FileSystems>())
|
||||
=> _styleSheetTreeService = styleSheetTreeService;
|
||||
|
||||
[ActivatorUtilitiesConstructor]
|
||||
[Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")]
|
||||
public AncestorsStylesheetTreeController(IStyleSheetTreeService styleSheetTreeService, FileSystems fileSystems)
|
||||
: base(styleSheetTreeService, fileSystems) =>
|
||||
_styleSheetTreeService = styleSheetTreeService;
|
||||
public AncestorsStylesheetTreeController(IStyleSheetTreeService styleSheetTreeService)
|
||||
: base(styleSheetTreeService)
|
||||
{
|
||||
}
|
||||
|
||||
[Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")]
|
||||
[Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")]
|
||||
public AncestorsStylesheetTreeController(IStyleSheetTreeService styleSheetTreeService, FileSystems fileSystems)
|
||||
: base(styleSheetTreeService, fileSystems)
|
||||
{
|
||||
}
|
||||
|
||||
[Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")]
|
||||
public AncestorsStylesheetTreeController(FileSystems fileSystems)
|
||||
: this(StaticServiceProvider.Instance.GetRequiredService<IStyleSheetTreeService>(), fileSystems)
|
||||
: base(fileSystems)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
+17
-17
@@ -1,36 +1,36 @@
|
||||
using Asp.Versioning;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Core.IO;
|
||||
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
|
||||
using Umbraco.Cms.Api.Management.Services.FileSystem;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Tree;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.IO;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.Stylesheet.Tree;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
public class ChildrenStylesheetTreeController : StylesheetTreeControllerBase
|
||||
{
|
||||
private readonly IStyleSheetTreeService _styleSheetTreeService;
|
||||
|
||||
// TODO Remove the static service provider, and replace with base when the other constructors are obsoleted.
|
||||
public ChildrenStylesheetTreeController(IStyleSheetTreeService styleSheetTreeService)
|
||||
: this(styleSheetTreeService, StaticServiceProvider.Instance.GetRequiredService<FileSystems>())
|
||||
=> _styleSheetTreeService = styleSheetTreeService;
|
||||
|
||||
[ActivatorUtilitiesConstructor]
|
||||
[Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")]
|
||||
public ChildrenStylesheetTreeController(IStyleSheetTreeService styleSheetTreeService, FileSystems fileSystems)
|
||||
: base(styleSheetTreeService, fileSystems) =>
|
||||
_styleSheetTreeService = styleSheetTreeService;
|
||||
|
||||
[Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")]
|
||||
public ChildrenStylesheetTreeController(FileSystems fileSystems)
|
||||
: this(StaticServiceProvider.Instance.GetRequiredService<IStyleSheetTreeService>(), fileSystems)
|
||||
public ChildrenStylesheetTreeController(IStyleSheetTreeService styleSheetTreeService)
|
||||
: base(styleSheetTreeService)
|
||||
{
|
||||
}
|
||||
|
||||
[Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")]
|
||||
public ChildrenStylesheetTreeController(IStyleSheetTreeService styleSheetTreeService, FileSystems fileSystems)
|
||||
: base(styleSheetTreeService, fileSystems)
|
||||
{
|
||||
}
|
||||
|
||||
[Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")]
|
||||
public ChildrenStylesheetTreeController(FileSystems fileSystems)
|
||||
: base(fileSystems)
|
||||
{
|
||||
}
|
||||
|
||||
[HttpGet("children")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(PagedViewModel<FileSystemTreeItemPresentationModel>), StatusCodes.Status200OK)]
|
||||
|
||||
+14
-15
@@ -1,34 +1,33 @@
|
||||
using Asp.Versioning;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Core.IO;
|
||||
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
|
||||
using Umbraco.Cms.Api.Management.Services.FileSystem;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Tree;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.IO;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.Stylesheet.Tree;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
public class RootStylesheetTreeController : StylesheetTreeControllerBase
|
||||
{
|
||||
private readonly IStyleSheetTreeService _styleSheetTreeService;
|
||||
|
||||
// TODO Remove the static service provider, and replace with base when the other constructors are obsoleted.
|
||||
public RootStylesheetTreeController(IStyleSheetTreeService styleSheetTreeService)
|
||||
: this(styleSheetTreeService, StaticServiceProvider.Instance.GetRequiredService<FileSystems>())
|
||||
=> _styleSheetTreeService = styleSheetTreeService;
|
||||
|
||||
[ActivatorUtilitiesConstructor]
|
||||
[Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")]
|
||||
public RootStylesheetTreeController(IStyleSheetTreeService styleSheetTreeService, FileSystems fileSystems)
|
||||
: base(styleSheetTreeService, fileSystems) =>
|
||||
_styleSheetTreeService = styleSheetTreeService;
|
||||
public RootStylesheetTreeController(IStyleSheetTreeService styleSheetTreeService)
|
||||
: base(styleSheetTreeService)
|
||||
{
|
||||
}
|
||||
|
||||
[Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")]
|
||||
[Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")]
|
||||
public RootStylesheetTreeController(IStyleSheetTreeService styleSheetTreeService, FileSystems fileSystems)
|
||||
: base(styleSheetTreeService, fileSystems)
|
||||
{
|
||||
}
|
||||
|
||||
[Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")]
|
||||
public RootStylesheetTreeController(FileSystems fileSystems)
|
||||
: this(StaticServiceProvider.Instance.GetRequiredService<IStyleSheetTreeService>(), fileSystems)
|
||||
: base(fileSystems)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
+13
-14
@@ -1,32 +1,31 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
|
||||
using Umbraco.Cms.Api.Management.Services.FileSystem;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Tree;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.IO;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.Stylesheet.Tree;
|
||||
|
||||
public class SiblingsStylesheetTreeController : StylesheetTreeControllerBase
|
||||
{
|
||||
private readonly IStyleSheetTreeService _styleSheetTreeService;
|
||||
|
||||
// TODO Remove the static service provider, and replace with base when the other constructors are obsoleted.
|
||||
public SiblingsStylesheetTreeController(IStyleSheetTreeService styleSheetTreeService)
|
||||
: this(styleSheetTreeService, StaticServiceProvider.Instance.GetRequiredService<FileSystems>())
|
||||
=> _styleSheetTreeService = styleSheetTreeService;
|
||||
|
||||
[ActivatorUtilitiesConstructor]
|
||||
[Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")]
|
||||
public SiblingsStylesheetTreeController(IStyleSheetTreeService styleSheetTreeService, FileSystems fileSystems)
|
||||
: base(styleSheetTreeService, fileSystems) =>
|
||||
_styleSheetTreeService = styleSheetTreeService;
|
||||
public SiblingsStylesheetTreeController(IStyleSheetTreeService styleSheetTreeService)
|
||||
: base(styleSheetTreeService)
|
||||
{
|
||||
}
|
||||
|
||||
[Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")]
|
||||
[Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")]
|
||||
public SiblingsStylesheetTreeController(IStyleSheetTreeService styleSheetTreeService, FileSystems fileSystems)
|
||||
: base(styleSheetTreeService, fileSystems)
|
||||
{
|
||||
}
|
||||
|
||||
[Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")]
|
||||
public SiblingsStylesheetTreeController(FileSystems fileSystems)
|
||||
: this(StaticServiceProvider.Instance.GetRequiredService<IStyleSheetTreeService>(), fileSystems)
|
||||
: base(fileSystems)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
+8
-12
@@ -1,11 +1,9 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Api.Management.Controllers.Tree;
|
||||
using Umbraco.Cms.Api.Management.Routing;
|
||||
using Umbraco.Cms.Api.Management.Services.FileSystem;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.IO;
|
||||
using Umbraco.Cms.Web.Common.Authorization;
|
||||
|
||||
@@ -16,30 +14,28 @@ namespace Umbraco.Cms.Api.Management.Controllers.Stylesheet.Tree;
|
||||
[Authorize(Policy = AuthorizationPolicies.TreeAccessStylesheets)]
|
||||
public class StylesheetTreeControllerBase : FileSystemTreeControllerBase
|
||||
{
|
||||
private readonly IStyleSheetTreeService _styleSheetTreeService;
|
||||
|
||||
// TODO Remove the static service provider, and replace with base when the other constructors are obsoleted.
|
||||
public StylesheetTreeControllerBase(IStyleSheetTreeService styleSheetTreeService)
|
||||
: this(styleSheetTreeService, StaticServiceProvider.Instance.GetRequiredService<FileSystems>()) =>
|
||||
_styleSheetTreeService = styleSheetTreeService;
|
||||
: base(styleSheetTreeService)
|
||||
{
|
||||
FileSystem = null!;
|
||||
}
|
||||
|
||||
// FileSystem is required therefore, we can't remove it without some wizadry. When obsoletion is due, remove this.
|
||||
[ActivatorUtilitiesConstructor]
|
||||
[Obsolete("Scheduled for removal in Umbraco 18.")]
|
||||
[Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")]
|
||||
public StylesheetTreeControllerBase(IStyleSheetTreeService styleSheetTreeService, FileSystems fileSystems)
|
||||
: base(styleSheetTreeService)
|
||||
{
|
||||
_styleSheetTreeService = styleSheetTreeService;
|
||||
FileSystem = fileSystems.ScriptsFileSystem ??
|
||||
throw new ArgumentException("Missing scripts file system", nameof(fileSystems));
|
||||
}
|
||||
|
||||
[Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 18.")]
|
||||
[Obsolete("Please use the constructor taking all parameters. Scheduled to be removed in Umbraco 19.")]
|
||||
public StylesheetTreeControllerBase(FileSystems fileSystems)
|
||||
: this(StaticServiceProvider.Instance.GetRequiredService<IStyleSheetTreeService>())
|
||||
: base()
|
||||
=> FileSystem = fileSystems.ScriptsFileSystem ??
|
||||
throw new ArgumentException("Missing scripts file system", nameof(fileSystems));
|
||||
|
||||
[Obsolete("Included in the service class. Scheduled to be removed in Umbraco 18.")]
|
||||
[Obsolete("Included in the service class. Scheduled to be removed in Umbraco 19.")]
|
||||
protected override IFileSystem FileSystem { get; }
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ using Umbraco.Cms.Api.Management.Extensions;
|
||||
using Umbraco.Cms.Api.Management.Services.FileSystem;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.FileSystem;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Tree;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.IO;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
@@ -13,23 +12,30 @@ namespace Umbraco.Cms.Api.Management.Controllers.Tree;
|
||||
|
||||
public abstract class FileSystemTreeControllerBase : ManagementApiControllerBase
|
||||
{
|
||||
private readonly IFileSystemTreeService _fileSystemTreeService;
|
||||
private readonly IFileSystemTreeService _fileSystemTreeService = null!;
|
||||
|
||||
[Obsolete("Has been moved to the individual services. Scheduled to be removed in Umbraco 18.")]
|
||||
/// <summary>
|
||||
/// Indicates whether to use the IFileSystemTreeService or the legacy implementation.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is retained to ensure that any controllers outside of the CMS that use this base class with the obsolete constructor
|
||||
/// continue to function until they can be updated to use the new service.
|
||||
/// To be removed along with the constructor taking no parameters in Umbraco 19.
|
||||
/// </remarks>
|
||||
private readonly bool _useFileSystemTreeService = true;
|
||||
|
||||
[Obsolete("Has been moved to the individual services. Scheduled to be removed in Umbraco 19.")]
|
||||
protected abstract IFileSystem FileSystem { get; }
|
||||
|
||||
[ActivatorUtilitiesConstructor]
|
||||
protected FileSystemTreeControllerBase(IFileSystemTreeService fileSystemTreeService) => _fileSystemTreeService = fileSystemTreeService;
|
||||
|
||||
[Obsolete("Use the other constructor. Scheduled for removal in Umbraco 18.")]
|
||||
protected FileSystemTreeControllerBase()
|
||||
: this(StaticServiceProvider.Instance.GetRequiredService<IScriptTreeService>())
|
||||
{
|
||||
}
|
||||
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 19.")]
|
||||
protected FileSystemTreeControllerBase() => _useFileSystemTreeService = false;
|
||||
|
||||
protected Task<ActionResult<PagedViewModel<FileSystemTreeItemPresentationModel>>> GetRoot(int skip, int take)
|
||||
{
|
||||
FileSystemTreeItemPresentationModel[] viewModels = _fileSystemTreeService.GetPathViewModels(string.Empty, skip, take, out var totalItems);
|
||||
FileSystemTreeItemPresentationModel[] viewModels = GetPathViewModels(string.Empty, skip, take, out var totalItems);
|
||||
|
||||
PagedViewModel<FileSystemTreeItemPresentationModel> result = PagedViewModel(viewModels, totalItems);
|
||||
return Task.FromResult<ActionResult<PagedViewModel<FileSystemTreeItemPresentationModel>>>(Ok(result));
|
||||
@@ -37,14 +43,14 @@ public abstract class FileSystemTreeControllerBase : ManagementApiControllerBase
|
||||
|
||||
protected Task<ActionResult<PagedViewModel<FileSystemTreeItemPresentationModel>>> GetChildren(string path, int skip, int take)
|
||||
{
|
||||
FileSystemTreeItemPresentationModel[] viewModels = _fileSystemTreeService.GetPathViewModels(path, skip, take, out var totalItems);
|
||||
FileSystemTreeItemPresentationModel[] viewModels = GetPathViewModels(path, skip, take, out var totalItems);
|
||||
|
||||
PagedViewModel<FileSystemTreeItemPresentationModel> result = PagedViewModel(viewModels, totalItems);
|
||||
return Task.FromResult<ActionResult<PagedViewModel<FileSystemTreeItemPresentationModel>>>(Ok(result));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the sibling of the targeted item based on its path.
|
||||
/// Gets the siblings of the targeted item based on its path.
|
||||
/// </summary>
|
||||
/// <param name="path">The path to the item.</param>
|
||||
/// <param name="before">The amount of siblings you want to fetch from before the items position in the array.</param>
|
||||
@@ -61,17 +67,19 @@ public abstract class FileSystemTreeControllerBase : ManagementApiControllerBase
|
||||
protected virtual Task<ActionResult<IEnumerable<FileSystemTreeItemPresentationModel>>> GetAncestors(string path, bool includeSelf = true)
|
||||
{
|
||||
path = path.VirtualPathToSystemPath();
|
||||
FileSystemTreeItemPresentationModel[] models = _fileSystemTreeService.GetAncestorModels(path, includeSelf);
|
||||
FileSystemTreeItemPresentationModel[] models = GetAncestorModels(path, includeSelf);
|
||||
|
||||
return Task.FromResult<ActionResult<IEnumerable<FileSystemTreeItemPresentationModel>>>(Ok(models));
|
||||
}
|
||||
|
||||
private PagedViewModel<FileSystemTreeItemPresentationModel> PagedViewModel(IEnumerable<FileSystemTreeItemPresentationModel> viewModels, long totalItems)
|
||||
=> new() { Total = totalItems, Items = viewModels };
|
||||
|
||||
[Obsolete("Has been moved to FileSystemTreeServiceBase. Scheduled for removal in Umbraco 18.")]
|
||||
[Obsolete("Has been moved to FileSystemTreeServiceBase. Scheduled for removal in Umbraco 19.")]
|
||||
protected virtual FileSystemTreeItemPresentationModel[] GetAncestorModels(string path, bool includeSelf)
|
||||
{
|
||||
if (_useFileSystemTreeService)
|
||||
{
|
||||
return _fileSystemTreeService.GetAncestorModels(path, includeSelf);
|
||||
}
|
||||
|
||||
var directories = path.Split(Path.DirectorySeparatorChar).Take(Range.EndAt(Index.FromEnd(1))).ToArray();
|
||||
var result = directories
|
||||
.Select((directory, index) => MapViewModel(string.Join(Path.DirectorySeparatorChar, directories.Take(index + 1)), directory, true))
|
||||
@@ -86,28 +94,59 @@ public abstract class FileSystemTreeControllerBase : ManagementApiControllerBase
|
||||
return result.ToArray();
|
||||
}
|
||||
|
||||
[Obsolete("Has been moved to FileSystemTreeServiceBase. Scheduled for removal in Umbraco 18.")]
|
||||
[Obsolete("Has been moved to FileSystemTreeServiceBase. Scheduled for removal in Umbraco 19.")]
|
||||
protected virtual string[] GetDirectories(string path) => FileSystem
|
||||
.GetDirectories(path)
|
||||
.OrderBy(directory => directory)
|
||||
.ToArray();
|
||||
|
||||
[Obsolete("Has been moved to FileSystemTreeServiceBase. Scheduled for removal in Umbraco 18.")]
|
||||
[Obsolete("Has been moved to FileSystemTreeServiceBase. Scheduled for removal in Umbraco 19.")]
|
||||
protected virtual string[] GetFiles(string path) => FileSystem
|
||||
.GetFiles(path)
|
||||
.OrderBy(file => file)
|
||||
.ToArray();
|
||||
|
||||
[Obsolete("Has been moved to FileSystemTreeServiceBase. Scheduled for removal in Umbraco 18.")]
|
||||
[Obsolete("Has been moved to FileSystemTreeServiceBase. Scheduled for removal in Umbraco 19.")]
|
||||
protected virtual bool DirectoryHasChildren(string path)
|
||||
=> FileSystem.GetFiles(path).Any() || FileSystem.GetDirectories(path).Any();
|
||||
|
||||
[Obsolete("Has been moved to FileSystemTreeServiceBase. Scheduled for removal in Umbraco 18.")]
|
||||
[Obsolete("Has been moved to FileSystemTreeServiceBase. Scheduled for removal in Umbraco 19.")]
|
||||
private string GetFileSystemItemName(bool isFolder, string itemPath) => isFolder
|
||||
? Path.GetFileName(itemPath)
|
||||
: FileSystem.GetFileName(itemPath);
|
||||
|
||||
[Obsolete("Has been moved to FileSystemTreeServiceBase. Scheduled for removal in Umbraco 18.")]
|
||||
private FileSystemTreeItemPresentationModel[] GetPathViewModels(string path, int skip, int take, out long totalItems)
|
||||
{
|
||||
if (_useFileSystemTreeService)
|
||||
{
|
||||
return _fileSystemTreeService.GetPathViewModels(path, skip, take, out totalItems);
|
||||
}
|
||||
|
||||
path = path.VirtualPathToSystemPath();
|
||||
var allItems = GetDirectories(path)
|
||||
.Select(directory => new { Path = directory, IsFolder = true })
|
||||
.Union(GetFiles(path).Select(file => new { Path = file, IsFolder = false }))
|
||||
.ToArray();
|
||||
|
||||
totalItems = allItems.Length;
|
||||
|
||||
FileSystemTreeItemPresentationModel ViewModel(string itemPath, bool isFolder)
|
||||
=> MapViewModel(
|
||||
itemPath,
|
||||
GetFileSystemItemName(isFolder, itemPath),
|
||||
isFolder);
|
||||
|
||||
return allItems
|
||||
.Skip(skip)
|
||||
.Take(take)
|
||||
.Select(item => ViewModel(item.Path, item.IsFolder))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private PagedViewModel<FileSystemTreeItemPresentationModel> PagedViewModel(IEnumerable<FileSystemTreeItemPresentationModel> viewModels, long totalItems)
|
||||
=> new() { Total = totalItems, Items = viewModels };
|
||||
|
||||
[Obsolete("Has been moved to FileSystemTreeServiceBase. Scheduled for removal in Umbraco 19.")]
|
||||
private FileSystemTreeItemPresentationModel MapViewModel(string path, string name, bool isFolder)
|
||||
{
|
||||
var parentPath = Path.GetDirectoryName(path);
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
using Asp.Versioning;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Api.Management.Controllers.UserGroup;
|
||||
using Umbraco.Cms.Api.Management.Routing;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.User;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Security.Authorization;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
@@ -25,11 +28,26 @@ public class UpdateUserGroupsUserController : UserGroupControllerBase
|
||||
{
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
private readonly IUserGroupService _userGroupService;
|
||||
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
|
||||
|
||||
public UpdateUserGroupsUserController(IAuthorizationService authorizationService, IUserGroupService userGroupService)
|
||||
[ActivatorUtilitiesConstructor]
|
||||
public UpdateUserGroupsUserController(
|
||||
IAuthorizationService authorizationService,
|
||||
IUserGroupService userGroupService,
|
||||
IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
|
||||
{
|
||||
_authorizationService = authorizationService;
|
||||
_userGroupService = userGroupService;
|
||||
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
|
||||
}
|
||||
|
||||
[Obsolete("Please use the constructor accepting all parameters. Scheduled for removal in Umbraco 19.")]
|
||||
public UpdateUserGroupsUserController(IAuthorizationService authorizationService, IUserGroupService userGroupService)
|
||||
: this(
|
||||
authorizationService,
|
||||
userGroupService,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IBackOfficeSecurityAccessor>())
|
||||
{
|
||||
}
|
||||
|
||||
[HttpPost("set-user-groups")]
|
||||
@@ -51,7 +69,8 @@ public class UpdateUserGroupsUserController : UserGroupControllerBase
|
||||
|
||||
Attempt<UserGroupOperationStatus> result = await _userGroupService.UpdateUserGroupsOnUsersAsync(
|
||||
requestModel.UserGroupIds.Select(x => x.Id).ToHashSet(),
|
||||
requestModel.UserIds.Select(x => x.Id).ToHashSet());
|
||||
requestModel.UserIds.Select(x => x.Id).ToHashSet(),
|
||||
CurrentUserKey(_backOfficeSecurityAccessor));
|
||||
|
||||
return result.Success
|
||||
? Ok()
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
using Umbraco.Cms.Api.Management.Factories;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Umbraco.Cms.Api.Common.Accessors;
|
||||
using Umbraco.Cms.Api.Common.Rendering;
|
||||
using Umbraco.Cms.Api.Management.Factories;
|
||||
using Umbraco.Cms.Api.Management.Mapping.Webhook;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
@@ -12,6 +16,11 @@ internal static class WebhooksBuilderExtensions
|
||||
builder.Services.AddUnique<IWebhookPresentationFactory, WebhookPresentationFactory>();
|
||||
builder.AddMapDefinition<WebhookEventMapDefinition>();
|
||||
|
||||
// We have to use TryAdd here, as if they are registered by the delivery API, we don't want to register them
|
||||
// Delivery API will also overwrite these IF it is enabled.
|
||||
builder.Services.TryAddScoped<IOutputExpansionStrategy, ElementOnlyOutputExpansionStrategy>();
|
||||
builder.Services.TryAddSingleton<IOutputExpansionStrategyAccessor, RequestContextOutputExpansionStrategyAccessor>();
|
||||
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ internal static class DocumentVariantStateHelper
|
||||
culture,
|
||||
content.Edited,
|
||||
content.Published,
|
||||
content.Trashed,
|
||||
content.AvailableCultures,
|
||||
content.EditedCultures ?? Enumerable.Empty<string>(),
|
||||
content.PublishedCultures);
|
||||
@@ -22,17 +23,23 @@ internal static class DocumentVariantStateHelper
|
||||
culture,
|
||||
content.Edited,
|
||||
content.Published,
|
||||
content.Trashed,
|
||||
content.CultureNames.Keys,
|
||||
content.EditedCultures,
|
||||
content.PublishedCultures);
|
||||
|
||||
private static DocumentVariantState GetState(IEntity entity, string? culture, bool edited, bool published, IEnumerable<string> availableCultures, IEnumerable<string> editedCultures, IEnumerable<string> publishedCultures)
|
||||
private static DocumentVariantState GetState(IEntity entity, string? culture, bool edited, bool published, bool trashed, IEnumerable<string> availableCultures, IEnumerable<string> editedCultures, IEnumerable<string> publishedCultures)
|
||||
{
|
||||
if (entity.Id <= 0 || (culture is not null && availableCultures.Contains(culture) is false))
|
||||
{
|
||||
return DocumentVariantState.NotCreated;
|
||||
}
|
||||
|
||||
if (trashed)
|
||||
{
|
||||
return DocumentVariantState.Trashed;
|
||||
}
|
||||
|
||||
var isDraft = published is false ||
|
||||
(culture != null && publishedCultures.Contains(culture) is false);
|
||||
if (isDraft)
|
||||
|
||||
@@ -37322,6 +37322,9 @@
|
||||
{
|
||||
"$ref": "#/components/schemas/DocumentPropertyValuePermissionPresentationModel"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/DocumentTypePermissionPresentationModel"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/UnknownTypePermissionPresentationModel"
|
||||
}
|
||||
@@ -37608,6 +37611,9 @@
|
||||
{
|
||||
"$ref": "#/components/schemas/DocumentPropertyValuePermissionPresentationModel"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/DocumentTypePermissionPresentationModel"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/UnknownTypePermissionPresentationModel"
|
||||
}
|
||||
@@ -38444,7 +38450,8 @@
|
||||
"type": "boolean"
|
||||
},
|
||||
"allowNonExistingSegmentsCreation": {
|
||||
"type": "boolean"
|
||||
"type": "boolean",
|
||||
"deprecated": true
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
@@ -39088,6 +39095,36 @@
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"DocumentTypePermissionPresentationModel": {
|
||||
"required": [
|
||||
"$type",
|
||||
"documentTypeAlias",
|
||||
"verbs"
|
||||
],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"$type": {
|
||||
"type": "string"
|
||||
},
|
||||
"verbs": {
|
||||
"uniqueItems": true,
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"documentTypeAlias": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"discriminator": {
|
||||
"propertyName": "$type",
|
||||
"mapping": {
|
||||
"DocumentTypePermissionPresentationModel": "#/components/schemas/DocumentTypePermissionPresentationModel"
|
||||
}
|
||||
}
|
||||
},
|
||||
"DocumentTypePropertyTypeContainerResponseModel": {
|
||||
"required": [
|
||||
"id",
|
||||
@@ -39683,7 +39720,8 @@
|
||||
"NotCreated",
|
||||
"Draft",
|
||||
"Published",
|
||||
"PublishedPendingChanges"
|
||||
"PublishedPendingChanges",
|
||||
"Trashed"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
@@ -40178,7 +40216,7 @@
|
||||
},
|
||||
"actionParameters": {
|
||||
"type": "object",
|
||||
"additionalProperties": { },
|
||||
"additionalProperties": {},
|
||||
"nullable": true
|
||||
}
|
||||
},
|
||||
@@ -40803,7 +40841,7 @@
|
||||
},
|
||||
"extensions": {
|
||||
"type": "array",
|
||||
"items": { }
|
||||
"items": {}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
@@ -44640,7 +44678,7 @@
|
||||
"nullable": true
|
||||
}
|
||||
},
|
||||
"additionalProperties": { }
|
||||
"additionalProperties": {}
|
||||
},
|
||||
"ProblemDetailsBuilderModel": {
|
||||
"type": "object",
|
||||
@@ -47813,6 +47851,9 @@
|
||||
{
|
||||
"$ref": "#/components/schemas/DocumentPropertyValuePermissionPresentationModel"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/DocumentTypePermissionPresentationModel"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/UnknownTypePermissionPresentationModel"
|
||||
}
|
||||
@@ -48250,6 +48291,9 @@
|
||||
{
|
||||
"$ref": "#/components/schemas/DocumentPropertyValuePermissionPresentationModel"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/DocumentTypePermissionPresentationModel"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/UnknownTypePermissionPresentationModel"
|
||||
}
|
||||
@@ -48932,4 +48976,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,9 +75,12 @@ public abstract class FileSystemTreeServiceBase : IFileSystemTreeService
|
||||
|
||||
public string[] GetFiles(string path) => FileSystem
|
||||
.GetFiles(path)
|
||||
.Where(FilterFile)
|
||||
.OrderBy(file => file)
|
||||
.ToArray();
|
||||
|
||||
protected virtual bool FilterFile(string file) => true;
|
||||
|
||||
public bool DirectoryHasChildren(string path)
|
||||
=> FileSystem.GetFiles(path).Any() || FileSystem.GetDirectories(path).Any();
|
||||
|
||||
|
||||
@@ -11,4 +11,6 @@ public class PartialViewTreeService : FileSystemTreeServiceBase, IPartialViewTre
|
||||
public PartialViewTreeService(FileSystems fileSystems) =>
|
||||
_partialViewFileSystem = fileSystems.PartialViewsFileSystem ??
|
||||
throw new ArgumentException("Missing partial views file system", nameof(fileSystems));
|
||||
|
||||
protected override bool FilterFile(string file) => file.ToLowerInvariant().EndsWith(".cshtml");
|
||||
}
|
||||
|
||||
@@ -11,4 +11,6 @@ public class ScriptTreeService : FileSystemTreeServiceBase, IScriptTreeService
|
||||
public ScriptTreeService(FileSystems fileSystems) =>
|
||||
_scriptFileSystem = fileSystems.ScriptsFileSystem ??
|
||||
throw new ArgumentException("Missing partial views file system", nameof(fileSystems));
|
||||
|
||||
protected override bool FilterFile(string file) => file.ToLowerInvariant().EndsWith(".js");
|
||||
}
|
||||
|
||||
@@ -10,5 +10,7 @@ public class StyleSheetTreeService : FileSystemTreeServiceBase, IStyleSheetTreeS
|
||||
|
||||
public StyleSheetTreeService(FileSystems fileSystems) =>
|
||||
_scriptFileSystem = fileSystems.StylesheetsFileSystem ??
|
||||
throw new ArgumentException("Missing partial views file system", nameof(fileSystems));
|
||||
throw new ArgumentException("Missing stylesheets file system", nameof(fileSystems));
|
||||
|
||||
protected override bool FilterFile(string file) => file.ToLowerInvariant().EndsWith(".css");
|
||||
}
|
||||
|
||||
@@ -24,4 +24,9 @@ public enum DocumentVariantState
|
||||
/// The item is published and there are pending changes
|
||||
/// </summary>
|
||||
PublishedPendingChanges = 4,
|
||||
|
||||
/// <summary>
|
||||
/// The item is in the recycle bin
|
||||
/// </summary>
|
||||
Trashed = 5,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace Umbraco.Cms.Core.Configuration.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Typed configuration options for back-office token cookie settings.
|
||||
/// </summary>
|
||||
[UmbracoOptions(Constants.Configuration.ConfigBackOfficeTokenCookie)]
|
||||
[Obsolete("This will be replaced with a different authentication scheme. Scheduled for removal in Umbraco 18.")]
|
||||
public class BackOfficeTokenCookieSettings
|
||||
{
|
||||
private const bool StaticEnabled = false;
|
||||
|
||||
private const string StaticSameSite = "Strict";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to enable access and refresh tokens in cookies.
|
||||
/// </summary>
|
||||
[DefaultValue(StaticEnabled)]
|
||||
[Obsolete("This is only configurable in Umbraco 16. Scheduled for removal in Umbraco 17.")]
|
||||
public bool Enabled { get; set; } = StaticEnabled;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the cookie SameSite configuration.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Valid values are "Unspecified", "None", "Lax" and "Strict" (default).
|
||||
/// </remarks>
|
||||
[DefaultValue(StaticSameSite)]
|
||||
public string SameSite { get; set; } = StaticSameSite;
|
||||
}
|
||||
@@ -181,6 +181,11 @@ public class GlobalSettings
|
||||
/// </summary>
|
||||
public bool IsSmtpServerConfigured => !string.IsNullOrWhiteSpace(Smtp?.Host);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether SMTP expiry is configured.
|
||||
/// </summary>
|
||||
public bool IsSmtpExpiryConfigured => Smtp?.EmailExpiration != null && Smtp?.EmailExpiration.HasValue == true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether there is a physical pickup directory configured.
|
||||
/// </summary>
|
||||
|
||||
@@ -34,6 +34,9 @@ public class SecuritySettings
|
||||
internal const string StaticAuthorizeCallbackLogoutPathName = "/umbraco/logout";
|
||||
internal const string StaticAuthorizeCallbackErrorPathName = "/umbraco/error";
|
||||
|
||||
internal const string StaticPasswordResetEmailExpiry = "01:00:00";
|
||||
internal const string StaticUserInviteEmailExpiry = "3.00:00:00";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to keep the user logged in.
|
||||
/// </summary>
|
||||
@@ -159,4 +162,16 @@ public class SecuritySettings
|
||||
/// </summary>
|
||||
[DefaultValue(StaticAuthorizeCallbackErrorPathName)]
|
||||
public string AuthorizeCallbackErrorPathName { get; set; } = StaticAuthorizeCallbackErrorPathName;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the expiry time for password reset emails.
|
||||
/// </summary>
|
||||
[DefaultValue(StaticPasswordResetEmailExpiry)]
|
||||
public TimeSpan PasswordResetEmailExpiry { get; set; } = TimeSpan.Parse(StaticPasswordResetEmailExpiry);
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the expiry time for user invite emails.
|
||||
/// </summary>
|
||||
[DefaultValue(StaticUserInviteEmailExpiry)]
|
||||
public TimeSpan UserInviteEmailExpiry { get; set; } = TimeSpan.Parse(StaticUserInviteEmailExpiry);
|
||||
}
|
||||
|
||||
@@ -96,4 +96,9 @@ public class SmtpSettings : ValidatableEntryBase
|
||||
/// Gets or sets a value for the SMTP password.
|
||||
/// </summary>
|
||||
public string? Password { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value for the time until an email expires.
|
||||
/// </summary>
|
||||
public TimeSpan? EmailExpiration { get; set; }
|
||||
}
|
||||
|
||||
@@ -64,6 +64,7 @@ public static partial class Constants
|
||||
public const string ConfigWebhook = ConfigPrefix + "Webhook";
|
||||
public const string ConfigWebhookPayloadType = ConfigWebhook + ":PayloadType";
|
||||
public const string ConfigCache = ConfigPrefix + "Cache";
|
||||
public const string ConfigBackOfficeTokenCookie = ConfigSecurity + ":BackOfficeTokenCookie";
|
||||
|
||||
public static class NamedOptions
|
||||
{
|
||||
|
||||
@@ -45,7 +45,7 @@ public abstract class ApiContentBuilderBase<T>
|
||||
|
||||
public virtual T? Build(IPublishedContent content)
|
||||
{
|
||||
IApiContentRoute? route = ApiContentRouteBuilder.Build(content);
|
||||
IApiContentRoute? route = ApiContentRouteBuilder.Build(content, VariationContextAccessor.VariationContext?.Culture);
|
||||
if (route is null)
|
||||
{
|
||||
return default;
|
||||
|
||||
@@ -86,7 +86,8 @@ public static partial class UmbracoBuilderExtensions
|
||||
.AddUmbracoOptions<HelpPageSettings>()
|
||||
.AddUmbracoOptions<DataTypesSettings>()
|
||||
.AddUmbracoOptions<WebhookSettings>()
|
||||
.AddUmbracoOptions<CacheSettings>();
|
||||
.AddUmbracoOptions<CacheSettings>()
|
||||
.AddUmbracoOptions<BackOfficeTokenCookieSettings>();
|
||||
|
||||
// Configure connection string and ensure it's updated when the configuration changes
|
||||
builder.Services.AddSingleton<IConfigureOptions<ConnectionStrings>, ConfigureConnectionStrings>();
|
||||
|
||||
@@ -81,46 +81,23 @@ public class UserEditorAuthorizationHelper
|
||||
return Attempt<string?>.Succeed();
|
||||
}
|
||||
|
||||
// d) a non-admin user can remove any groups but can only add groups they themselves belong to
|
||||
if (userGroupAliases != null)
|
||||
{
|
||||
var savingGroupAliases = userGroupAliases.ToArray();
|
||||
var existingGroupAliases = savingUser == null
|
||||
IEnumerable<string> requestedGroupAliases = userGroupAliases.ToArray();
|
||||
IEnumerable<string> existingGroupAliases = savingUser == null
|
||||
? []
|
||||
: savingUser.Groups.Select(x => x.Alias).ToArray();
|
||||
: savingUser.Groups.Select(x => x.Alias);
|
||||
IEnumerable<string> performingUserGroupAliases = currentUser?.Groups.Select(x => x.Alias) ?? Enumerable.Empty<string>();
|
||||
|
||||
IEnumerable<string> addedGroupAliases = savingGroupAliases.Except(existingGroupAliases);
|
||||
IReadOnlyList<string> unauthorized = UserGroupAssignmentAuthorization
|
||||
.GetUnauthorizedGroupAssignments(performingUserGroupAliases, requestedGroupAliases, existingGroupAliases);
|
||||
|
||||
// As we know the current user is not admin, it is only allowed to use groups that the user do have themselves.
|
||||
var savingGroupAliasesNotAllowed = addedGroupAliases
|
||||
.Except(currentUser?.Groups.Select(x => x.Alias) ?? Enumerable.Empty<string>()).ToArray();
|
||||
if (savingGroupAliasesNotAllowed.Any())
|
||||
if (unauthorized.Count > 0)
|
||||
{
|
||||
return Attempt.Fail("Cannot assign the group(s) '" + string.Join(", ", savingGroupAliasesNotAllowed) +
|
||||
return Attempt.Fail("Cannot assign the group(s) '" + string.Join(", ", unauthorized) +
|
||||
"', the current user is not part of them or admin");
|
||||
}
|
||||
|
||||
// only validate any groups that have changed.
|
||||
// a non-admin user can remove groups and add groups that they have access to
|
||||
// but they cannot add a group that they do not have access to or that grants them
|
||||
// path or section access that they don't have access to.
|
||||
var newGroups = savingUser == null
|
||||
? savingGroupAliases
|
||||
: savingGroupAliases.Except(savingUser.Groups.Select(x => x.Alias)).ToArray();
|
||||
|
||||
var userGroupsChanged = savingUser != null && newGroups.Length > 0;
|
||||
|
||||
if (userGroupsChanged)
|
||||
{
|
||||
// d) A user cannot assign a group to another user that they do not belong to
|
||||
var currentUserGroups = currentUser?.Groups.Select(x => x.Alias).ToArray();
|
||||
foreach (var group in newGroups)
|
||||
{
|
||||
if (currentUserGroups?.Contains(group) == false)
|
||||
{
|
||||
return Attempt.Fail("Cannot assign the group " + group + ", the current user is not a member");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Attempt<string?>.Succeed();
|
||||
|
||||
@@ -74,7 +74,7 @@ public class EmailNotificationMethod : NotificationMethodBase
|
||||
var subject = _textService?.Localize("healthcheck", "scheduledHealthCheckEmailSubject", new[] { host });
|
||||
|
||||
EmailMessage mailMessage = CreateMailMessage(subject, message);
|
||||
Task? task = _emailSender?.SendAsync(mailMessage, Constants.Web.EmailTypes.HealthCheck);
|
||||
Task? task = _emailSender?.SendAsync(mailMessage, Constants.Web.EmailTypes.HealthCheck, false, null);
|
||||
if (task is not null)
|
||||
{
|
||||
await task;
|
||||
|
||||
@@ -7,9 +7,28 @@ namespace Umbraco.Cms.Core.Mail;
|
||||
/// </summary>
|
||||
public interface IEmailSender
|
||||
{
|
||||
/// <summary>
|
||||
/// Sends a message asynchronously.
|
||||
/// </summary>
|
||||
[Obsolete("Please use the overload with expires parameter. Scheduled for removal in Umbraco 18.")]
|
||||
Task SendAsync(EmailMessage message, string emailType);
|
||||
|
||||
/// <summary>
|
||||
/// Sends a message asynchronously.
|
||||
/// </summary>
|
||||
[Obsolete("Please use the overload with expires parameter. Scheduled for removal in Umbraco 18.")]
|
||||
Task SendAsync(EmailMessage message, string emailType, bool enableNotification);
|
||||
|
||||
/// <summary>
|
||||
/// Sends a message asynchronously.
|
||||
/// </summary>
|
||||
Task SendAsync(EmailMessage message, string emailType, bool enableNotification = false, TimeSpan? expires = null)
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
=> SendAsync(message, emailType, enableNotification);
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
|
||||
/// <summary>
|
||||
/// Verifies if the email sender is configured to send emails.
|
||||
/// </summary>
|
||||
bool CanSendRequiredEmail();
|
||||
}
|
||||
|
||||
@@ -12,6 +12,10 @@ internal sealed class NotImplementedEmailSender : IEmailSender
|
||||
throw new NotImplementedException(
|
||||
"To send an Email ensure IEmailSender is implemented with a custom implementation");
|
||||
|
||||
public Task SendAsync(EmailMessage message, string emailType, bool enableNotification, TimeSpan? expires) =>
|
||||
throw new NotImplementedException(
|
||||
"To send an Email ensure IEmailSender is implemented with a custom implementation");
|
||||
|
||||
public bool CanSendRequiredEmail()
|
||||
=> throw new NotImplementedException(
|
||||
"To send an Email ensure IEmailSender is implemented with a custom implementation");
|
||||
|
||||
@@ -12,7 +12,7 @@ public class X : OEmbedProviderBase
|
||||
{
|
||||
}
|
||||
|
||||
public override string ApiEndpoint => "http://publish.twitter.com/oembed";
|
||||
public override string ApiEndpoint => "https://publish.x.com/oembed";
|
||||
|
||||
public override string[] UrlSchemeRegex => new[] { @"(https?:\/\/(www\.)?)(twitter|x)\.com\/.*\/status\/.*" };
|
||||
|
||||
|
||||
@@ -23,4 +23,11 @@ public class RichTextBlockValue : BlockValue<RichTextBlockLayoutItem>
|
||||
/// <inheritdoc />
|
||||
[JsonIgnore]
|
||||
public override string PropertyEditorAlias => Constants.PropertyEditors.Aliases.RichText;
|
||||
|
||||
/// <inheritdoc />
|
||||
#pragma warning disable CS0672 // Member overrides obsolete member
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
public override bool SupportsBlockLayoutAlias(string alias) => base.SupportsBlockLayoutAlias(alias) || alias.Equals("Umbraco.TinyMCE");
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
#pragma warning restore CS0672 // Member overrides obsolete member
|
||||
}
|
||||
|
||||
@@ -53,6 +53,12 @@ public interface IPublishedContentTypeFactory
|
||||
/// </summary>
|
||||
PublishedDataType GetDataType(int id);
|
||||
|
||||
/// <summary>
|
||||
/// Clears the internal data type cache.
|
||||
/// </summary>
|
||||
void ClearDataTypeCache()
|
||||
{ }
|
||||
|
||||
/// <summary>
|
||||
/// Notifies the factory of datatype changes.
|
||||
/// </summary>
|
||||
|
||||
@@ -65,6 +65,22 @@ public class PublishedContentTypeFactory : IPublishedContentTypeFactory
|
||||
return dataType;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ClearDataTypeCache()
|
||||
{
|
||||
if (_publishedDataTypes is null)
|
||||
{
|
||||
// Not initialized yet, so skip and avoid lock
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_publishedDataTypesLocker)
|
||||
{
|
||||
// Clear cache (and let it lazy initialize again later)
|
||||
_publishedDataTypes = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void NotifyDataTypeChanges(params int[] ids)
|
||||
{
|
||||
|
||||
@@ -24,6 +24,7 @@ public sealed class ContentPublishedNotification : EnumerableObjectNotification<
|
||||
|
||||
public ContentPublishedNotification(IEnumerable<IContent> target, EventMessages messages, bool includeDescendants)
|
||||
: base(target, messages) => IncludeDescendants = includeDescendants;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a enumeration of <see cref="IContent"/> which are being published.
|
||||
/// </summary>
|
||||
|
||||
@@ -3,12 +3,14 @@ using System.Globalization;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Xml.Linq;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Umbraco.Cms.Core.Cache;
|
||||
using Umbraco.Cms.Core.IO;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.Editors;
|
||||
using Umbraco.Cms.Core.Models.Validation;
|
||||
using Umbraco.Cms.Core.PropertyEditors.Validators;
|
||||
using Umbraco.Cms.Core.Serialization;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Strings;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
@@ -20,6 +22,9 @@ namespace Umbraco.Cms.Core.PropertyEditors;
|
||||
[DataContract]
|
||||
public class DataValueEditor : IDataValueEditor
|
||||
{
|
||||
private const string ContentCacheKeyFormat = nameof(DataValueEditor) + "_Content_{0}";
|
||||
private const string MediaCacheKeyFormat = nameof(DataValueEditor) + "_Media_{0}";
|
||||
|
||||
private readonly IJsonSerializer? _jsonSerializer;
|
||||
private readonly IShortStringHelper _shortStringHelper;
|
||||
|
||||
@@ -415,4 +420,155 @@ public class DataValueEditor : IDataValueEditor
|
||||
|
||||
return value.TryConvertTo(valueType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a <see cref="IContent"/> instance by its unique identifier, using the provided request cache to avoid redundant
|
||||
/// lookups within the same request.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method caches content lookups for the duration of the current request to improve performance when the same content
|
||||
/// item may be accessed multiple times. This is particularly useful in scenarios involving multiple languages or blocks.
|
||||
/// </remarks>
|
||||
/// <param name="key">The unique identifier of the content item to retrieve.</param>
|
||||
/// <param name="requestCache">The request-scoped cache used to store and retrieve content items for the duration of the current request.</param>
|
||||
/// <param name="contentService">The content service used to fetch the content item if it is not found in the cache.</param>
|
||||
/// <returns>The <see cref="IContent"/> instance corresponding to the specified key, or null if no such content item exists.</returns>
|
||||
[Obsolete("This method is available for support of request caching retrieved entities in derived property value editors. " +
|
||||
"The intention is to supersede this with lazy loaded read locks, which will make this unnecessary. " +
|
||||
"Scheduled for removal in Umbraco 19.")]
|
||||
protected static IContent? GetAndCacheContentById(Guid key, IRequestCache requestCache, IContentService contentService)
|
||||
{
|
||||
if (requestCache.IsAvailable is false)
|
||||
{
|
||||
return contentService.GetById(key);
|
||||
}
|
||||
|
||||
var cacheKey = string.Format(ContentCacheKeyFormat, key);
|
||||
IContent? content = requestCache.GetCacheItem<IContent?>(cacheKey);
|
||||
if (content is null)
|
||||
{
|
||||
content = contentService.GetById(key);
|
||||
if (content is not null)
|
||||
{
|
||||
requestCache.Set(cacheKey, content);
|
||||
}
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the specified <see cref="IContent"/> item to the request cache using its unique key.
|
||||
/// </summary>
|
||||
/// <param name="content">The content item to cache.</param>
|
||||
/// <param name="requestCache">The request cache in which to store the content item.</param>
|
||||
[Obsolete("This method is available for support of request caching retrieved entities in derived property value editors. " +
|
||||
"The intention is to supersede this with lazy loaded read locks, which will make this unnecessary. " +
|
||||
"Scheduled for removal in Umbraco 19.")]
|
||||
protected static void CacheContentById(IContent content, IRequestCache requestCache)
|
||||
{
|
||||
if (requestCache.IsAvailable is false)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var cacheKey = string.Format(ContentCacheKeyFormat, content.Key);
|
||||
requestCache.Set(cacheKey, content);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a <see cref="IMedia"/> instance by its unique identifier, using the provided request cache to avoid redundant
|
||||
/// lookups within the same request.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method caches media lookups for the duration of the current request to improve performance when the same media
|
||||
/// item may be accessed multiple times. This is particularly useful in scenarios involving multiple languages or blocks.
|
||||
/// </remarks>
|
||||
/// <param name="key">The unique identifier of the media item to retrieve.</param>
|
||||
/// <param name="requestCache">The request-scoped cache used to store and retrieve media items for the duration of the current request.</param>
|
||||
/// <param name="mediaService">The media service used to fetch the media item if it is not found in the cache.</param>
|
||||
/// <returns>The <see cref="IMedia"/> instance corresponding to the specified key, or null if no such media item exists.</returns>
|
||||
[Obsolete("This method is available for support of request caching retrieved entities in derived property value editors. " +
|
||||
"The intention is to supersede this with lazy loaded read locks, which will make this unnecessary. " +
|
||||
"Scheduled for removal in Umbraco 19.")]
|
||||
protected static IMedia? GetAndCacheMediaById(Guid key, IRequestCache requestCache, IMediaService mediaService)
|
||||
{
|
||||
if (requestCache.IsAvailable is false)
|
||||
{
|
||||
return mediaService.GetById(key);
|
||||
}
|
||||
|
||||
var cacheKey = string.Format(MediaCacheKeyFormat, key);
|
||||
IMedia? media = requestCache.GetCacheItem<IMedia?>(cacheKey);
|
||||
|
||||
if (media is null)
|
||||
{
|
||||
media = mediaService.GetById(key);
|
||||
if (media is not null)
|
||||
{
|
||||
requestCache.Set(cacheKey, media);
|
||||
}
|
||||
}
|
||||
|
||||
return media;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the specified <see cref="IMedia"/> item to the request cache using its unique key.
|
||||
/// </summary>
|
||||
/// <param name="media">The media item to cache.</param>
|
||||
/// <param name="requestCache">The request cache in which to store the media item.</param>
|
||||
[Obsolete("This method is available for support of request caching retrieved entities in derived property value editors. " +
|
||||
"The intention is to supersede this with lazy loaded read locks, which will make this unnecessary. " +
|
||||
"Scheduled for removal in Umbraco 19.")]
|
||||
protected static void CacheMediaById(IMedia media, IRequestCache requestCache)
|
||||
{
|
||||
if (requestCache.IsAvailable is false)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var cacheKey = string.Format(MediaCacheKeyFormat, media.Key);
|
||||
requestCache.Set(cacheKey, media);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the content item identified by the specified key is present in the request cache.
|
||||
/// </summary>
|
||||
/// <param name="key">The unique identifier for the content item to check for in the cache.</param>
|
||||
/// <param name="requestCache">The request cache in which to look for the content item.</param>
|
||||
/// <returns>true if the content item is already cached in the request cache; otherwise, false.</returns>
|
||||
[Obsolete("This method is available for support of request caching retrieved entities in derived property value editors. " +
|
||||
"The intention is to supersede this with lazy loaded read locks, which will make this unnecessary. " +
|
||||
"Scheduled for removal in Umbraco 19.")]
|
||||
protected static bool IsContentAlreadyCached(Guid key, IRequestCache requestCache)
|
||||
{
|
||||
if (requestCache.IsAvailable is false)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var cacheKey = string.Format(ContentCacheKeyFormat, key);
|
||||
return requestCache.GetCacheItem<IContent?>(cacheKey) is not null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the media item identified by the specified key is present in the request cache.
|
||||
/// </summary>
|
||||
/// <param name="key">The unique identifier for the media item to check for in the cache.</param>
|
||||
/// <param name="requestCache">The request cache in which to look for the media item.</param>
|
||||
/// <returns>true if the media item is already cached in the request cache; otherwise, false.</returns>
|
||||
[Obsolete("This method is available for support of request caching retrieved entities in derived property value editors. " +
|
||||
"The intention is to supersede this with lazy loaded read locks, which will make this unnecessary. " +
|
||||
"Scheduled for removal in Umbraco 19.")]
|
||||
protected static bool IsMediaAlreadyCached(Guid key, IRequestCache requestCache)
|
||||
{
|
||||
if (requestCache.IsAvailable is false)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var cacheKey = string.Format(MediaCacheKeyFormat, key);
|
||||
return requestCache.GetCacheItem<IMedia?>(cacheKey) is not null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
namespace Umbraco.Cms.Core.PropertyEditors;
|
||||
|
||||
/// <summary>
|
||||
/// Optionally implemented by property editors, this defines a contract for caching entities that are referenced in block values.
|
||||
/// </summary>
|
||||
[Obsolete("This interface is available for support of request caching retrieved entities in property value editors that implement it. " +
|
||||
"The intention is to supersede this with lazy loaded read locks, which will make this unnecessary. " +
|
||||
"Scheduled for removal in Umbraco 19.")]
|
||||
public interface ICacheReferencedEntities
|
||||
{
|
||||
/// <summary>
|
||||
/// Caches the entities referenced by the provided block data values.
|
||||
/// </summary>
|
||||
/// <param name="values">An enumerable collection of block values that may contain the entities to be cached.</param>
|
||||
[Obsolete("This method is available for support of request caching retrieved entities in derived property value editors. " +
|
||||
"The intention is to supersede this with lazy loaded read locks, which will make this unnecessary. " +
|
||||
"Scheduled for removal in Umbraco 19.")]
|
||||
void CacheReferencedEntities(IEnumerable<object> values);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
namespace Umbraco.Cms.Core.Security;
|
||||
|
||||
/// <summary>
|
||||
/// Shared authorization logic for user group assignment.
|
||||
/// </summary>
|
||||
public static class UserGroupAssignmentAuthorization
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns the group aliases that the performing user is not authorized to assign.
|
||||
/// </summary>
|
||||
/// <param name="performingUserGroupAliases">The group aliases the performing user belongs to.</param>
|
||||
/// <param name="requestedGroupAliases">The group aliases being assigned to the target user.</param>
|
||||
/// <param name="existingGroupAliases">The group aliases the target user currently belongs to.</param>
|
||||
/// <returns>
|
||||
/// Group aliases that are being added but the performing user does not belong to.
|
||||
/// An empty collection means the assignment is authorized.
|
||||
/// </returns>
|
||||
/// <remarks>
|
||||
/// Non-admin users can remove any groups but can only add groups they themselves belong to.
|
||||
/// Callers should check for admin status before calling this method, as admins bypass this check.
|
||||
/// </remarks>
|
||||
public static IReadOnlyList<string> GetUnauthorizedGroupAssignments(
|
||||
IEnumerable<string> performingUserGroupAliases,
|
||||
IEnumerable<string> requestedGroupAliases,
|
||||
IEnumerable<string> existingGroupAliases)
|
||||
{
|
||||
var performingGroups = performingUserGroupAliases.ToHashSet(StringComparer.InvariantCultureIgnoreCase);
|
||||
var existingGroups = existingGroupAliases.ToHashSet(StringComparer.InvariantCultureIgnoreCase);
|
||||
|
||||
return requestedGroupAliases
|
||||
.Where(alias => existingGroups.Contains(alias) is false && performingGroups.Contains(alias) is false)
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ using Umbraco.Cms.Core.Models.ContentEditing;
|
||||
using Umbraco.Cms.Core.Models.ContentPublishing;
|
||||
using Umbraco.Cms.Core.Scoping;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.Cms.Core.Web;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Core.Services;
|
||||
@@ -26,6 +27,7 @@ internal sealed class ContentPublishingService : IContentPublishingService
|
||||
private readonly IRelationService _relationService;
|
||||
private readonly ILogger<ContentPublishingService> _logger;
|
||||
private readonly ILongRunningOperationService _longRunningOperationService;
|
||||
private readonly IUmbracoContextFactory _umbracoContextFactory;
|
||||
|
||||
public ContentPublishingService(
|
||||
ICoreScopeProvider coreScopeProvider,
|
||||
@@ -37,7 +39,8 @@ internal sealed class ContentPublishingService : IContentPublishingService
|
||||
IOptionsMonitor<ContentSettings> optionsMonitor,
|
||||
IRelationService relationService,
|
||||
ILogger<ContentPublishingService> logger,
|
||||
ILongRunningOperationService longRunningOperationService)
|
||||
ILongRunningOperationService longRunningOperationService,
|
||||
IUmbracoContextFactory umbracoContextFactory)
|
||||
{
|
||||
_coreScopeProvider = coreScopeProvider;
|
||||
_contentService = contentService;
|
||||
@@ -53,6 +56,7 @@ internal sealed class ContentPublishingService : IContentPublishingService
|
||||
{
|
||||
_contentSettings = contentSettings;
|
||||
});
|
||||
_umbracoContextFactory = umbracoContextFactory;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -290,7 +294,7 @@ internal sealed class ContentPublishingService : IContentPublishingService
|
||||
return MapInternalPublishingAttempt(minimalAttempt);
|
||||
}
|
||||
|
||||
_logger.LogInformation("Starting async background thread for publishing branch.");
|
||||
_logger.LogDebug("Starting long running operation for publishing branch {Key} on background thread.", key);
|
||||
Attempt<Guid, LongRunningOperationEnqueueStatus> enqueueAttempt = await _longRunningOperationService.RunAsync(
|
||||
PublishBranchOperationType,
|
||||
async _ => await PerformPublishBranchAsync(key, cultures, publishBranchFilter, userKey, returnContent: false),
|
||||
@@ -324,6 +328,10 @@ internal sealed class ContentPublishingService : IContentPublishingService
|
||||
Guid userKey,
|
||||
bool returnContent)
|
||||
{
|
||||
// Ensure we have an UmbracoContext in case running on a background thread so operations that run in the published notification handlers
|
||||
// have access to this (e.g. webhooks).
|
||||
using UmbracoContextReference umbracoContextReference = _umbracoContextFactory.EnsureUmbracoContext();
|
||||
|
||||
using ICoreScope scope = _coreScopeProvider.CreateCoreScope();
|
||||
IContent? content = _contentService.GetById(key);
|
||||
if (content is null)
|
||||
|
||||
@@ -2207,7 +2207,7 @@ public class ContentService : RepositoryService, IContentService
|
||||
variesByCulture ? culturesPublished.IsCollectionEmpty() ? null : culturesPublished : ["*"],
|
||||
null,
|
||||
eventMessages));
|
||||
scope.Notifications.Publish(new ContentPublishedNotification(publishedDocuments, eventMessages).WithState(notificationState));
|
||||
scope.Notifications.Publish(new ContentPublishedNotification(publishedDocuments, eventMessages, true).WithState(notificationState));
|
||||
|
||||
scope.Complete();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.ContentTypeEditing;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.Cms.Core.Strings;
|
||||
@@ -407,7 +407,7 @@ internal abstract class ContentTypeEditingServiceBase<TContentType, TContentType
|
||||
}
|
||||
|
||||
// This this method gets aliases across documents, members, and media, so it covers it all
|
||||
private bool ContentTypeAliasIsInUse(string alias) => _contentTypeService.GetAllContentTypeAliases().Contains(alias);
|
||||
private bool ContentTypeAliasIsInUse(string alias) => _contentTypeService.GetAllContentTypeAliases().InvariantContains(alias);
|
||||
|
||||
private bool ContentTypeAliasCanBeUsedFor(string alias, Guid key)
|
||||
{
|
||||
|
||||
@@ -175,7 +175,7 @@ internal sealed class MediaTypeEditingService : ContentTypeEditingServiceBase<IM
|
||||
continue;
|
||||
}
|
||||
|
||||
allowedFileExtensionsByMediaType[mediaType] = fileUploadConfiguration.FileExtensions;
|
||||
allowedFileExtensionsByMediaType[mediaType] = fileUploadConfiguration.FileExtensions ?? []; // Although we never expect null here, legacy data type configuration did allow it.
|
||||
}
|
||||
|
||||
return allowedFileExtensionsByMediaType;
|
||||
|
||||
@@ -432,7 +432,8 @@ public class DocumentUrlService : IDocumentUrlService
|
||||
|
||||
if (draftUrlSegments.Any() is false)
|
||||
{
|
||||
_logger.LogWarning("No draft URL segments found for document {DocumentKey} in culture {Culture}", document.Key, culture ?? "{null}");
|
||||
// Log at debug level because this is expected when a document is not published in a given language.
|
||||
_logger.LogDebug("No draft URL segments found for document {DocumentKey} in culture {Culture}", document.Key, culture ?? "{null}");
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -2,7 +2,22 @@ using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
|
||||
namespace Umbraco.Cms.Core.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Defines a service for asynchronously retrieving embeddable HTML markup for a specified resource using the oEmbed
|
||||
/// protocol.
|
||||
/// </summary>
|
||||
public interface IOEmbedService
|
||||
{
|
||||
/// <summary>
|
||||
/// Asynchronously retrieves the embeddable HTML markup for the specified resource.
|
||||
/// </summary>
|
||||
/// <remarks>The returned markup is suitable for embedding in web pages. The width and height parameters
|
||||
/// may be ignored by some providers depending on their capabilities.</remarks>
|
||||
/// <param name="url">The URI of the resource to retrieve markup for. Must be a valid, absolute URI.</param>
|
||||
/// <param name="width">The optional maximum width, in pixels, for the embedded content. If null, the default width is used.</param>
|
||||
/// <param name="height">The optional maximum height, in pixels, for the embedded content. If null, the default height is used.</param>
|
||||
/// <param name="cancellationToken">A token to monitor for cancellation requests. The operation is canceled if the token is triggered.</param>
|
||||
/// <returns>A task that represents the asynchronous operation. The result contains an Attempt with the HTML markup if
|
||||
/// successful, or an oEmbed operation status indicating the reason for failure.</returns>
|
||||
Task<Attempt<string, OEmbedOperationStatus>> GetMarkupAsync(Uri url, int? width, int? height, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.Membership;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
|
||||
@@ -107,8 +107,25 @@ public interface IUserGroupService
|
||||
/// <param name="userGroupKeys">The user groups the users should be part of.</param>
|
||||
/// <param name="userKeys">The user whose groups we want to alter.</param>
|
||||
/// <returns>An attempt indicating if the operation was a success as well as a more detailed <see cref="UserGroupOperationStatus"/>.</returns>
|
||||
[Obsolete("Please use the overload accepting all parameters. Scheduled for removal in Umbraco 19.")]
|
||||
Task<Attempt<UserGroupOperationStatus>> UpdateUserGroupsOnUsersAsync(ISet<Guid> userGroupKeys, ISet<Guid> userKeys);
|
||||
|
||||
/// <summary>
|
||||
/// Updates the users to have the groups specified, with authorization checks based on the performing user.
|
||||
/// </summary>
|
||||
/// <param name="userGroupKeys">The user groups the users should be part of.</param>
|
||||
/// <param name="userKeys">The user whose groups we want to alter.</param>
|
||||
/// <param name="performingUserKey">The key of the user performing the operation.</param>
|
||||
/// <returns>An attempt indicating if the operation was a success as well as a more detailed <see cref="UserGroupOperationStatus"/>.</returns>
|
||||
/// <remarks>
|
||||
/// Non-admin users can only add groups they themselves belong to. Removing groups is always allowed.
|
||||
/// </remarks>
|
||||
// TODO (V18): Remove default implementation.
|
||||
Task<Attempt<UserGroupOperationStatus>> UpdateUserGroupsOnUsersAsync(ISet<Guid> userGroupKeys, ISet<Guid> userKeys, Guid performingUserKey)
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
=> UpdateUserGroupsOnUsersAsync(userGroupKeys, userKeys);
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
|
||||
Task<Attempt<UserGroupOperationStatus>> AddUsersToUserGroupAsync(UsersToUserGroupManipulationModel addUsersModel, Guid performingUserKey);
|
||||
Task<Attempt<UserGroupOperationStatus>> RemoveUsersFromUserGroupAsync(UsersToUserGroupManipulationModel removeUsersModel, Guid performingUserKey);
|
||||
}
|
||||
|
||||
@@ -557,7 +557,7 @@ public class NotificationService : INotificationService
|
||||
{
|
||||
ThreadPool.QueueUserWorkItem(state =>
|
||||
{
|
||||
if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug))
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Begin processing notifications.");
|
||||
}
|
||||
@@ -569,9 +569,9 @@ public class NotificationService : INotificationService
|
||||
{
|
||||
try
|
||||
{
|
||||
_emailSender.SendAsync(request.Mail, Constants.Web.EmailTypes.Notification).GetAwaiter()
|
||||
_emailSender.SendAsync(request.Mail, Constants.Web.EmailTypes.Notification, false, null).GetAwaiter()
|
||||
.GetResult();
|
||||
if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug))
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Notification '{Action}' sent to {Username} ({Email})", request.Action, request.UserName, request.Email);
|
||||
}
|
||||
|
||||
@@ -6,22 +6,30 @@ using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
|
||||
namespace Umbraco.Cms.Core.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Implements <see cref="IOEmbedService"/> for retrieving embeddable HTML markup using the oEmbed protocol.
|
||||
/// </summary>
|
||||
public class OEmbedService : IOEmbedService
|
||||
{
|
||||
private readonly EmbedProvidersCollection _embedProvidersCollection;
|
||||
private readonly ILogger<OEmbedService> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="OEmbedService"/> class.
|
||||
/// </summary>
|
||||
public OEmbedService(EmbedProvidersCollection embedProvidersCollection, ILogger<OEmbedService> logger)
|
||||
{
|
||||
_embedProvidersCollection = embedProvidersCollection;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<Attempt<string, OEmbedOperationStatus>> GetMarkupAsync(Uri url, int? maxWidth, int? maxHeight, CancellationToken cancellationToken)
|
||||
{
|
||||
// Find the first provider that supports the URL
|
||||
IEmbedProvider? matchedProvider = _embedProvidersCollection
|
||||
.FirstOrDefault(provider => provider.UrlSchemeRegex.Any(regex=>new Regex(regex, RegexOptions.IgnoreCase).IsMatch(url.OriginalString)));
|
||||
.FirstOrDefault(provider => provider.UrlSchemeRegex
|
||||
.Any(regex => new Regex(regex, RegexOptions.IgnoreCase).IsMatch(url.OriginalString)));
|
||||
|
||||
if (matchedProvider is null)
|
||||
{
|
||||
@@ -39,8 +47,8 @@ public class OEmbedService : IOEmbedService
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Unexpected exception happened while trying to get oembed markup. Provider: {Provider}",matchedProvider.GetType().Name);
|
||||
Attempt.FailWithStatus(OEmbedOperationStatus.UnexpectedException, string.Empty, e);
|
||||
_logger.LogError(e, "Unexpected exception happened while trying to get oEmbed markup. Provider: {Provider}", matchedProvider.GetType().Name);
|
||||
return Attempt.FailWithStatus(OEmbedOperationStatus.UnexpectedException, string.Empty, e);
|
||||
}
|
||||
|
||||
return Attempt.FailWithStatus(OEmbedOperationStatus.ProviderReturnedInvalidResult, string.Empty);
|
||||
|
||||
@@ -8,6 +8,7 @@ using Umbraco.Cms.Core.Persistence;
|
||||
using Umbraco.Cms.Core.Persistence.Querying;
|
||||
using Umbraco.Cms.Core.Persistence.Repositories;
|
||||
using Umbraco.Cms.Core.Scoping;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Services.AuthorizationStatus;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.Extensions;
|
||||
@@ -210,9 +211,24 @@ internal sealed class UserGroupService : RepositoryService, IUserGroupService
|
||||
return Attempt.Succeed(UserGroupOperationStatus.Success);
|
||||
}
|
||||
|
||||
public async Task<Attempt<UserGroupOperationStatus>> UpdateUserGroupsOnUsersAsync(
|
||||
// TODO (V19): Collapse the following three methods into a single one, once the obsolete overload
|
||||
// of UpdateUserGroupsOnUsersAsync is removed from the interface.
|
||||
|
||||
public Task<Attempt<UserGroupOperationStatus>> UpdateUserGroupsOnUsersAsync(
|
||||
ISet<Guid> userGroupKeys,
|
||||
ISet<Guid> userKeys)
|
||||
=> UpdateUserGroupsOnUsersInternalAsync(userGroupKeys, userKeys, performingUserKey: null);
|
||||
|
||||
public Task<Attempt<UserGroupOperationStatus>> UpdateUserGroupsOnUsersAsync(
|
||||
ISet<Guid> userGroupKeys,
|
||||
ISet<Guid> userKeys,
|
||||
Guid performingUserKey)
|
||||
=> UpdateUserGroupsOnUsersInternalAsync(userGroupKeys, userKeys, performingUserKey);
|
||||
|
||||
private async Task<Attempt<UserGroupOperationStatus>> UpdateUserGroupsOnUsersInternalAsync(
|
||||
ISet<Guid> userGroupKeys,
|
||||
ISet<Guid> userKeys,
|
||||
Guid? performingUserKey)
|
||||
{
|
||||
using ICoreScope scope = ScopeProvider.CreateCoreScope();
|
||||
|
||||
@@ -222,6 +238,40 @@ internal sealed class UserGroupService : RepositoryService, IUserGroupService
|
||||
.Select(x => x.ToReadOnlyGroup())
|
||||
.ToArray();
|
||||
|
||||
// Authorize the performing user if provided.
|
||||
if (performingUserKey.HasValue)
|
||||
{
|
||||
IUser? performingUser = await _userService.GetAsync(performingUserKey.Value);
|
||||
if (performingUser is null)
|
||||
{
|
||||
scope.Complete();
|
||||
return Attempt.Fail(UserGroupOperationStatus.MissingUser);
|
||||
}
|
||||
|
||||
if (performingUser.IsAdmin() is false)
|
||||
{
|
||||
string[] performingUserGroupAliases = performingUser.Groups.Select(g => g.Alias).ToArray();
|
||||
string[] requestedGroupAliases = userGroups.Select(g => g.Alias).ToArray();
|
||||
|
||||
foreach (IUser user in users)
|
||||
{
|
||||
IEnumerable<string> existingGroupAliases = user.Groups.Select(g => g.Alias);
|
||||
|
||||
IReadOnlyList<string> unauthorized = UserGroupAssignmentAuthorization
|
||||
.GetUnauthorizedGroupAssignments(performingUserGroupAliases, requestedGroupAliases, existingGroupAliases);
|
||||
|
||||
if (unauthorized.Count > 0)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"The performing user is not allowed to assign user group(s) '{GroupAliases}' because they do not belong to them.",
|
||||
string.Join(", ", unauthorized));
|
||||
scope.Complete();
|
||||
return Attempt.Fail(UserGroupOperationStatus.Unauthorized);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This means that we're potentially de-admining a user, which might cause the admin group to be empty.
|
||||
if (userGroupKeys.Contains(Constants.Security.AdminGroupKey) is false)
|
||||
{
|
||||
|
||||
@@ -10,7 +10,7 @@ using Umbraco.Cms.Core.Configuration.Models;
|
||||
namespace Umbraco.Cms.Infrastructure.Examine.DependencyInjection;
|
||||
|
||||
/// <summary>
|
||||
/// Configures the index options to construct the Examine indexes
|
||||
/// Configures the index options to construct the Examine indexes.
|
||||
/// </summary>
|
||||
public sealed class ConfigureIndexOptions : IConfigureNamedOptions<LuceneDirectoryIndexOptions>
|
||||
{
|
||||
@@ -18,6 +18,9 @@ public sealed class ConfigureIndexOptions : IConfigureNamedOptions<LuceneDirecto
|
||||
private readonly IUmbracoIndexConfig _umbracoIndexConfig;
|
||||
private readonly IDeliveryApiContentIndexFieldDefinitionBuilder _deliveryApiContentIndexFieldDefinitionBuilder;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ConfigureIndexOptions"/> class.
|
||||
/// </summary>
|
||||
public ConfigureIndexOptions(
|
||||
IUmbracoIndexConfig umbracoIndexConfig,
|
||||
IOptions<IndexCreatorSettings> settings,
|
||||
@@ -28,24 +31,27 @@ public sealed class ConfigureIndexOptions : IConfigureNamedOptions<LuceneDirecto
|
||||
_deliveryApiContentIndexFieldDefinitionBuilder = deliveryApiContentIndexFieldDefinitionBuilder;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Configure(string? name, LuceneDirectoryIndexOptions options)
|
||||
{
|
||||
// When creating FieldDefinitions with Umbraco defaults, pass in any already defined to avoid overwriting
|
||||
// those added via a package or custom code.
|
||||
switch (name)
|
||||
{
|
||||
case Constants.UmbracoIndexes.InternalIndexName:
|
||||
options.Analyzer = new CultureInvariantWhitespaceAnalyzer();
|
||||
options.Validator = _umbracoIndexConfig.GetContentValueSetValidator();
|
||||
options.FieldDefinitions = new UmbracoFieldDefinitionCollection();
|
||||
options.FieldDefinitions = new UmbracoFieldDefinitionCollection(options.FieldDefinitions);
|
||||
break;
|
||||
case Constants.UmbracoIndexes.ExternalIndexName:
|
||||
options.Analyzer = new StandardAnalyzer(LuceneInfo.CurrentVersion);
|
||||
options.Validator = _umbracoIndexConfig.GetPublishedContentValueSetValidator();
|
||||
options.FieldDefinitions = new UmbracoFieldDefinitionCollection();
|
||||
options.FieldDefinitions = new UmbracoFieldDefinitionCollection(options.FieldDefinitions);
|
||||
break;
|
||||
case Constants.UmbracoIndexes.MembersIndexName:
|
||||
options.Analyzer = new CultureInvariantWhitespaceAnalyzer();
|
||||
options.Validator = _umbracoIndexConfig.GetMemberValueSetValidator();
|
||||
options.FieldDefinitions = new UmbracoFieldDefinitionCollection();
|
||||
options.FieldDefinitions = new UmbracoFieldDefinitionCollection(options.FieldDefinitions);
|
||||
break;
|
||||
case Constants.UmbracoIndexes.DeliveryApiContentIndexName:
|
||||
options.Analyzer = new StandardAnalyzer(LuceneInfo.CurrentVersion);
|
||||
@@ -64,6 +70,7 @@ public sealed class ConfigureIndexOptions : IConfigureNamedOptions<LuceneDirecto
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Configure(LuceneDirectoryIndexOptions options)
|
||||
=> throw new NotImplementedException("This is never called and is just part of the interface");
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ public static partial class UmbracoBuilderExtensions
|
||||
|
||||
// Add post migration notification handlers
|
||||
builder.AddNotificationHandler<UmbracoPlanExecutedNotification, ClearCsrfCookieHandler>();
|
||||
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
|
||||
+26
-4
@@ -59,7 +59,19 @@ internal sealed class DeliveryApiContentIndexHandleContentChanges : DeliveryApiC
|
||||
RemoveFromIndex(pendingRemovals, index);
|
||||
pendingRemovals.Clear();
|
||||
|
||||
Reindex(content, index);
|
||||
ReIndexResult reIndexResult = Reindex(content, index);
|
||||
|
||||
|
||||
// When we get to this point, we are dealing with either
|
||||
// a refresh node or a refresh branch (see reindex =...).
|
||||
// A refresh branch can be many things, the Reindex function takes care of most scenarios.
|
||||
// But it only reindexes descendants if the base node has any changed cultures (see comments in that function)
|
||||
// So by checking what kind of operation it did when the initial indexrequest is for a refresh branch,
|
||||
// we can support reindexing a branch while the base node was unchanged.
|
||||
if (reIndexResult == ReIndexResult.Updated && changeTypes.HasType(TreeChangeTypes.RefreshBranch))
|
||||
{
|
||||
ReindexDescendants(content, index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,7 +80,7 @@ internal sealed class DeliveryApiContentIndexHandleContentChanges : DeliveryApiC
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
|
||||
private void Reindex(IContent content, IIndex index)
|
||||
private ReIndexResult Reindex(IContent content, IIndex index)
|
||||
{
|
||||
// get the currently indexed cultures for the content
|
||||
CulturePublishStatus[] existingCultures = index
|
||||
@@ -95,16 +107,19 @@ internal sealed class DeliveryApiContentIndexHandleContentChanges : DeliveryApiC
|
||||
// we likely got here because a removal triggered a "refresh branch" notification, now we
|
||||
// need to delete every last culture of this content and all descendants
|
||||
RemoveFromIndex(content.Id, index);
|
||||
return;
|
||||
return ReIndexResult.Removed;
|
||||
}
|
||||
|
||||
// if the published state changed of any culture, chances are there are similar changes ot the content descendants
|
||||
// if the published state changed of any culture, chances are there are similar changes at the content descendants
|
||||
// that need to be reflected in the index, so we'll reindex all descendants
|
||||
var changedCulturePublishStatus = indexedCultures.Intersect(existingCultures).Count() != existingCultures.Length;
|
||||
if (changedCulturePublishStatus)
|
||||
{
|
||||
ReindexDescendants(content, index);
|
||||
return ReIndexResult.UpdatedWithDescendants;
|
||||
}
|
||||
|
||||
return ReIndexResult.Updated;
|
||||
}
|
||||
|
||||
private CulturePublishStatus[] UpdateIndex(IContent content, IIndex index)
|
||||
@@ -179,4 +194,11 @@ internal sealed class DeliveryApiContentIndexHandleContentChanges : DeliveryApiC
|
||||
|
||||
public override int GetHashCode() => HashCode.Combine(Culture, Published);
|
||||
}
|
||||
|
||||
private enum ReIndexResult
|
||||
{
|
||||
Updated,
|
||||
UpdatedWithDescendants,
|
||||
Removed,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,11 +30,29 @@ public class UmbracoFieldDefinitionCollection : FieldDefinitionCollection
|
||||
new(UmbracoExamineFieldNames.VariesByCultureFieldName, FieldDefinitionTypes.Raw),
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UmbracoFieldDefinitionCollection"/> class containing
|
||||
/// the default Umbraco field definitions.
|
||||
/// </summary>
|
||||
public UmbracoFieldDefinitionCollection()
|
||||
: base(UmbracoIndexFieldDefinitions)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UmbracoFieldDefinitionCollection"/> class containing the containing
|
||||
/// the default Umbraco field definitions, augmented or overridden by the provided definitions.
|
||||
/// </summary>
|
||||
/// <param name="definitions">Existing collection of field definitions.</param>
|
||||
public UmbracoFieldDefinitionCollection(FieldDefinitionCollection definitions)
|
||||
: base(UmbracoIndexFieldDefinitions)
|
||||
{
|
||||
foreach (FieldDefinition definition in definitions)
|
||||
{
|
||||
AddOrUpdate(definition);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Overridden to dynamically add field definitions for culture variations
|
||||
/// </summary>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Cache;
|
||||
using Umbraco.Cms.Core.Configuration;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
@@ -28,7 +30,10 @@ public class UnattendedUpgrader : INotificationAsyncHandler<RuntimeUnattendedUpg
|
||||
private readonly IRuntimeState _runtimeState;
|
||||
private readonly IUmbracoVersion _umbracoVersion;
|
||||
private readonly UnattendedSettings _unattendedSettings;
|
||||
private readonly DistributedCache _distributedCache;
|
||||
private readonly ILogger<UnattendedUpgrader> _logger;
|
||||
|
||||
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 19.")]
|
||||
public UnattendedUpgrader(
|
||||
IProfilingLogger profilingLogger,
|
||||
IUmbracoVersion umbracoVersion,
|
||||
@@ -36,13 +41,36 @@ public class UnattendedUpgrader : INotificationAsyncHandler<RuntimeUnattendedUpg
|
||||
IRuntimeState runtimeState,
|
||||
PackageMigrationRunner packageMigrationRunner,
|
||||
IOptions<UnattendedSettings> unattendedSettings)
|
||||
: this(
|
||||
profilingLogger,
|
||||
umbracoVersion,
|
||||
databaseBuilder,
|
||||
runtimeState,
|
||||
packageMigrationRunner,
|
||||
unattendedSettings,
|
||||
StaticServiceProvider.Instance.GetRequiredService<DistributedCache>(),
|
||||
StaticServiceProvider.Instance.GetRequiredService<ILogger<UnattendedUpgrader>>())
|
||||
{
|
||||
_profilingLogger = profilingLogger ?? throw new ArgumentNullException(nameof(profilingLogger));
|
||||
_umbracoVersion = umbracoVersion ?? throw new ArgumentNullException(nameof(umbracoVersion));
|
||||
_databaseBuilder = databaseBuilder ?? throw new ArgumentNullException(nameof(databaseBuilder));
|
||||
_runtimeState = runtimeState ?? throw new ArgumentNullException(nameof(runtimeState));
|
||||
}
|
||||
|
||||
public UnattendedUpgrader(
|
||||
IProfilingLogger profilingLogger,
|
||||
IUmbracoVersion umbracoVersion,
|
||||
DatabaseBuilder databaseBuilder,
|
||||
IRuntimeState runtimeState,
|
||||
PackageMigrationRunner packageMigrationRunner,
|
||||
IOptions<UnattendedSettings> unattendedSettings,
|
||||
DistributedCache distributedCache,
|
||||
ILogger<UnattendedUpgrader> logger)
|
||||
{
|
||||
_profilingLogger = profilingLogger;
|
||||
_umbracoVersion = umbracoVersion;
|
||||
_databaseBuilder = databaseBuilder;
|
||||
_runtimeState = runtimeState;
|
||||
_packageMigrationRunner = packageMigrationRunner;
|
||||
_unattendedSettings = unattendedSettings.Value;
|
||||
_distributedCache = distributedCache;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task HandleAsync(RuntimeUnattendedUpgradeNotification notification, CancellationToken cancellationToken)
|
||||
@@ -109,8 +137,13 @@ public class UnattendedUpgrader : INotificationAsyncHandler<RuntimeUnattendedUpg
|
||||
try
|
||||
{
|
||||
await _packageMigrationRunner.RunPackagePlansAsync(pendingMigrations);
|
||||
notification.UnattendedUpgradeResult = RuntimeUnattendedUpgradeNotification.UpgradeResult
|
||||
.PackageMigrationComplete;
|
||||
notification.UnattendedUpgradeResult = RuntimeUnattendedUpgradeNotification.UpgradeResult.PackageMigrationComplete;
|
||||
|
||||
// Migration plans may have changed published content, so refresh the distributed cache to ensure consistency on first request.
|
||||
_distributedCache.RefreshAllPublishedSnapshot();
|
||||
_logger.LogInformation(
|
||||
"Migration plans run: {Plans}. Triggered refresh of distributed published content cache.",
|
||||
string.Join(", ", pendingMigrations));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -15,28 +15,44 @@ namespace Umbraco.Cms.Infrastructure.Mail
|
||||
public class BasicSmtpEmailSenderClient : IEmailSenderClient
|
||||
{
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
public BasicSmtpEmailSenderClient(IOptionsMonitor<GlobalSettings> globalSettings)
|
||||
{
|
||||
_globalSettings = globalSettings.CurrentValue;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public BasicSmtpEmailSenderClient(IOptionsMonitor<GlobalSettings> globalSettings)
|
||||
=> _globalSettings = globalSettings.CurrentValue;
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task SendAsync(EmailMessage message)
|
||||
=> await SendAsync(message, null);
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task SendAsync(EmailMessage message, TimeSpan? expires)
|
||||
{
|
||||
using var client = new SmtpClient();
|
||||
|
||||
await client.ConnectAsync(
|
||||
_globalSettings.Smtp!.Host,
|
||||
_globalSettings.Smtp.Port,
|
||||
_globalSettings.Smtp.Port,
|
||||
(SecureSocketOptions)(int)_globalSettings.Smtp.SecureSocketOptions);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(_globalSettings.Smtp.Username) &&
|
||||
!string.IsNullOrWhiteSpace(_globalSettings.Smtp.Password))
|
||||
!string.IsNullOrWhiteSpace(_globalSettings.Smtp.Password))
|
||||
{
|
||||
await client.AuthenticateAsync(_globalSettings.Smtp.Username, _globalSettings.Smtp.Password);
|
||||
}
|
||||
|
||||
var mimeMessage = message.ToMimeMessage(_globalSettings.Smtp!.From);
|
||||
|
||||
if (_globalSettings.IsSmtpExpiryConfigured)
|
||||
{
|
||||
expires ??= _globalSettings.Smtp.EmailExpiration;
|
||||
}
|
||||
|
||||
if (expires.HasValue)
|
||||
{
|
||||
// `Expires` header needs to be in RFC 1123/2822 compatible format
|
||||
mimeMessage.Headers.Add("Expires", DateTimeOffset.UtcNow.Add(expires.GetValueOrDefault()).ToString("R"));
|
||||
}
|
||||
|
||||
if (_globalSettings.Smtp.DeliveryMethod == SmtpDeliveryMethod.Network)
|
||||
{
|
||||
await client.SendAsync(mimeMessage);
|
||||
|
||||
@@ -30,6 +30,9 @@ public class EmailSender : IEmailSender
|
||||
private GlobalSettings _globalSettings;
|
||||
private readonly IEmailSenderClient _emailSenderClient;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="EmailSender"/> class.
|
||||
/// </summary>
|
||||
[Obsolete("Please use the non-obsolete constructor. Will be removed in V17.")]
|
||||
public EmailSender(
|
||||
ILogger<EmailSender> logger,
|
||||
@@ -39,6 +42,9 @@ public class EmailSender : IEmailSender
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="EmailSender"/> class.
|
||||
/// </summary>
|
||||
[Obsolete("Please use the non-obsolete constructor. Will be removed in V17.")]
|
||||
public EmailSender(
|
||||
ILogger<EmailSender> logger,
|
||||
@@ -55,6 +61,9 @@ public class EmailSender : IEmailSender
|
||||
globalSettings.OnChange(x => _globalSettings = x);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="EmailSender"/> class.
|
||||
/// </summary>
|
||||
[ActivatorUtilitiesConstructor]
|
||||
public EmailSender(
|
||||
ILogger<EmailSender> logger,
|
||||
@@ -72,19 +81,19 @@ public class EmailSender : IEmailSender
|
||||
globalSettings.OnChange(x => _globalSettings = x);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends the message async
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
/// <inheritdoc/>
|
||||
public async Task SendAsync(EmailMessage message, string emailType) =>
|
||||
await SendAsyncInternal(message, emailType, false);
|
||||
await SendAsyncInternal(message, emailType, false, null);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task SendAsync(EmailMessage message, string emailType, bool enableNotification) =>
|
||||
await SendAsyncInternal(message, emailType, enableNotification);
|
||||
await SendAsyncInternal(message, emailType, enableNotification, null);
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the application should be able to send a required application email
|
||||
/// </summary>
|
||||
/// <inheritdoc/>
|
||||
public async Task SendAsync(EmailMessage message, string emailType, bool enableNotification = false, TimeSpan? expires = null) =>
|
||||
await SendAsyncInternal(message, emailType, enableNotification, expires);
|
||||
|
||||
/// <inheritdoc/>
|
||||
/// <remarks>
|
||||
/// We assume this is possible if either an event handler is registered or an smtp server is configured
|
||||
/// or a pickup directory location is configured
|
||||
@@ -93,7 +102,7 @@ public class EmailSender : IEmailSender
|
||||
|| _globalSettings.IsPickupDirectoryLocationConfigured
|
||||
|| _notificationHandlerRegistered;
|
||||
|
||||
private async Task SendAsyncInternal(EmailMessage message, string emailType, bool enableNotification)
|
||||
private async Task SendAsyncInternal(EmailMessage message, string emailType, bool enableNotification, TimeSpan? expires)
|
||||
{
|
||||
if (enableNotification)
|
||||
{
|
||||
@@ -104,7 +113,7 @@ public class EmailSender : IEmailSender
|
||||
// if a handler handled sending the email then don't continue.
|
||||
if (notification.IsHandled)
|
||||
{
|
||||
if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug))
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug(
|
||||
"The email sending for {Subject} was handled by a notification handler",
|
||||
@@ -116,7 +125,7 @@ public class EmailSender : IEmailSender
|
||||
|
||||
if (!_globalSettings.IsSmtpServerConfigured && !_globalSettings.IsPickupDirectoryLocationConfigured)
|
||||
{
|
||||
if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug))
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug(
|
||||
"Could not send email for {Subject}. It was not handled by a notification handler and there is no SMTP configured.",
|
||||
@@ -173,7 +182,6 @@ public class EmailSender : IEmailSender
|
||||
while (true);
|
||||
}
|
||||
|
||||
await _emailSenderClient.SendAsync(message);
|
||||
await _emailSenderClient.SendAsync(message, expires);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3,15 +3,25 @@ using Umbraco.Cms.Core.Models.Email;
|
||||
namespace Umbraco.Cms.Infrastructure.Mail.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Client for sending an email from a MimeMessage
|
||||
/// Client for sending an email from a MimeMessage.
|
||||
/// </summary>
|
||||
public interface IEmailSenderClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Sends the email message
|
||||
/// Sends the email message.
|
||||
/// </summary>
|
||||
/// <param name="message"></param>
|
||||
/// <returns></returns>
|
||||
/// <param name="message">The <see cref="EmailMessage"/> to send.</param>
|
||||
[Obsolete("Please use the overload taking all parameters. Scheduled for removal in Umbraco 18.")]
|
||||
public Task SendAsync(EmailMessage message);
|
||||
|
||||
/// <summary>
|
||||
/// Sends the email message with an expiration date.
|
||||
/// </summary>
|
||||
/// <param name="message">The <see cref="EmailMessage"/> to send.</param>
|
||||
/// <param name="expires">An optional time for expiry.</param>
|
||||
public Task SendAsync(EmailMessage message, TimeSpan? expires)
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
=> SendAsync(message);
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ using Umbraco.Cms.Core.Cache;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Migrations;
|
||||
using Umbraco.Cms.Core.Models.Membership;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
using Umbraco.Cms.Core.Scoping;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
@@ -51,9 +52,12 @@ public class MigrationPlanExecutor : IMigrationPlanExecutor
|
||||
private readonly DistributedCache _distributedCache;
|
||||
private readonly IScopeAccessor _scopeAccessor;
|
||||
private readonly ICoreScopeProvider _scopeProvider;
|
||||
private readonly IPublishedContentTypeFactory _publishedContentTypeFactory;
|
||||
|
||||
private bool _rebuildCache;
|
||||
private bool _invalidateBackofficeUserAccess;
|
||||
|
||||
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 19.")]
|
||||
public MigrationPlanExecutor(
|
||||
ICoreScopeProvider scopeProvider,
|
||||
IScopeAccessor scopeAccessor,
|
||||
@@ -65,6 +69,33 @@ public class MigrationPlanExecutor : IMigrationPlanExecutor
|
||||
IKeyValueService keyValueService,
|
||||
IServiceScopeFactory serviceScopeFactory,
|
||||
AppCaches appCaches)
|
||||
: this(
|
||||
scopeProvider,
|
||||
scopeAccessor,
|
||||
loggerFactory,
|
||||
migrationBuilder,
|
||||
databaseFactory,
|
||||
databaseCacheRebuilder,
|
||||
distributedCache,
|
||||
keyValueService,
|
||||
serviceScopeFactory,
|
||||
appCaches,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IPublishedContentTypeFactory>())
|
||||
{
|
||||
}
|
||||
|
||||
public MigrationPlanExecutor(
|
||||
ICoreScopeProvider scopeProvider,
|
||||
IScopeAccessor scopeAccessor,
|
||||
ILoggerFactory loggerFactory,
|
||||
IMigrationBuilder migrationBuilder,
|
||||
IUmbracoDatabaseFactory databaseFactory,
|
||||
IDatabaseCacheRebuilder databaseCacheRebuilder,
|
||||
DistributedCache distributedCache,
|
||||
IKeyValueService keyValueService,
|
||||
IServiceScopeFactory serviceScopeFactory,
|
||||
AppCaches appCaches,
|
||||
IPublishedContentTypeFactory publishedContentTypeFactory)
|
||||
{
|
||||
_scopeProvider = scopeProvider;
|
||||
_scopeAccessor = scopeAccessor;
|
||||
@@ -76,6 +107,7 @@ public class MigrationPlanExecutor : IMigrationPlanExecutor
|
||||
_serviceScopeFactory = serviceScopeFactory;
|
||||
_appCaches = appCaches;
|
||||
_distributedCache = distributedCache;
|
||||
_publishedContentTypeFactory = publishedContentTypeFactory;
|
||||
_logger = _loggerFactory.CreateLogger<MigrationPlanExecutor>();
|
||||
}
|
||||
|
||||
@@ -301,6 +333,7 @@ public class MigrationPlanExecutor : IMigrationPlanExecutor
|
||||
_appCaches.IsolatedCaches.ClearAllCaches();
|
||||
await _databaseCacheRebuilder.RebuildAsync(false);
|
||||
_distributedCache.RefreshAllPublishedSnapshot();
|
||||
_publishedContentTypeFactory.ClearDataTypeCache();
|
||||
}
|
||||
|
||||
private async Task RevokeBackofficeTokens()
|
||||
|
||||
+17
-6
@@ -6,7 +6,9 @@ using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.Cms.Infrastructure.Persistence;
|
||||
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_16_3_0;
|
||||
|
||||
@@ -69,7 +71,7 @@ public class MigrateMediaTypeLabelProperties : AsyncMigrationBase
|
||||
|
||||
private void IfNotExistsCreateBytesLabel()
|
||||
{
|
||||
if (Database.Exists<NodeDto>(Constants.DataTypes.LabelBytes))
|
||||
if (NodeExists(_labelBytesDataTypeKey))
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -89,7 +91,7 @@ public class MigrateMediaTypeLabelProperties : AsyncMigrationBase
|
||||
CreateDate = DateTime.Now,
|
||||
};
|
||||
|
||||
_ = Database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, nodeDto);
|
||||
Database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, nodeDto);
|
||||
|
||||
var dataTypeDto = new DataTypeDto
|
||||
{
|
||||
@@ -100,12 +102,12 @@ public class MigrateMediaTypeLabelProperties : AsyncMigrationBase
|
||||
Configuration = "{\"umbracoDataValueType\":\"BIGINT\", \"labelTemplate\":\"{=value | bytes}\"}",
|
||||
};
|
||||
|
||||
_ = Database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, dataTypeDto);
|
||||
Database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, dataTypeDto);
|
||||
}
|
||||
|
||||
private void IfNotExistsCreatePixelsLabel()
|
||||
{
|
||||
if (Database.Exists<NodeDto>(Constants.DataTypes.LabelPixels))
|
||||
if (NodeExists(_labelPixelsDataTypeKey))
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -125,7 +127,7 @@ public class MigrateMediaTypeLabelProperties : AsyncMigrationBase
|
||||
CreateDate = DateTime.Now,
|
||||
};
|
||||
|
||||
_ = Database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, nodeDto);
|
||||
Database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, nodeDto);
|
||||
|
||||
var dataTypeDto = new DataTypeDto
|
||||
{
|
||||
@@ -136,7 +138,16 @@ public class MigrateMediaTypeLabelProperties : AsyncMigrationBase
|
||||
Configuration = "{\"umbracoDataValueType\":\"INT\", \"labelTemplate\":\"{=value}px\"}",
|
||||
};
|
||||
|
||||
_ = Database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, dataTypeDto);
|
||||
Database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, dataTypeDto);
|
||||
}
|
||||
|
||||
private bool NodeExists(Guid uniqueId)
|
||||
{
|
||||
Sql<ISqlContext> sql = Database.SqlContext.Sql()
|
||||
.Select<NodeDto>(x => x.NodeId)
|
||||
.From<NodeDto>()
|
||||
.Where<NodeDto>(x => x.UniqueId == uniqueId);
|
||||
return Database.FirstOrDefault<NodeDto>(sql) is not null;
|
||||
}
|
||||
|
||||
private async Task MigrateMediaTypeLabels()
|
||||
|
||||
+17
-4
@@ -1132,25 +1132,38 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
|
||||
|
||||
IEnumerable<PropertyDataDto> propertyDataDtos = PropertyFactory.BuildDtos(entity.ContentType.Variations, entity.VersionId, publishedVersionId, entity.Properties, LanguageRepository, out edited, out editedCultures);
|
||||
|
||||
var toUpdate = new List<PropertyDataDto>();
|
||||
var toInsert = new List<PropertyDataDto>();
|
||||
foreach (PropertyDataDto propertyDataDto in propertyDataDtos)
|
||||
{
|
||||
// Check if this already exists and update, else insert a new one
|
||||
if (propertyTypeToPropertyData.TryGetValue((propertyDataDto.PropertyTypeId, propertyDataDto.VersionId, propertyDataDto.LanguageId, propertyDataDto.Segment), out PropertyDataDto? propData))
|
||||
{
|
||||
propertyDataDto.Id = propData.Id;
|
||||
Database.Update(propertyDataDto);
|
||||
toUpdate.Add(propertyDataDto);
|
||||
}
|
||||
else
|
||||
{
|
||||
// TODO: we can speed this up: Use BulkInsert and then do one SELECT to re-retrieve the property data inserted with assigned IDs.
|
||||
// This is a perfect thing to benchmark with Benchmark.NET to compare perf between Nuget releases.
|
||||
Database.Insert(propertyDataDto);
|
||||
toInsert.Add(propertyDataDto);
|
||||
}
|
||||
|
||||
// track which ones have been processed
|
||||
existingPropDataIds.Remove(propertyDataDto.Id);
|
||||
}
|
||||
|
||||
if (toUpdate.Count > 0)
|
||||
{
|
||||
var updateBatch = toUpdate
|
||||
.Select(x => UpdateBatch.For(x))
|
||||
.ToList();
|
||||
Database.UpdateBatch(updateBatch, new BatchOptions { BatchSize = 100 });
|
||||
}
|
||||
|
||||
if (toInsert.Count > 0)
|
||||
{
|
||||
Database.InsertBulk(toInsert);
|
||||
}
|
||||
|
||||
// For any remaining that haven't been processed they need to be deleted
|
||||
if (existingPropDataIds.Count > 0)
|
||||
{
|
||||
|
||||
+7
-3
@@ -1381,9 +1381,13 @@ AND umbracoNode.id <> @id",
|
||||
}
|
||||
else if (ev.Key.langId.HasValue)
|
||||
{
|
||||
// This should never happen! If a property culture is flagged as edited then the culture must exist at the document level
|
||||
throw new PanicException(
|
||||
$"The existing DocumentCultureVariationDto was not found for node {ev.Key.nodeId} and language {ev.Key.langId}");
|
||||
// This can happen when a property changes from invariant to variant and the content
|
||||
// was only created in non-default languages. The invariant property data gets migrated
|
||||
// to the default language, but no DocumentCultureVariationDto exists for the default
|
||||
// language because the content was never created in that language.
|
||||
// In this case, we simply skip updating the edited flag since there's no document
|
||||
// culture variation record to update.
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1328,6 +1328,11 @@ public class DocumentRepository : ContentRepositoryBase<int, IContent, DocumentR
|
||||
|
||||
entity.ResetDirtyProperties();
|
||||
|
||||
// We need to flush the isolated cache by key explicitly here.
|
||||
// The ContentCacheRefresher does the same thing, but by the time it's invoked, custom notification handlers
|
||||
// might have already consumed the cached version (which at this point is the previous version).
|
||||
IsolatedCache.ClearByKey(RepositoryCacheKeys.GetKey<IContent, Guid>(entity.Key));
|
||||
|
||||
// troubleshooting
|
||||
//if (Database.ExecuteScalar<int>($"SELECT COUNT(*) FROM {Constants.DatabaseSchema.Tables.DocumentVersion} JOIN {Constants.DatabaseSchema.Tables.ContentVersion} ON {Constants.DatabaseSchema.Tables.DocumentVersion}.id={Constants.DatabaseSchema.Tables.ContentVersion}.id WHERE published=1 AND nodeId=" + content.Id) > 1)
|
||||
//{
|
||||
|
||||
@@ -270,7 +270,7 @@ public class MediaRepository : ContentRepositoryBase<int, IMedia, MediaRepositor
|
||||
"DELETE FROM " + Constants.DatabaseSchema.Tables.UserGroup2GranularPermission + " WHERE uniqueId IN (SELECT uniqueId FROM umbracoNode WHERE id = @id)",
|
||||
"DELETE FROM " + Constants.DatabaseSchema.Tables.UserStartNode + " WHERE startNode = @id",
|
||||
"UPDATE " + Constants.DatabaseSchema.Tables.UserGroup +
|
||||
" SET startContentId = NULL WHERE startContentId = @id",
|
||||
" SET startMediaId = NULL WHERE startMediaId = @id",
|
||||
"DELETE FROM " + Constants.DatabaseSchema.Tables.Relation + " WHERE parentId = @id",
|
||||
"DELETE FROM " + Constants.DatabaseSchema.Tables.Relation + " WHERE childId = @id",
|
||||
"DELETE FROM " + Constants.DatabaseSchema.Tables.TagRelationship + " WHERE nodeId = @id",
|
||||
@@ -480,6 +480,11 @@ public class MediaRepository : ContentRepositoryBase<int, IMedia, MediaRepositor
|
||||
OnUowRefreshedEntity(new MediaRefreshNotification(entity, new EventMessages()));
|
||||
|
||||
entity.ResetDirtyProperties();
|
||||
|
||||
// We need to flush the isolated cache by key explicitly here.
|
||||
// The MediaCacheRefresher does the same thing, but by the time it's invoked, custom notification handlers
|
||||
// might have already consumed the cached version (which at this point is the previous version).
|
||||
IsolatedCache.ClearByKey(RepositoryCacheKeys.GetKey<IMedia, Guid>(entity.Key));
|
||||
}
|
||||
|
||||
protected override void PersistDeletedItem(IMedia entity)
|
||||
|
||||
@@ -51,7 +51,7 @@ public abstract class BlockEditorPropertyValueEditor<TValue, TLayout> : BlockVal
|
||||
languageService,
|
||||
ioHelper,
|
||||
attribute,
|
||||
StaticServiceProvider.Instance.GetRequiredService<ILogger>())
|
||||
StaticServiceProvider.Instance.GetRequiredService<ILogger<BlockEditorPropertyValueEditor<TValue, TLayout>>>())
|
||||
{
|
||||
}
|
||||
|
||||
@@ -135,6 +135,8 @@ public abstract class BlockEditorPropertyValueEditor<TValue, TLayout> : BlockVal
|
||||
BlockEditorData<TValue, TLayout>? currentBlockEditorData = SafeParseBlockEditorData(currentValue);
|
||||
BlockEditorData<TValue, TLayout>? blockEditorData = SafeParseBlockEditorData(editorValue.Value);
|
||||
|
||||
CacheReferencedEntities(blockEditorData);
|
||||
|
||||
// We can skip MapBlockValueFromEditor if both editorValue and currentValue values are empty.
|
||||
if (IsBlockEditorDataEmpty(currentBlockEditorData) && IsBlockEditorDataEmpty(blockEditorData))
|
||||
{
|
||||
|
||||
@@ -43,6 +43,45 @@ public abstract class BlockValuePropertyValueEditorBase<TValue, TLayout> : DataV
|
||||
_languageService = languageService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Caches referenced entities for all property values with supporting property editors within the specified block editor data
|
||||
/// optimising subsequent retrieval of entities when parsing and converting property values.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method iterates through all property values associated with data editors in the provided
|
||||
/// block editor data and invokes caching for referenced entities where supported by the property editor.
|
||||
/// </remarks>
|
||||
/// <param name="blockEditorData">The block editor data containing content and settings property values to analyze for referenced entities.</param>
|
||||
[Obsolete("This method is available for support of request caching retrieved entities in derived property value editors. " +
|
||||
"The intention is to supersede this with lazy loaded read locks, which will make this unnecessary. " +
|
||||
"Scheduled for removal in Umbraco 19.")]
|
||||
protected void CacheReferencedEntities(BlockEditorData<TValue, TLayout>? blockEditorData)
|
||||
{
|
||||
// Group property values by their associated data editor alias.
|
||||
IEnumerable<IGrouping<string, BlockPropertyValue>> valuesByDataEditors = (blockEditorData?.BlockValue.ContentData ?? []).Union(blockEditorData?.BlockValue.SettingsData ?? [])
|
||||
.SelectMany(x => x.Values)
|
||||
.Where(x => x.EditorAlias is not null && x.Value is not null)
|
||||
.GroupBy(x => x.EditorAlias!);
|
||||
|
||||
// Iterate through each group and cache referenced entities if supported by the data editor.
|
||||
foreach (IGrouping<string, BlockPropertyValue> valueByDataEditor in valuesByDataEditors)
|
||||
{
|
||||
IDataEditor? dataEditor = _propertyEditors[valueByDataEditor.Key];
|
||||
if (dataEditor is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
IDataValueEditor valueEditor = dataEditor.GetValueEditor();
|
||||
|
||||
if (valueEditor is ICacheReferencedEntities valueEditorWithPrecaching)
|
||||
{
|
||||
valueEditorWithPrecaching.CacheReferencedEntities(valueByDataEditor.Select(x => x.Value!));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract IEnumerable<UmbracoEntityReference> GetReferences(object? value);
|
||||
|
||||
|
||||
@@ -10,8 +10,11 @@ using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Core.PropertyEditors;
|
||||
|
||||
internal sealed class ColorPickerConfigurationEditor : ConfigurationEditor<ColorPickerConfiguration>
|
||||
internal sealed partial class ColorPickerConfigurationEditor : ConfigurationEditor<ColorPickerConfiguration>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ColorPickerConfigurationEditor"/> class.
|
||||
/// </summary>
|
||||
public ColorPickerConfigurationEditor(IIOHelper ioHelper, IConfigurationEditorJsonSerializer configurationEditorJsonSerializer)
|
||||
: base(ioHelper)
|
||||
{
|
||||
@@ -19,13 +22,17 @@ internal sealed class ColorPickerConfigurationEditor : ConfigurationEditor<Color
|
||||
items.Validators.Add(new ColorListValidator(configurationEditorJsonSerializer));
|
||||
}
|
||||
|
||||
internal sealed class ColorListValidator : IValueValidator
|
||||
internal sealed partial class ColorListValidator : IValueValidator
|
||||
{
|
||||
private readonly IConfigurationEditorJsonSerializer _configurationEditorJsonSerializer;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ColorListValidator"/> class.
|
||||
/// </summary>
|
||||
public ColorListValidator(IConfigurationEditorJsonSerializer configurationEditorJsonSerializer)
|
||||
=> _configurationEditorJsonSerializer = configurationEditorJsonSerializer;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IEnumerable<ValidationResult> Validate(object? value, string? valueType, object? dataTypeConfiguration, PropertyValidationContext validationContext)
|
||||
{
|
||||
var stringValue = value?.ToString();
|
||||
@@ -46,17 +53,53 @@ internal sealed class ColorPickerConfigurationEditor : ConfigurationEditor<Color
|
||||
|
||||
if (items is null)
|
||||
{
|
||||
yield return new ValidationResult($"The configuration value {stringValue} is not a valid color picker configuration", new[] { "items" });
|
||||
yield return new ValidationResult($"The configuration value {stringValue} is not a valid color picker configuration", ["items"]);
|
||||
yield break;
|
||||
}
|
||||
|
||||
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var duplicates = new List<string>();
|
||||
foreach (ColorPickerConfiguration.ColorPickerItem item in items)
|
||||
{
|
||||
if (Regex.IsMatch(item.Value, "^([0-9a-f]{3}|[0-9a-f]{6})$", RegexOptions.IgnoreCase) == false)
|
||||
if (ColorPattern().IsMatch(item.Value) == false)
|
||||
{
|
||||
yield return new ValidationResult($"The value {item.Value} is not a valid hex color", new[] { "items" });
|
||||
yield return new ValidationResult($"The value {item.Value} is not a valid hex color", ["items"]);
|
||||
continue;
|
||||
}
|
||||
|
||||
var normalized = Normalize(item.Value);
|
||||
if (seen.Add(normalized) is false)
|
||||
{
|
||||
duplicates.Add(normalized);
|
||||
}
|
||||
}
|
||||
|
||||
if (duplicates.Count > 0)
|
||||
{
|
||||
yield return new ValidationResult(
|
||||
$"Duplicate color values are not allowed: {string.Join(", ", duplicates)}",
|
||||
["items"]);
|
||||
}
|
||||
}
|
||||
|
||||
private static string Normalize(string? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var normalizedValue = value.Trim().ToLowerInvariant();
|
||||
|
||||
if (normalizedValue.Length == 3)
|
||||
{
|
||||
normalizedValue = $"{normalizedValue[0]}{normalizedValue[0]}{normalizedValue[1]}{normalizedValue[1]}{normalizedValue[2]}{normalizedValue[2]}";
|
||||
}
|
||||
|
||||
return normalizedValue;
|
||||
}
|
||||
|
||||
[GeneratedRegex("^([0-9a-f]{3}|[0-9a-f]{6})$", RegexOptions.IgnoreCase, "en-GB")]
|
||||
private static partial Regex ColorPattern();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ public class MediaPicker3PropertyEditor : DataEditor
|
||||
/// <summary>
|
||||
/// Defines the value editor for the media picker property editor.
|
||||
/// </summary>
|
||||
internal sealed class MediaPicker3PropertyValueEditor : DataValueEditor, IDataValueReference
|
||||
internal sealed class MediaPicker3PropertyValueEditor : DataValueEditor, IDataValueReference, ICacheReferencedEntities
|
||||
{
|
||||
private readonly IDataTypeConfigurationCache _dataTypeReadCache;
|
||||
private readonly IJsonSerializer _jsonSerializer;
|
||||
@@ -61,6 +61,7 @@ public class MediaPicker3PropertyEditor : DataEditor
|
||||
private readonly ITemporaryFileService _temporaryFileService;
|
||||
private readonly IScopeProvider _scopeProvider;
|
||||
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
|
||||
private readonly AppCaches _appCaches;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MediaPicker3PropertyValueEditor"/> class.
|
||||
@@ -93,6 +94,8 @@ public class MediaPicker3PropertyEditor : DataEditor
|
||||
_scopeProvider = scopeProvider;
|
||||
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
|
||||
_dataTypeReadCache = dataTypeReadCache;
|
||||
_appCaches = appCaches;
|
||||
|
||||
var validators = new TypedJsonValidatorRunner<List<MediaWithCropsDto>, MediaPicker3Configuration>(
|
||||
jsonSerializer,
|
||||
new MinMaxValidator(localizedTextService),
|
||||
@@ -102,6 +105,27 @@ public class MediaPicker3PropertyEditor : DataEditor
|
||||
Validators.Add(validators);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void CacheReferencedEntities(IEnumerable<object> values)
|
||||
{
|
||||
var mediaKeys = values
|
||||
.SelectMany(value => Deserialize(_jsonSerializer, value))
|
||||
.Select(dto => dto.MediaKey)
|
||||
.Distinct()
|
||||
.Where(x => IsMediaAlreadyCached(x, _appCaches.RequestCache) is false)
|
||||
.ToList();
|
||||
if (mediaKeys.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
IEnumerable<IMedia> mediaItems = _mediaService.GetByIds(mediaKeys);
|
||||
foreach (IMedia media in mediaItems)
|
||||
{
|
||||
CacheMediaById(media, _appCaches.RequestCache);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IEnumerable<UmbracoEntityReference> GetReferences(object? value)
|
||||
{
|
||||
@@ -203,7 +227,7 @@ public class MediaPicker3PropertyEditor : DataEditor
|
||||
|
||||
foreach (MediaWithCropsDto mediaWithCropsDto in mediaWithCropsDtos)
|
||||
{
|
||||
IMedia? media = _mediaService.GetById(mediaWithCropsDto.MediaKey);
|
||||
IMedia? media = GetAndCacheMediaById(mediaWithCropsDto.MediaKey, _appCaches.RequestCache, _mediaService);
|
||||
mediaWithCropsDto.MediaTypeAlias = media?.ContentType.Alias ?? unknownMediaType;
|
||||
}
|
||||
|
||||
@@ -217,7 +241,7 @@ public class MediaPicker3PropertyEditor : DataEditor
|
||||
foreach (MediaWithCropsDto mediaWithCropsDto in mediaWithCropsDtos)
|
||||
{
|
||||
// if the media already exist, don't bother with it
|
||||
if (_mediaService.GetById(mediaWithCropsDto.MediaKey) != null)
|
||||
if (GetAndCacheMediaById(mediaWithCropsDto.MediaKey, _appCaches.RequestCache, _mediaService) != null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -457,18 +481,7 @@ public class MediaPicker3PropertyEditor : DataEditor
|
||||
|
||||
foreach (var typeAlias in distinctTypeAliases)
|
||||
{
|
||||
// Cache media type lookups since the same media type is likely to be used multiple times in validation,
|
||||
// particularly if we have multiple languages and blocks.
|
||||
var cacheKey = string.Format(MediaTypeCacheKeyFormat, typeAlias);
|
||||
string? typeKey = _appCaches.RequestCache.GetCacheItem<string?>(cacheKey);
|
||||
if (typeKey is null)
|
||||
{
|
||||
typeKey = _mediaTypeService.Get(typeAlias)?.Key.ToString();
|
||||
if (typeKey is not null)
|
||||
{
|
||||
_appCaches.RequestCache.Set(cacheKey, typeKey);
|
||||
}
|
||||
}
|
||||
string? typeKey = GetMediaTypeKey(typeAlias);
|
||||
|
||||
if (typeKey is null || allowedTypes.Contains(typeKey) is false)
|
||||
{
|
||||
@@ -483,6 +496,31 @@ public class MediaPicker3PropertyEditor : DataEditor
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
private string? GetMediaTypeKey(string typeAlias)
|
||||
{
|
||||
// Cache media type lookups since the same media type is likely to be used multiple times in validation,
|
||||
// particularly if we have multiple languages and blocks.
|
||||
string? GetMediaTypeKeyFromService(string typeAlias) => _mediaTypeService.Get(typeAlias)?.Key.ToString();
|
||||
|
||||
if (_appCaches.RequestCache.IsAvailable is false)
|
||||
{
|
||||
return GetMediaTypeKeyFromService(typeAlias);
|
||||
}
|
||||
|
||||
var cacheKey = string.Format(MediaTypeCacheKeyFormat, typeAlias);
|
||||
string? typeKey = _appCaches.RequestCache.GetCacheItem<string?>(cacheKey);
|
||||
if (typeKey is null)
|
||||
{
|
||||
typeKey = GetMediaTypeKeyFromService(typeAlias);
|
||||
if (typeKey is not null)
|
||||
{
|
||||
_appCaches.RequestCache.Set(cacheKey, typeKey);
|
||||
}
|
||||
}
|
||||
|
||||
return typeKey;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -3,7 +3,10 @@
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Runtime.Serialization;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Umbraco.Cms.Core.Cache;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.IO;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.ContentEditing;
|
||||
@@ -19,14 +22,16 @@ using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Core.PropertyEditors;
|
||||
|
||||
public class MultiUrlPickerValueEditor : DataValueEditor, IDataValueReference
|
||||
public class MultiUrlPickerValueEditor : DataValueEditor, IDataValueReference, ICacheReferencedEntities
|
||||
{
|
||||
private readonly ILogger<MultiUrlPickerValueEditor> _logger;
|
||||
private readonly IPublishedUrlProvider _publishedUrlProvider;
|
||||
private readonly IJsonSerializer _jsonSerializer;
|
||||
private readonly IContentService _contentService;
|
||||
private readonly IMediaService _mediaService;
|
||||
private readonly AppCaches _appCaches;
|
||||
|
||||
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 19.")]
|
||||
public MultiUrlPickerValueEditor(
|
||||
ILogger<MultiUrlPickerValueEditor> logger,
|
||||
ILocalizedTextService localizedTextService,
|
||||
@@ -37,19 +42,102 @@ public class MultiUrlPickerValueEditor : DataValueEditor, IDataValueReference
|
||||
IIOHelper ioHelper,
|
||||
IContentService contentService,
|
||||
IMediaService mediaService)
|
||||
: this(
|
||||
logger,
|
||||
localizedTextService,
|
||||
shortStringHelper,
|
||||
attribute,
|
||||
publishedUrlProvider,
|
||||
jsonSerializer,
|
||||
ioHelper,
|
||||
contentService,
|
||||
mediaService,
|
||||
StaticServiceProvider.Instance.GetRequiredService<AppCaches>())
|
||||
{
|
||||
}
|
||||
|
||||
public MultiUrlPickerValueEditor(
|
||||
ILogger<MultiUrlPickerValueEditor> logger,
|
||||
ILocalizedTextService localizedTextService,
|
||||
IShortStringHelper shortStringHelper,
|
||||
DataEditorAttribute attribute,
|
||||
IPublishedUrlProvider publishedUrlProvider,
|
||||
IJsonSerializer jsonSerializer,
|
||||
IIOHelper ioHelper,
|
||||
IContentService contentService,
|
||||
IMediaService mediaService,
|
||||
AppCaches appCaches)
|
||||
: base(shortStringHelper, jsonSerializer, ioHelper, attribute)
|
||||
{
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_logger = logger;
|
||||
_publishedUrlProvider = publishedUrlProvider;
|
||||
|
||||
_jsonSerializer = jsonSerializer;
|
||||
_contentService = contentService;
|
||||
_mediaService = mediaService;
|
||||
_appCaches = appCaches;
|
||||
|
||||
Validators.Add(new TypedJsonValidatorRunner<LinkDisplay[], MultiUrlPickerConfiguration>(
|
||||
_jsonSerializer,
|
||||
new MinMaxValidator(localizedTextService)));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void CacheReferencedEntities(IEnumerable<object> values)
|
||||
{
|
||||
var dtos = values
|
||||
.Select(value =>
|
||||
{
|
||||
var asString = value is string str ? str : value.ToString();
|
||||
if (string.IsNullOrEmpty(asString))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return _jsonSerializer.Deserialize<List<LinkDto>>(asString);
|
||||
})
|
||||
.WhereNotNull()
|
||||
.SelectMany(x => x)
|
||||
.Where(x => x.Type == Constants.UdiEntityType.Document || x.Type == Constants.UdiEntityType.Media)
|
||||
.ToList();
|
||||
|
||||
IList<Guid> contentKeys = GetKeys(Constants.UdiEntityType.Document, dtos);
|
||||
IList<Guid> mediaKeys = GetKeys(Constants.UdiEntityType.Media, dtos);
|
||||
|
||||
if (contentKeys.Count > 0)
|
||||
{
|
||||
IEnumerable<IContent> contentItems = _contentService.GetByIds(contentKeys);
|
||||
foreach (IContent content in contentItems)
|
||||
{
|
||||
CacheContentById(content, _appCaches.RequestCache);
|
||||
}
|
||||
}
|
||||
|
||||
if (mediaKeys.Count > 0)
|
||||
{
|
||||
IEnumerable<IMedia> mediaItems = _mediaService.GetByIds(mediaKeys);
|
||||
foreach (IMedia media in mediaItems)
|
||||
{
|
||||
CacheMediaById(media, _appCaches.RequestCache);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private IList<Guid> GetKeys(string entityType, IEnumerable<LinkDto> dtos) =>
|
||||
dtos
|
||||
.Where(x => x.Type == entityType)
|
||||
.Select(x => x.Unique ?? (x.Udi is not null ? x.Udi.Guid : Guid.Empty))
|
||||
.Where(x => x != Guid.Empty)
|
||||
.Distinct()
|
||||
.Where(x => IsAlreadyCached(x, entityType) is false)
|
||||
.ToList();
|
||||
|
||||
private bool IsAlreadyCached(Guid key, string entityType) => entityType switch
|
||||
{
|
||||
Constants.UdiEntityType.Document => IsContentAlreadyCached(key, _appCaches.RequestCache),
|
||||
Constants.UdiEntityType.Media => IsMediaAlreadyCached(key, _appCaches.RequestCache),
|
||||
_ => false,
|
||||
};
|
||||
|
||||
public IEnumerable<UmbracoEntityReference> GetReferences(object? value)
|
||||
{
|
||||
var asString = value == null ? string.Empty : value is string str ? str : value.ToString();
|
||||
@@ -105,7 +193,7 @@ public class MultiUrlPickerValueEditor : DataValueEditor, IDataValueReference
|
||||
if (dto.Udi.EntityType == Constants.UdiEntityType.Document)
|
||||
{
|
||||
url = _publishedUrlProvider.GetUrl(dto.Udi.Guid, UrlMode.Relative, culture);
|
||||
IContent? c = _contentService.GetById(dto.Udi.Guid);
|
||||
IContent? c = GetAndCacheContentById(dto.Udi.Guid, _appCaches.RequestCache, _contentService);
|
||||
|
||||
if (c is not null)
|
||||
{
|
||||
@@ -119,7 +207,7 @@ public class MultiUrlPickerValueEditor : DataValueEditor, IDataValueReference
|
||||
else if (dto.Udi.EntityType == Constants.UdiEntityType.Media)
|
||||
{
|
||||
url = _publishedUrlProvider.GetMediaUrl(dto.Udi.Guid, UrlMode.Relative, culture);
|
||||
IMedia? m = _mediaService.GetById(dto.Udi.Guid);
|
||||
IMedia? m = GetAndCacheMediaById(dto.Udi.Guid, _appCaches.RequestCache, _mediaService);
|
||||
if (m is not null)
|
||||
{
|
||||
published = m.Trashed is false;
|
||||
@@ -207,6 +295,12 @@ public class MultiUrlPickerValueEditor : DataValueEditor, IDataValueReference
|
||||
[DataMember(Name = "target")]
|
||||
public string? Target { get; set; }
|
||||
|
||||
[DataMember(Name = "unique")]
|
||||
public Guid? Unique { get; set; }
|
||||
|
||||
[DataMember(Name = "type")]
|
||||
public string? Type { get; set; }
|
||||
|
||||
[DataMember(Name = "udi")]
|
||||
public GuidUdi? Udi { get; set; }
|
||||
|
||||
|
||||
+4
-3
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) Umbraco.
|
||||
// See LICENSE for more details.
|
||||
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Umbraco.Cms.Core.Models.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
@@ -53,10 +54,10 @@ public class ImageCropperValueConverter : PropertyValueConverterBase, IDeliveryA
|
||||
{
|
||||
value = _jsonSerializer.Deserialize<ImageCropperValue>(sourceString);
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch (JsonException ex)
|
||||
{
|
||||
// cannot deserialize, assume it may be a raw image URL
|
||||
_logger.LogError(ex, "Could not deserialize string '{JsonString}' into an image cropper value.", sourceString);
|
||||
// Cannot deserialize, assume it may be a raw image URL.
|
||||
_logger.LogDebug(ex, "Could not deserialize string '{JsonString}' into an image cropper value.", sourceString);
|
||||
value = new ImageCropperValue { Src = sourceString };
|
||||
}
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ public class EmailUserForgotPasswordSender : IUserForgotPasswordSender
|
||||
|
||||
var message = new EmailMessage(senderEmail, address.ToString(), emailSubject, emailBody, true);
|
||||
|
||||
await _emailSender.SendAsync(message, Constants.Web.EmailTypes.PasswordReset, true);
|
||||
await _emailSender.SendAsync(message, Constants.Web.EmailTypes.PasswordReset, true, _securitySettings.PasswordResetEmailExpiry);
|
||||
}
|
||||
|
||||
public bool CanSend() => _securitySettings.AllowPasswordReset && _emailSender.CanSendRequiredEmail();
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
using System.Globalization;
|
||||
using System.Globalization;
|
||||
using System.Net;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using MimeKit;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Mail;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.Email;
|
||||
@@ -18,15 +20,31 @@ public class EmailUserInviteSender : IUserInviteSender
|
||||
private readonly IEmailSender _emailSender;
|
||||
private readonly ILocalizedTextService _localizedTextService;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly SecuritySettings _securitySettings;
|
||||
|
||||
[Obsolete("Please use the constructor with all parameters. Scheduled for removal in Umbraco 18.")]
|
||||
public EmailUserInviteSender(
|
||||
IEmailSender emailSender,
|
||||
ILocalizedTextService localizedTextService,
|
||||
IOptions<GlobalSettings> globalSettings)
|
||||
: this(
|
||||
emailSender,
|
||||
localizedTextService,
|
||||
globalSettings,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IOptions<SecuritySettings>>())
|
||||
{
|
||||
}
|
||||
|
||||
public EmailUserInviteSender(
|
||||
IEmailSender emailSender,
|
||||
ILocalizedTextService localizedTextService,
|
||||
IOptions<GlobalSettings> globalSettings,
|
||||
IOptions<SecuritySettings> securitySettings)
|
||||
{
|
||||
_emailSender = emailSender;
|
||||
_localizedTextService = localizedTextService;
|
||||
_globalSettings = globalSettings.Value;
|
||||
_securitySettings = securitySettings.Value;
|
||||
}
|
||||
|
||||
public async Task InviteUser(UserInvitationMessage invite)
|
||||
@@ -67,7 +85,7 @@ public class EmailUserInviteSender : IUserInviteSender
|
||||
|
||||
var message = new EmailMessage(senderEmail, address.ToString(), emailSubject, emailBody, true);
|
||||
|
||||
await _emailSender.SendAsync(message, Constants.Web.EmailTypes.UserInvite, true);
|
||||
await _emailSender.SendAsync(message, Constants.Web.EmailTypes.UserInvite, true, _securitySettings.UserInviteEmailExpiry);
|
||||
}
|
||||
|
||||
public bool CanSendInvites() => _emailSender.CanSendRequiredEmail();
|
||||
|
||||
@@ -95,7 +95,7 @@ public class IdentityMapDefinition : IMapDefinition
|
||||
target.IsApproved = source.IsApproved;
|
||||
target.SecurityStamp = source.SecurityStamp;
|
||||
DateTime? lockedOutUntil = source.LastLockoutDate?.AddMinutes(_securitySettings.UserDefaultLockoutTimeInMinutes);
|
||||
target.LockoutEnd = source.IsLockedOut ? (lockedOutUntil ?? DateTime.MaxValue).ToUniversalTime() : null;
|
||||
target.LockoutEnd = source.IsLockedOut ? lockedOutUntil ?? DateTime.MaxValue : null;
|
||||
target.Kind = source.Kind;
|
||||
}
|
||||
|
||||
@@ -114,16 +114,46 @@ public class IdentityMapDefinition : IMapDefinition
|
||||
target.IsApproved = source.IsApproved;
|
||||
target.SecurityStamp = source.SecurityStamp;
|
||||
DateTime? lockedOutUntil = source.LastLockoutDate?.AddMinutes(_securitySettings.MemberDefaultLockoutTimeInMinutes);
|
||||
target.LockoutEnd = source.IsLockedOut ? (lockedOutUntil ?? DateTime.MaxValue).ToUniversalTime() : null;
|
||||
target.LockoutEnd = GetLockoutEnd(source);
|
||||
target.LastLockoutDateUtc = GetLastLockoutDateUtc(source);
|
||||
target.CreatedDateUtc = EnsureUtcWithServerTime(source.CreateDate);
|
||||
target.Comments = source.Comments;
|
||||
target.LastLockoutDateUtc = source.LastLockoutDate == DateTime.MinValue
|
||||
? null
|
||||
: source.LastLockoutDate?.ToUniversalTime();
|
||||
target.CreatedDateUtc = source.CreateDate.ToUniversalTime();
|
||||
target.Key = source.Key;
|
||||
target.MemberTypeAlias = source.ContentTypeAlias;
|
||||
target.TwoFactorEnabled = _twoFactorLoginService.IsTwoFactorEnabledAsync(source.Key).GetAwaiter().GetResult();
|
||||
|
||||
// NB: same comments re AutoMapper as per BackOfficeUser
|
||||
}
|
||||
|
||||
private DateTimeOffset? GetLockoutEnd(IMember source)
|
||||
{
|
||||
if (source.IsLockedOut is false)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
DateTime? lockedOutUntil = source.LastLockoutDate?.AddMinutes(_securitySettings.MemberDefaultLockoutTimeInMinutes);
|
||||
if (lockedOutUntil.HasValue is false)
|
||||
{
|
||||
return DateTime.MaxValue;
|
||||
}
|
||||
|
||||
return EnsureUtcWithServerTime(lockedOutUntil.Value);
|
||||
}
|
||||
|
||||
private static DateTime? GetLastLockoutDateUtc(IMember source)
|
||||
{
|
||||
if (source.LastLockoutDate is null || source.LastLockoutDate == DateTime.MinValue)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return EnsureUtcWithServerTime(source.LastLockoutDate.Value);
|
||||
}
|
||||
|
||||
private static DateTime EnsureUtcWithServerTime(DateTime date) =>
|
||||
|
||||
// We have a server time value here, but the Kind is UTC, so we can't use .ToUniversalTime() to convert to the UTC
|
||||
// value that the LockoutEnd property expects. We need to create a DateTimeOffset with the correct offset.
|
||||
DateTime.SpecifyKind(date, DateTimeKind.Local).ToUniversalTime();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.Json.Serialization;
|
||||
using Umbraco.Cms.Core.Models.Blocks;
|
||||
using Umbraco.Extensions;
|
||||
@@ -121,7 +122,17 @@ public class JsonBlockValueConverter : JsonConverter<BlockValue>
|
||||
}
|
||||
|
||||
private List<BlockItemData> DeserializeBlockItemData(ref Utf8JsonReader reader, JsonSerializerOptions options, Type typeToConvert, string propertyName)
|
||||
=> DeserializeListOf<BlockItemData>(ref reader, options, typeToConvert, propertyName);
|
||||
{
|
||||
try
|
||||
{
|
||||
return DeserializeListOf<BlockItemData>(ref reader, options, typeToConvert, propertyName);
|
||||
}
|
||||
catch (JsonException ex) when (ex.Path?.EndsWith(".values") is true)
|
||||
{
|
||||
// If we hit a JsonException due to the "values" property conflict, attempt the fallback deserialization
|
||||
return FallbackBlockItemDataDeserialization(ref reader, options);
|
||||
}
|
||||
}
|
||||
|
||||
private List<BlockItemVariation> DeserializeBlockVariation(ref Utf8JsonReader reader, JsonSerializerOptions options, Type typeToConvert, string propertyName)
|
||||
=> DeserializeListOf<BlockItemVariation>(ref reader, options, typeToConvert, propertyName);
|
||||
@@ -224,5 +235,38 @@ public class JsonBlockValueConverter : JsonConverter<BlockValue>
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Obsolete("Only needed to support the old data schema. Remove in V18.")]
|
||||
private static List<BlockItemData> FallbackBlockItemDataDeserialization(ref Utf8JsonReader reader, JsonSerializerOptions options)
|
||||
{
|
||||
JsonArray? arrayElement = JsonSerializer.Deserialize<JsonArray>(ref reader, options);
|
||||
|
||||
return arrayElement?
|
||||
.Select(itemElement => DeserializeBlockItemData(itemElement, options))
|
||||
.OfType<BlockItemData>()
|
||||
.ToList() ?? [];
|
||||
}
|
||||
|
||||
[Obsolete("Only needed to support the old data schema. Remove in V18.")]
|
||||
private static BlockItemData? DeserializeBlockItemData(JsonNode? jsonNode, JsonSerializerOptions options)
|
||||
{
|
||||
if (jsonNode is not JsonObject jsonObject || jsonObject.ContainsKey("values") is false)
|
||||
{
|
||||
// Nothing to be done, just deserialize as usual
|
||||
return jsonNode.Deserialize<BlockItemData>(options);
|
||||
}
|
||||
|
||||
// Handle the "values" property conflict by extracting the "values" property first and adding it to the
|
||||
// RawPropertyValues dictionary after deserialization
|
||||
JsonNode? values = jsonObject["values"];
|
||||
jsonObject.Remove("values");
|
||||
|
||||
BlockItemData? blockItemData = jsonObject.Deserialize<BlockItemData>(options);
|
||||
if (blockItemData is not null)
|
||||
{
|
||||
blockItemData.RawPropertyValues["values"] = values.Deserialize<object?>(options);
|
||||
}
|
||||
|
||||
return blockItemData;
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user