Compare commits
97
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 |
+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"
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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()
|
||||
|
||||
+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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -52,10 +52,8 @@ 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 const string MediaCacheKeyFormat = nameof(MediaPicker3PropertyValueEditor) + "_Media_{0}";
|
||||
|
||||
private readonly IDataTypeConfigurationCache _dataTypeReadCache;
|
||||
private readonly IJsonSerializer _jsonSerializer;
|
||||
private readonly IMediaImportService _mediaImportService;
|
||||
@@ -107,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)
|
||||
{
|
||||
@@ -208,31 +227,13 @@ public class MediaPicker3PropertyEditor : DataEditor
|
||||
|
||||
foreach (MediaWithCropsDto mediaWithCropsDto in mediaWithCropsDtos)
|
||||
{
|
||||
IMedia? media = GetMediaById(mediaWithCropsDto.MediaKey);
|
||||
IMedia? media = GetAndCacheMediaById(mediaWithCropsDto.MediaKey, _appCaches.RequestCache, _mediaService);
|
||||
mediaWithCropsDto.MediaTypeAlias = media?.ContentType.Alias ?? unknownMediaType;
|
||||
}
|
||||
|
||||
return mediaWithCropsDtos.Where(m => m.MediaTypeAlias != unknownMediaType).ToList();
|
||||
}
|
||||
|
||||
private IMedia? GetMediaById(Guid key)
|
||||
{
|
||||
// Cache media lookups in case the same media is handled multiple times across a save operation,
|
||||
// which is possible, particularly if we have multiple languages and blocks.
|
||||
var cacheKey = string.Format(MediaCacheKeyFormat, key);
|
||||
IMedia? media = _appCaches.RequestCache.GetCacheItem<IMedia?>(cacheKey);
|
||||
if (media is null)
|
||||
{
|
||||
media = _mediaService.GetById(key);
|
||||
if (media is not null)
|
||||
{
|
||||
_appCaches.RequestCache.Set(cacheKey, media);
|
||||
}
|
||||
}
|
||||
|
||||
return media;
|
||||
}
|
||||
|
||||
private List<MediaWithCropsDto> HandleTemporaryMediaUploads(List<MediaWithCropsDto> mediaWithCropsDtos, MediaPicker3Configuration configuration)
|
||||
{
|
||||
var invalidDtos = new List<MediaWithCropsDto>();
|
||||
@@ -240,7 +241,7 @@ public class MediaPicker3PropertyEditor : DataEditor
|
||||
foreach (MediaWithCropsDto mediaWithCropsDto in mediaWithCropsDtos)
|
||||
{
|
||||
// if the media already exist, don't bother with it
|
||||
if (GetMediaById(mediaWithCropsDto.MediaKey) != null)
|
||||
if (GetAndCacheMediaById(mediaWithCropsDto.MediaKey, _appCaches.RequestCache, _mediaService) != null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -480,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)
|
||||
{
|
||||
@@ -506,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 };
|
||||
}
|
||||
|
||||
|
||||
@@ -108,7 +108,7 @@ internal sealed class DatabaseCacheRebuilder : IDatabaseCacheRebuilder
|
||||
|
||||
_logger.LogWarning(
|
||||
"Database cache was serialized using {CurrentSerializer}. Currently configured cache serializer {Serializer}. Rebuilding database cache.",
|
||||
currentSerializer,
|
||||
currentSerializer == 0 ? "None" : currentSerializer,
|
||||
serializer);
|
||||
|
||||
using (_profilingLogger.TraceDuration<DatabaseCacheRebuilder>($"Rebuilding database cache with {serializer} serializer"))
|
||||
|
||||
+1
-2
@@ -1,4 +1,3 @@
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
@@ -74,7 +73,7 @@ public static class UmbracoBuilderExtensions
|
||||
builder.AddNotificationAsyncHandler<ContentTypeDeletedNotification, CacheRefreshingNotificationHandler>();
|
||||
builder.AddNotificationAsyncHandler<MediaTypeRefreshedNotification, CacheRefreshingNotificationHandler>();
|
||||
builder.AddNotificationAsyncHandler<MediaTypeDeletedNotification, CacheRefreshingNotificationHandler>();
|
||||
builder.AddNotificationAsyncHandler<UmbracoApplicationStartedNotification, SeedingNotificationHandler>();
|
||||
builder.AddNotificationAsyncHandler<UmbracoApplicationStartingNotification, SeedingNotificationHandler>();
|
||||
builder.AddCacheSeeding();
|
||||
return builder;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Collections.Concurrent;
|
||||
using Microsoft.Extensions.Caching.Hybrid;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.HybridCache.Extensions;
|
||||
@@ -7,19 +8,24 @@ namespace Umbraco.Cms.Infrastructure.HybridCache.Extensions;
|
||||
/// </summary>
|
||||
internal static class HybridCacheExtensions
|
||||
{
|
||||
// Per-key semaphores to ensure the GetOrCreateAsync + RemoveAsync sequence
|
||||
// executes atomically for a given cache key.
|
||||
private static readonly ConcurrentDictionary<string, SemaphoreSlim> _keyLocks = new();
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the cache contains an item with a matching key.
|
||||
/// </summary>
|
||||
/// <param name="cache">An instance of <see cref="Microsoft.Extensions.Caching.Hybrid.HybridCache"/></param>
|
||||
/// <param name="key">The name (key) of the item to search for in the cache.</param>
|
||||
/// <param name="token">The cancellation token.</param>
|
||||
/// <returns>True if the item exists already. False if it doesn't.</returns>
|
||||
/// <remarks>
|
||||
/// Hat-tip: https://github.com/dotnet/aspnetcore/discussions/57191
|
||||
/// Will never add or alter the state of any items in the cache.
|
||||
/// </remarks>
|
||||
public static async Task<bool> ExistsAsync<T>(this Microsoft.Extensions.Caching.Hybrid.HybridCache cache, string key)
|
||||
public static async Task<bool> ExistsAsync<T>(this Microsoft.Extensions.Caching.Hybrid.HybridCache cache, string key, CancellationToken token)
|
||||
{
|
||||
(bool exists, _) = await TryGetValueAsync<T>(cache, key);
|
||||
(bool exists, _) = await TryGetValueAsync<T>(cache, key, token).ConfigureAwait(false);
|
||||
return exists;
|
||||
}
|
||||
|
||||
@@ -29,34 +35,55 @@ internal static class HybridCacheExtensions
|
||||
/// <typeparam name="T">The type of the value of the item in the cache.</typeparam>
|
||||
/// <param name="cache">An instance of <see cref="Microsoft.Extensions.Caching.Hybrid.HybridCache"/></param>
|
||||
/// <param name="key">The name (key) of the item to search for in the cache.</param>
|
||||
/// <param name="token">The cancellation token.</param>
|
||||
/// <returns>A tuple of <see cref="bool"/> and the object (if found) retrieved from the cache.</returns>
|
||||
/// <remarks>
|
||||
/// Hat-tip: https://github.com/dotnet/aspnetcore/discussions/57191
|
||||
/// Will never add or alter the state of any items in the cache.
|
||||
/// </remarks>
|
||||
public static async Task<(bool Exists, T? Value)> TryGetValueAsync<T>(this Microsoft.Extensions.Caching.Hybrid.HybridCache cache, string key)
|
||||
public static async Task<(bool Exists, T? Value)> TryGetValueAsync<T>(this Microsoft.Extensions.Caching.Hybrid.HybridCache cache, string key, CancellationToken token)
|
||||
{
|
||||
var exists = true;
|
||||
|
||||
T? result = await cache.GetOrCreateAsync<object, T>(
|
||||
key,
|
||||
null!,
|
||||
(_, _) =>
|
||||
{
|
||||
exists = false;
|
||||
return new ValueTask<T>(default(T)!);
|
||||
},
|
||||
new HybridCacheEntryOptions(),
|
||||
null,
|
||||
CancellationToken.None);
|
||||
// Acquire a per-key semaphore so that GetOrCreateAsync and the possible RemoveAsync
|
||||
// complete without another thread retrieving/creating the same key in-between.
|
||||
SemaphoreSlim sem = _keyLocks.GetOrAdd(key, _ => new SemaphoreSlim(1, 1));
|
||||
|
||||
// In checking for the existence of the item, if not found, we will have created a cache entry with a null value.
|
||||
// So remove it again.
|
||||
if (exists is false)
|
||||
await sem.WaitAsync().ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
await cache.RemoveAsync(key);
|
||||
}
|
||||
T? result = await cache.GetOrCreateAsync<T?>(
|
||||
key,
|
||||
cancellationToken =>
|
||||
{
|
||||
exists = false;
|
||||
return default;
|
||||
},
|
||||
new HybridCacheEntryOptions(),
|
||||
null,
|
||||
token).ConfigureAwait(false);
|
||||
|
||||
return (exists, result);
|
||||
// In checking for the existence of the item, if not found, we will have created a cache entry with a null value.
|
||||
// So remove it again. Because we're holding the per-key lock there is no chance another thread
|
||||
// will observe the temporary entry between GetOrCreateAsync and RemoveAsync.
|
||||
if (exists is false)
|
||||
{
|
||||
await cache.RemoveAsync(key).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return (exists, result);
|
||||
}
|
||||
finally
|
||||
{
|
||||
sem.Release();
|
||||
|
||||
// Only remove the semaphore mapping if it still points to the same instance we used.
|
||||
// This avoids removing another thread's semaphore or corrupting the map.
|
||||
if (_keyLocks.TryGetValue(key, out SemaphoreSlim? current) && ReferenceEquals(current, sem))
|
||||
{
|
||||
_keyLocks.TryRemove(key, out _);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ internal sealed class PublishedContentFactory : IPublishedContentFactory
|
||||
/// <inheritdoc/>
|
||||
public IPublishedContent? ToIPublishedContent(ContentCacheNode contentCacheNode, bool preview)
|
||||
{
|
||||
var cacheKey = $"{nameof(PublishedContentFactory)}DocumentCache_{contentCacheNode.Id}_{preview}";
|
||||
var cacheKey = $"{nameof(PublishedContentFactory)}DocumentCache_{contentCacheNode.Id}_{preview}_{contentCacheNode.Data?.VersionDate.Ticks ?? 0}";
|
||||
IPublishedContent? publishedContent = null;
|
||||
if (_appCaches.RequestCache.IsAvailable)
|
||||
{
|
||||
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.Events;
|
||||
@@ -9,7 +9,7 @@ using Umbraco.Cms.Infrastructure.HybridCache.Services;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.HybridCache.NotificationHandlers;
|
||||
|
||||
internal sealed class SeedingNotificationHandler : INotificationAsyncHandler<UmbracoApplicationStartedNotification>
|
||||
internal sealed class SeedingNotificationHandler : INotificationAsyncHandler<UmbracoApplicationStartingNotification>
|
||||
{
|
||||
private readonly IDocumentCacheService _documentCacheService;
|
||||
private readonly IMediaCacheService _mediaCacheService;
|
||||
@@ -24,7 +24,7 @@ internal sealed class SeedingNotificationHandler : INotificationAsyncHandler<Umb
|
||||
_globalSettings = globalSettings.Value;
|
||||
}
|
||||
|
||||
public async Task HandleAsync(UmbracoApplicationStartedNotification notification,
|
||||
public async Task HandleAsync(UmbracoApplicationStartingNotification notification,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
|
||||
|
||||
@@ -205,7 +205,7 @@ internal sealed class DocumentCacheService : IDocumentCacheService
|
||||
|
||||
var cacheKey = GetCacheKey(key, false);
|
||||
|
||||
var existsInCache = await _hybridCache.ExistsAsync<ContentCacheNode>(cacheKey);
|
||||
var existsInCache = await _hybridCache.ExistsAsync<ContentCacheNode?>(cacheKey, cancellationToken).ConfigureAwait(false);
|
||||
if (existsInCache is false)
|
||||
{
|
||||
uncachedKeys.Add(key);
|
||||
@@ -278,7 +278,7 @@ internal sealed class DocumentCacheService : IDocumentCacheService
|
||||
return false;
|
||||
}
|
||||
|
||||
return await _hybridCache.ExistsAsync<ContentCacheNode>(GetCacheKey(keyAttempt.Result, preview));
|
||||
return await _hybridCache.ExistsAsync<ContentCacheNode?>(GetCacheKey(keyAttempt.Result, preview), CancellationToken.None);
|
||||
}
|
||||
|
||||
public async Task RefreshContentAsync(IContent content)
|
||||
|
||||
@@ -133,7 +133,7 @@ internal sealed class MediaCacheService : IMediaCacheService
|
||||
return false;
|
||||
}
|
||||
|
||||
return await _hybridCache.ExistsAsync<ContentCacheNode>($"{keyAttempt.Result}");
|
||||
return await _hybridCache.ExistsAsync<ContentCacheNode?>($"{keyAttempt.Result}", CancellationToken.None);
|
||||
}
|
||||
|
||||
public async Task RefreshMediaAsync(IMedia media)
|
||||
@@ -170,7 +170,7 @@ internal sealed class MediaCacheService : IMediaCacheService
|
||||
|
||||
var cacheKey = GetCacheKey(key, false);
|
||||
|
||||
var existsInCache = await _hybridCache.ExistsAsync<ContentCacheNode>(cacheKey);
|
||||
var existsInCache = await _hybridCache.ExistsAsync<ContentCacheNode?>(cacheKey, CancellationToken.None);
|
||||
if (existsInCache is false)
|
||||
{
|
||||
uncachedKeys.Add(key);
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.Persistence.Repositories;
|
||||
using Umbraco.Cms.Core.Web;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Web.Common.Repositories;
|
||||
@@ -11,21 +14,35 @@ internal sealed class WebProfilerRepository : IWebProfilerRepository
|
||||
private const string QueryName = "umbDebug";
|
||||
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
private readonly ICookieManager _cookieManager;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
|
||||
public WebProfilerRepository(IHttpContextAccessor httpContextAccessor)
|
||||
public WebProfilerRepository(IHttpContextAccessor httpContextAccessor, ICookieManager cookieManager, IOptions<GlobalSettings> globalSettings)
|
||||
{
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
_cookieManager = cookieManager;
|
||||
_globalSettings = globalSettings.Value;
|
||||
}
|
||||
|
||||
public void SetStatus(int userId, bool status)
|
||||
{
|
||||
if (status)
|
||||
{
|
||||
_httpContextAccessor.GetRequiredHttpContext().Response.Cookies.Append(CookieName, "1", new CookieOptions { Expires = DateTime.Now.AddYears(1) });
|
||||
// This cookie enables debug profiling on the front-end without needing query strings or headers.
|
||||
// It uses SameSite=Strict, so it only works when the BackOffice and front-end share the same domain.
|
||||
// It's marked httpOnly to prevent JavaScript access (the server reads it, not client-side code).
|
||||
// No expiration is set, so it's a session cookie and will be deleted when the browser closes.
|
||||
// For cross-site setups, use the query string (?umbDebug=true) or header (X-UMB-DEBUG) instead.
|
||||
_cookieManager.SetCookieValue(
|
||||
CookieName,
|
||||
"1",
|
||||
httpOnly: true,
|
||||
secure: _globalSettings.UseHttps,
|
||||
sameSiteMode: "Strict");
|
||||
}
|
||||
else
|
||||
{
|
||||
_httpContextAccessor.GetRequiredHttpContext().Response.Cookies.Delete(CookieName);
|
||||
_cookieManager.ExpireCookie(CookieName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +60,6 @@ internal sealed class WebProfilerRepository : IWebProfilerRepository
|
||||
return xUmbDebug;
|
||||
}
|
||||
|
||||
return request.Cookies.ContainsKey(CookieName);
|
||||
return _cookieManager.HasCookie(CookieName);
|
||||
}
|
||||
}
|
||||
|
||||
+11
-1
@@ -26,6 +26,7 @@ If you have an existing Vite server running, you can run the task **Backoffice A
|
||||
### Run a Front-end server against a local Umbraco instance
|
||||
|
||||
#### 1. Configure Umbraco instance
|
||||
|
||||
Enable the front-end server communicating with the Backend server(Umbraco instance) you need need to correct the `appsettings.json` of your project.
|
||||
|
||||
For code contributions use the backend project of `/src/Umbraco.Web.UI`.
|
||||
@@ -38,7 +39,11 @@ Open this file in an editor: `/src/Umbraco.Web.UI/appsettings.Development.json`
|
||||
"BackOfficeHost": "http://localhost:5173",
|
||||
"AuthorizeCallbackPathName": "/oauth_complete",
|
||||
"AuthorizeCallbackLogoutPathName": "/logout",
|
||||
"AuthorizeCallbackErrorPathName": "/error",
|
||||
"AuthorizeCallbackErrorPathName": "/error",,
|
||||
"BackOfficeTokenCookie": {
|
||||
"Enabled": true,
|
||||
"SameSite": "None"
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -46,10 +51,15 @@ Open this file in an editor: `/src/Umbraco.Web.UI/appsettings.Development.json`
|
||||
|
||||
This will override the backoffice host URL, enabling the Client to run from a different origin.
|
||||
|
||||
> [!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.
|
||||
|
||||
#### 2. Start Umbraco
|
||||
|
||||
Then start the backend server by running the command: `dotnet run` in the `/src/Umbraco.Web.UI` folder.
|
||||
|
||||
#### 3. Start Frontend server
|
||||
|
||||
Now start the frontend server by running the command: `npm run dev:server` in the `/src/Umbraco.Web.UI.Client` folder.
|
||||
|
||||
Finally open `http://localhost:5173` in your browser.
|
||||
|
||||
@@ -2,6 +2,14 @@
|
||||
|
||||
This package contains the types for the Umbraco Backoffice.
|
||||
|
||||
## Preview
|
||||
|
||||
A live preview of the latest backoffice build from the main branch is available at:
|
||||
|
||||
**[backofficepreview.umbraco.com](https://backofficepreview.umbraco.com/)**
|
||||
|
||||
This preview is automatically deployed via GitHub Actions whenever changes are pushed to main or version branches.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
|
||||
+492
-492
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@umbraco-cms/backoffice",
|
||||
"license": "MIT",
|
||||
"version": "16.4.0-rc",
|
||||
"version": "16.6.0-rc",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": null,
|
||||
@@ -212,8 +212,8 @@
|
||||
"generate:ui-api-docs": "npm run generate:check-const-test && typedoc --options typedoc.config.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22",
|
||||
"npm": ">=10.9"
|
||||
"node": ">=22.17.1",
|
||||
"npm": ">=10.9.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"element-internals-polyfill": "^3.0.2"
|
||||
@@ -261,7 +261,7 @@
|
||||
"typescript": "5.9.3",
|
||||
"typescript-eslint": "^8.45.0",
|
||||
"typescript-json-schema": "^0.65.1",
|
||||
"vite": "^7.1.9",
|
||||
"vite": "^7.1.11",
|
||||
"vite-plugin-static-copy": "^3.1.3",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"web-component-analyzer": "^2.0.0"
|
||||
|
||||
+3
-2
@@ -83,8 +83,9 @@ export class UmbBackofficeHeaderSectionsElement extends UmbLitElement {
|
||||
|
||||
const clickedSectionAlias = manifest.alias;
|
||||
|
||||
// If the clicked section is the same as the current section, we just load the original section path to load the section root
|
||||
if (this._currentSectionAlias === clickedSectionAlias) {
|
||||
// If preventUrlRetention is set to true then go to the section root.
|
||||
// Or if the clicked section is the current active one, then navigate to the section root
|
||||
if (manifest?.meta.preventUrlRetention === true || this._currentSectionAlias === clickedSectionAlias) {
|
||||
const sectionPath = this.#getSectionPath(manifest);
|
||||
history.pushState(null, '', sectionPath);
|
||||
return;
|
||||
|
||||
@@ -6,7 +6,7 @@ import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||
export class UmbPreviewExitElement extends UmbLitElement {
|
||||
async #onClick() {
|
||||
const previewContext = await this.getContext(UMB_PREVIEW_CONTEXT);
|
||||
previewContext?.exitPreview(0);
|
||||
await previewContext?.exitPreview(0);
|
||||
}
|
||||
|
||||
override render() {
|
||||
|
||||
@@ -6,7 +6,7 @@ import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||
export class UmbPreviewOpenWebsiteElement extends UmbLitElement {
|
||||
async #onClick() {
|
||||
const previewContext = await this.getContext(UMB_PREVIEW_CONTEXT);
|
||||
previewContext?.openWebsite();
|
||||
await previewContext?.openWebsite();
|
||||
}
|
||||
|
||||
override render() {
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { UmbBooleanState, UmbStringState } from '@umbraco-cms/backoffice/observable-api';
|
||||
import { tryExecute } from '@umbraco-cms/backoffice/resources';
|
||||
import { umbConfirmModal } from '@umbraco-cms/backoffice/modal';
|
||||
import { DocumentService } from '@umbraco-cms/backoffice/external/backend-api';
|
||||
import { UmbBooleanState, UmbStringState } from '@umbraco-cms/backoffice/observable-api';
|
||||
import { UmbContextBase } from '@umbraco-cms/backoffice/class-api';
|
||||
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
|
||||
import { UmbDocumentPreviewRepository } from '@umbraco-cms/backoffice/document';
|
||||
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
|
||||
import { UMB_SERVER_CONTEXT } from '@umbraco-cms/backoffice/server';
|
||||
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
|
||||
|
||||
const UMB_LOCALSTORAGE_SESSION_KEY = 'umb:previewSessions';
|
||||
|
||||
@@ -89,6 +91,19 @@ export class UmbPreviewContext extends UmbContextBase {
|
||||
});
|
||||
}
|
||||
|
||||
async #getPublishedUrl(): Promise<string | null> {
|
||||
if (!this.#unique) return null;
|
||||
|
||||
// NOTE: We should be reusing `UmbDocumentUrlRepository` here, but the preview app doesn't register the `itemStore` extensions, so can't resolve/consume `UMB_DOCUMENT_URL_STORE_CONTEXT`. [LK]
|
||||
const { data } = await tryExecute(this, DocumentService.getDocumentUrls({ query: { id: [this.#unique] } }));
|
||||
|
||||
if (!data?.length) return null;
|
||||
const urlInfo = this.#culture ? data[0].urlInfos.find((x) => x.culture === this.#culture) : data[0].urlInfos[0];
|
||||
|
||||
if (!urlInfo?.url) return null;
|
||||
return urlInfo.url.startsWith('/') ? `${this.#serverUrl}${urlInfo.url}` : urlInfo.url;
|
||||
}
|
||||
|
||||
#getSessionCount(): number {
|
||||
return Math.max(Number(localStorage.getItem(UMB_LOCALSTORAGE_SESSION_KEY)), 0) || 0;
|
||||
}
|
||||
@@ -170,7 +185,12 @@ export class UmbPreviewContext extends UmbContextBase {
|
||||
this.#webSocket = undefined;
|
||||
}
|
||||
|
||||
const url = this.#previewUrl.getValue() as string;
|
||||
let url = await this.#getPublishedUrl();
|
||||
|
||||
if (!url) {
|
||||
url = this.#previewUrl.getValue() as string;
|
||||
}
|
||||
|
||||
window.location.replace(url);
|
||||
}
|
||||
|
||||
@@ -190,8 +210,13 @@ export class UmbPreviewContext extends UmbContextBase {
|
||||
return this.getHostElement().shadowRoot?.querySelector('#wrapper') as HTMLElement;
|
||||
}
|
||||
|
||||
openWebsite() {
|
||||
const url = this.#previewUrl.getValue() as string;
|
||||
async openWebsite() {
|
||||
let url = await this.#getPublishedUrl();
|
||||
|
||||
if (!url) {
|
||||
url = this.#previewUrl.getValue() as string;
|
||||
}
|
||||
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ export class UmbPreviewElement extends UmbLitElement {
|
||||
src=${this._previewUrl}
|
||||
title="Page preview"
|
||||
@load=${this.#onIFrameLoad}
|
||||
sandbox="allow-scripts allow-same-origin"></iframe>
|
||||
sandbox="allow-scripts allow-same-origin allow-forms"></iframe>
|
||||
</div>
|
||||
</div>
|
||||
<div id="menu">
|
||||
|
||||
@@ -2525,13 +2525,19 @@ export default {
|
||||
profiling: {
|
||||
performanceProfiling: 'Performance profiling',
|
||||
performanceProfilingDescription:
|
||||
"<p>Umbraco currently runs in debug mode. This means you can use the built-in performance profiler to assess the performance when rendering pages.</p><p>If you want to activate the profiler for a specific page rendering, simply add <strong>umbDebug=true</strong> to the querystring when requesting the page.</p><p>If you want the profiler to be activated by default for all page renderings, you can use the toggle below. It will set a cookie in your browser, which then activates the profiler automatically. In other words, the profiler will only be active by default in <em>your</em> browser - not everyone else's.</p>",
|
||||
"<p>Umbraco currently runs in debug mode. This means you can use the built-in performance profiler to assess the performance when rendering pages.</p><p>If you want to activate the profiler for a specific page rendering, simply add <strong>umbDebug=true</strong> to the querystring when requesting the page.</p><p>If you want the profiler to be activated by default for all page renderings, you can use the toggle below. It will set a cookie in your browser, which then activates the profiler automatically. In other words, the profiler will only be active by default in <em>your</em> browser - not everyone else's.</p><p><strong>Note:</strong> This will only work if the Backoffice is currently located on the same URL as the front-end website.</p>",
|
||||
activateByDefault: 'Activate the profiler by default',
|
||||
reminder: 'Friendly reminder',
|
||||
reminderDescription:
|
||||
'<p>You should never let a production site run in debug mode. Debug mode is turned off by setting <strong>Umbraco:CMS:Hosting:Debug</strong> to <strong>false</strong> in appsettings.json, appsettings.{Environment}.json or via an environment variable.</p>',
|
||||
profilerEnabledDescription:
|
||||
"<p>Umbraco currently does not run in debug mode, so you can't use the built-in profiler. This is how it should be for a production site.</p><p>Debug mode is turned on by setting <strong>Umbraco:CMS:Hosting:Debug</strong> to <strong>true</strong> in appsettings.json, appsettings.{Environment}.json or via an environment variable.</p>",
|
||||
errorEnablingProfilerTitle: 'Error enabling profiler',
|
||||
errorEnablingProfilerDescription:
|
||||
'It was not possible to enable the profiler. Check that you are accessing the Backoffice on the same URL as the front-end website, and try again. If the problem persists, please check the log for more details.',
|
||||
errorDisablingProfilerTitle: 'Error disabling profiler',
|
||||
errorDisablingProfilerDescription:
|
||||
'It was not possible to disable the profiler. Try again, and if the problem persists, please check the log for more details.',
|
||||
},
|
||||
settingsDashboardVideos: {
|
||||
trainingHeadline: 'Hours of Umbraco training videos are only a click away',
|
||||
@@ -2869,29 +2875,52 @@ export default {
|
||||
ar: 'العربية',
|
||||
bs: 'Bosanski',
|
||||
cs: 'Česky',
|
||||
'cs-cz': 'Česky (Czechia)',
|
||||
cy: 'Cymraeg',
|
||||
'cy-gb': 'Cymraeg (UK)',
|
||||
da: 'Dansk',
|
||||
'da-dk': 'Dansk (Danmark)',
|
||||
de: 'Deutsch',
|
||||
'de-de': 'Deutsch (Deutschland)',
|
||||
'de-ch': 'Deutsch (Schweiz)',
|
||||
en: 'English (UK)',
|
||||
'en-us': 'English (US)',
|
||||
es: 'Español',
|
||||
'es-es': 'Español (España)',
|
||||
fr: 'Français',
|
||||
he: 'Hebrew',
|
||||
'fr-fr': 'Français (France)',
|
||||
'fr-ch': 'Français (Suisse)',
|
||||
he: 'עברית',
|
||||
'he-il': 'עברית (ישראל)',
|
||||
hr: 'Hrvatski',
|
||||
'hr-hr': 'Hrvatski (Hrvatska)',
|
||||
it: 'Italiano',
|
||||
'it-it': 'Italiano (Italia)',
|
||||
'it-ch': 'Italiano (Svizzera)',
|
||||
ja: '日本語',
|
||||
'ja-jp': '日本語 (日本)',
|
||||
ko: '한국어',
|
||||
'ko-kr': '한국어 (한국)',
|
||||
nb: 'Norsk Bokmål',
|
||||
'nb-no': 'Norsk (Bokmål)',
|
||||
nl: 'Nederlands',
|
||||
'nl-nl': 'Nederlands (Nederland)',
|
||||
pl: 'Polski',
|
||||
'pl-pl': 'Polski (Polska)',
|
||||
pt: 'Português',
|
||||
'pt-br': 'Português (Brasil)',
|
||||
ro: 'Romana',
|
||||
ro: 'Română',
|
||||
'ro-ro': 'Română (România)',
|
||||
ru: 'Русский',
|
||||
'ru-ru': 'Русский (Россия)',
|
||||
sv: 'Svenska',
|
||||
'sv-se': 'Svenska (Sverige)',
|
||||
tr: 'Türkçe',
|
||||
'tr-tr': 'Türkçe (Türkiye Cumhuriyeti)',
|
||||
uk: 'Українська',
|
||||
'uk-ua': 'Українська (Україна)',
|
||||
zh: '中文',
|
||||
'zh-cn': '中文(简体,中国)',
|
||||
'zh-tw': '中文(正體,台灣)',
|
||||
vi: 'Tiếng Việt',
|
||||
},
|
||||
|
||||
@@ -2836,34 +2836,4 @@ export default {
|
||||
resetUrlMessage: 'Bạn có chắc chắn muốn đặt lại URL này không?',
|
||||
resetUrlLabel: 'Đặt lại',
|
||||
},
|
||||
uiCulture: {
|
||||
ar: 'العربية',
|
||||
bs: 'Bosanski',
|
||||
cs: 'Česky',
|
||||
cy: 'Cymraeg',
|
||||
da: 'Dansk',
|
||||
de: 'Deutsch',
|
||||
en: 'English (UK)',
|
||||
'en-us': 'English (US)',
|
||||
es: 'Español',
|
||||
fr: 'Français',
|
||||
he: 'Hebrew',
|
||||
hr: 'Hrvatski',
|
||||
it: 'Italiano',
|
||||
ja: '日本語',
|
||||
ko: '한국어',
|
||||
nb: 'Norsk Bokmål',
|
||||
nl: 'Nederlands',
|
||||
pl: 'Polski',
|
||||
pt: 'Português',
|
||||
'pt-br': 'Português (Brasil)',
|
||||
ro: 'Romana',
|
||||
ru: 'Русский',
|
||||
sv: 'Svenska',
|
||||
tr: 'Türkçe',
|
||||
uk: 'Українська',
|
||||
zh: '中文',
|
||||
'zh-tw': '中文(正體,台灣)',
|
||||
vi: 'Tiếng Việt',
|
||||
},
|
||||
} as UmbLocalizationDictionary;
|
||||
|
||||
+41
-7
@@ -77,6 +77,41 @@ export class RedirectRequestHandler extends AuthorizationRequestHandler {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup all stale authorization requests and configurations from storage.
|
||||
* This scans localStorage for any keys matching the appauth patterns and removes them,
|
||||
* including the authorization request handle key.
|
||||
*/
|
||||
public cleanupStaleAuthorizationData(): Promise<void> {
|
||||
// Check if we're in a browser environment with localStorage
|
||||
if (typeof window === 'undefined' || !window.localStorage) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const keysToRemove: string[] = [];
|
||||
|
||||
// Scan localStorage for all appauth-related keys
|
||||
for (let i = 0; i < window.localStorage.length; i++) {
|
||||
const key = window.localStorage.key(i);
|
||||
if (
|
||||
key &&
|
||||
(key.includes('_appauth_authorization_request') ||
|
||||
key.includes('_appauth_authorization_service_configuration') ||
|
||||
key === AUTHORIZATION_REQUEST_HANDLE_KEY)
|
||||
) {
|
||||
keysToRemove.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove all found stale keys
|
||||
const removePromises = keysToRemove.map((key) => this.storageBackend.removeItem(key));
|
||||
return Promise.all(removePromises).then(() => {
|
||||
if (keysToRemove.length > 0) {
|
||||
log(`Cleaned up ${keysToRemove.length} stale authorization data entries`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to introspect the contents of storage backend and completes the
|
||||
* request.
|
||||
@@ -119,12 +154,8 @@ export class RedirectRequestHandler extends AuthorizationRequestHandler {
|
||||
} else {
|
||||
authorizationResponse = new AuthorizationResponse({ code: code, state: state });
|
||||
}
|
||||
// cleanup state
|
||||
return Promise.all([
|
||||
this.storageBackend.removeItem(AUTHORIZATION_REQUEST_HANDLE_KEY),
|
||||
this.storageBackend.removeItem(authorizationRequestKey(handle)),
|
||||
this.storageBackend.removeItem(authorizationServiceConfigurationKey(handle)),
|
||||
]).then(() => {
|
||||
// cleanup all authorization data including current and stale entries
|
||||
return this.cleanupStaleAuthorizationData().then(() => {
|
||||
log('Delivering authorization response');
|
||||
return {
|
||||
request: request,
|
||||
@@ -134,7 +165,10 @@ export class RedirectRequestHandler extends AuthorizationRequestHandler {
|
||||
});
|
||||
} else {
|
||||
log('Mismatched request (state and request_uri) dont match.');
|
||||
return Promise.resolve(null);
|
||||
// cleanup all authorization data even on mismatch to prevent stale PKCE data
|
||||
return this.cleanupStaleAuthorizationData().then(() => {
|
||||
return null;
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
@@ -35,6 +35,7 @@ export class FetchRequestor extends Requestor {
|
||||
const requestInit: RequestInit = {};
|
||||
requestInit.method = settings.method;
|
||||
requestInit.mode = 'cors';
|
||||
requestInit.credentials = settings.credentials ?? 'include';
|
||||
|
||||
if (settings.data) {
|
||||
if (settings.method && settings.method.toUpperCase() === 'POST') {
|
||||
|
||||
+2
-2
@@ -6,7 +6,7 @@
|
||||
"build": "vite build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@umbraco-ui/uui": "^1.16.0-rc.0",
|
||||
"@umbraco-ui/uui-css": "^1.16.0-rc.0"
|
||||
"@umbraco-ui/uui": "^1.16.0",
|
||||
"@umbraco-ui/uui-css": "^1.16.0"
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user