Compare commits

..
Author SHA1 Message Date
5b54bed406 Merge commit from fork
* Fixed parsing of node if in content and media permission querystring handlers to retrieve expected value when multiple are provided in the querystring.

* Add HttpPost attributes to backoffice endpoints that should only accept post requests.

* Bumped version to 13.6.1.

* Narrow PermissionQueryString parsing to the releveant UmbracoObjectType

* Add missed update from v10

---------

Co-authored-by: Sven Geusens <sge@umbraco.dk>
2025-03-11 05:11:08 +01:00
twoday-rolandkockandAndy Butland 2ec6ff4ebc bumped imagesharp to prevent CVE-2025-27598 (#18602) 2025-03-09 08:43:52 +01:00
Andy Butland 451b5f96e7 Use windows for build agent to avoid hanging issues with Linux. 2025-02-28 06:57:35 +01:00
Andy Butland 15ebae5025 Bumped version to 13.7.1. 2025-02-28 06:57:00 +01:00
Andy Butland eaea7a67ba Bump version to 13.7.0. 2025-02-26 08:14:26 +01:00
6496 changed files with 387444 additions and 171010 deletions
-1
View File
@@ -1,4 +1,3 @@
**/*
!tests/Umbraco.Tests.Integration/bin/**
!tests/Umbraco.Tests.UnitTests/bin/**
**/node_modules
-10
View File
@@ -13,16 +13,6 @@ If the answer is yes, please read on. Otherwise, make sure to head on over [to t
↖️ You can jump to any section by using the "table of contents" button ( ![Table of contents icon](img/tableofcontentsicon.svg) ) above.
## Getting Started:
To run umbraco, we first need to initialize the client git submodule:
* Execute `git submodule update --init` to get the files into Umbraco.Web.UI.Client project
* If you are going to work on the Backoffice, you can either go to the Umbraco.Web.UI.Client folder and check out a new branch or set it up in your IDE, which will allow you to commit to each repository simultaneously:
* **Rider**: Preferences -> Version Control -> Directory Mappings -> Click the '+' sign
* If you get a white page delete Umbraco.Cms.StaticAssets\wwwroot\umbraco folder and run `npm ci && npm run build:for:cms` inside Umbraco.Web.UI.Client folder to clear out any leftover files from older versions.
### Latest version
* If you want to get the latest changes from the client repository, run `git submodule update` again which will pull the latest main branch.
## Debugging source locally
+18
View File
@@ -0,0 +1,18 @@
# New backoffice
> **Warning**:
> This is an early WIP and is set not to be packable since we don't want to release this yet. There will be breaking changes in these projects.
This solution folder contains the projects for the new backoffice. If you're looking to fix or improve the existing CMS, this is not the place to do it, although we do very much appreciate your efforts.
### Project structure
Since the new backoffice API is still very much a work in progress, we've created new projects for the new backoffice API:
* Umbrao.Cms.ManagementApi - The "presentation layer" for the management API
* "New" versions of existing projects, should be merged with the existing projects when the new API is released:
* Umbraco.New.Cms.Core
* Umbraco.New.Cms.Infrastructure
* Umbraco.New.Cms.Web.Common
This also means that we have to use "InternalsVisibleTo" for the new projects since these should be able to access the internal classes since they will when they get merged.
+21 -26
View File
@@ -2,52 +2,47 @@
[![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](../LICENSE.md)
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](CONTRIBUTING.md)
![Mastodon Follow](https://img.shields.io/mastodon/follow/110661369750014952?domain=https%3A%2F%2Fumbracocommunity.social)
[![Follow Umbraco on Twitter](https://img.shields.io/badge/Follow-blue?logo=twitter&logoColor=fff)](https://twitter.com/intent/follow?screen_name=umbraco)
[![Chat about Umbraco on Discord](https://img.shields.io/discord/869656431308189746?logo=discord&logoColor=fff)](https://discord.gg/umbraco)
[![Read what's going on in the Umbraco Discord chat now](https://img.shields.io/badge/read-discord-blue)](https://discord-chats.umbraco.com)
[![Build status](https://img.shields.io/azure-devops/build/umbraco/Umbraco%2520Cms/301?logo=azurepipelines&label=Azure%20Pipelines)](https://umbraco.visualstudio.com/Umbraco%20Cms/_build?definitionId=301)
[![Open in GitHub Codespaces](https://img.shields.io/badge/Open%20in%20GitHub%20Codespaces-525252?logo=github)](https://github.com/codespaces/new?hide_repo_select=true&ref=contrib&repo=10601208&machine=basicLinux32gb&devcontainer_path=.devcontainer%2Fdevcontainer.json&location=WestEurope)
### Umbraco is a free and open source .NET content management system. Our mission is to help you deliver delightful digital experiences by making Umbraco friendly, simpler and social.
Umbraco is the friendliest, most flexible and fastest growing ASP.NET CMS, and used by more than 500,000 websites worldwide. Our mission is to help you deliver delightful digital experiences by making Umbraco friendly, simpler and social.
Learn more at [umbraco.com](https://umbraco.com)
<p align="center">
<img src="img/logo.png" alt="Umbraco Logo" />
<img src="img/logo.png" alt="Umbraco Logo" />
</p>
## <a name="install"></a>Looking to install Umbraco?
See the official [Umbraco website](https://umbraco.com) for an introduction, core mission and values of the product and team behind it.
You can get started using the following commands on Windows, Linux and MacOS (after installing the [.NET Runtime and SDK](https://docs.umbraco.com/umbraco-cms/fundamentals/setup/requirements)):
- [Getting Started](#getting-started)
- [Documentation](#documentation)
- [Community](#join-the-umbraco-community)
- [Contributing](#contributing)
```
dotnet new install Umbraco.Templates
dotnet new umbraco --name MyProject
cd MyProject
dotnet run
```
Please also see our [Code of Conduct](https://github.com/umbraco/.github/blob/main/.github/CODE_OF_CONDUCT.md).
## Getting Started
[Umbraco Cloud](https://umbraco.com/cloud) is the easiest and fastest way to use Umbraco yet, with full support for all your custom .NET code and integrations. You're up and running in less than a minute, and your life will be made easier with automated upgrades and a built-in deployment engine. We offer a free 14-day trial, no credit card needed.
If you want to DIY, then you can [download Umbraco]((https://our.umbraco.com/download)) either as a ZIP file or via NuGet. It's the same version of Umbraco CMS that powers Umbraco Cloud, but you'll need to find a place to host it yourself, and handling deployments and upgrades will be all up to you.
## Documentation
Our [comprehensive documentation](https://docs.umbraco.com/umbraco-cms) takes you from the fundamentals on how to start with Umbraco to deploying it to production.
The documentation for Umbraco CMS can be found [on Our Umbraco](https://docs.umbraco.com/). The source for the Umbraco docs is [open source as well](https://github.com/umbraco/UmbracoDocs) and we're happy to look at your documentation contributions.
Some important documentation links to get you started:
## Join the Umbraco community
- [Installing Umbraco CMS](https://docs.umbraco.com/umbraco-cms/fundamentals/setup/install)
- [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)
Our friendly community is available 24/7 at the community hub, we call ["Our Umbraco"](https://our.umbraco.com/). Our Umbraco features forums for questions and answers, documentation, downloadable plugins for Umbraco, and a rich collection of community resources.
## Get help
Besides "Our", we all support each other in our [Community Discord Server](https://discord.gg/umbraco) and on Twitter: [Umbraco HQ](https://twitter.com/umbraco), [Release Updates](https://twitter.com/umbracoproject), [#umbraco](https://twitter.com/hashtag/umbraco)
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 both a social space but also has channels for questions and answers. Feel free to lurk or join in with your own questions. Or just post your daily Wordle score, up to you!
## Looking to contribute back to Umbraco?
You came to the right place! Our GitHub repository is available for all kinds of contributions:
- [Create a bug report](https://github.com/umbraco/Umbraco-CMS/issues)
- [Create a feature request](https://github.com/umbraco/Umbraco-CMS/discussions)
## Contributing
Umbraco is contribution-focused and community-driven. If you want to contribute back to the Umbraco source code, please check out our [guide to contributing](CONTRIBUTING.md).
### Tip: You should not run Umbraco from source code found here. Umbraco is extremely extensible and can do whatever you need. Instead, [install Umbraco as noted above](#looking-to-install-umbraco) and then [extend it any way you want to](https://docs.umbraco.com/umbraco-cms/extending/).
-38
View File
@@ -1,38 +0,0 @@
# .github/release.yml
changelog:
exclude:
labels:
- ignore-for-release
- duplicate
- wontfix
categories:
- title: 🙌 Notable Changes
labels:
- category/notable
- title: 💥 Breaking Changes
labels:
- category/breaking
- title: 🐛 Bug Fixes
labels:
- type/bug
- category/bug
- type/improvement
- title: 📄 Documentation
labels:
- category/documentation
- title: 🏠 Internal
labels:
- internal
- title: 📦 Dependencies
labels:
- dependencies
- title: 🌈 A11Y
labels:
- category/accessibility
- title: 🚀 New Features
labels:
- type/feature
- title: Other Changes
labels:
- '*'
-2
View File
@@ -23,7 +23,6 @@ env:
DOTNET_GENERATE_ASPNET_CERTIFICATE: false
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
DOTNET_CLI_TELEMETRY_OPTOUT: true
NODE_OPTIONS: --max_old_space_size=16384
jobs:
CodeQL-Build:
@@ -38,7 +37,6 @@ jobs:
uses: actions/checkout@v3
with:
fetch-depth: 0
submodules: true
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
+5 -11
View File
@@ -50,6 +50,10 @@ tools/docfx/
# Ignore rule for clearing out Belle (avoid rebuilding all the time)
preserve.belle
# Ignore rule for output of generated documentation files from grunt docserve
/src/Umbraco.Web.UI.Docs/api/
/src/Umbraco.Web.UI.Docs/package-lock.json
# csharp-docs
/build/csharp-docs/api/
/build/csharp-docs/_site/
@@ -64,13 +68,7 @@ preserve.belle
/build/docs.zip
/build/ui-docs.zip
/build/csharp-docs.zip
/src/Umbraco.Cms.StaticAssets/wwwroot/umbraco/auth
/src/Umbraco.Cms.StaticAssets/wwwroot/umbraco/backoffice
/src/Umbraco.Cms.StaticAssets/wwwroot/umbraco/assets
/src/Umbraco.Cms.StaticAssets/wwwroot/umbraco/js
/src/Umbraco.Cms.StaticAssets/wwwroot/umbraco/lib
/src/Umbraco.Cms.StaticAssets/wwwroot/umbraco/views
/src/Umbraco.Cms.StaticAssets/wwwroot/umbraco/login
/src/Umbraco.Cms.StaticAssets/wwwroot/umbraco/
# Environment specific data
/src/Umbraco.Web.UI.Client/[Bb]uild/
@@ -93,7 +91,6 @@ preserve.belle
# Tests
/tests/Umbraco.Tests.AcceptanceTest/.env
/tests/Umbraco.Tests.AcceptanceTest/playwright/.auth
/tests/Umbraco.Tests.Integration.SqlCe/DatabaseContextTests.sdf
/tests/Umbraco.Tests.Integration.SqlCe/[Uu]mbraco/[Dd]ata/TEMP/
/tests/Umbraco.Tests.Integration/appsettings.Tests.Local.json
@@ -106,13 +103,10 @@ preserve.belle
# Ignore auto-generated schema
/src/Umbraco.Cms.Targets/tasks/
/src/Umbraco.Cms.Targets/appsettings-schema.*.json
/src/Umbraco.Cms.Targets/umbraco-package-schema.json
/src/Umbraco.Web.UI/appsettings-schema.json
/src/Umbraco.Web.UI/appsettings-schema.*.json
/src/Umbraco.Web.UI/umbraco-package-schema.json
/tests/Umbraco.Tests.Integration/appsettings-schema.json
/tests/Umbraco.Tests.Integration/appsettings-schema.*.json
/tests/Umbraco.Tests.Integration/umbraco-package-schema.json
/src/Umbraco.Cms/appsettings-schema.json
playwright-report
trace.zip
-3
View File
@@ -1,3 +0,0 @@
[submodule "src/Umbraco.Web.UI.Client"]
path = src/Umbraco.Web.UI.Client
url = https://github.com/umbraco/Umbraco.CMS.Backoffice.git
+1 -2
View File
@@ -14,7 +14,6 @@
<NeutralLanguage>en-US</NeutralLanguage>
<Nullable>enable</Nullable>
<WarningsAsErrors>nullable</WarningsAsErrors>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<ImplicitUsings>enable</ImplicitUsings>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<WarnOnPackingNonPackableProject>false</WarnOnPackingNonPackableProject>
@@ -31,7 +30,7 @@
<PropertyGroup>
<GenerateCompatibilitySuppressionFile>false</GenerateCompatibilitySuppressionFile>
<EnablePackageValidation>true</EnablePackageValidation>
<PackageValidationBaselineVersion>14.0.0</PackageValidationBaselineVersion>
<PackageValidationBaselineVersion>13.0.0</PackageValidationBaselineVersion>
<EnableStrictModeForCompatibleFrameworksInPackage>true</EnableStrictModeForCompatibleFrameworksInPackage>
<EnableStrictModeForCompatibleTfms>true</EnableStrictModeForCompatibleTfms>
</PropertyGroup>
+16 -8
View File
@@ -6,7 +6,7 @@
<!-- Global packages (private, build-time packages for all projects) -->
<ItemGroup>
<GlobalPackageReference Include="Nerdbank.GitVersioning" Version="3.6.139" />
<GlobalPackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
<GlobalPackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.507" />
<GlobalPackageReference Include="Umbraco.Code" Version="2.2.0" />
<GlobalPackageReference Include="Umbraco.GitVersioning.Extensions" Version="0.2.0" />
</ItemGroup>
@@ -20,7 +20,6 @@
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.11" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.11" />
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
@@ -34,6 +33,7 @@
<PackageVersion Include="Microsoft.Extensions.Options" Version="8.0.2" />
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Options.DataAnnotations" Version="8.0.0" />
<PackageVersion Include="System.Runtime.Caching" Version="8.0.1" />
</ItemGroup>
<!-- Umbraco packages -->
<ItemGroup>
@@ -42,13 +42,12 @@
</ItemGroup>
<!-- Third-party packages -->
<ItemGroup>
<PackageVersion Include="Asp.Versioning.Mvc" Version="8.1.0" />
<PackageVersion Include="Asp.Versioning.Mvc.ApiExplorer" Version="8.1.0" />
<PackageVersion Include="Asp.Versioning.Mvc" Version="7.1.1" />
<PackageVersion Include="Asp.Versioning.Mvc.ApiExplorer" Version="7.1.0" />
<PackageVersion Include="Dazinator.Extensions.FileProviders" Version="2.0.0" />
<PackageVersion Include="Examine" Version="3.5.0" />
<PackageVersion Include="Examine.Core" Version="3.5.0" />
<PackageVersion Include="HtmlAgilityPack" Version="1.11.71" />
<PackageVersion Include="JsonPatch.Net" Version="3.1.1" />
<PackageVersion Include="K4os.Compression.LZ4" Version="1.3.8" />
<PackageVersion Include="MailKit" Version="4.8.0" />
<PackageVersion Include="Markdown" Version="2.2.1" />
@@ -56,11 +55,12 @@
<PackageVersion Include="MiniProfiler.AspNetCore.Mvc" Version="4.3.8" />
<PackageVersion Include="MiniProfiler.Shared" Version="4.3.8" />
<PackageVersion Include="ncrontab" Version="3.3.3" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
<PackageVersion Include="NPoco" Version="5.7.1" />
<PackageVersion Include="NPoco.SqlServer" Version="5.7.1" />
<PackageVersion Include="OpenIddict.Abstractions" Version="5.7.0" />
<PackageVersion Include="OpenIddict.AspNetCore" Version="5.7.0" />
<PackageVersion Include="OpenIddict.EntityFrameworkCore" Version="5.7.0" />
<PackageVersion Include="OpenIddict.Abstractions" Version="4.10.1" />
<PackageVersion Include="OpenIddict.AspNetCore" Version="4.10.1" />
<PackageVersion Include="OpenIddict.EntityFrameworkCore" Version="4.10.1" />
<PackageVersion Include="Serilog" Version="3.1.1" />
<PackageVersion Include="Serilog.AspNetCore" Version="8.0.3" />
<PackageVersion Include="Serilog.Enrichers.Process" Version="2.0.2" />
@@ -75,6 +75,8 @@
<PackageVersion Include="Serilog.Sinks.Map" Version="1.0.2" />
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.7" />
<PackageVersion Include="SixLabors.ImageSharp.Web" Version="3.1.3" />
<PackageVersion Include="Smidge.InMemory" Version="4.4.0" />
<PackageVersion Include="Smidge.Nuglify" Version="4.5.1" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.9.0" />
</ItemGroup>
<!-- Transitive pinned versions (only required because our direct dependencies have vulnerable versions of transitive dependencies) -->
@@ -91,5 +93,11 @@
<PackageVersion Include="Microsoft.IdentityModel.JsonWebTokens" Version="7.7.1" />
<!-- Examine.Lucene bring in a vulnerable version of Lucene.Net.Replicator -->
<PackageVersion Include="Lucene.Net.Replicator" Version="4.8.0-beta00017" />
<!-- Both OpenIddict.AspNetCore, Microsoft.EntityFrameworkCore.* bring in a vulnerable version of Microsoft.Extensions.Caching.Memory -->
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="8.0.1" />
<!-- Both Azure.Identity, Microsoft.EntityFrameworkCore.SqlServer,NPoco.SqlServer, and more bring in a vulnerable version of System.Text.Json -->
<PackageVersion Include="System.Text.Json" Version="8.0.5" />
<!-- Both Microsoft.EntityFrameworkCore.SqlServer and NPoco.SqlServer bring in a vulnerable version of Microsoft.Data.SqlClient -->
<PackageVersion Include="Microsoft.Data.SqlClient" Version="5.2.2" />
</ItemGroup>
</Project>
+136 -197
View File
@@ -5,8 +5,8 @@ parameters:
displayName: Run SQL Server Integration Tests
type: boolean
default: false
- name: sqlServerLinuxAcceptanceTests
displayName: Run SQL Server Linux Acceptance Tests
- name: sqlServerAcceptanceTests
displayName: Run SQL Server Acceptance Tests
type: boolean
default: false
- name: myGetDeploy
@@ -17,10 +17,6 @@ parameters:
displayName: Deploy to NuGet
type: boolean
default: false
- name: npmDeploy
displayName: Deploy to Npm
type: boolean
default: false
- name: buildApiDocs
displayName: Build API docs
type: boolean
@@ -63,14 +59,14 @@ variables:
DOTNET_GENERATE_ASPNET_CERTIFICATE: false
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
DOTNET_CLI_TELEMETRY_OPTOUT: true
npm_config_cache: $(Pipeline.Workspace)/.npm_client
NODE_OPTIONS: --max_old_space_size=16384
stages:
###############################################
## Build
###############################################
- stage: Build
variables:
npm_config_cache: $(Pipeline.Workspace)/.npm_client
jobs:
- job: A
displayName: Build Umbraco CMS
@@ -78,21 +74,49 @@ stages:
vmImage: 'windows-latest'
steps:
- checkout: self
submodules: true
submodules: false
lfs: false,
fetchDepth: 500
- task: NodeTool@0
displayName: Use Node.js $(nodeVersion)
retryCountOnTaskFailure: 3
inputs:
versionSpec: $(nodeVersion)
- task: Cache@2
displayName: Cache node_modules
inputs:
key: '"npm_client" | "$(Agent.OS)" | $(Build.SourcesDirectory)/src/Umbraco.Web.UI.Client/package-lock.json'
restoreKeys: |
"npm_client" | "$(Agent.OS)"
"npm_client"
path: $(npm_config_cache)
- script: npm ci --no-fund --no-audit --prefer-offline
workingDirectory: src/Umbraco.Web.UI.Client
displayName: Run npm ci (Backoffice)
- task: gulp@0
displayName: Run gulp build (Backoffice)
inputs:
gulpFile: src/Umbraco.Web.UI.Client/gulpfile.js
targets: coreBuild
workingDirectory: src/Umbraco.Web.UI.Client
- task: npm@1
displayName: Run npm ci (Login)
inputs:
command: custom
workingDir: src/Umbraco.Web.UI.Login
verbose: false
customCommand: ci
- task: npm@1
displayName: Run npm build (Login)
inputs:
command: custom
workingDir: src/Umbraco.Web.UI.Login
verbose: false
customCommand: run build
- task: UseDotNet@2
displayName: Use .NET SDK from global.json
inputs:
useGlobalJson: true
- template: templates/backoffice-install.yml
- script: npm run build:for:cms
displayName: Run build (Bellissima)
workingDirectory: src/Umbraco.Web.UI.Client
- script: npm ci --no-fund --no-audit --prefer-offline
displayName: Run npm ci (Login)
workingDirectory: src/Umbraco.Web.UI.Login
- script: npm run build
displayName: Run npm build (Login)
workingDirectory: src/Umbraco.Web.UI.Login
- task: DotNetCoreCLI@2
displayName: Run dotnet restore
inputs:
@@ -116,31 +140,6 @@ stages:
targetPath: $(Build.SourcesDirectory)
artifactName: build_output
- job: B
displayName: Build Bellissima Package
pool:
vmImage: 'ubuntu-latest'
steps:
- checkout: self
submodules: true
- template: templates/backoffice-install.yml
- script: npm run build:for:npm
displayName: Run build:for:npm
workingDirectory: src/Umbraco.Web.UI.Client
- bash: |
echo "##[command]Running npm pack"
echo "##[debug]Output directory: $(Build.ArtifactStagingDirectory)"
mkdir $(Build.ArtifactStagingDirectory)/npm
npm pack --pack-destination $(Build.ArtifactStagingDirectory)/npm
mv .npmrc $(Build.ArtifactStagingDirectory)/npm/
displayName: Run npm pack
workingDirectory: src/Umbraco.Web.UI.Client
- task: PublishPipelineArtifact@1
displayName: Publish Bellissima npm artifact
inputs:
targetPath: $(Build.ArtifactStagingDirectory)/npm
artifactName: npm
- stage: Build_Docs
condition: and(succeeded(), or(eq(dependencies.Build.outputs['A.build.NBGV_PublicRelease'], 'True'), ${{parameters.buildApiDocs}}))
displayName: Prepare API Documentation
@@ -202,45 +201,41 @@ stages:
displayName: Build js API Reference
pool:
vmImage: 'ubuntu-latest'
variables:
BASE_PATH: /v$(umbracoMajorVersion)/ui
steps:
- checkout: self
submodules: true
- template: templates/backoffice-install.yml
- script: npm run storybook:build
displayName: Build Storybook
env:
VITE_BASE_PATH: $(BASE_PATH)/
workingDirectory: $(Build.SourcesDirectory)/src/Umbraco.Web.UI.Client
- script: sed -i "s|/umbraco/backoffice|$(BASE_PATH)/umbraco/backoffice|" assets/*.js
displayName: Replace BASE_PATH on assets
workingDirectory: $(Build.SourcesDirectory)/src/Umbraco.Web.UI.Client/storybook-static
- task: ArchiveFiles@2
displayName: Archive Storybook
submodules: false
lfs: false,
fetchDepth: 1
fetchFilter: tree:0
- task: NodeTool@0
displayName: Use Node.js 10.15.x
retryCountOnTaskFailure: 3
inputs:
rootFolderOrFile: $(Build.SourcesDirectory)/src/Umbraco.Web.UI.Client/storybook-static
versionSpec: 10.15.x # Won't work with higher versions
- script: |
npm ci --no-fund --no-audit --prefer-offline
npx gulp docs
major="$(umbracoMajorVersion)"
echo "major version: $major"
baseUrl="https://apidocs.umbraco.com/v$major/ui/"
echo "baseUrl: $baseUrl"
sed -i "s|baseUrl = .*|baseUrl = '$baseUrl',|" api/index.html
displayName: Generate js Docs
workingDirectory: $(Build.SourcesDirectory)/src/Umbraco.Web.UI.Docs
- task: ArchiveFiles@2
displayName: Archive js Docs
inputs:
rootFolderOrFile: $(Build.SourcesDirectory)/src/Umbraco.Web.UI.Docs/api
includeRootFolder: false
archiveFile: $(Build.ArtifactStagingDirectory)/ui-docs.zip
- task: PublishPipelineArtifact@1
displayName: Publish Storybook
displayName: Publish js Docs
inputs:
targetPath: $(Build.ArtifactStagingDirectory)/ui-docs.zip
artifact: ui-docs
- script: npm run generate:ui-api-docs
displayName: Generate API Docs
workingDirectory: $(Build.SourcesDirectory)/src/Umbraco.Web.UI.Client
- task: ArchiveFiles@2
displayName: Archive UI API Docs
inputs:
rootFolderOrFile: $(Build.SourcesDirectory)/src/Umbraco.Web.UI.Client/ui-api
includeRootFolder: false
archiveFile: $(Build.ArtifactStagingDirectory)/ui-api-docs.zip
- task: PublishPipelineArtifact@1
displayName: Publish UI API Docs
inputs:
targetPath: $(Build.ArtifactStagingDirectory)/ui-api-docs.zip
artifact: ui-api-docs
###############################################
## Test
@@ -263,6 +258,11 @@ stages:
pool:
vmImage: $(vmImage)
steps:
- checkout: self
submodules: false
lfs: false,
fetchDepth: 1
fetchFilter: tree:0
- task: DownloadPipelineArtifact@2
displayName: Download build artifacts
inputs:
@@ -302,6 +302,11 @@ stages:
variables:
Tests__Database__DatabaseType: 'Sqlite'
steps:
- checkout: self
submodules: false
lfs: false,
fetchDepth: 1
fetchFilter: tree:0
# Setup test environment
- task: DownloadPipelineArtifact@2
displayName: Download build artifacts
@@ -332,9 +337,8 @@ stages:
# Integration Tests (SQL Server)
- job:
timeoutInMinutes: 180
# condition: or(eq(stageDependencies.Build.A.outputs['build.NBGV_PublicRelease'], 'True'), ${{parameters.sqlServerIntegrationTests}}) # Outcommented due to timeouts
condition: eq(${{parameters.sqlServerIntegrationTests}}, True)
timeoutInMinutes: 120
condition: or(eq(stageDependencies.Build.A.outputs['build.NBGV_PublicRelease'], 'True'), ${{parameters.sqlServerIntegrationTests}})
displayName: Integration Tests (SQL Server)
strategy:
matrix:
@@ -430,10 +434,24 @@ stages:
CONNECTIONSTRINGS__UMBRACODBDSN_PROVIDERNAME: Microsoft.Data.Sqlite
strategy:
matrix:
Linux:
vmImage: 'ubuntu-latest'
Windows:
vmImage: 'windows-latest'
LinuxPart1Of3:
vmImage: "ubuntu-latest"
testCommand: "npm run test -- --shard=1/3"
LinuxPart2Of3:
vmImage: "ubuntu-latest"
testCommand: "npm run test -- --shard=2/3"
LinuxPart3Of3:
vmImage: "ubuntu-latest"
testCommand: "npm run test -- --shard=3/3"
WindowsPart1Of3:
vmImage: "windows-latest"
testCommand: "npm run test -- --shard=1/3"
WindowsPart2Of3:
vmImage: "windows-latest"
testCommand: "npm run test -- --shard=2/3"
WindowsPart3Of3:
vmImage: "windows-latest"
testCommand: "npm run test -- --shard=3/3"
pool:
vmImage: $(vmImage)
steps:
@@ -458,8 +476,7 @@ stages:
- pwsh: |
"UMBRACO_USER_LOGIN=$(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSEREMAIL)
UMBRACO_USER_PASSWORD=$(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD)
URL=$(ASPNETCORE_URLS)
STORAGE_STAGE_PATH=$(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/playwright/.auth/user.json" | Out-File .env
URL=$(ASPNETCORE_URLS)" | Out-File .env
displayName: Generate .env
workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest
@@ -506,10 +523,6 @@ stages:
condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT'))
workingDirectory: $(Agent.BuildDirectory)/app
# Ensures we have the package wait-on installed
- pwsh: npm install wait-on
displayName: Install wait-on package
# Wait for application to start responding to requests
- pwsh: npx wait-on -v --interval 1000 --timeout 120000 $(ASPNETCORE_URLS)
displayName: Wait for application
@@ -521,7 +534,7 @@ stages:
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
# Test
- pwsh: npm run smokeTestSqlite --ignore-certificate-errors
- pwsh: $(testCommand)
displayName: Run Playwright tests
continueOnError: true
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
@@ -542,7 +555,7 @@ stages:
# Copy artifacts
- pwsh: |
if (Test-Path tests/Umbraco.Tests.AcceptanceTest/results/*) {
Copy-Item tests/Umbraco.Tests.AcceptanceTest/results $(Build.ArtifactStagingDirectory) -Recurse
Copy-Item tests/Umbraco.Tests.AcceptanceTest/results/* $(Build.ArtifactStagingDirectory) -Recurse
}
displayName: Copy Playwright results
condition: succeededOrFailed()
@@ -553,23 +566,40 @@ stages:
condition: succeededOrFailed()
inputs:
targetPath: $(Build.ArtifactStagingDirectory)
artifact: 'Acceptance Tests - $(Agent.JobName) - Attempt #$(System.JobAttempt)'
artifact: "Acceptance Test Results - $(Agent.JobName) - Attempt #$(System.JobAttempt)"
- job:
displayName: E2E Tests (SQL Server)
condition: or(eq(stageDependencies.Build.A.outputs['build.NBGV_PublicRelease'], 'True'), ${{parameters.sqlServerAcceptanceTests}})
variables:
# Connection string
CONNECTIONSTRINGS__UMBRACODBDSN: Data Source=(localdb)\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\Umbraco.mdf;Integrated Security=True
CONNECTIONSTRINGS__UMBRACODBDSN_PROVIDERNAME: Microsoft.Data.SqlClient
strategy:
matrix:
${{ if eq(parameters.sqlServerLinuxAcceptanceTests, True) }} :
Linux:
vmImage: 'ubuntu-latest'
SA_PASSWORD: $(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD)
CONNECTIONSTRINGS__UMBRACODBDSN: 'Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);TrustServerCertificate=True'
Windows:
vmImage: 'windows-latest'
LinuxPart1Of3:
testCommand: "npm run testSqlite -- --shard=1/3"
vmImage: "ubuntu-latest"
SA_PASSWORD: $(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD)
CONNECTIONSTRINGS__UMBRACODBDSN: "Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);TrustServerCertificate=True"
LinuxPart2Of3:
testCommand: "npm run testSqlite -- --shard=2/3"
vmImage: "ubuntu-latest"
SA_PASSWORD: $(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD)
CONNECTIONSTRINGS__UMBRACODBDSN: "Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);TrustServerCertificate=True"
LinuxPart3Of3:
testCommand: "npm run testSqlite -- --shard=3/3"
vmImage: "ubuntu-latest"
SA_PASSWORD: $(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD)
CONNECTIONSTRINGS__UMBRACODBDSN: "Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);TrustServerCertificate=True"
WindowsPart1Of3:
vmImage: "windows-latest"
testCommand: "npm run testSqlite -- --shard=1/3"
WindowsPart2Of3:
vmImage: "windows-latest"
testCommand: "npm run testSqlite -- --shard=2/3"
WindowsPart3Of3:
vmImage: "windows-latest"
testCommand: "npm run testSqlite -- --shard=3/3"
pool:
vmImage: $(vmImage)
steps:
@@ -593,8 +623,7 @@ stages:
- pwsh: |
"UMBRACO_USER_LOGIN=$(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSEREMAIL)
UMBRACO_USER_PASSWORD=$(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD)
URL=$(ASPNETCORE_URLS)
STORAGE_STAGE_PATH=$(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/playwright/.auth/user.json" | Out-File .env
URL=$(ASPNETCORE_URLS)" | Out-File .env
displayName: Generate .env
workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest
@@ -661,7 +690,7 @@ stages:
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
# Test
- pwsh: npm run smokeTest --ignore-certificate-errors
- pwsh: $(testCommand)
displayName: Run Playwright tests
continueOnError: true
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
@@ -691,7 +720,7 @@ stages:
# Copy artifacts
- pwsh: |
if (Test-Path tests/Umbraco.Tests.AcceptanceTest/results/*) {
Copy-Item tests/Umbraco.Tests.AcceptanceTest/results $(Build.ArtifactStagingDirectory) -Recurse
Copy-Item tests/Umbraco.Tests.AcceptanceTest/results/* $(Build.ArtifactStagingDirectory) -Recurse
}
displayName: Copy Playwright results
condition: succeededOrFailed()
@@ -702,7 +731,7 @@ stages:
condition: succeededOrFailed()
inputs:
targetPath: $(Build.ArtifactStagingDirectory)
artifact: 'Acceptance Tests - $(Agent.JobName) - Attempt #$(System.JobAttempt)'
artifact: "Acceptance Test Results - $(Agent.JobName) - Attempt #$(System.JobAttempt)"
###############################################
## Release
@@ -712,7 +741,7 @@ stages:
dependsOn:
- Unit
- Integration
# - E2E
# - E2E # TODO: Enable when stable.
condition: and(succeeded(), or(eq(dependencies.Build.outputs['A.build.NBGV_PublicRelease'], 'True'), ${{parameters.myGetDeploy}}))
jobs:
- job:
@@ -734,41 +763,6 @@ stages:
publishFeedCredentials: 'MyGet - Umbraco Nightly'
${{ else }}:
publishFeedCredentials: 'MyGet - Pre-releases'
- job:
displayName: Push to pre-release feed (npm)
steps:
- checkout: none
- download: current
artifact: npm
- bash: |
# Check if we are on a nightly build
if [ $isNightly = "False" ]; then
echo "##[debug]Prerelease build detected"
registry="https://www.myget.org/F/umbracoprereleases/npm/"
else
echo "##[debug]Nightly build detected"
registry="https://www.myget.org/F/umbraconightly/npm/"
fi
echo "@umbraco-cms:registry=$registry" >> .npmrc
env:
isNightly: ${{parameters.isNightly}}
workingDirectory: $(Pipeline.Workspace)/npm
displayName: Add scoped registry to .npmrc
- task: npmAuthenticate@0
displayName: Authenticate with npm (MyGet)
inputs:
workingFile: '$(Pipeline.Workspace)/npm/.npmrc'
customEndpoint: 'MyGet (npm) - Umbracoprereleases, MyGet (npm) - Umbraconightly'
- bash: |
# Setup temp npm project to load in defaults from the local .npmrc
npm init -y
# Find the first .tgz file in the current directory and publish it
files=( ./*.tgz )
npm publish "${files[0]}"
displayName: Push to npm (MyGet)
workingDirectory: $(Pipeline.Workspace)/npm
- stage: Deploy_NuGet
displayName: NuGet release
dependsOn:
@@ -793,42 +787,12 @@ stages:
nuGetFeedType: 'external'
publishFeedCredentials: 'NuGet - Umbraco.*'
- stage: Deploy_Npm
displayName: Npm release
condition: and(succeeded(), or(eq(dependencies.Build.outputs['A.build.NBGV_PublicRelease'], 'True'), ${{parameters.npmDeploy}}))
dependsOn:
- Deploy_NuGet
jobs:
- job: Publish
displayName: Push to NPM
steps:
- checkout: none
- download: current
artifact: npm
- bash: echo "@umbraco-cms:registry=https://registry.npmjs.org" >> .npmrc
workingDirectory: $(Pipeline.Workspace)/npm
displayName: Add scoped registry to .npmrc
- task: npmAuthenticate@0
displayName: Authenticate with npm
inputs:
workingFile: $(Pipeline.Workspace)/npm/.npmrc
customEndpoint: 'NPM - Umbraco Backoffice'
- script: |
# Setup temp npm project to load in defaults from the local .npmrc
npm init -y
# Find the first .tgz file in the current directory and publish it
files=( ./*.tgz )
npm publish "${files[0]}"
displayName: Push to npm
workingDirectory: $(Pipeline.Workspace)/npm
- stage: Upload_API_Docs
pool:
vmImage: 'windows-latest' # Apparently AzureFileCopy is windows only :(
variables:
umbracoMajorVersion: $[ stageDependencies.Build.A.outputs['build.NBGV_VersionMajor'] ]
displayName: Upload API Documentation
displayName: Upload API Documention
dependsOn:
- Build
- Deploy_NuGet
@@ -856,9 +820,8 @@ stages:
storage: umbracoapidocs
ContainerName: '$web'
BlobPrefix: v$(umbracoMajorVersion)/csharp
CleanTargetBeforeCopy: true
- job:
displayName: Upload Storybook
displayName: Upload js Docs
steps:
- checkout: none
- task: DownloadPipelineArtifact@2
@@ -871,7 +834,7 @@ stages:
archiveFilePatterns: $(Build.SourcesDirectory)/ui-docs.zip
destinationFolder: $(Build.ArtifactStagingDirectory)/ui-docs
- task: AzureFileCopy@4
displayName: 'Copy Storybook to blob storage'
displayName: 'Copy UI Docs to blob storage'
inputs:
SourcePath: '$(Build.ArtifactStagingDirectory)/ui-docs/*'
azureSubscription: umbraco-storage
@@ -879,27 +842,3 @@ stages:
storage: umbracoapidocs
ContainerName: '$web'
BlobPrefix: v$(umbracoMajorVersion)/ui
CleanTargetBeforeCopy: true
- job:
displayName: Upload UI API Docs
steps:
- checkout: none
- task: DownloadPipelineArtifact@2
displayName: Download artifact
inputs:
artifact: ui-api-docs
path: $(Build.SourcesDirectory)
- task: ExtractFiles@1
inputs:
archiveFilePatterns: $(Build.SourcesDirectory)/ui-api-docs.zip
destinationFolder: $(Build.ArtifactStagingDirectory)/ui-api-docs
- task: AzureFileCopy@4
displayName: 'Copy UI API Docs to blob storage'
inputs:
SourcePath: '$(Build.ArtifactStagingDirectory)/ui-api-docs/*'
azureSubscription: umbraco-storage
Destination: AzureBlob
storage: umbracoapidocs
ContainerName: '$web'
BlobPrefix: v$(umbracoMajorVersion)/ui-api
CleanTargetBeforeCopy: true
-399
View File
@@ -1,399 +0,0 @@
name: Nightly_E2E_Test_$(TeamProject)_$(Build.DefinitionName)_$(SourceBranchName)_$(Date:yyyyMMdd)$(Rev:.r)
pr: none
trigger: none
# schedules:
# - cron: '0 0 * * *'
# displayName: Daily midnight build
# branches:
# include:
# - v14/dev
variables:
nodeVersion: 20
solution: umbraco.sln
buildConfiguration: Release
UMBRACO__CMS__GLOBAL__ID: 00000000-0000-0000-0000-000000000042
DOTNET_NOLOGO: true
DOTNET_GENERATE_ASPNET_CERTIFICATE: false
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
DOTNET_CLI_TELEMETRY_OPTOUT: true
npm_config_cache: $(Pipeline.Workspace)/.npm_client
NODE_OPTIONS: --max_old_space_size=16384
parameters:
- name: runSmokeTests
displayName: Run the smoke tests
type: boolean
default: false
stages:
###############################################
## Build
###############################################
- stage: Build
jobs:
- job: A
displayName: Build Umbraco CMS
pool:
vmImage: 'ubuntu-latest'
steps:
- checkout: self
fetchDepth: 0
submodules: true
- task: UseDotNet@2
displayName: Use .NET SDK from global.json
inputs:
useGlobalJson: true
- template: templates/backoffice-install.yml
- script: npm run build:for:cms
displayName: Run build (Bellissima)
workingDirectory: src/Umbraco.Web.UI.Client
- script: npm ci --no-fund --no-audit --prefer-offline
displayName: Run npm ci (Login)
workingDirectory: src/Umbraco.Web.UI.Login
- script: npm run build
displayName: Run npm build (Login)
workingDirectory: src/Umbraco.Web.UI.Login
- task: DotNetCoreCLI@2
displayName: Run dotnet restore
inputs:
command: restore
projects: $(solution)
- task: DotNetCoreCLI@2
name: build
displayName: Run dotnet build and generate NuGet packages
inputs:
command: build
projects: $(solution)
arguments: '--configuration $(buildConfiguration) --no-restore --property:ContinuousIntegrationBuild=true --property:GeneratePackageOnBuild=true --property:PackageOutputPath=$(Build.ArtifactStagingDirectory)/nupkg'
- task: PublishPipelineArtifact@1
displayName: Publish nupkg
inputs:
targetPath: $(Build.ArtifactStagingDirectory)/nupkg
artifactName: nupkg
- task: PublishPipelineArtifact@1
displayName: Publish build artifacts
inputs:
targetPath: $(Build.SourcesDirectory)
artifactName: build_output
- stage: E2E
displayName: E2E Tests
dependsOn: Build
variables:
npm_config_cache: $(Pipeline.Workspace)/.npm_e2e
# Enable console logging in Release mode
SERILOG__WRITETO__0__NAME: Async
SERILOG__WRITETO__0__ARGS__CONFIGURE__0__NAME: Console
# Set unattended install settings
UMBRACO__CMS__UNATTENDED__INSTALLUNATTENDED: true
UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERNAME: Playwright Test
UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD: UmbracoAcceptance123!
UMBRACO__CMS__UNATTENDED__UNATTENDEDUSEREMAIL: playwright@umbraco.com
# Custom Umbraco settings
UMBRACO__CMS__CONTENT__CONTENTVERSIONCLEANUPPOLICY__ENABLECLEANUP: false
UMBRACO__CMS__GLOBAL__DISABLEELECTIONFORSINGLESERVER: true
UMBRACO__CMS__GLOBAL__INSTALLMISSINGDATABASE: true
UMBRACO__CMS__GLOBAL__ID: 00000000-0000-0000-0000-000000000042
UMBRACO__CMS__GLOBAL__VERSIONCHECKPERIOD: 0
UMBRACO__CMS__GLOBAL__USEHTTPS: true
UMBRACO__CMS__HEALTHCHECKS__NOTIFICATION__ENABLED: false
UMBRACO__CMS__KEEPALIVE__DISABLEKEEPALIVETASK: true
UMBRACO__CMS__WEBROUTING__UMBRACOAPPLICATIONURL: https://localhost:44331/
ASPNETCORE_URLS: https://localhost:44331
jobs:
# E2E Tests
- job:
displayName: E2E Tests (SQLite)
timeoutInMinutes: 180
variables:
# Connection string
CONNECTIONSTRINGS__UMBRACODBDSN: Data Source=Umbraco;Mode=Memory;Cache=Shared;Foreign Keys=True;Pooling=True
CONNECTIONSTRINGS__UMBRACODBDSN_PROVIDERNAME: Microsoft.Data.Sqlite
strategy:
matrix:
Linux:
vmImage: 'ubuntu-latest'
Windows:
vmImage: 'windows-latest'
pool:
vmImage: $(vmImage)
steps:
# Setup test environment
- task: DownloadPipelineArtifact@2
displayName: Download NuGet artifacts
inputs:
artifact: nupkg
path: $(Agent.BuildDirectory)/app/nupkg
- task: NodeTool@0
displayName: Use Node.js $(nodeVersion)
retryCountOnTaskFailure: 3
inputs:
versionSpec: $(nodeVersion)
- task: UseDotNet@2
displayName: Use .NET SDK from global.json
inputs:
useGlobalJson: true
- pwsh: |
"UMBRACO_USER_LOGIN=$(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSEREMAIL)
UMBRACO_USER_PASSWORD=$(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD)
URL=$(ASPNETCORE_URLS)
STORAGE_STAGE_PATH=$(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/playwright/.auth/user.json" | Out-File .env
displayName: Generate .env
workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest
# 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: $(npm_config_cache)
- script: npm ci --no-fund --no-audit --prefer-offline
workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest
displayName: Restore NPM packages
# Build application
- pwsh: |
$cmsVersion = "$(Build.BuildNumber)" -replace "\+",".g"
dotnet new nugetconfig
dotnet nuget add source ./nupkg --name Local
dotnet new install Umbraco.Templates::$cmsVersion
dotnet new umbraco --name UmbracoProject --version $cmsVersion --exclude-gitignore --no-restore --no-update-check
dotnet restore UmbracoProject
cp $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest.UmbracoProject/*.cs UmbracoProject
dotnet build UmbracoProject --configuration $(buildConfiguration) --no-restore
dotnet dev-certs https
displayName: Build application
workingDirectory: $(Agent.BuildDirectory)/app
# Run application
- bash: |
nohup dotnet run --project UmbracoProject --configuration $(buildConfiguration) --no-build --no-launch-profile > $(Build.ArtifactStagingDirectory)/playwright.log 2>&1 &
echo "##vso[task.setvariable variable=AcceptanceTestProcessId]$!"
displayName: Run application (Linux)
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
workingDirectory: $(Agent.BuildDirectory)/app
- pwsh: |
$process = Start-Process dotnet "run --project UmbracoProject --configuration $(buildConfiguration) --no-build --no-launch-profile 2>&1" -PassThru -NoNewWindow -RedirectStandardOutput $(Build.ArtifactStagingDirectory)/playwright.log
Write-Host "##vso[task.setvariable variable=AcceptanceTestProcessId]$($process.Id)"
displayName: Run application (Windows)
condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT'))
workingDirectory: $(Agent.BuildDirectory)/app
# Ensures we have the package wait-on installed
- pwsh: npm install wait-on
displayName: Install wait-on package
# Wait for application to start responding to requests
- pwsh: npx wait-on -v --interval 1000 --timeout 120000 $(ASPNETCORE_URLS)
displayName: Wait for application
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
# Install Playwright and dependencies
- pwsh: npx playwright install --with-deps
displayName: Install Playwright
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
# Test
- ${{ if eq(parameters.runSmokeTests, true) }}:
pwsh: npm run smokeTestSqlite --ignore-certificate-errors
${{ else }}:
pwsh: npm run testSqlite --ignore-certificate-errors
displayName: Run Playwright tests
continueOnError: true
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
env:
CI: true
CommitId: $(Build.SourceVersion)
AgentOs: $(Agent.OS)
# Stop application
- bash: kill -15 $(AcceptanceTestProcessId)
displayName: Stop application (Linux)
condition: and(succeeded(), ne(variables.AcceptanceTestProcessId, ''), eq(variables['Agent.OS'], 'Linux'))
- pwsh: Stop-Process -Id $(AcceptanceTestProcessId)
displayName: Stop application (Windows)
condition: and(succeeded(), ne(variables.AcceptanceTestProcessId, ''), eq(variables['Agent.OS'], 'Windows_NT'))
# Copy artifacts
- pwsh: |
if (Test-Path tests/Umbraco.Tests.AcceptanceTest/results/*) {
Copy-Item tests/Umbraco.Tests.AcceptanceTest/results $(Build.ArtifactStagingDirectory) -Recurse
}
displayName: Copy Playwright results
condition: succeededOrFailed()
# Publish
- task: PublishPipelineArtifact@1
displayName: Publish test artifacts
condition: succeededOrFailed()
inputs:
targetPath: $(Build.ArtifactStagingDirectory)
artifact: 'Acceptance Tests - $(Agent.JobName) - Attempt #$(System.JobAttempt)'
- job:
displayName: E2E Tests (SQL Server)
timeoutInMinutes: 180
variables:
# Connection string
CONNECTIONSTRINGS__UMBRACODBDSN: Data Source=(localdb)\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\Umbraco.mdf;Integrated Security=True
CONNECTIONSTRINGS__UMBRACODBDSN_PROVIDERNAME: Microsoft.Data.SqlClient
strategy:
matrix:
Linux:
vmImage: 'ubuntu-latest'
SA_PASSWORD: $(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD)
CONNECTIONSTRINGS__UMBRACODBDSN: 'Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);TrustServerCertificate=True'
Windows:
vmImage: 'windows-latest'
pool:
vmImage: $(vmImage)
steps:
# Setup test environment
- task: DownloadPipelineArtifact@2
displayName: Download NuGet artifacts
inputs:
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=$(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSEREMAIL)
UMBRACO_USER_PASSWORD=$(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD)
URL=$(ASPNETCORE_URLS)
STORAGE_STAGE_PATH=$(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/playwright/.auth/user.json" | Out-File .env
displayName: Generate .env
workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest
# 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: $(npm_config_cache)
- script: npm ci --no-fund --no-audit --prefer-offline
workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest
displayName: Restore NPM packages
# Build application
- pwsh: |
$cmsVersion = "$(Build.BuildNumber)" -replace "\+",".g"
dotnet new nugetconfig
dotnet nuget add source ./nupkg --name Local
dotnet new install Umbraco.Templates::$cmsVersion
dotnet new umbraco --name UmbracoProject --version $cmsVersion --exclude-gitignore --no-restore --no-update-check
dotnet restore UmbracoProject
cp $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest.UmbracoProject/*.cs UmbracoProject
dotnet build UmbracoProject --configuration $(buildConfiguration) --no-restore
dotnet dev-certs https
displayName: Build application
workingDirectory: $(Agent.BuildDirectory)/app
# Start SQL Server
- powershell: docker run --name mssql -d -p 1433:1433 -e "ACCEPT_EULA=Y" -e "MSSQL_SA_PASSWORD=$(SA_PASSWORD)" mcr.microsoft.com/mssql/server:2022-latest
displayName: Start SQL Server Docker image (Linux)
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
- pwsh: SqlLocalDB start MSSQLLocalDB
displayName: Start SQL Server LocalDB (Windows)
condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT'))
# Run application
- bash: |
nohup dotnet run --project UmbracoProject --configuration $(buildConfiguration) --no-build --no-launch-profile > $(Build.ArtifactStagingDirectory)/playwright.log 2>&1 &
echo "##vso[task.setvariable variable=AcceptanceTestProcessId]$!"
displayName: Run application (Linux)
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
workingDirectory: $(Agent.BuildDirectory)/app
- pwsh: |
$process = Start-Process dotnet "run --project UmbracoProject --configuration $(buildConfiguration) --no-build --no-launch-profile 2>&1" -PassThru -NoNewWindow -RedirectStandardOutput $(Build.ArtifactStagingDirectory)/playwright.log
Write-Host "##vso[task.setvariable variable=AcceptanceTestProcessId]$($process.Id)"
displayName: Run application (Windows)
condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT'))
workingDirectory: $(Agent.BuildDirectory)/app
# Ensures we have the package wait-on installed
- pwsh: npm install wait-on
displayName: Install wait-on package
# Wait for application to start responding to requests
- pwsh: npx wait-on -v --interval 1000 --timeout 120000 $(ASPNETCORE_URLS)
displayName: Wait for application
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
# Install Playwright and dependencies
- pwsh: npx playwright install --with-deps
displayName: Install Playwright
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
# Test
- ${{ if eq(parameters.runSmokeTests, true) }}:
pwsh: npm run smokeTest --ignore-certificate-errors
${{ else }}:
pwsh: npm run test --ignore-certificate-errors
displayName: Run Playwright tests
continueOnError: true
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
env:
CI: true
CommitId: $(Build.SourceVersion)
AgentOs: $(Agent.OS)
# Stop application
- bash: kill -15 $(AcceptanceTestProcessId)
displayName: Stop application (Linux)
condition: and(succeeded(), ne(variables.AcceptanceTestProcessId, ''), eq(variables['Agent.OS'], 'Linux'))
- pwsh: Stop-Process -Id $(AcceptanceTestProcessId)
displayName: Stop application (Windows)
condition: and(succeeded(), ne(variables.AcceptanceTestProcessId, ''), eq(variables['Agent.OS'], 'Windows_NT'))
# Stop SQL Server
- pwsh: docker stop mssql
displayName: Stop SQL Server Docker image (Linux)
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
- pwsh: SqlLocalDB stop MSSQLLocalDB
displayName: Stop SQL Server LocalDB (Windows)
condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT'))
# Copy artifacts
- pwsh: |
if (Test-Path tests/Umbraco.Tests.AcceptanceTest/results/*) {
Copy-Item tests/Umbraco.Tests.AcceptanceTest/results $(Build.ArtifactStagingDirectory) -Recurse
}
displayName: Copy Playwright results
condition: succeededOrFailed()
# Publish
- task: PublishPipelineArtifact@1
displayName: Publish test artifacts
condition: succeededOrFailed()
inputs:
targetPath: $(Build.ArtifactStagingDirectory)
artifact: 'Acceptance Tests - $(Agent.JobName) - Attempt #$(System.JobAttempt)'
-31
View File
@@ -1,31 +0,0 @@
steps:
- task: NodeTool@0
displayName: Use Node.js
retryCountOnTaskFailure: 3
inputs:
versionSource: 'fromFile'
versionFilePath: src/Umbraco.Web.UI.Client/.nvmrc
- bash: |
echo "##[command]Install nbgv"
dotnet tool install --tool-path . nbgv
echo "##[command]Running nbgv get-version"
PACKAGE_VERSION=$(nbgv get-version -v NpmPackageVersion)
echo "##[command]Running npm version"
echo "##[debug]Version: $PACKAGE_VERSION"
cd src/Umbraco.Web.UI.Client
npm version $PACKAGE_VERSION --allow-same-version --no-git-tag-version
displayName: Set NPM Version
- task: Cache@2
displayName: Cache node_modules
inputs:
key: '"npm_client" | "$(Agent.OS)"| $(Build.SourcesDirectory)/src/Umbraco.Web.UI.Client/package-lock.json'
restoreKeys: |
"npm_client" | "$(Agent.OS)"
"npm_client"
path: $(npm_config_cache)
- script: npm ci --no-fund --no-audit --prefer-offline
displayName: Run npm ci (Bellissima)
workingDirectory: src/Umbraco.Web.UI.Client
+1 -1
View File
@@ -1,6 +1,6 @@
{
"sdk": {
"version": "8.0.300",
"version": "8.0.100",
"rollForward": "latestFeature"
}
}
@@ -1,6 +1,5 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Common.Builders;
@@ -9,8 +8,6 @@ public class ProblemDetailsBuilder
private string? _title;
private string? _detail;
private string? _type;
private string? _operationStatus;
private IDictionary<string, object>? _extensions;
public ProblemDetailsBuilder WithTitle(string title)
{
@@ -30,45 +27,11 @@ public class ProblemDetailsBuilder
return this;
}
public ProblemDetailsBuilder WithOperationStatus<TEnum>(TEnum operationStatus)
where TEnum : Enum
{
_operationStatus = operationStatus.ToString();
return this;
}
public ProblemDetailsBuilder WithRequestModelErrors(IDictionary<string, string[]> errors)
=> WithExtension(nameof(HttpValidationProblemDetails.Errors).ToFirstLowerInvariant(), errors);
public ProblemDetailsBuilder WithExtension(string key, object value)
{
_extensions ??= new Dictionary<string, object>();
_extensions[key] = value;
return this;
}
public ProblemDetails Build()
{
var problemDetails = new ProblemDetails
public ProblemDetails Build() =>
new()
{
Title = _title,
Detail = _detail,
Type = _type ?? "Error",
};
if (_operationStatus is not null)
{
problemDetails.Extensions["operationStatus"] = _operationStatus;
}
if (_extensions is not null)
{
foreach (KeyValuePair<string, object> extension in _extensions)
{
problemDetails.Extensions[extension.Key] = extension.Value;
}
}
return problemDetails;
}
}
@@ -1,15 +0,0 @@
using Microsoft.Extensions.Options;
using OpenIddict.Server.AspNetCore;
using Umbraco.Cms.Core.Configuration.Models;
namespace Umbraco.Cms.Api.Common.Configuration;
internal class ConfigureOpenIddict : IConfigureOptions<OpenIddictServerAspNetCoreOptions>
{
private readonly IOptions<GlobalSettings> _globalSettings;
public ConfigureOpenIddict(IOptions<GlobalSettings> globalSettings) => _globalSettings = globalSettings;
public void Configure(OpenIddictServerAspNetCoreOptions options)
=> options.DisableTransportSecurityRequirement = _globalSettings.Value.UseHttps is false;
}
@@ -12,22 +12,16 @@ namespace Umbraco.Cms.Api.Common.Configuration;
public class ConfigureUmbracoSwaggerGenOptions : IConfigureOptions<SwaggerGenOptions>
{
private readonly IOptions<ApiVersioningOptions> _apiVersioningOptions;
private readonly IOperationIdSelector _operationIdSelector;
private readonly ISchemaIdSelector _schemaIdSelector;
[Obsolete("Use non-obsolete constructor. This will be removed in Umbraco 15.")]
public ConfigureUmbracoSwaggerGenOptions(
IOptions<ApiVersioningOptions> apiVersioningOptions,
IOperationIdSelector operationIdSelector,
ISchemaIdSelector schemaIdSelector)
: this(operationIdSelector, schemaIdSelector)
{
}
public ConfigureUmbracoSwaggerGenOptions(
IOperationIdSelector operationIdSelector,
ISchemaIdSelector schemaIdSelector)
{
_apiVersioningOptions = apiVersioningOptions;
_operationIdSelector = operationIdSelector;
_schemaIdSelector = schemaIdSelector;
}
@@ -40,10 +34,10 @@ public class ConfigureUmbracoSwaggerGenOptions : IConfigureOptions<SwaggerGenOpt
{
Title = "Default API",
Version = "Latest",
Description = "All endpoints not defined under specific APIs",
Description = "All endpoints not defined under specific APIs"
});
swaggerGenOptions.CustomOperationIds(description => _operationIdSelector.OperationId(description));
swaggerGenOptions.CustomOperationIds(description => _operationIdSelector.OperationId(description, _apiVersioningOptions.Value));
swaggerGenOptions.DocInclusionPredicate((name, api) =>
{
if (string.IsNullOrWhiteSpace(api.GroupName))
@@ -1,62 +0,0 @@
using Microsoft.AspNetCore.Http;
using OpenIddict.Server;
using OpenIddict.Validation;
using Umbraco.Cms.Core;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Common.DependencyInjection;
public class ProcessRequestContextHandler
: IOpenIddictServerHandler<OpenIddictServerEvents.ProcessRequestContext>, IOpenIddictValidationHandler<OpenIddictValidationEvents.ProcessRequestContext>
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly string[] _pathsToHandle;
public ProcessRequestContextHandler(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
var backOfficePathSegment = Constants.System.DefaultUmbracoPath.TrimStart(Constants.CharArrays.Tilde)
.EnsureStartsWith('/')
.EnsureEndsWith('/');
_pathsToHandle = [backOfficePathSegment, "/.well-known/openid-configuration"];
}
public ValueTask HandleAsync(OpenIddictServerEvents.ProcessRequestContext context)
{
if (SkipOpenIddictHandlingForRequest())
{
context.SkipRequest();
}
return ValueTask.CompletedTask;
}
public ValueTask HandleAsync(OpenIddictValidationEvents.ProcessRequestContext context)
{
if (SkipOpenIddictHandlingForRequest())
{
context.SkipRequest();
}
return ValueTask.CompletedTask;
}
private bool SkipOpenIddictHandlingForRequest()
{
var requestPath = _httpContextAccessor.HttpContext?.Request.Path.Value;
if (requestPath.IsNullOrWhiteSpace())
{
return false;
}
foreach (var path in _pathsToHandle)
{
if (requestPath.StartsWith(path))
{
return false;
}
}
return true;
}
}
@@ -11,18 +11,11 @@ public static class UmbracoBuilderApiExtensions
{
public static IUmbracoBuilder AddUmbracoApiOpenApiUI(this IUmbracoBuilder builder)
{
if (builder.Services.Any(x => !x.IsKeyedService && x.ImplementationType == typeof(OperationIdSelector)))
{
return builder;
}
builder.Services.AddSwaggerGen();
builder.Services.ConfigureOptions<ConfigureUmbracoSwaggerGenOptions>();
builder.Services.AddSingleton<IUmbracoJsonTypeInfoResolver, UmbracoJsonTypeInfoResolver>();
builder.Services.AddSingleton<IOperationIdSelector, OperationIdSelector>();
builder.Services.AddSingleton<IOperationIdHandler, OperationIdHandler>();
builder.Services.AddSingleton<ISchemaIdSelector, SchemaIdSelector>();
builder.Services.AddSingleton<ISchemaIdHandler, SchemaIdHandler>();
builder.Services.Configure<UmbracoPipelineOptions>(options => options.AddFilter(new SwaggerRouteTemplatePipelineFilter("UmbracoApiCommon")));
return builder;
@@ -1,26 +1,23 @@
using System.Security.Cryptography;
using Microsoft.Extensions.Configuration;
using System.Security.Cryptography;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;
using OpenIddict.Server;
using OpenIddict.Validation;
using Umbraco.Cms.Api.Common.Configuration;
using Umbraco.Cms.Api.Common.Security;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Infrastructure.BackgroundJobs.Jobs;
using Umbraco.Extensions;
using Umbraco.Cms.Infrastructure.HostedServices;
namespace Umbraco.Cms.Api.Common.DependencyInjection;
public static class UmbracoBuilderAuthExtensions
{
private static bool _initialized;
public static IUmbracoBuilder AddUmbracoOpenIddict(this IUmbracoBuilder builder)
{
if (builder.Services.Any(x => !x.IsKeyedService && x.ImplementationType == typeof(OpenIddictCleanupJob)) is false)
if (_initialized is false)
{
ConfigureOpenIddict(builder);
_initialized = true;
}
return builder;
@@ -34,19 +31,16 @@ public static class UmbracoBuilderAuthExtensions
{
// Enable the authorization and token endpoints.
// - important: member endpoints MUST be added before backoffice endpoints to ensure that auto-discovery works for members
// FIXME: swap paths here so member API is first (see comment above)
options
.SetAuthorizationEndpointUris(
Paths.MemberApi.AuthorizationEndpoint.TrimStart(Constants.CharArrays.ForwardSlash),
Paths.BackOfficeApi.AuthorizationEndpoint.TrimStart(Constants.CharArrays.ForwardSlash))
Paths.MemberApi.AuthorizationEndpoint.TrimStart(Constants.CharArrays.ForwardSlash))
.SetTokenEndpointUris(
Paths.MemberApi.TokenEndpoint.TrimStart(Constants.CharArrays.ForwardSlash),
Paths.BackOfficeApi.TokenEndpoint.TrimStart(Constants.CharArrays.ForwardSlash))
Paths.MemberApi.TokenEndpoint.TrimStart(Constants.CharArrays.ForwardSlash))
.SetLogoutEndpointUris(
Paths.MemberApi.LogoutEndpoint.TrimStart(Constants.CharArrays.ForwardSlash),
Paths.BackOfficeApi.LogoutEndpoint.TrimStart(Constants.CharArrays.ForwardSlash))
Paths.MemberApi.LogoutEndpoint.TrimStart(Constants.CharArrays.ForwardSlash))
.SetRevocationEndpointUris(
Paths.MemberApi.RevokeEndpoint.TrimStart(Constants.CharArrays.ForwardSlash),
Paths.BackOfficeApi.RevokeEndpoint.TrimStart(Constants.CharArrays.ForwardSlash))
Paths.MemberApi.RevokeEndpoint.TrimStart(Constants.CharArrays.ForwardSlash))
.SetUserinfoEndpointUris(
Paths.MemberApi.UserinfoEndpoint.TrimStart(Constants.CharArrays.ForwardSlash));
@@ -69,17 +63,6 @@ public static class UmbracoBuilderAuthExtensions
.UseReferenceAccessTokens()
.UseReferenceRefreshTokens();
// Apply sliding window expiry based on the configured max login lifetime
GlobalSettings globalSettings = builder.Config
.GetSection(Constants.Configuration.ConfigGlobal)
.Get<GlobalSettings>() ?? new GlobalSettings();
TimeSpan timeOut = globalSettings.TimeOut;
// Make the access token lifetime 25% of the refresh token lifetime, to help ensure that new access tokens
// are obtained by the client before the refresh token expires.
options.SetAccessTokenLifetime(new TimeSpan(timeOut.Ticks / 4));
options.SetRefreshTokenLifetime(timeOut);
// Use ASP.NET Core Data Protection for tokens instead of JWT.
// This is more secure, and has the added benefit of having a high throughput
// but means that all servers (such as in a load balanced setup)
@@ -102,13 +85,6 @@ public static class UmbracoBuilderAuthExtensions
options
.AddEncryptionKey(new SymmetricSecurityKey(RandomNumberGenerator.GetBytes(32))) // generate a cryptographically secure random 256-bits key
.AddSigningKey(new RsaSecurityKey(RSA.Create(keySizeInBits: 2048))); // generate RSA key with recommended size of 2048-bits
// Add custom handler for the "ProcessRequestContext" server event, to stop OpenIddict from handling
// every last request to the server (including front-end requests).
options.AddEventHandler<OpenIddictServerEvents.ProcessRequestContext>(configuration =>
{
configuration.UseSingletonHandler<ProcessRequestContextHandler>().SetOrder(OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreHandlers.ResolveRequestUri.Descriptor.Order - 1);
});
})
// Register the OpenIddict validation components.
@@ -126,16 +102,8 @@ public static class UmbracoBuilderAuthExtensions
// Use ASP.NET Core Data Protection for tokens instead of JWT. (see note in AddServer)
options.UseDataProtection();
// Add custom handler for the "ProcessRequestContext" validation event, to stop OpenIddict from handling
// every last request to the server (including front-end requests).
options.AddEventHandler<OpenIddictValidationEvents.ProcessRequestContext>(configuration =>
{
configuration.UseSingletonHandler<ProcessRequestContextHandler>().SetOrder(OpenIddict.Validation.AspNetCore.OpenIddictValidationAspNetCoreHandlers.ResolveRequestUri.Descriptor.Order - 1);
});
});
builder.Services.AddRecurringBackgroundJob<OpenIddictCleanupJob>();
builder.Services.ConfigureOptions<ConfigureOpenIddict>();
builder.Services.AddHostedService<OpenIddictCleanup>();
}
}
@@ -1,50 +0,0 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core;
namespace Umbraco.Cms.Api.Common.Mvc.ActionResults;
/// <summary>
/// A "created at" action result with no response body.
/// </summary>
public sealed class EmptyCreatedAtActionResult : ActionResult
{
private readonly string _actionName;
private readonly string _controllerName;
private readonly object _routeValues;
private readonly string _resourceIdentifier;
public EmptyCreatedAtActionResult(string actionName, string controllerName, object routeValues, string resourceIdentifier)
{
_actionName = actionName;
_controllerName = controllerName;
_routeValues = routeValues;
_resourceIdentifier = resourceIdentifier;
}
public override void ExecuteResult(ActionContext context)
{
ArgumentNullException.ThrowIfNull(context);
HttpRequest request = context.HttpContext.Request;
IUrlHelper urlHelper = context.HttpContext.RequestServices.GetRequiredService<IUrlHelperFactory>().GetUrlHelper(context);
var url = urlHelper.Action(
_actionName,
_controllerName,
_routeValues,
request.Scheme,
request.Host.ToUriComponent());
if (string.IsNullOrEmpty(url))
{
throw new InvalidOperationException("No routes could be found that matched the provided route components");
}
context.HttpContext.Response.StatusCode = StatusCodes.Status201Created;
context.HttpContext.Response.Headers.Location = url;
context.HttpContext.Response.Headers[Constants.Headers.GeneratedResource] = _resourceIdentifier;
}
}
@@ -1,11 +0,0 @@
namespace Umbraco.Cms.Api.Common.OpenApi;
/// <summary>
/// Marker interface that ensure the type have a "$type" discriminator in the open api schema.
/// </summary>
/// <remarks>
/// This is required when an endpoint can receive different types, to ensure the correct type is deserialized.
/// </remarks>
public interface IOpenApiDiscriminator
{
}
@@ -1,10 +0,0 @@
using Microsoft.AspNetCore.Mvc.ApiExplorer;
namespace Umbraco.Cms.Api.Common.OpenApi;
public interface IOperationIdHandler
{
bool CanHandle(ApiDescription apiDescription);
string Handle(ApiDescription apiDescription);
}
@@ -5,8 +5,5 @@ namespace Umbraco.Cms.Api.Common.OpenApi;
public interface IOperationIdSelector
{
[Obsolete("Use overload that only takes ApiDescription instead. This will be removed in Umbraco 15.")]
string? OperationId(ApiDescription apiDescription, ApiVersioningOptions apiVersioningOptions);
string? OperationId(ApiDescription apiDescription);
}
@@ -1,8 +0,0 @@
namespace Umbraco.Cms.Api.Common.OpenApi;
public interface ISchemaIdHandler
{
bool CanHandle(Type type);
string Handle(Type type);
}
@@ -1,94 +0,0 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.Extensions.Options;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Common.OpenApi;
// NOTE: Left unsealed on purpose, so it is extendable.
public class OperationIdHandler : IOperationIdHandler
{
private readonly ApiVersioningOptions _apiVersioningOptions;
public OperationIdHandler(IOptions<ApiVersioningOptions> apiVersioningOptions)
=> _apiVersioningOptions = apiVersioningOptions.Value;
public bool CanHandle(ApiDescription apiDescription)
{
if (apiDescription.ActionDescriptor is not ControllerActionDescriptor controllerActionDescriptor)
{
return false;
}
return CanHandle(apiDescription, controllerActionDescriptor);
}
protected virtual bool CanHandle(ApiDescription apiDescription, ControllerActionDescriptor controllerActionDescriptor)
=> controllerActionDescriptor.ControllerTypeInfo.Namespace?.StartsWith("Umbraco.Cms.Api") is true;
public virtual string Handle(ApiDescription apiDescription)
=> UmbracoOperationId(apiDescription);
/// <summary>
/// Generates a unique operation identifier for a given API following Umbraco's operation id naming conventions.
/// </summary>
protected string UmbracoOperationId(ApiDescription apiDescription)
{
if (apiDescription.ActionDescriptor is not ControllerActionDescriptor controllerActionDescriptor)
{
throw new ArgumentException($"This handler operates only on {nameof(ControllerActionDescriptor)}.");
}
ApiVersion defaultVersion = _apiVersioningOptions.DefaultApiVersion;
var httpMethod = apiDescription.HttpMethod?.ToLower().ToFirstUpper() ?? "Get";
// if the route info "Name" is supplied we'll use this explicitly as the operation ID
// - usage example: [HttpGet("my-api/route}", Name = "MyCustomRoute")]
if (string.IsNullOrWhiteSpace(apiDescription.ActionDescriptor.AttributeRouteInfo?.Name) == false)
{
var explicitOperationId = apiDescription.ActionDescriptor.AttributeRouteInfo!.Name;
return explicitOperationId.InvariantStartsWith(httpMethod)
? explicitOperationId
: $"{httpMethod}{explicitOperationId}";
}
var relativePath = apiDescription.RelativePath;
if (string.IsNullOrWhiteSpace(relativePath))
{
throw new InvalidOperationException(
$"There is no relative path for controller action {apiDescription.ActionDescriptor.RouteValues["controller"]}");
}
// Remove the prefixed base path with version, e.g. /umbraco/management/api/v1/tracked-reference/{id} => tracked-reference/{id}
var unprefixedRelativePath = OperationIdRegexes
.VersionPrefixRegex()
.Replace(relativePath, string.Empty);
// Remove template placeholders, e.g. tracked-reference/{id} => tracked-reference/Id
var formattedOperationId = OperationIdRegexes
.TemplatePlaceholdersRegex()
.Replace(unprefixedRelativePath, m => $"By{m.Groups[1].Value.ToFirstUpper()}");
// Remove dashes (-) and slashes (/) and convert the following letter to uppercase with
// the word "By" in front, e.g. tracked-reference/Id => TrackedReferenceById
formattedOperationId = OperationIdRegexes
.ToCamelCaseRegex()
.Replace(formattedOperationId, m => m.Groups[1].Value.ToUpper());
// Get map to version attribute
string? version = null;
var versionAttributeValue = controllerActionDescriptor.MethodInfo.GetMapToApiVersionAttributeValue();
// We only want to add a version, if it is not the default one.
if (string.Equals(versionAttributeValue, defaultVersion.ToString()) == false)
{
version = versionAttributeValue;
}
// Return the operation ID with the formatted http method verb in front, e.g. GetTrackedReferenceById
return $"{httpMethod}{formattedOperationId.ToFirstUpper()}{version}";
}
}
@@ -1,27 +1,79 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Mvc.Controllers;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Common.OpenApi;
public class OperationIdSelector : IOperationIdSelector
{
private readonly IEnumerable<IOperationIdHandler> _operationIdHandlers;
[Obsolete("Use non-obsolete constructor. This will be removed in Umbraco 15.")]
public OperationIdSelector()
: this(Enumerable.Empty<IOperationIdHandler>())
public virtual string? OperationId(ApiDescription apiDescription, ApiVersioningOptions apiVersioningOptions)
{
if (apiDescription.ActionDescriptor is not ControllerActionDescriptor controllerActionDescriptor
|| controllerActionDescriptor.ControllerTypeInfo.Namespace?.StartsWith("Umbraco.Cms.Api") is not true)
{
return null;
}
return UmbracoOperationId(apiDescription, apiVersioningOptions);
}
public OperationIdSelector(IEnumerable<IOperationIdHandler> operationIdHandlers)
=> _operationIdHandlers = operationIdHandlers;
[Obsolete("Use overload that only takes ApiDescription instead. This will be removed in Umbraco 15.")]
public virtual string? OperationId(ApiDescription apiDescription, ApiVersioningOptions apiVersioningOptions) => OperationId(apiDescription);
public virtual string? OperationId(ApiDescription apiDescription)
protected string? UmbracoOperationId(ApiDescription apiDescription, ApiVersioningOptions apiVersioningOptions)
{
IOperationIdHandler? handler = _operationIdHandlers.FirstOrDefault(h => h.CanHandle(apiDescription));
return handler?.Handle(apiDescription);
if (apiDescription.ActionDescriptor is not ControllerActionDescriptor controllerActionDescriptor)
{
return null;
}
ApiVersion defaultVersion = apiVersioningOptions.DefaultApiVersion;
var httpMethod = apiDescription.HttpMethod?.ToLower().ToFirstUpper() ?? "Get";
// if the route info "Name" is supplied we'll use this explicitly as the operation ID
// - usage example: [HttpGet("my-api/route}", Name = "MyCustomRoute")]
if (string.IsNullOrWhiteSpace(apiDescription.ActionDescriptor.AttributeRouteInfo?.Name) == false)
{
var explicitOperationId = apiDescription.ActionDescriptor.AttributeRouteInfo!.Name;
return explicitOperationId.InvariantStartsWith(httpMethod)
? explicitOperationId
: $"{httpMethod}{explicitOperationId}";
}
var relativePath = apiDescription.RelativePath;
if (string.IsNullOrWhiteSpace(relativePath))
{
throw new Exception(
$"There is no relative path for controller action {apiDescription.ActionDescriptor.RouteValues["controller"]}");
}
// Remove the prefixed base path with version, e.g. /umbraco/management/api/v1/tracked-reference/{id} => tracked-reference/{id}
var unprefixedRelativePath = OperationIdRegexes
.VersionPrefixRegex()
.Replace(relativePath, string.Empty);
// Remove template placeholders, e.g. tracked-reference/{id} => tracked-reference/Id
var formattedOperationId = OperationIdRegexes
.TemplatePlaceholdersRegex()
.Replace(unprefixedRelativePath, m => $"By{m.Groups[1].Value.ToFirstUpper()}");
// Remove dashes (-) and slashes (/) and convert the following letter to uppercase with
// the word "By" in front, e.g. tracked-reference/Id => TrackedReferenceById
formattedOperationId = OperationIdRegexes
.ToCamelCaseRegex()
.Replace(formattedOperationId, m => m.Groups[1].Value.ToUpper());
//Get map to version attribute
string? version = null;
var versionAttributeValue = controllerActionDescriptor.MethodInfo.GetMapToApiVersionAttributeValue();
// We only wanna add a version, if it is not the default one.
if (string.Equals(versionAttributeValue, defaultVersion.ToString()) == false)
{
version = versionAttributeValue;
}
// Return the operation ID with the formatted http method verb in front, e.g. GetTrackedReferenceById
return $"{httpMethod}{formattedOperationId.ToFirstUpper()}{version}";
}
}
@@ -1,51 +0,0 @@
using System.Text.RegularExpressions;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Common.OpenApi;
// NOTE: Left unsealed on purpose, so it is extendable.
public class SchemaIdHandler : ISchemaIdHandler
{
public virtual bool CanHandle(Type type)
=> type.Namespace?.StartsWith("Umbraco.Cms") is true;
public virtual string Handle(Type type)
=> UmbracoSchemaId(type);
/// <summary>
/// Generates a sanitized and consistent schema identifier for a given type following Umbraco's schema id naming conventions.
/// </summary>
protected string UmbracoSchemaId(Type type)
{
var name = SanitizedTypeName(type);
name = HandleGenerics(name, type);
if (name.EndsWith("Model") == false)
{
// because some models names clash with common classes in TypeScript (i.e. Document),
// we need to add a "Model" postfix to all models
name = $"{name}Model";
}
// make absolutely sure we don't pass any invalid named by removing all non-word chars
return Regex.Replace(name, @"[^\w]", string.Empty);
}
private string SanitizedTypeName(Type t) => t.Name
// first grab the "non-generic" part of any generic type name (i.e. "PagedViewModel`1" becomes "PagedViewModel")
.Split('`').First()
// then remove the "ViewModel" postfix from type names
.TrimEnd("ViewModel");
private string HandleGenerics(string name, Type type)
{
if (!type.IsGenericType)
{
return name;
}
// use attribute custom name or append the generic type names, ultimately turning i.e. "PagedViewModel<RelationItemViewModel>" into "PagedRelationItem"
return $"{name}{string.Join(string.Empty, type.GenericTypeArguments.Select(SanitizedTypeName))}";
}
}
@@ -1,15 +1,36 @@
namespace Umbraco.Cms.Api.Common.OpenApi;
using System.Text.RegularExpressions;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Common.OpenApi;
public class SchemaIdSelector : ISchemaIdSelector
{
private readonly IEnumerable<ISchemaIdHandler> _schemaIdHandlers;
public SchemaIdSelector(IEnumerable<ISchemaIdHandler> schemaIdHandlers)
=> _schemaIdHandlers = schemaIdHandlers;
public virtual string SchemaId(Type type)
=> type.Namespace?.StartsWith("Umbraco.Cms") is true ? UmbracoSchemaId(type) : type.Name;
protected string UmbracoSchemaId(Type type)
{
ISchemaIdHandler? handler = _schemaIdHandlers.FirstOrDefault(h => h.CanHandle(type));
return handler?.Handle(type) ?? type.Name;
string SanitizedTypeName(Type t) => t.Name
// first grab the "non generic" part of any generic type name (i.e. "PagedViewModel`1" becomes "PagedViewModel")
.Split('`').First()
// then remove the "ViewModel" postfix from type names
.TrimEnd("ViewModel");
var name = SanitizedTypeName(type);
if (type.IsGenericType)
{
// append the generic type names, ultimately turning i.e. "PagedViewModel<RelationItemViewModel>" into "PagedRelationItem"
name = $"{name}{string.Join(string.Empty, type.GenericTypeArguments.Select(SanitizedTypeName))}";
}
if (name.EndsWith("Model") == false)
{
// because some models names clash with common classes in TypeScript (i.e. Document),
// we need to add a "Model" postfix to all models
name = $"{name}Model";
}
// make absolutely sure we don't pass any invalid named by removing all non-word chars
return Regex.Replace(name, @"[^\w]", string.Empty);
}
}
@@ -62,11 +62,6 @@ public class SwaggerRouteTemplatePipelineFilter : UmbracoPipelineFilter
swaggerUiOptions.SwaggerEndpoint($"{name}/swagger.json", $"{apiInfo.Title}");
}
// Add custom configuration from https://swagger.io/docs/open-source-tools/swagger-ui/usage/configuration/
swaggerUiOptions.ConfigObject.PersistAuthorization = true; // persists authorization data so it would not be lost on browser close/refresh
swaggerUiOptions.ConfigObject.Filter = string.Empty; // Enable the filter with an empty string as default filter.
swaggerUiOptions.OAuthClientId(Constants.OAuthClientIds.Swagger);
swaggerUiOptions.OAuthUsePkce();
}
+1 -18
View File
@@ -1,24 +1,7 @@
using Umbraco.Cms.Core;
namespace Umbraco.Cms.Api.Common.Security;
namespace Umbraco.Cms.Api.Common.Security;
public static class Paths
{
public static class BackOfficeApi
{
public const string EndpointTemplate = "security/back-office";
public static readonly string AuthorizationEndpoint = EndpointPath($"{EndpointTemplate}/authorize");
public static readonly string TokenEndpoint = EndpointPath($"{EndpointTemplate}/token");
public static readonly string LogoutEndpoint = EndpointPath($"{EndpointTemplate}/signout");
public static readonly string RevokeEndpoint = EndpointPath($"{EndpointTemplate}/revoke");
private static string EndpointPath(string relativePath) => $"/umbraco{Constants.Web.ManagementApiPath}v1/{relativePath}";
}
public static class MemberApi
{
public const string EndpointTemplate = "security/member";
@@ -5,6 +5,4 @@ namespace Umbraco.Cms.Api.Common.Serialization;
public interface IUmbracoJsonTypeInfoResolver : IJsonTypeInfoResolver
{
IEnumerable<Type> FindSubTypes(Type type);
string? GetTypeDiscriminatorValue(Type type);
}
@@ -1,10 +1,7 @@
using System.Collections.Concurrent;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
using Umbraco.Cms.Api.Common.OpenApi;
using Umbraco.Cms.Core.Composing;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Common.Serialization;
@@ -14,60 +11,51 @@ public sealed class UmbracoJsonTypeInfoResolver : DefaultJsonTypeInfoResolver, I
private readonly ConcurrentDictionary<Type, ISet<Type>> _subTypesCache = new ConcurrentDictionary<Type, ISet<Type>>();
public UmbracoJsonTypeInfoResolver(ITypeFinder typeFinder)
=> _typeFinder = typeFinder;
{
_typeFinder = typeFinder;
}
public IEnumerable<Type> FindSubTypes(Type type)
{
JsonDerivedTypeAttribute[] explicitJsonDerivedTypes = type
.GetCustomAttributes<JsonDerivedTypeAttribute>(false)
.ToArray();
if (explicitJsonDerivedTypes.Any())
{
return explicitJsonDerivedTypes.Select(a => a.DerivedType);
}
if (type.IsInterface is false)
{
// IMPORTANT: do NOT return an empty enumerable here. it will cause nullability to fail on reference
// properties, because "$ref" does not mix and match well with "nullable" in OpenAPI.
// see also https://github.com/OAI/OpenAPI-Specification/issues/1368
return new[] { type };
}
if (_subTypesCache.TryGetValue(type, out ISet<Type>? cachedResult))
{
return cachedResult;
}
var result = _typeFinder.FindClassesOfType(type).OrderBy(x => x.Name).ToHashSet();
var result = _typeFinder.FindClassesOfType(type).ToHashSet();
_subTypesCache.TryAdd(type, result);
return result;
}
public string? GetTypeDiscriminatorValue(Type type)
{
JsonDerivedTypeAttribute? jsonDerivedTypeAttribute = type
.GetBaseTypes(false)
.WhereNotNull()
.SelectMany(baseType => baseType.GetCustomAttributes<JsonDerivedTypeAttribute>(false))
.FirstOrDefault(attr => attr.DerivedType == type);
if (jsonDerivedTypeAttribute is not null)
{
// IMPORTANT: do NOT perform fallback to type.Name here - it will work for the schema generation,
// but not for the actual serialization, and then it's only going to cause confusion.
return jsonDerivedTypeAttribute.TypeDiscriminator?.ToString();
}
return typeof(IOpenApiDiscriminator).IsAssignableFrom(type) ? type.Name : null;
}
public override JsonTypeInfo GetTypeInfo(Type type, JsonSerializerOptions options)
{
JsonTypeInfo result = base.GetTypeInfo(type, options);
return type.IsInterface
? GetTypeInfoForInterface(result, type, options)
: result;
if (type.IsInterface)
{
return GetTypeInfoForInterface(result, type, options);
}
else
{
return GetTypeInfoForClass(result, type, options);
}
}
private JsonTypeInfo GetTypeInfoForClass(JsonTypeInfo result, Type type, JsonSerializerOptions options)
{
if (result.Kind != JsonTypeInfoKind.Object || !type.GetInterfaces().Any())
{
return result;
}
JsonPolymorphismOptions jsonPolymorphismOptions = result.PolymorphismOptions ?? new JsonPolymorphismOptions();
jsonPolymorphismOptions.DerivedTypes.Add(new JsonDerivedType(type, type.Name));
result.PolymorphismOptions = jsonPolymorphismOptions;
return result;
}
private JsonTypeInfo GetTypeInfoForInterface(JsonTypeInfo result, Type type, JsonSerializerOptions options)
@@ -3,13 +3,12 @@
<Title>Umbraco CMS - API Common</Title>
<Description>Contains the bits and pieces that are shared between the Umbraco CMS APIs.</Description>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Asp.Versioning.Mvc "/>
<PackageReference Include="Asp.Versioning.Mvc.ApiExplorer" />
<PackageReference Include="Swashbuckle.AspNetCore" />
<PackageReference Include="OpenIddict.Abstractions" />
<PackageReference Include="OpenIddict.AspNetCore" />
@@ -24,6 +23,5 @@
<ItemGroup>
<ProjectReference Include="..\Umbraco.Core\Umbraco.Core.csproj" />
<ProjectReference Include="..\Umbraco.Web.Common\Umbraco.Web.Common.csproj" />
</ItemGroup>
</Project>
@@ -1,9 +1,12 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Models.DeliveryApi;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Api.Delivery.Controllers.Content;
@@ -13,6 +16,32 @@ public class ByIdContentApiController : ContentApiItemControllerBase
{
private readonly IRequestMemberAccessService _requestMemberAccessService;
[Obsolete($"Please use the constructor that does not accept {nameof(IPublicAccessService)}. Will be removed in V14.")]
public ByIdContentApiController(
IApiPublishedContentCache apiPublishedContentCache,
IApiContentResponseBuilder apiContentResponseBuilder,
IPublicAccessService publicAccessService)
: this(
apiPublishedContentCache,
apiContentResponseBuilder,
StaticServiceProvider.Instance.GetRequiredService<IRequestMemberAccessService>())
{
}
[Obsolete($"Please use the constructor that does not accept {nameof(IPublicAccessService)}. Will be removed in V14.")]
public ByIdContentApiController(
IApiPublishedContentCache apiPublishedContentCache,
IApiContentResponseBuilder apiContentResponseBuilder,
IPublicAccessService publicAccessService,
IRequestMemberAccessService requestMemberAccessService)
: this(
apiPublishedContentCache,
apiContentResponseBuilder,
requestMemberAccessService)
{
}
[ActivatorUtilitiesConstructor]
public ByIdContentApiController(
IApiPublishedContentCache apiPublishedContentCache,
IApiContentResponseBuilder apiContentResponseBuilder,
@@ -1,9 +1,12 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Models.DeliveryApi;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.Services;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Delivery.Controllers.Content;
@@ -14,6 +17,32 @@ public class ByIdsContentApiController : ContentApiItemControllerBase
{
private readonly IRequestMemberAccessService _requestMemberAccessService;
[Obsolete($"Please use the constructor that does not accept {nameof(IPublicAccessService)}. Will be removed in V14.")]
public ByIdsContentApiController(
IApiPublishedContentCache apiPublishedContentCache,
IApiContentResponseBuilder apiContentResponseBuilder,
IPublicAccessService publicAccessService)
: this(
apiPublishedContentCache,
apiContentResponseBuilder,
StaticServiceProvider.Instance.GetRequiredService<IRequestMemberAccessService>())
{
}
[Obsolete($"Please use the constructor that does not accept {nameof(IPublicAccessService)}. Will be removed in V14.")]
public ByIdsContentApiController(
IApiPublishedContentCache apiPublishedContentCache,
IApiContentResponseBuilder apiContentResponseBuilder,
IPublicAccessService publicAccessService,
IRequestMemberAccessService requestMemberAccessService)
: this(
apiPublishedContentCache,
apiContentResponseBuilder,
requestMemberAccessService)
{
}
[ActivatorUtilitiesConstructor]
public ByIdsContentApiController(
IApiPublishedContentCache apiPublishedContentCache,
IApiContentResponseBuilder apiContentResponseBuilder,
@@ -21,6 +21,43 @@ public class ByRouteContentApiController : ContentApiItemControllerBase
private readonly IRequestMemberAccessService _requestMemberAccessService;
private const string PreviewContentRequestPathPrefix = $"/{Constants.DeliveryApi.Routing.PreviewContentPathPrefix}";
[Obsolete($"Please use the constructor that does not accept {nameof(IPublicAccessService)}. Will be removed in V14.")]
public ByRouteContentApiController(
IApiPublishedContentCache apiPublishedContentCache,
IApiContentResponseBuilder apiContentResponseBuilder,
IPublicAccessService publicAccessService,
IRequestRoutingService requestRoutingService,
IRequestRedirectService requestRedirectService,
IRequestPreviewService requestPreviewService)
: this(
apiPublishedContentCache,
apiContentResponseBuilder,
requestRoutingService,
requestRedirectService,
requestPreviewService,
StaticServiceProvider.Instance.GetRequiredService<IRequestMemberAccessService>())
{
}
[Obsolete($"Please use the constructor that does not accept {nameof(IPublicAccessService)}. Will be removed in V14.")]
public ByRouteContentApiController(
IApiPublishedContentCache apiPublishedContentCache,
IApiContentResponseBuilder apiContentResponseBuilder,
IPublicAccessService publicAccessService,
IRequestRoutingService requestRoutingService,
IRequestRedirectService requestRedirectService,
IRequestPreviewService requestPreviewService,
IRequestMemberAccessService requestMemberAccessService)
: this(
apiPublishedContentCache,
apiContentResponseBuilder,
requestRoutingService,
requestRedirectService,
requestPreviewService,
requestMemberAccessService)
{
}
[Obsolete($"Please use the constructor that accepts {nameof(IApiContentPathResolver)}. Will be removed in V15.")]
public ByRouteContentApiController(
IApiPublishedContentCache apiPublishedContentCache,
@@ -1,18 +1,35 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Api.Delivery.Controllers.Content;
public abstract class ContentApiItemControllerBase : ContentApiControllerBase
{
// TODO: Remove this in V14 when the obsolete constructors have been removed
private readonly IPublicAccessService _publicAccessService;
[Obsolete($"Please use the constructor that does not accept {nameof(IPublicAccessService)}. Will be removed in V14.")]
protected ContentApiItemControllerBase(
IApiPublishedContentCache apiPublishedContentCache,
IApiContentResponseBuilder apiContentResponseBuilder,
IPublicAccessService publicAccessService)
: this(apiPublishedContentCache, apiContentResponseBuilder)
{
}
protected ContentApiItemControllerBase(
IApiPublishedContentCache apiPublishedContentCache,
IApiContentResponseBuilder apiContentResponseBuilder)
: base(apiPublishedContentCache, apiContentResponseBuilder)
{
}
=> _publicAccessService = StaticServiceProvider.Instance.GetRequiredService<IPublicAccessService>();
[Obsolete($"Please use {nameof(IPublicAccessService)} to test for content protection. Will be removed in V14.")]
protected bool IsProtected(IPublishedContent content) => _publicAccessService.IsProtected(content.Path);
protected async Task<IActionResult?> HandleMemberAccessAsync(IPublishedContent contentItem, IRequestMemberAccessService requestMemberAccessService)
{
@@ -1,9 +1,11 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.DeliveryApi;
using Umbraco.Cms.Core.Models.PublishedContent;
@@ -19,6 +21,20 @@ public class QueryContentApiController : ContentApiControllerBase
private readonly IRequestMemberAccessService _requestMemberAccessService;
private readonly IApiContentQueryService _apiContentQueryService;
[Obsolete($"Please use the constructor that accepts {nameof(IRequestMemberAccessService)}. Will be removed in V14.")]
public QueryContentApiController(
IApiPublishedContentCache apiPublishedContentCache,
IApiContentResponseBuilder apiContentResponseBuilderBuilder,
IApiContentQueryService apiContentQueryService)
: this(
apiPublishedContentCache,
apiContentResponseBuilderBuilder,
apiContentQueryService,
StaticServiceProvider.Instance.GetRequiredService<IRequestMemberAccessService>())
{
}
[ActivatorUtilitiesConstructor]
public QueryContentApiController(
IApiPublishedContentCache apiPublishedContentCache,
IApiContentResponseBuilder apiContentResponseBuilderBuilder,
@@ -1,20 +1,16 @@
using System.Net;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Common.Attributes;
using Umbraco.Cms.Api.Common.Filters;
using Umbraco.Cms.Api.Delivery.Configuration;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Features;
using Umbraco.Cms.Web.Common.Authorization;
namespace Umbraco.Cms.Api.Delivery.Controllers;
[ApiController]
[JsonOptionsName(Constants.JsonOptionsNames.DeliveryApi)]
[MapToApi(DeliveryApiConfiguration.ApiName)]
[Authorize(Policy = AuthorizationPolicies.UmbracoFeatureEnabled)]
public abstract class DeliveryApiControllerBase : Controller, IUmbracoFeature
public abstract class DeliveryApiControllerBase : Controller
{
protected string DecodePath(string path)
{
@@ -1,4 +1,4 @@
using System.Security.Claims;
using System.Security.Claims;
using Asp.Versioning;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Authentication;
@@ -15,12 +15,13 @@ using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Web.Common.Security;
using Umbraco.Extensions;
using IdentitySignInResult = Microsoft.AspNetCore.Identity.SignInResult;
using SignInResult = Microsoft.AspNetCore.Mvc.SignInResult;
using IdentitySignInResult = Microsoft.AspNetCore.Identity.SignInResult;
namespace Umbraco.Cms.Api.Delivery.Controllers.Security;
[ApiVersion("1.0")]
[ApiController]
[VersionedDeliveryApiRoute(Common.Security.Paths.MemberApi.EndpointTemplate)]
[ApiExplorerSettings(IgnoreApi = true)]
public class MemberController : DeliveryApiControllerBase
@@ -0,0 +1,18 @@
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace Umbraco.Cms.Api.Delivery.Filters;
[Obsolete($"Superseded by {nameof(SwaggerContentDocumentationFilter)} and {nameof(SwaggerMediaDocumentationFilter)}. Will be removed in V14.")]
public class SwaggerDocumentationFilter : IOperationFilter, IParameterFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
// retained for backwards compat
}
public void Apply(OpenApiParameter parameter, ParameterFilterContext context)
{
// retained for backwards compat
}
}
@@ -1,4 +1,4 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core;
@@ -1,4 +1,4 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using OpenIddict.Abstractions;
using Umbraco.Cms.Core.Configuration.Models;
@@ -73,7 +73,7 @@ internal sealed class RevokeMemberAuthenticationTokensNotificationHandler
return;
}
_logger.LogInformation("Revoking {count} active tokens for member with ID {id}", tokens.Length, member.Id);
_logger.LogInformation("Deleting {count} active tokens for member with ID {id}", tokens.Length, member.Id);
foreach (var token in tokens)
{
await _tokenManager.DeleteAsync(token);
@@ -1,7 +1,56 @@
using Umbraco.Cms.Infrastructure.Serialization;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
using Umbraco.Cms.Core.Models.DeliveryApi;
namespace Umbraco.Cms.Api.Delivery.Json;
// see https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/polymorphism?pivots=dotnet-7-0
public class DeliveryApiJsonTypeResolver : ContentJsonTypeResolverBase
{ }
public class DeliveryApiJsonTypeResolver : DefaultJsonTypeInfoResolver
{
public override JsonTypeInfo GetTypeInfo(Type type, JsonSerializerOptions options)
{
JsonTypeInfo jsonTypeInfo = base.GetTypeInfo(type, options);
Type[] derivedTypes = GetDerivedTypes(jsonTypeInfo);
if (derivedTypes.Length > 0)
{
ConfigureJsonPolymorphismOptions(jsonTypeInfo, derivedTypes);
}
return jsonTypeInfo;
}
protected virtual Type[] GetDerivedTypes(JsonTypeInfo jsonTypeInfo)
{
if (jsonTypeInfo.Type == typeof(IApiContent))
{
return new[] { typeof(ApiContent) };
}
if (jsonTypeInfo.Type == typeof(IApiContentResponse))
{
return new[] { typeof(ApiContentResponse) };
}
if (jsonTypeInfo.Type == typeof(IRichTextElement))
{
return new[] { typeof(RichTextRootElement), typeof(RichTextGenericElement), typeof(RichTextTextElement) };
}
return Array.Empty<Type>();
}
protected void ConfigureJsonPolymorphismOptions(JsonTypeInfo jsonTypeInfo, params Type[] derivedTypes)
{
jsonTypeInfo.PolymorphismOptions = new JsonPolymorphismOptions
{
UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization,
};
foreach (Type derivedType in derivedTypes)
{
jsonTypeInfo.PolymorphismOptions.DerivedTypes.Add(new JsonDerivedType(derivedType));
}
}
}
@@ -1,88 +0,0 @@
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Umbraco.Cms.Core.DeliveryApi;
namespace Umbraco.Cms.Api.Delivery.Json;
public abstract class DeliveryApiVersionAwareJsonConverterBase<T> : JsonConverter<T>
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly JsonConverter<T> _defaultConverter = (JsonConverter<T>)JsonSerializerOptions.Default.GetConverter(typeof(T));
public DeliveryApiVersionAwareJsonConverterBase(IHttpContextAccessor httpContextAccessor)
=> _httpContextAccessor = httpContextAccessor;
/// <inheritdoc />
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
=> _defaultConverter.Read(ref reader, typeToConvert, options);
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
Type type = typeof(T);
var apiVersion = GetApiVersion();
// Get the properties in the specified order
PropertyInfo[] properties = type.GetProperties().OrderBy(GetPropertyOrder).ToArray();
writer.WriteStartObject();
foreach (PropertyInfo property in properties)
{
// Filter out properties based on the API version
var include = apiVersion is null || ShouldIncludeProperty(property, apiVersion.Value);
if (include is false)
{
continue;
}
var propertyName = property.Name;
writer.WritePropertyName(options.PropertyNamingPolicy?.ConvertName(propertyName) ?? propertyName);
JsonSerializer.Serialize(writer, property.GetValue(value), options);
}
writer.WriteEndObject();
}
private int? GetApiVersion()
{
HttpContext? httpContext = _httpContextAccessor.HttpContext;
ApiVersion? apiVersion = httpContext?.GetRequestedApiVersion();
return apiVersion?.MajorVersion;
}
private int GetPropertyOrder(PropertyInfo prop)
{
var attribute = prop.GetCustomAttribute<JsonPropertyOrderAttribute>();
return attribute?.Order ?? 0;
}
/// <summary>
/// Determines whether a property should be included based on version bounds.
/// </summary>
/// <param name="propertyInfo">The property info.</param>
/// <param name="version">An integer representing an API version.</param>
/// <returns><c>true</c> if the property should be included; otherwise, <c>false</c>.</returns>
private bool ShouldIncludeProperty(PropertyInfo propertyInfo, int version)
{
var attribute = propertyInfo
.GetCustomAttributes(typeof(IncludeInApiVersionAttribute), false)
.FirstOrDefault();
if (attribute is not IncludeInApiVersionAttribute apiVersionAttribute)
{
return true; // No attribute means include the property
}
// Check if the version is within the specified bounds
var isWithinMinVersion = apiVersionAttribute.MinVersion.HasValue is false || version >= apiVersionAttribute.MinVersion.Value;
var isWithinMaxVersion = apiVersionAttribute.MaxVersion.HasValue is false || version <= apiVersionAttribute.MaxVersion.Value;
return isWithinMinVersion && isWithinMaxVersion;
}
}
@@ -15,8 +15,7 @@ public sealed class AncestorsSelector : QueryOptionBase, ISelectorHandler
private readonly IRequestPreviewService _requestPreviewService;
[Obsolete("Please use the non-obsolete constructor. Will be removed in V17.")]
public AncestorsSelector(
IPublishedSnapshotAccessor publishedSnapshotAccessor,
public AncestorsSelector(IPublishedSnapshotAccessor publishedSnapshotAccessor,
IRequestRoutingService requestRoutingService)
: this(publishedSnapshotAccessor, requestRoutingService, StaticServiceProvider.Instance.GetRequiredService<IRequestPreviewService>())
{
@@ -1,4 +1,4 @@
using OpenIddict.Abstractions;
using OpenIddict.Abstractions;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Infrastructure.Security;
@@ -36,7 +36,7 @@ public class MemberApplicationManager : OpenIdDictApplicationManagerBase, IMembe
{
DisplayName = "Umbraco member access",
ClientId = Constants.OAuthClientIds.Member,
ClientType = OpenIddictConstants.ClientTypes.Public,
Type = OpenIddictConstants.ClientTypes.Public,
Permissions =
{
OpenIddictConstants.Permissions.Endpoints.Authorization,
@@ -48,6 +48,17 @@ internal sealed class ApiContentQueryProvider : IApiContentQueryProvider
}
[Obsolete($"Use the {nameof(ExecuteQuery)} method that accepts {nameof(ProtectedAccess)}. Will be removed in V14.")]
public PagedModel<Guid> ExecuteQuery(
SelectorOption selectorOption,
IList<FilterOption> filterOptions,
IList<SortOption> sortOptions,
string culture,
bool preview,
int skip,
int take)
=> ExecuteQuery(selectorOption, filterOptions, sortOptions, culture, ProtectedAccess.None, preview, skip, take);
/// <inheritdoc/>
public PagedModel<Guid> ExecuteQuery(
SelectorOption selectorOption,
@@ -37,6 +37,15 @@ internal sealed class ApiContentQueryService : IApiContentQueryService
_requestPreviewService = requestPreviewService;
}
[Obsolete($"Use the {nameof(ExecuteQuery)} method that accepts {nameof(ProtectedAccess)}. Will be removed in V14.")]
public Attempt<PagedModel<Guid>, ApiContentQueryOperationStatus> ExecuteQuery(
string? fetch,
IEnumerable<string> filters,
IEnumerable<string> sorts,
int skip,
int take)
=> ExecuteQuery(fetch, filters, sorts, ProtectedAccess.None, skip, take);
/// <inheritdoc/>
public Attempt<PagedModel<Guid>, ApiContentQueryOperationStatus> ExecuteQuery(
string? fetch,
@@ -1,4 +1,4 @@
using System.Security.Claims;
using System.Security.Claims;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
@@ -3,11 +3,7 @@
<Title>Umbraco CMS - Delivery API</Title>
<Description>Contains the presentation layer for the Umbraco CMS Delivery API.</Description>
</PropertyGroup>
<PropertyGroup>
<!-- TODO: [ASP0019] use IHeaderDictionary.Append or the indexer to append or set headers,
and remove this override -->
<WarningsNotAsErrors>ASP0019</WarningsNotAsErrors>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Umbraco.Cms.Api.Common\Umbraco.Cms.Api.Common.csproj" />
<ProjectReference Include="..\Umbraco.Web.Common\Umbraco.Web.Common.csproj" />
@@ -1,254 +0,0 @@
using System.Security.Claims;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Api.Management.Security;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Net;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Web.Common.Security;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Management.Configuration;
/// <summary>
/// Used to configure <see cref="CookieAuthenticationOptions" /> for the back office authentication type
/// </summary>
public class ConfigureBackOfficeCookieOptions : IConfigureNamedOptions<CookieAuthenticationOptions>
{
private readonly IDataProtectionProvider _dataProtection;
private readonly GlobalSettings _globalSettings;
private readonly IIpResolver _ipResolver;
private readonly IRuntimeState _runtimeState;
private readonly SecuritySettings _securitySettings;
private readonly IUserService _userService;
private readonly TimeProvider _timeProvider;
/// <summary>
/// Initializes a new instance of the <see cref="ConfigureBackOfficeCookieOptions" /> class.
/// </summary>
/// <param name="securitySettings">The <see cref="SecuritySettings" /> options</param>
/// <param name="globalSettings">The <see cref="GlobalSettings" /> options</param>
/// <param name="runtimeState">The <see cref="IRuntimeState" /></param>
/// <param name="dataProtection">The <see cref="IDataProtectionProvider" /></param>
/// <param name="userService">The <see cref="IUserService" /></param>
/// <param name="ipResolver">The <see cref="IIpResolver" /></param>
/// <param name="timeProvider">The <see cref="TimeProvider" /></param>
public ConfigureBackOfficeCookieOptions(
IOptions<SecuritySettings> securitySettings,
IOptions<GlobalSettings> globalSettings,
IRuntimeState runtimeState,
IDataProtectionProvider dataProtection,
IUserService userService,
IIpResolver ipResolver,
TimeProvider timeProvider)
{
_securitySettings = securitySettings.Value;
_globalSettings = globalSettings.Value;
_runtimeState = runtimeState;
_dataProtection = dataProtection;
_userService = userService;
_ipResolver = ipResolver;
_timeProvider = timeProvider;
}
/// <inheritdoc />
public void Configure(string? name, CookieAuthenticationOptions options)
{
if (name != Constants.Security.BackOfficeAuthenticationType)
{
return;
}
Configure(options);
}
/// <inheritdoc />
public void Configure(CookieAuthenticationOptions options)
{
options.SlidingExpiration = false;
options.ExpireTimeSpan = _globalSettings.TimeOut;
options.Cookie.Domain = _securitySettings.AuthCookieDomain;
options.Cookie.Name = _securitySettings.AuthCookieName;
options.Cookie.HttpOnly = true;
options.Cookie.SecurePolicy =
_globalSettings.UseHttps ? CookieSecurePolicy.Always : CookieSecurePolicy.SameAsRequest;
options.Cookie.Path = "/";
// NOTE: matches route in BackOfficeLoginController
const string backOfficeLoginPath = "/umbraco/login";
options.LoginPath = backOfficeLoginPath;
options.LogoutPath = backOfficeLoginPath;
options.AccessDeniedPath = backOfficeLoginPath;
options.DataProtectionProvider = _dataProtection;
// NOTE: This is borrowed directly from aspnetcore source
// Note: the purpose for the data protector must remain fixed for interop to work.
IDataProtector dataProtector = options.DataProtectionProvider.CreateProtector(
"Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationMiddleware",
Constants.Security.BackOfficeAuthenticationType,
"v2");
var ticketDataFormat = new TicketDataFormat(dataProtector);
options.TicketDataFormat = new BackOfficeSecureDataFormat(_globalSettings.TimeOut, ticketDataFormat);
options.Events = new CookieAuthenticationEvents
{
// IMPORTANT! If you set any of OnRedirectToLogin, OnRedirectToAccessDenied, OnRedirectToLogout, OnRedirectToReturnUrl
// you need to be aware that this will bypass the default behavior of returning the correct status codes for ajax requests and
// not redirecting for non-ajax requests. This is because the default behavior is baked into this class here:
// https://github.com/dotnet/aspnetcore/blob/master/src/Security/Authentication/Cookies/src/CookieAuthenticationEvents.cs#L58
// It would be possible to re-use the default behavior if any of these need to be set but that must be taken into account else
// our back office requests will not function correctly. For now we don't need to set/configure any of these callbacks because
// the defaults work fine with our setup.
OnValidatePrincipal = async ctx =>
{
// We need to resolve the BackOfficeSecurityStampValidator per request as a requirement (even in aspnetcore they do this)
BackOfficeSecurityStampValidator securityStampValidator =
ctx.HttpContext.RequestServices.GetRequiredService<BackOfficeSecurityStampValidator>();
// Same goes for the signinmanager
IBackOfficeSignInManager signInManager =
ctx.HttpContext.RequestServices.GetRequiredService<IBackOfficeSignInManager>();
ClaimsIdentity? backOfficeIdentity = ctx.Principal?.GetUmbracoIdentity();
if (backOfficeIdentity == null)
{
ctx.RejectPrincipal();
await signInManager.SignOutAsync();
}
// ensure the thread culture is set
backOfficeIdentity?.EnsureCulture();
EnsureTicketRenewalIfKeepUserLoggedIn(ctx);
// add or update a claim to track when the cookie expires, we use this to track time remaining
backOfficeIdentity?.AddOrUpdateClaim(new Claim(
Constants.Security.TicketExpiresClaimType,
ctx.Properties.ExpiresUtc!.Value.ToString("o"),
ClaimValueTypes.DateTime,
Constants.Security.BackOfficeAuthenticationType,
Constants.Security.BackOfficeAuthenticationType,
backOfficeIdentity));
await securityStampValidator.ValidateAsync(ctx);
// We have to manually specify Issued and Expires,
// because the SecurityStampValidator refreshes the principal every 30 minutes,
// When the principal is refreshed the Issued is update to time of refresh, however, the Expires remains unchanged
// When we then try and renew, the difference of issued and expires effectively becomes the new ExpireTimeSpan
// meaning we effectively lose 30 minutes of our ExpireTimeSpan for EVERY principal refresh if we don't
// https://github.com/dotnet/aspnetcore/blob/main/src/Security/Authentication/Cookies/src/CookieAuthenticationHandler.cs#L115
ctx.Properties.IssuedUtc = _timeProvider.GetUtcNow();
ctx.Properties.ExpiresUtc = _timeProvider.GetUtcNow().Add(_globalSettings.TimeOut);
ctx.ShouldRenew = true;
},
OnSigningIn = ctx =>
{
// occurs when sign in is successful but before the ticket is written to the outbound cookie
ClaimsIdentity? backOfficeIdentity = ctx.Principal?.GetUmbracoIdentity();
if (backOfficeIdentity != null)
{
// generate a session id and assign it
// create a session token - if we are configured and not in an upgrade state then use the db, otherwise just generate one
Guid session = _runtimeState.Level == RuntimeLevel.Run
? _userService.CreateLoginSession(
backOfficeIdentity.GetId()!.Value,
_ipResolver.GetCurrentRequestIpAddress())
: Guid.NewGuid();
// add our session claim
backOfficeIdentity.AddClaim(new Claim(
Constants.Security.SessionIdClaimType,
session.ToString(),
ClaimValueTypes.String,
Constants.Security.BackOfficeAuthenticationType,
Constants.Security.BackOfficeAuthenticationType,
backOfficeIdentity));
// since it is a cookie-based authentication add that claim
backOfficeIdentity.AddClaim(new Claim(
ClaimTypes.CookiePath,
"/",
ClaimValueTypes.String,
Constants.Security.BackOfficeAuthenticationType,
Constants.Security.BackOfficeAuthenticationType,
backOfficeIdentity));
}
return Task.CompletedTask;
},
OnSignedIn = ctx =>
{
// occurs when sign in is successful and after the ticket is written to the outbound cookie
// When we are signed in with the cookie, assign the principal to the current HttpContext
ctx.HttpContext.SetPrincipalForRequest(ctx.Principal);
return Task.CompletedTask;
},
OnSigningOut = ctx =>
{
// Clear the user's session on sign out
if (ctx.HttpContext?.User?.Identity != null)
{
var claimsIdentity = ctx.HttpContext.User.Identity as ClaimsIdentity;
var sessionId = claimsIdentity?.FindFirstValue(Constants.Security.SessionIdClaimType);
if (sessionId.IsNullOrWhiteSpace() == false && Guid.TryParse(sessionId, out Guid guidSession))
{
_userService.ClearLoginSession(guidSession);
}
}
// Remove all of our cookies
var cookies = new[]
{
_securitySettings.AuthCookieName,
Constants.Web.PreviewCookieName, Constants.Security.BackOfficeExternalCookieName,
Constants.Web.CsrfValidationCookieName
};
foreach (var cookie in cookies)
{
ctx.Options.CookieManager.DeleteCookie(ctx.HttpContext!, cookie, new CookieOptions { Path = "/" });
}
return Task.CompletedTask;
}
};
}
/// <summary>
/// Ensures the ticket is renewed if the <see cref="SecuritySettings.KeepUserLoggedIn" /> is set to true
/// and the current request is for the get user seconds endpoint
/// </summary>
/// <param name="context">The <see cref="CookieValidatePrincipalContext" /></param>
private void EnsureTicketRenewalIfKeepUserLoggedIn(CookieValidatePrincipalContext context)
{
if (!_securitySettings.KeepUserLoggedIn)
{
return;
}
DateTimeOffset currentUtc = _timeProvider.GetUtcNow();
DateTimeOffset? issuedUtc = context.Properties.IssuedUtc;
DateTimeOffset? expiresUtc = context.Properties.ExpiresUtc;
if (expiresUtc.HasValue && issuedUtc.HasValue)
{
TimeSpan timeElapsed = currentUtc.Subtract(issuedUtc.Value);
TimeSpan timeRemaining = expiresUtc.Value.Subtract(currentUtc);
// if it's time to renew, then do it
if (timeRemaining < timeElapsed)
{
context.ShouldRenew = true;
}
}
}
}
@@ -1,28 +0,0 @@
using Microsoft.Extensions.Options;
using Umbraco.Cms.Api.Management.Security;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Web.Common.Security;
namespace Umbraco.Cms.Api.Management.Configuration;
/// <summary>
/// Configures the back office security stamp options.
/// </summary>
public class ConfigureBackOfficeSecurityStampValidatorOptions : IConfigureOptions<BackOfficeSecurityStampValidatorOptions>
{
private readonly SecuritySettings _securitySettings;
private readonly TimeProvider _timeProvider;
public ConfigureBackOfficeSecurityStampValidatorOptions(IOptions<SecuritySettings> securitySettings, TimeProvider timeProvider)
{
_timeProvider = timeProvider;
_securitySettings = securitySettings.Value;
}
/// <inheritdoc />
public void Configure(BackOfficeSecurityStampValidatorOptions options)
{
options.TimeProvider = _timeProvider;
ConfigureSecurityStampOptions.ConfigureOptions(options, _securitySettings);
}
}
@@ -1,66 +0,0 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
using Umbraco.Cms.Api.Common.OpenApi;
using Umbraco.Cms.Api.Common.Serialization;
using Umbraco.Cms.Api.Management.DependencyInjection;
using Umbraco.Cms.Api.Management.OpenApi;
namespace Umbraco.Cms.Api.Management.Configuration;
public class ConfigureUmbracoManagementApiSwaggerGenOptions : IConfigureOptions<SwaggerGenOptions>
{
private IUmbracoJsonTypeInfoResolver _umbracoJsonTypeInfoResolver;
public ConfigureUmbracoManagementApiSwaggerGenOptions(IUmbracoJsonTypeInfoResolver umbracoJsonTypeInfoResolver)
{
_umbracoJsonTypeInfoResolver = umbracoJsonTypeInfoResolver;
}
public void Configure(SwaggerGenOptions swaggerGenOptions)
{
swaggerGenOptions.SwaggerDoc(
ManagementApiConfiguration.ApiName,
new OpenApiInfo
{
Title = ManagementApiConfiguration.ApiTitle,
Version = "Latest",
Description = "This shows all APIs available in this version of Umbraco - including all the legacy apis that are available for backward compatibility",
});
swaggerGenOptions.OperationFilter<ResponseHeaderOperationFilter>();
swaggerGenOptions.SelectSubTypesUsing(_umbracoJsonTypeInfoResolver.FindSubTypes);
swaggerGenOptions.UseOneOfForPolymorphism();
// Ensure all types that implements the IOpenApiDiscriminator have a $type property in the OpenApi schema with the default value (The class name) that is expected by the server
swaggerGenOptions.SelectDiscriminatorNameUsing(type => _umbracoJsonTypeInfoResolver.GetTypeDiscriminatorValue(type) is not null ? "$type" : null);
swaggerGenOptions.SelectDiscriminatorValueUsing(_umbracoJsonTypeInfoResolver.GetTypeDiscriminatorValue);
swaggerGenOptions.AddSecurityDefinition(
ManagementApiConfiguration.ApiSecurityName,
new OpenApiSecurityScheme
{
In = ParameterLocation.Header,
Name = "Umbraco",
Type = SecuritySchemeType.OAuth2,
Description = "Umbraco Authentication",
Flows = new OpenApiOAuthFlows
{
AuthorizationCode = new OpenApiOAuthFlow
{
AuthorizationUrl =
new Uri(Common.Security.Paths.BackOfficeApi.AuthorizationEndpoint, UriKind.Relative),
TokenUrl = new Uri(Common.Security.Paths.BackOfficeApi.TokenEndpoint, UriKind.Relative)
}
}
});
// Sets Security requirement on backoffice apis
swaggerGenOptions.OperationFilter<BackOfficeSecurityRequirementsOperationFilter>();
swaggerGenOptions.OperationFilter<NotificationHeaderFilter>();
swaggerGenOptions.SchemaFilter<RequireNonNullablePropertiesSchemaFilter>();
}
}
@@ -1,69 +0,0 @@
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Hosting;
namespace Umbraco.Cms.Api.Management;
[BindProperties]
public class BackOfficeLoginModel
{
/// <summary>
/// Gets or sets the value of the "ReturnUrl" query parameter or defaults to the configured Umbraco directory.
/// </summary>
[FromQuery(Name = "ReturnUrl")]
public string? ReturnUrl { get; set; }
/// <summary>
/// The configured Umbraco directory.
/// </summary>
public string? UmbracoUrl { get; set; }
public bool UserIsAlreadyLoggedIn { get; set; }
}
[ApiExplorerSettings(IgnoreApi=true)]
[Route(LoginPath)]
public class BackOfficeLoginController : Controller
{
public const string LoginPath = "/umbraco/login";
private readonly IHostingEnvironment _hostingEnvironment;
private readonly GlobalSettings _globalSettings;
public BackOfficeLoginController(
IOptionsSnapshot<GlobalSettings> globalSettings,
IHostingEnvironment hostingEnvironment)
{
_hostingEnvironment = hostingEnvironment;
_globalSettings = globalSettings.Value ?? throw new ArgumentNullException(nameof(globalSettings));
}
// GET
public async Task<IActionResult> Index(CancellationToken cancellationToken, BackOfficeLoginModel model)
{
AuthenticateResult cookieAuthResult = await HttpContext.AuthenticateAsync(Constants.Security.BackOfficeAuthenticationType);
if (cookieAuthResult.Succeeded)
{
model.UserIsAlreadyLoggedIn = true;
}
if (string.IsNullOrEmpty(model.UmbracoUrl))
{
model.UmbracoUrl = _hostingEnvironment.ToAbsolute(_globalSettings.UmbracoPath);
}
if (string.IsNullOrEmpty(model.ReturnUrl))
{
model.ReturnUrl = model.UmbracoUrl;
}
if ( Uri.TryCreate(model.ReturnUrl, UriKind.Relative, out _) is false) // Needs to test for relative and not absolute, as /whatever/ is an absolute path on linux
{
return BadRequest("ReturnUrl must be a relative path.");
}
return View("/umbraco/UmbracoLogin/Index.cshtml", model);
}
}
@@ -1,95 +0,0 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
using Umbraco.Cms.Api.Management.ViewModels.Content;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.ContentEditing;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Management.Controllers.Content;
public abstract class ContentCollectionControllerBase<TContent, TCollectionResponseModel, TValueResponseModelBase, TVariantResponseModel> : ManagementApiControllerBase
where TContent : class, IContentBase
where TCollectionResponseModel : ContentResponseModelBase<TValueResponseModelBase, TVariantResponseModel>
where TValueResponseModelBase : ValueModelBase
where TVariantResponseModel : VariantResponseModelBase
{
private readonly IUmbracoMapper _mapper;
protected ContentCollectionControllerBase(IUmbracoMapper mapper) => _mapper = mapper;
protected IActionResult CollectionResult(ListViewPagedModel<TContent> result)
{
PagedModel<TContent> collectionItemsResult = result.Items;
ListViewConfiguration collectionConfiguration = result.ListViewConfiguration;
var collectionPropertyAliases = collectionConfiguration
.IncludeProperties
.Select(p => p.Alias)
.WhereNotNull()
.ToArray();
List<TCollectionResponseModel> collectionResponseModels =
_mapper.MapEnumerable<TContent, TCollectionResponseModel>(collectionItemsResult.Items, context =>
{
context.SetIncludedProperties(collectionPropertyAliases);
});
var pageViewModel = new PagedViewModel<TCollectionResponseModel>
{
Items = collectionResponseModels,
Total = collectionItemsResult.Total,
};
return Ok(pageViewModel);
}
protected IActionResult ContentCollectionOperationStatusResult(ContentCollectionOperationStatus status, string type) =>
OperationStatusResult(status, problemDetailsBuilder => status switch
{
ContentCollectionOperationStatus.CollectionNotFound => new NotFoundObjectResult(problemDetailsBuilder
.WithTitle("Collection data type could not be found")
.WithDetail($"No collection data type was found for the corresponding {type} type. Ensure that the default and/or a custom collection data types exists")
.Build()),
ContentCollectionOperationStatus.ContentNotCollection => new BadRequestObjectResult(problemDetailsBuilder
.WithTitle($"The {type} item is not configured as a collection")
.WithDetail($"The specified {type} is not configured as a collection")
.Build()),
ContentCollectionOperationStatus.ContentNotFound => new NotFoundObjectResult(problemDetailsBuilder
.WithTitle($"The specified {type} could not be found")
.Build()),
ContentCollectionOperationStatus.ContentTypeNotFound => new BadRequestObjectResult(problemDetailsBuilder
.WithTitle($"The related {type} type could not be found")
.Build()),
ContentCollectionOperationStatus.DataTypeNotCollection => new BadRequestObjectResult(problemDetailsBuilder
.WithTitle("Data type id does not belong to a collection")
.WithDetail("The specified data type does not represent a collection")
.Build()),
ContentCollectionOperationStatus.DataTypeNotContentProperty => new BadRequestObjectResult(problemDetailsBuilder
.WithTitle($"Data type id is not a {type} property")
.WithDetail($"The specified data type is not part of the {type} properties")
.Build()),
ContentCollectionOperationStatus.DataTypeNotFound => new NotFoundObjectResult(problemDetailsBuilder
.WithTitle("The specified collection data type could not be found")
.Build()),
ContentCollectionOperationStatus.DataTypeWithoutContentType => new BadRequestObjectResult(problemDetailsBuilder
.WithTitle($"Missing {type} when specifying a collection data type")
.WithDetail($"The specified collection data type needs to be used in conjunction with a {type} item.")
.Build()),
ContentCollectionOperationStatus.MissingPropertiesInCollectionConfiguration => new BadRequestObjectResult(problemDetailsBuilder
.WithTitle("Missing properties in collection configuration")
.WithDetail("No properties are configured to display in the collection configuration")
.Build()),
ContentCollectionOperationStatus.OrderByNotPartOfCollectionConfiguration => new BadRequestObjectResult(problemDetailsBuilder
.WithTitle("Order by value is not a property on the configured collection")
.WithDetail("The specified orderBy property is not part of the collection configuration")
.Build()),
_ => new ObjectResult("Unknown content collection operation status")
{
StatusCode = StatusCodes.Status500InternalServerError,
},
});
}
@@ -1,138 +0,0 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.Content;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models.ContentEditing;
using Umbraco.Cms.Core.Models.ContentEditing.Validation;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Management.Controllers.Content;
public abstract class ContentControllerBase : ManagementApiControllerBase
{
protected IActionResult ContentEditingOperationStatusResult(ContentEditingOperationStatus status)
=> OperationStatusResult(status, problemDetailsBuilder => status switch
{
ContentEditingOperationStatus.CancelledByNotification => BadRequest(problemDetailsBuilder
.WithTitle("Cancelled by notification")
.WithDetail("A notification handler prevented the content operation.")
.Build()),
ContentEditingOperationStatus.ContentTypeNotFound => NotFound(problemDetailsBuilder
.WithTitle("The requested content type could not be found")
.Build()),
ContentEditingOperationStatus.ContentTypeCultureVarianceMismatch => BadRequest(problemDetailsBuilder
.WithTitle("Content type culture variance mismatch")
.WithDetail("The content type variance did not match that of the passed content data.")
.Build()),
ContentEditingOperationStatus.NotFound => NotFound(problemDetailsBuilder
.WithTitle("The content could not be found")
.Build()),
ContentEditingOperationStatus.ParentNotFound => NotFound(problemDetailsBuilder
.WithTitle("The targeted content parent could not be found")
.Build()),
ContentEditingOperationStatus.ParentInvalid => BadRequest(problemDetailsBuilder
.WithTitle("Invalid parent")
.WithDetail("The targeted parent was not valid for the operation.")
.Build()),
ContentEditingOperationStatus.NotAllowed => BadRequest(problemDetailsBuilder
.WithTitle("Operation not permitted")
.WithDetail(
"The attempted operation was not permitted, likely due to a permission/configuration mismatch with the operation.")
.Build()),
ContentEditingOperationStatus.TemplateNotFound => NotFound(problemDetailsBuilder
.WithTitle("The template could not be found")
.Build()),
ContentEditingOperationStatus.TemplateNotAllowed => BadRequest(problemDetailsBuilder
.WithTitle("Template not allowed")
.WithDetail("The selected template was not allowed for the operation.")
.Build()),
ContentEditingOperationStatus.PropertyTypeNotFound => NotFound(problemDetailsBuilder
.WithTitle("One or more property types could not be found")
.Build()),
ContentEditingOperationStatus.InTrash => BadRequest(problemDetailsBuilder
.WithTitle("Content is in the recycle bin")
.WithDetail("Could not perform the operation because the targeted content was in the recycle bin.")
.Build()),
ContentEditingOperationStatus.NotInTrash => BadRequest(problemDetailsBuilder
.WithTitle("Content is not in the recycle bin")
.WithDetail("The attempted operation requires the targeted content to be in the recycle bin.")
.Build()),
ContentEditingOperationStatus.SortingInvalid => BadRequest(problemDetailsBuilder
.WithTitle("Invalid sorting options")
.WithDetail("The supplied sorting operations were invalid. Additional details can be found in the log.")
.Build()),
ContentEditingOperationStatus.InvalidCulture => BadRequest(problemDetailsBuilder
.WithTitle("Invalid culture")
.WithDetail("One or more of the supplied culture codes did not match the configured languages.")
.Build()),
ContentEditingOperationStatus.DuplicateKey => BadRequest(problemDetailsBuilder
.WithTitle("Invalid Id")
.WithDetail("The supplied id is already in use.")
.Build()),
ContentEditingOperationStatus.DuplicateName => BadRequest(problemDetailsBuilder
.WithTitle("Duplicate name")
.WithDetail("The supplied name is already in use for the same content type.")
.Build()),
ContentEditingOperationStatus.Unknown => StatusCode(
StatusCodes.Status500InternalServerError,
problemDetailsBuilder
.WithTitle("Unknown error. Please see the log for more details.")
.Build()),
_ => StatusCode(StatusCodes.Status500InternalServerError, problemDetailsBuilder
.WithTitle("Unknown content operation status.")
.Build()),
});
protected IActionResult ContentEditingOperationStatusResult<TContentModelBase, TValueModel, TVariantModel>(
ContentEditingOperationStatus status,
TContentModelBase requestModel,
ContentValidationResult validationResult)
where TContentModelBase : ContentModelBase<TValueModel, TVariantModel>
where TValueModel : ValueModelBase
where TVariantModel : VariantModelBase
{
if (status is not ContentEditingOperationStatus.PropertyValidationError)
{
return ContentEditingOperationStatusResult(status);
}
var errors = new SortedDictionary<string, string[]>();
var missingPropertyModels = new List<PropertyValidationResponseModel>();
foreach (PropertyValidationError validationError in validationResult.ValidationErrors)
{
TValueModel? requestValue = requestModel.Values.FirstOrDefault(value =>
value.Alias == validationError.Alias
&& value.Culture == validationError.Culture
&& value.Segment == validationError.Segment);
if (requestValue is null)
{
missingPropertyModels.Add(MapMissingProperty(validationError));
continue;
}
var index = requestModel.Values.IndexOf(requestValue);
var key = $"$.{nameof(ContentModelBase<TValueModel, TVariantModel>.Values).ToFirstLowerInvariant()}[{index}].{nameof(ValueModelBase.Value).ToFirstLowerInvariant()}{validationError.JsonPath}";
errors.Add(key, validationError.ErrorMessages);
}
return OperationStatusResult(status, problemDetailsBuilder
=> BadRequest(problemDetailsBuilder
.WithTitle("Validation failed")
.WithDetail("One or more properties did not pass validation")
.WithRequestModelErrors(errors)
.WithExtension("missingValues", missingPropertyModels.ToArray())
.Build()));
}
private PropertyValidationResponseModel MapMissingProperty(PropertyValidationError source) =>
new()
{
Alias = source.Alias,
Segment = source.Segment,
Culture = source.Culture,
Messages = source.ErrorMessages,
};
}
@@ -1,43 +0,0 @@
using System.Globalization;
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Api.Management.ViewModels.Culture;
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Api.Management.Controllers.Culture;
[ApiVersion("1.0")]
public class AllCultureController : CultureControllerBase
{
private readonly IUmbracoMapper _umbracoMapper;
private readonly ICultureService _cultureService;
public AllCultureController(IUmbracoMapper umbracoMapper, ICultureService cultureService)
{
_umbracoMapper = umbracoMapper;
_cultureService = cultureService;
}
/// <summary>
/// Returns all cultures available for creating languages.
/// </summary>
/// <returns></returns>
[HttpGet]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedViewModel<CultureReponseModel>), StatusCodes.Status200OK)]
public async Task<PagedViewModel<CultureReponseModel>> GetAll(CancellationToken cancellationToken, int skip = 0, int take = 100)
{
CultureInfo[] all = _cultureService.GetValidCultureInfos();
var viewModel = new PagedViewModel<CultureReponseModel>
{
Items = _umbracoMapper.MapEnumerable<CultureInfo, CultureReponseModel>(all.Skip(skip).Take(take)),
Total = all.Length
};
return await Task.FromResult(viewModel);
}
}
@@ -1,10 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Routing;
namespace Umbraco.Cms.Api.Management.Controllers.Culture;
[VersionedApiBackOfficeRoute("culture")]
[ApiExplorerSettings(GroupName = "Culture")]
public abstract class CultureControllerBase : ManagementApiControllerBase
{
}
@@ -1,37 +0,0 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.DataType;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Api.Management.Controllers.DataType;
[ApiVersion("1.0")]
public class ByKeyDataTypeController : DataTypeControllerBase
{
private readonly IDataTypeService _dataTypeService;
private readonly IUmbracoMapper _umbracoMapper;
public ByKeyDataTypeController(IDataTypeService dataTypeService, IUmbracoMapper umbracoMapper)
{
_dataTypeService = dataTypeService;
_umbracoMapper = umbracoMapper;
}
[HttpGet("{id:guid}")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(DataTypeResponseModel), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> ByKey(CancellationToken cancellationToken, Guid id)
{
IDataType? dataType = await _dataTypeService.GetAsync(id);
if (dataType == null)
{
return DataTypeNotFound();
}
return Ok(_umbracoMapper.Map<DataTypeResponseModel>(dataType));
}
}
@@ -1,31 +0,0 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Api.Management.ViewModels.DataType;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
namespace Umbraco.Cms.Api.Management.Controllers.DataType;
[ApiVersion("1.0")]
public class ConfigurationDataTypeController : DataTypeControllerBase
{
private readonly DataTypesSettings _dataTypesSettings;
public ConfigurationDataTypeController(IOptionsSnapshot<DataTypesSettings> dataTypesSettings) => _dataTypesSettings = dataTypesSettings.Value;
[HttpGet("configuration")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(DatatypeConfigurationResponseModel), StatusCodes.Status200OK)]
public Task<IActionResult> Configuration(CancellationToken cancellationToken)
{
var responseModel = new DatatypeConfigurationResponseModel
{
CanBeChanged = _dataTypesSettings.CanBeChanged,
DocumentListViewId = Constants.DataTypes.Guids.ListViewContentGuid,
MediaListViewId = Constants.DataTypes.Guids.ListViewMediaGuid,
};
return Task.FromResult<IActionResult>(Ok(responseModel));
}
}
@@ -1,46 +0,0 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.DataType;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Cms.Web.Common.Authorization;
namespace Umbraco.Cms.Api.Management.Controllers.DataType;
[ApiVersion("1.0")]
[Authorize(Policy = AuthorizationPolicies.TreeAccessDataTypes)]
public class CopyDataTypeController : DataTypeControllerBase
{
private readonly IDataTypeService _dataTypeService;
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
public CopyDataTypeController(IDataTypeService dataTypeService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
{
_dataTypeService = dataTypeService;
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
}
[HttpPost("{id:guid}/copy")]
[MapToApiVersion("1.0")]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> Copy(CancellationToken cancellationToken, Guid id, CopyDataTypeRequestModel copyDataTypeRequestModel)
{
IDataType? source = await _dataTypeService.GetAsync(id);
if (source is null)
{
return DataTypeNotFound();
}
Attempt<IDataType, DataTypeOperationStatus> result = await _dataTypeService.CopyAsync(source, copyDataTypeRequestModel.Target?.Id, CurrentUserKey(_backOfficeSecurityAccessor));
return result.Success
? CreatedAtId<ByKeyDataTypeController>(controller => nameof(controller.ByKey), result.Result.Key)
: DataTypeOperationStatusResult(result.Status);
}
}
@@ -1,50 +0,0 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.ViewModels.DataType;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Cms.Web.Common.Authorization;
namespace Umbraco.Cms.Api.Management.Controllers.DataType;
[ApiVersion("1.0")]
[Authorize(Policy = AuthorizationPolicies.TreeAccessDataTypes)]
public class CreateDataTypeController : DataTypeControllerBase
{
private readonly IDataTypeService _dataTypeService;
private readonly IDataTypePresentationFactory _dataTypePresentationFactory;
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
public CreateDataTypeController(IDataTypeService dataTypeService, IDataTypePresentationFactory dataTypePresentationFactory, IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
{
_dataTypeService = dataTypeService;
_dataTypePresentationFactory = dataTypePresentationFactory;
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
}
[HttpPost]
[MapToApiVersion("1.0")]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> Create(CancellationToken cancellationToken, CreateDataTypeRequestModel createDataTypeRequestModel)
{
var attempt = await _dataTypePresentationFactory.CreateAsync(createDataTypeRequestModel);
if (!attempt.Success)
{
return DataTypeOperationStatusResult(attempt.Status);
}
Attempt<IDataType, DataTypeOperationStatus> result = await _dataTypeService.CreateAsync(attempt.Result, CurrentUserKey(_backOfficeSecurityAccessor));
return result.Success
? CreatedAtId<ByKeyDataTypeController>(controller => nameof(controller.ByKey), result.Result.Key)
: DataTypeOperationStatusResult(result.Status);
}
}
@@ -1,62 +0,0 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Common.Builders;
using Umbraco.Cms.Api.Management.Routing;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Cms.Web.Common.Authorization;
namespace Umbraco.Cms.Api.Management.Controllers.DataType;
[VersionedApiBackOfficeRoute(Constants.UdiEntityType.DataType)]
[ApiExplorerSettings(GroupName = "Data Type")]
[Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentOrMediaOrContentTypes)]
public abstract class DataTypeControllerBase : ManagementApiControllerBase
{
protected IActionResult DataTypeOperationStatusResult(DataTypeOperationStatus status) =>
OperationStatusResult(status, problemDetailsBuilder => status switch
{
DataTypeOperationStatus.InvalidConfiguration => BadRequest(problemDetailsBuilder
.WithTitle("Invalid data type configuration")
.WithDetail("The supplied data type configuration was not valid. Please see the log for more details.")
.Build()),
DataTypeOperationStatus.NotFound => DataTypeNotFound(problemDetailsBuilder),
DataTypeOperationStatus.InvalidName => BadRequest(problemDetailsBuilder
.WithTitle("Invalid data type name")
.WithDetail("The data type name must be non-empty and no longer than 255 characters.")
.Build()),
DataTypeOperationStatus.ParentNotContainer => BadRequest(problemDetailsBuilder
.WithTitle("parent id is not a container")
.WithDetail("The parent id does not represent a container.")
.Build()),
DataTypeOperationStatus.DuplicateKey => BadRequest(problemDetailsBuilder
.WithTitle("The id is already used")
.WithDetail("The data type id must be unique.")
.Build()),
DataTypeOperationStatus.CancelledByNotification => BadRequest(problemDetailsBuilder
.WithTitle("Cancelled by notification")
.WithDetail("A notification handler prevented the data type operation.")
.Build()),
DataTypeOperationStatus.PropertyEditorNotFound => NotFound(problemDetailsBuilder
.WithTitle("The targeted property editor was not found.")
.Build()),
DataTypeOperationStatus.ParentNotFound => NotFound(problemDetailsBuilder
.WithTitle("The targeted parent for the data type operation was not found.")
.Build()),
DataTypeOperationStatus.NonDeletable => BadRequest(problemDetailsBuilder
.WithTitle("The data type is non-deletable")
.WithDetail("The specified data type is required by the system and cannot be deleted.")
.Build()),
_ => StatusCode(StatusCodes.Status500InternalServerError, problemDetailsBuilder
.WithTitle("Unknown data type operation status.")
.Build()),
});
protected IActionResult DataTypeNotFound() => OperationStatusResult(DataTypeOperationStatus.NotFound, DataTypeNotFound);
private IActionResult DataTypeNotFound(ProblemDetailsBuilder problemDetailsBuilder)
=> NotFound(problemDetailsBuilder
.WithTitle("The data type could not be found")
.Build());
}
@@ -1,40 +0,0 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Cms.Web.Common.Authorization;
namespace Umbraco.Cms.Api.Management.Controllers.DataType;
[ApiVersion("1.0")]
[Authorize(Policy = AuthorizationPolicies.TreeAccessDataTypes)]
public class DeleteDataTypeController : DataTypeControllerBase
{
private readonly IDataTypeService _dataTypeService;
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
public DeleteDataTypeController(IDataTypeService dataTypeService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
{
_dataTypeService = dataTypeService;
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
}
[HttpDelete("{id:guid}")]
[MapToApiVersion("1.0")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> Delete(CancellationToken cancellationToken, Guid id)
{
Attempt<IDataType?, DataTypeOperationStatus> result = await _dataTypeService.DeleteAsync(id, CurrentUserKey(_backOfficeSecurityAccessor));
return result.Success
? Ok()
: DataTypeOperationStatusResult(result.Status);
}
}
@@ -1,15 +0,0 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Routing;
using Umbraco.Cms.Core;
using Umbraco.Cms.Web.Common.Authorization;
namespace Umbraco.Cms.Api.Management.Controllers.DataType.Filter;
[ApiExplorerSettings(GroupName = "Data Type")]
[VersionedApiBackOfficeRoute($"{Constants.Web.RoutePath.Filter}/{Constants.UdiEntityType.DataType}")]
// This auth policy might become problematic, as when getting DataTypes on Media types, you don't need access to the document tree.
[Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentsOrDocumentTypes)]
public abstract class DataTypeFilterControllerBase : ManagementApiControllerBase
{
}
@@ -1,46 +0,0 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
using Umbraco.Cms.Api.Management.ViewModels.DataType.Item;
using Umbraco.Cms.Api.Management.ViewModels.Language;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Api.Management.Controllers.DataType.Filter;
[ApiVersion("1.0")]
public class FilterDataTypeFilterController : DataTypeFilterControllerBase
{
private readonly IDataTypeService _dataTypeService;
private readonly IUmbracoMapper _mapper;
public FilterDataTypeFilterController(IDataTypeService dataTypeService, IUmbracoMapper mapper)
{
_dataTypeService = dataTypeService;
_mapper = mapper;
}
[HttpGet]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedViewModel<DataTypeItemResponseModel>), StatusCodes.Status200OK)]
public async Task<IActionResult> Filter(
CancellationToken cancellationToken,
int skip = 0,
int take = 100,
string name = "",
string? editorUiAlias = null,
string? editorAlias = null)
{
PagedModel<IDataType> dataTypes = await _dataTypeService.FilterAsync(name, editorUiAlias, editorAlias, skip, take);
List<DataTypeItemResponseModel> responseModels = _mapper.MapEnumerable<IDataType, DataTypeItemResponseModel>(dataTypes.Items);
var viewModel = new PagedViewModel<DataTypeItemResponseModel>
{
Total = dataTypes.Total,
Items = responseModels,
};
return Ok(viewModel);
}
}
@@ -1,25 +0,0 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.Folder;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Api.Management.Controllers.DataType.Folder;
[ApiVersion("1.0")]
public class ByKeyDataTypeFolderController : DataTypeFolderControllerBase
{
public ByKeyDataTypeFolderController(
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
IDataTypeContainerService dataTypeContainerService)
: base(backOfficeSecurityAccessor, dataTypeContainerService)
{
}
[HttpGet("{id:guid}")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(FolderResponseModel), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> ByKey(CancellationToken cancellationToken, Guid id) => await GetFolderAsync(id);
}
@@ -1,29 +0,0 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.Folder;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Api.Management.Controllers.DataType.Folder;
[ApiVersion("1.0")]
public class CreateDataTypeFolderController : DataTypeFolderControllerBase
{
public CreateDataTypeFolderController(
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
IDataTypeContainerService dataTypeContainerService)
: base(backOfficeSecurityAccessor, dataTypeContainerService)
{
}
[HttpPost]
[MapToApiVersion("1.0")]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> Create(CancellationToken cancellationToken, CreateFolderRequestModel createFolderRequestModel)
=> await CreateFolderAsync<ByKeyDataTypeFolderController>(
createFolderRequestModel,
controller => nameof(controller.ByKey));
}
@@ -1,23 +0,0 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Routing;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Web.Common.Authorization;
namespace Umbraco.Cms.Api.Management.Controllers.DataType.Folder;
[VersionedApiBackOfficeRoute($"{Constants.UdiEntityType.DataType}/folder")]
[ApiExplorerSettings(GroupName = "Data Type")]
[Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)]
public abstract class DataTypeFolderControllerBase : FolderManagementControllerBase<IDataType>
{
protected DataTypeFolderControllerBase(
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
IDataTypeContainerService dataTypeContainerService)
: base(backOfficeSecurityAccessor, dataTypeContainerService)
{
}
}
@@ -1,25 +0,0 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Api.Management.Controllers.DataType.Folder;
[ApiVersion("1.0")]
public class DeleteDataTypeFolderController : DataTypeFolderControllerBase
{
public DeleteDataTypeFolderController(
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
IDataTypeContainerService dataTypeContainerService)
: base(backOfficeSecurityAccessor, dataTypeContainerService)
{
}
[HttpDelete("{id:guid}")]
[MapToApiVersion("1.0")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> Delete(CancellationToken cancellationToken, Guid id) => await DeleteFolderAsync(id);
}
@@ -1,30 +0,0 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.Folder;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Api.Management.Controllers.DataType.Folder;
[ApiVersion("1.0")]
public class UpdateDataTypeFolderController : DataTypeFolderControllerBase
{
public UpdateDataTypeFolderController(
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
IDataTypeContainerService dataTypeContainerService)
: base(backOfficeSecurityAccessor, dataTypeContainerService)
{
}
[HttpPut("{id:guid}")]
[MapToApiVersion("1.0")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> Update(
CancellationToken cancellationToken,
Guid id,
UpdateFolderResponseModel updateFolderResponseModel)
=> await UpdateFolderAsync(id, updateFolderResponseModel);
}
@@ -1,32 +0,0 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
namespace Umbraco.Cms.Api.Management.Controllers.DataType;
[ApiVersion("1.0")]
public class IsUsedDataTypeController : DataTypeControllerBase
{
private readonly IDataTypeUsageService _dataTypeUsageService;
public IsUsedDataTypeController(IDataTypeUsageService dataTypeUsageService)
{
_dataTypeUsageService = dataTypeUsageService;
}
[HttpGet("{id:guid}/is-used")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> IsUsed(CancellationToken cancellationToken, Guid id)
{
Attempt<bool, DataTypeOperationStatus> result = await _dataTypeUsageService.HasSavedValuesAsync(id);
return result.Success
? Ok(result.Result)
: DataTypeOperationStatusResult(result.Status);
}
}
@@ -1,11 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Routing;
using Umbraco.Cms.Core;
namespace Umbraco.Cms.Api.Management.Controllers.DataType.Item;
[VersionedApiBackOfficeRoute($"{Constants.Web.RoutePath.Item}/{Constants.UdiEntityType.DataType}")]
[ApiExplorerSettings(GroupName = "Data Type")]
public class DatatypeItemControllerBase : ManagementApiControllerBase
{
}
@@ -1,48 +0,0 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.DataType.Item;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Api.Management.Controllers.DataType.Item;
[ApiVersion("1.0")]
public class ItemDatatypeItemController : DatatypeItemControllerBase
{
private readonly IDataTypeService _dataTypeService;
private readonly IUmbracoMapper _mapper;
public ItemDatatypeItemController(IDataTypeService dataTypeService, IUmbracoMapper mapper)
{
_dataTypeService = dataTypeService;
_mapper = mapper;
}
[HttpGet]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(IEnumerable<DataTypeItemResponseModel>), StatusCodes.Status200OK)]
public async Task<IActionResult> Item(
CancellationToken cancellationToken,
[FromQuery(Name = "id")] HashSet<Guid> ids)
{
if (ids.Count is 0)
{
return Ok(Enumerable.Empty<DataTypeItemResponseModel>());
}
var dataTypes = new List<IDataType>();
foreach (Guid id in ids)
{
IDataType? dataType = await _dataTypeService.GetAsync(id);
if (dataType is not null)
{
dataTypes.Add(dataType);
}
}
List<DataTypeItemResponseModel> responseModels = _mapper.MapEnumerable<IDataType, DataTypeItemResponseModel>(dataTypes);
return Ok(responseModels);
}
}
@@ -1,46 +0,0 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.DataType.Item;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Entities;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Api.Management.Controllers.DataType.Item;
[ApiVersion("1.0")]
public class SearchDataTypeItemController : DatatypeItemControllerBase
{
private readonly IEntitySearchService _entitySearchService;
private readonly IDataTypeService _dataTypeService;
private readonly IUmbracoMapper _mapper;
public SearchDataTypeItemController(IEntitySearchService entitySearchService, IDataTypeService dataTypeService, IUmbracoMapper mapper)
{
_entitySearchService = entitySearchService;
_dataTypeService = dataTypeService;
_mapper = mapper;
}
[HttpGet("search")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedModel<DataTypeItemResponseModel>), StatusCodes.Status200OK)]
public async Task<IActionResult> Search(CancellationToken cancellationToken, string query, int skip = 0, int take = 100)
{
PagedModel<IEntitySlim> searchResult = _entitySearchService.Search(UmbracoObjectTypes.DataType, query, skip, take);
if (searchResult.Items.Any() is false)
{
return Ok(new PagedModel<DataTypeItemResponseModel> { Total = searchResult.Total });
}
IEnumerable<IDataType> dataTypes = await _dataTypeService.GetAllAsync(searchResult.Items.Select(item => item.Key).ToArray());
var result = new PagedModel<DataTypeItemResponseModel>
{
Items = _mapper.MapEnumerable<IDataType, DataTypeItemResponseModel>(dataTypes),
Total = searchResult.Total
};
return Ok(result);
}
}
@@ -1,46 +0,0 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.DataType;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Cms.Web.Common.Authorization;
namespace Umbraco.Cms.Api.Management.Controllers.DataType;
[ApiVersion("1.0")]
[Authorize(Policy = AuthorizationPolicies.TreeAccessDataTypes)]
public class MoveDataTypeController : DataTypeControllerBase
{
private readonly IDataTypeService _dataTypeService;
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
public MoveDataTypeController(IDataTypeService dataTypeService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
{
_dataTypeService = dataTypeService;
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
}
[HttpPut("{id:guid}/move")]
[MapToApiVersion("1.0")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> Move(CancellationToken cancellationToken, Guid id, MoveDataTypeRequestModel moveDataTypeRequestModel)
{
IDataType? source = await _dataTypeService.GetAsync(id);
if (source is null)
{
return DataTypeNotFound();
}
Attempt<IDataType, DataTypeOperationStatus> result = await _dataTypeService.MoveAsync(source, moveDataTypeRequestModel.Target?.Id, CurrentUserKey(_backOfficeSecurityAccessor));
return result.Success
? Ok()
: DataTypeOperationStatusResult(result.Status);
}
}
@@ -1,39 +0,0 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.ViewModels.DataType;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
namespace Umbraco.Cms.Api.Management.Controllers.DataType;
[ApiVersion("1.0")]
public class ReferencesDataTypeController : DataTypeControllerBase
{
private readonly IDataTypeService _dataTypeService;
private readonly IDataTypeReferencePresentationFactory _dataTypeReferencePresentationFactory;
public ReferencesDataTypeController(IDataTypeService dataTypeService, IDataTypeReferencePresentationFactory dataTypeReferencePresentationFactory)
{
_dataTypeService = dataTypeService;
_dataTypeReferencePresentationFactory = dataTypeReferencePresentationFactory;
}
[HttpGet("{id:guid}/references")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(DataTypeReferenceResponseModel[]), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> References(CancellationToken cancellationToken, Guid id)
{
Attempt<IReadOnlyDictionary<Udi, IEnumerable<string>>, DataTypeOperationStatus> result = await _dataTypeService.GetReferencesAsync(id);
if (result.Success == false)
{
return DataTypeOperationStatusResult(result.Status);
}
DataTypeReferenceResponseModel[] viewModels = _dataTypeReferencePresentationFactory.CreateDataTypeReferenceViewModels(result.Result).ToArray();
return Ok(viewModels);
}
}
@@ -1,22 +0,0 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Api.Management.Controllers.DataType.Tree;
[ApiVersion("1.0")]
public class AncestorsDataTypeTreeController : DataTypeTreeControllerBase
{
public AncestorsDataTypeTreeController(IEntityService entityService, IDataTypeService dataTypeService)
: base(entityService, dataTypeService)
{
}
[HttpGet("ancestors")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(IEnumerable<DataTypeTreeItemResponseModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<DataTypeTreeItemResponseModel>>> Ancestors(CancellationToken cancellationToken, Guid descendantId)
=> await GetAncestors(descendantId);
}
@@ -1,26 +0,0 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
namespace Umbraco.Cms.Api.Management.Controllers.DataType.Tree;
[ApiVersion("1.0")]
public class ChildrenDataTypeTreeController : DataTypeTreeControllerBase
{
public ChildrenDataTypeTreeController(IEntityService entityService, IDataTypeService dataTypeService)
: base(entityService, dataTypeService)
{
}
[HttpGet("children")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedViewModel<DataTypeTreeItemResponseModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedViewModel<DataTypeTreeItemResponseModel>>> Children(CancellationToken cancellationToken, Guid parentId, int skip = 0, int take = 100, bool foldersOnly = false)
{
RenderFoldersOnly(foldersOnly);
return await GetChildren(parentId, skip, take);
}
}
@@ -1,61 +0,0 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Controllers.Tree;
using Umbraco.Cms.Api.Management.Routing;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Entities;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Web.Common.Authorization;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Management.Controllers.DataType.Tree;
[VersionedApiBackOfficeRoute($"{Constants.Web.RoutePath.Tree}/{Constants.UdiEntityType.DataType}")]
[ApiExplorerSettings(GroupName = "Data Type")]
[Authorize(Policy = AuthorizationPolicies.TreeAccessDataTypes)]
public class DataTypeTreeControllerBase : FolderTreeControllerBase<DataTypeTreeItemResponseModel>
{
private readonly IDataTypeService _dataTypeService;
public DataTypeTreeControllerBase(IEntityService entityService, IDataTypeService dataTypeService)
: base(entityService) =>
_dataTypeService = dataTypeService;
protected override UmbracoObjectTypes ItemObjectType => UmbracoObjectTypes.DataType;
protected override UmbracoObjectTypes FolderObjectType => UmbracoObjectTypes.DataTypeContainer;
protected override Ordering ItemOrdering
{
get
{
var ordering = Ordering.By(nameof(Infrastructure.Persistence.Dtos.NodeDto.NodeObjectType), Direction.Descending); // We need to override to change direction
ordering.Next = Ordering.By(nameof(Infrastructure.Persistence.Dtos.NodeDto.Text));
return ordering;
}
}
protected override DataTypeTreeItemResponseModel[] MapTreeItemViewModels(Guid? parentId, IEntitySlim[] entities)
{
Dictionary<int, IDataType> dataTypes = entities.Any()
? _dataTypeService
.GetAllAsync(entities.Select(entity => entity.Key).ToArray()).GetAwaiter().GetResult()
.ToDictionary(contentType => contentType.Id)
: new Dictionary<int, IDataType>();
return entities.Select(entity =>
{
DataTypeTreeItemResponseModel responseModel = MapTreeItemViewModel(parentId, entity);
if (dataTypes.TryGetValue(entity.Id, out IDataType? dataType))
{
responseModel.EditorUiAlias = dataType.EditorUiAlias;
responseModel.IsDeletable = dataType.IsDeletableDataType();
}
return responseModel;
}).ToArray();
}
}
@@ -1,27 +0,0 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
using Umbraco.Cms.Api.Management.ViewModels.DataType.Item;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
namespace Umbraco.Cms.Api.Management.Controllers.DataType.Tree;
[ApiVersion("1.0")]
public class RootDataTypeTreeController : DataTypeTreeControllerBase
{
public RootDataTypeTreeController(IEntityService entityService, IDataTypeService dataTypeService)
: base(entityService, dataTypeService)
{
}
[HttpGet("root")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedViewModel<DataTypeTreeItemResponseModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedViewModel<DataTypeTreeItemResponseModel>>> Root(CancellationToken cancellationToken, int skip = 0, int take = 100, bool foldersOnly = false)
{
RenderFoldersOnly(foldersOnly);
return await GetRoot(skip, take);
}
}
@@ -1,55 +0,0 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.ViewModels.DataType;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Cms.Web.Common.Authorization;
namespace Umbraco.Cms.Api.Management.Controllers.DataType;
[ApiVersion("1.0")]
[Authorize(Policy = AuthorizationPolicies.TreeAccessDataTypes)]
public class UpdateDataTypeController : DataTypeControllerBase
{
private readonly IDataTypeService _dataTypeService;
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
private IDataTypePresentationFactory _dataTypePresentationFactory;
public UpdateDataTypeController(IDataTypeService dataTypeService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor, IDataTypePresentationFactory dataTypePresentationFactory)
{
_dataTypeService = dataTypeService;
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
_dataTypePresentationFactory = dataTypePresentationFactory;
}
[HttpPut("{id:guid}")]
[MapToApiVersion("1.0")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> Update(CancellationToken cancellationToken, Guid id, UpdateDataTypeRequestModel updateDataTypeViewModel)
{
IDataType? current = await _dataTypeService.GetAsync(id);
if (current == null)
{
return DataTypeNotFound();
}
Attempt<IDataType, DataTypeOperationStatus> attempt = await _dataTypePresentationFactory.CreateAsync(updateDataTypeViewModel, current);
if (!attempt.Success)
{
return DataTypeOperationStatusResult(attempt.Status);
}
Attempt<IDataType, DataTypeOperationStatus> result = await _dataTypeService.UpdateAsync(attempt.Result, CurrentUserKey(_backOfficeSecurityAccessor));
return result.Success
? Ok()
: DataTypeOperationStatusResult(result.Status);
}
}
@@ -1,43 +0,0 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Api.Management.ViewModels.Dictionary;
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
using Umbraco.Cms.Core;
namespace Umbraco.Cms.Api.Management.Controllers.Dictionary;
[ApiVersion("1.0")]
public class AllDictionaryController : DictionaryControllerBase
{
private readonly IDictionaryItemService _dictionaryItemService;
private readonly IUmbracoMapper _umbracoMapper;
public AllDictionaryController(IDictionaryItemService dictionaryItemService, IUmbracoMapper umbracoMapper)
{
_dictionaryItemService = dictionaryItemService;
_umbracoMapper = umbracoMapper;
}
[HttpGet]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedViewModel<DictionaryOverviewResponseModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedViewModel<DictionaryOverviewResponseModel>>> All(
CancellationToken cancellationToken,
string? filter = null,
int skip = 0,
int take = 100)
{
// unfortunately we can't paginate here...we'll have to get all and paginate in memory
IDictionaryItem[] items = (await _dictionaryItemService.GetDescendantsAsync(Constants.System.RootKey, filter)).ToArray();
var model = new PagedViewModel<DictionaryOverviewResponseModel>
{
Total = items.Length,
Items = _umbracoMapper.MapEnumerable<IDictionaryItem, DictionaryOverviewResponseModel>(items.Skip(skip).Take(take))
};
return await Task.FromResult(model);
}
}
@@ -1,37 +0,0 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Api.Management.ViewModels.Dictionary;
namespace Umbraco.Cms.Api.Management.Controllers.Dictionary;
[ApiVersion("1.0")]
public class ByKeyDictionaryController : DictionaryControllerBase
{
private readonly IDictionaryItemService _dictionaryItemService;
private readonly IDictionaryPresentationFactory _dictionaryPresentationFactory;
public ByKeyDictionaryController(IDictionaryItemService dictionaryItemService, IDictionaryPresentationFactory dictionaryPresentationFactory)
{
_dictionaryItemService = dictionaryItemService;
_dictionaryPresentationFactory = dictionaryPresentationFactory;
}
[HttpGet($"{{{nameof(id)}:guid}}")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(DictionaryItemResponseModel), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> ByKey(CancellationToken cancellationToken, Guid id)
{
IDictionaryItem? dictionary = await _dictionaryItemService.GetAsync(id);
if (dictionary == null)
{
return DictionaryItemNotFound();
}
return Ok(await _dictionaryPresentationFactory.CreateDictionaryItemViewModelAsync(dictionary));
}
}
@@ -1,67 +0,0 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Api.Management.ViewModels.Dictionary;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Security.Authorization;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Cms.Web.Common.Authorization;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Management.Controllers.Dictionary;
[ApiVersion("1.0")]
public class CreateDictionaryController : DictionaryControllerBase
{
private readonly IDictionaryItemService _dictionaryItemService;
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
private readonly IDictionaryPresentationFactory _dictionaryPresentationFactory;
private readonly IAuthorizationService _authorizationService;
public CreateDictionaryController(
IDictionaryItemService dictionaryItemService,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
IDictionaryPresentationFactory dictionaryPresentationFactory,
IAuthorizationService authorizationService)
{
_dictionaryItemService = dictionaryItemService;
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
_dictionaryPresentationFactory = dictionaryPresentationFactory;
_authorizationService = authorizationService;
}
[HttpPost]
[MapToApiVersion("1.0")]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status409Conflict)]
public async Task<IActionResult> Create(
CancellationToken cancellationToken,
CreateDictionaryItemRequestModel createDictionaryItemRequestModel)
{
IDictionaryItem created = await _dictionaryPresentationFactory.MapCreateModelToDictionaryItemAsync(createDictionaryItemRequestModel);
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
User,
new DictionaryPermissionResource(createDictionaryItemRequestModel.Translations.Select(t => t.IsoCode)),
AuthorizationPolicies.DictionaryPermissionByResource);
if (authorizationResult.Succeeded is false)
{
return Forbidden();
}
Attempt<IDictionaryItem, DictionaryItemOperationStatus> result =
await _dictionaryItemService.CreateAsync(created, CurrentUserKey(_backOfficeSecurityAccessor));
return result.Success
? CreatedAtId<ByKeyDictionaryController>(controller => nameof(controller.ByKey), result.Result!.Key)
: DictionaryItemOperationStatusResult(result.Status);
}
}
@@ -1,37 +0,0 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
namespace Umbraco.Cms.Api.Management.Controllers.Dictionary;
[ApiVersion("1.0")]
public class DeleteDictionaryController : DictionaryControllerBase
{
private readonly IDictionaryItemService _dictionaryItemService;
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
public DeleteDictionaryController(IDictionaryItemService dictionaryItemService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
{
_dictionaryItemService = dictionaryItemService;
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
}
[HttpDelete($"{{{nameof(id)}:guid}}")]
[MapToApiVersion("1.0")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> Delete(CancellationToken cancellationToken, Guid id)
{
Attempt<IDictionaryItem?, DictionaryItemOperationStatus> result = await _dictionaryItemService.DeleteAsync(id, CurrentUserKey(_backOfficeSecurityAccessor));
return result.Success
? Ok()
: DictionaryItemOperationStatusResult(result.Status);
}
}
@@ -1,46 +0,0 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Common.Builders;
using Umbraco.Cms.Api.Management.Routing;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Cms.Web.Common.Authorization;
namespace Umbraco.Cms.Api.Management.Controllers.Dictionary;
[VersionedApiBackOfficeRoute("dictionary")]
[ApiExplorerSettings(GroupName = "Dictionary")]
[Authorize(Policy = AuthorizationPolicies.TreeAccessDictionary)]
public abstract class DictionaryControllerBase : ManagementApiControllerBase
{
protected IActionResult DictionaryItemOperationStatusResult(DictionaryItemOperationStatus status) =>
OperationStatusResult(status, problemDetailsBuilder => status switch
{
DictionaryItemOperationStatus.DuplicateItemKey => Conflict(problemDetailsBuilder
.WithTitle("Duplicate dictionary item name detected")
.WithDetail("Another dictionary item exists with the same name. Dictionary item names must be unique.")
.Build()),
DictionaryItemOperationStatus.ItemNotFound => DictionaryItemNotFound(problemDetailsBuilder),
DictionaryItemOperationStatus.ParentNotFound => NotFound(problemDetailsBuilder
.WithTitle("The dictionary item parent could not be found")
.Build()),
DictionaryItemOperationStatus.CancelledByNotification => BadRequest(problemDetailsBuilder
.WithTitle("Cancelled by notification")
.WithDetail("A notification handler prevented the dictionary item operation.")
.Build()),
DictionaryItemOperationStatus.InvalidParent => BadRequest(problemDetailsBuilder
.WithTitle("Invalid parent")
.WithDetail("The targeted parent dictionary item is not valid for this dictionary item operation.")
.Build()),
_ => StatusCode(StatusCodes.Status500InternalServerError, problemDetailsBuilder
.WithTitle("Unknown dictionary operation status.")
.Build()),
});
protected IActionResult DictionaryItemNotFound() => OperationStatusResult(DictionaryItemOperationStatus.ItemNotFound, DictionaryItemNotFound);
private IActionResult DictionaryItemNotFound(ProblemDetailsBuilder problemDetailsBuilder)
=> NotFound(problemDetailsBuilder
.WithTitle("The dictionary item could not be found")
.Build());
}
@@ -1,41 +0,0 @@
using System.Net.Mime;
using System.Text;
using System.Xml.Linq;
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Management.Controllers.Dictionary;
[ApiVersion("1.0")]
public class ExportDictionaryController : DictionaryControllerBase
{
private readonly IDictionaryItemService _dictionaryItemService;
private readonly IEntityXmlSerializer _entityXmlSerializer;
public ExportDictionaryController(IDictionaryItemService dictionaryItemService, IEntityXmlSerializer entityXmlSerializer)
{
_dictionaryItemService = dictionaryItemService;
_entityXmlSerializer = entityXmlSerializer;
}
[HttpGet("{id:guid}/export")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> Export(CancellationToken cancellationToken, Guid id, bool includeChildren = false)
{
IDictionaryItem? dictionaryItem = await _dictionaryItemService.GetAsync(id);
if (dictionaryItem is null)
{
return DictionaryItemNotFound();
}
XElement xml = _entityXmlSerializer.Serialize(dictionaryItem, includeChildren);
return File(Encoding.UTF8.GetBytes(xml.ToDataString()), MediaTypeNames.Application.Octet, $"{dictionaryItem.ItemKey}.udt");
}
}
@@ -1,66 +0,0 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Common.Builders;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Api.Management.Services;
using Umbraco.Cms.Api.Management.Services.OperationStatus;
using Umbraco.Cms.Api.Management.ViewModels.Dictionary;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Security;
namespace Umbraco.Cms.Api.Management.Controllers.Dictionary;
[ApiVersion("1.0")]
public class ImportDictionaryController : DictionaryControllerBase
{
private readonly IDictionaryItemImportService _dictionaryItemImportService;
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
public ImportDictionaryController(
IDictionaryItemImportService dictionaryItemImportService,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
{
_dictionaryItemImportService = dictionaryItemImportService;
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
}
[HttpPost("import")]
[MapToApiVersion("1.0")]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> Import(
CancellationToken cancellationToken,
ImportDictionaryRequestModel importDictionaryRequestModel)
{
Attempt<IDictionaryItem?, DictionaryImportOperationStatus> result = await _dictionaryItemImportService
.ImportDictionaryItemFromUdtFileAsync(
importDictionaryRequestModel.TemporaryFile.Id,
importDictionaryRequestModel.Parent?.Id,
CurrentUserKey(_backOfficeSecurityAccessor));
return result.Success
? CreatedAtId<ByKeyDictionaryController>(controller => nameof(controller.ByKey), result.Result!.Key)
: OperationStatusResult(result.Status, problemDetailsBuilder => result.Status switch
{
DictionaryImportOperationStatus.ParentNotFound => NotFound(problemDetailsBuilder
.WithTitle("The parent dictionary item could not be found.")
.Build()),
DictionaryImportOperationStatus.TemporaryFileNotFound => NotFound(problemDetailsBuilder
.WithTitle("The temporary file with specified id could not be found.")
.Build()),
DictionaryImportOperationStatus.InvalidFileType => BadRequest(problemDetailsBuilder
.WithTitle("Invalid file type")
.WithDetail("The dictionary import only supports UDT files.")
.Build()),
DictionaryImportOperationStatus.InvalidFileContent => BadRequest(problemDetailsBuilder
.WithTitle("Invalid file content")
.WithDetail("The uploaded file could not be read as a valid UDT file.")
.Build()),
_ => StatusCode(StatusCodes.Status500InternalServerError, problemDetailsBuilder
.WithTitle("Unknown dictionary import operation status.")
.Build()),
});
}
}
@@ -1,11 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Routing;
using Umbraco.Cms.Core;
namespace Umbraco.Cms.Api.Management.Controllers.Dictionary.Item;
[VersionedApiBackOfficeRoute($"{Constants.Web.RoutePath.Item}/dictionary")]
[ApiExplorerSettings(GroupName = "Dictionary")]
public class DictionaryItemControllerBase : ManagementApiControllerBase
{
}
@@ -1,40 +0,0 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.Dictionary.Item;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Api.Management.Controllers.Dictionary.Item;
[ApiVersion("1.0")]
public class ItemDictionaryItemController : DictionaryItemControllerBase
{
private readonly IDictionaryItemService _dictionaryItemService;
private readonly IUmbracoMapper _mapper;
public ItemDictionaryItemController(IDictionaryItemService dictionaryItemService, IUmbracoMapper mapper)
{
_dictionaryItemService = dictionaryItemService;
_mapper = mapper;
}
[HttpGet]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(IEnumerable<DictionaryItemItemResponseModel>), StatusCodes.Status200OK)]
public async Task<IActionResult> Item(
CancellationToken cancellationToken,
[FromQuery(Name = "id")] HashSet<Guid> ids)
{
if (ids.Count is 0)
{
return Ok(Enumerable.Empty<DictionaryItemItemResponseModel>());
}
IEnumerable<IDictionaryItem> dictionaryItems = await _dictionaryItemService.GetManyAsync(ids.ToArray());
List<DictionaryItemItemResponseModel> responseModels = _mapper.MapEnumerable<IDictionaryItem, DictionaryItemItemResponseModel>(dictionaryItems);
return Ok(responseModels);
}
}
@@ -1,50 +0,0 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.Dictionary;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
namespace Umbraco.Cms.Api.Management.Controllers.Dictionary;
[ApiVersion("1.0")]
public class MoveDictionaryController : DictionaryControllerBase
{
private readonly IDictionaryItemService _dictionaryItemService;
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
public MoveDictionaryController(IDictionaryItemService dictionaryItemService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
{
_dictionaryItemService = dictionaryItemService;
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
}
[HttpPut("{id:guid}/move")]
[MapToApiVersion("1.0")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> Move(
CancellationToken cancellationToken,
Guid id,
MoveDictionaryRequestModel moveDictionaryRequestModel)
{
IDictionaryItem? source = await _dictionaryItemService.GetAsync(id);
if (source == null)
{
return DictionaryItemNotFound();
}
Attempt<IDictionaryItem, DictionaryItemOperationStatus> result = await _dictionaryItemService.MoveAsync(
source,
moveDictionaryRequestModel.Target?.Id,
CurrentUserKey(_backOfficeSecurityAccessor));
return result.Success
? Ok()
: DictionaryItemOperationStatusResult(result.Status);
}
}
@@ -1,22 +0,0 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Api.Management.Controllers.Dictionary.Tree;
[ApiVersion("1.0")]
public class AncestorsDictionaryTreeController : DictionaryTreeControllerBase
{
public AncestorsDictionaryTreeController(IEntityService entityService, IDictionaryItemService dictionaryItemService)
: base(entityService, dictionaryItemService)
{
}
[HttpGet("ancestors")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(IEnumerable<NamedEntityTreeItemResponseModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<NamedEntityTreeItemResponseModel>>> Ancestors(CancellationToken cancellationToken, Guid descendantId)
=> await GetAncestors(descendantId);
}
@@ -1,28 +0,0 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
namespace Umbraco.Cms.Api.Management.Controllers.Dictionary.Tree;
[ApiVersion("1.0")]
public class ChildrenDictionaryTreeController : DictionaryTreeControllerBase
{
public ChildrenDictionaryTreeController(IEntityService entityService, IDictionaryItemService dictionaryItemService)
: base(entityService, dictionaryItemService)
{
}
[HttpGet("children")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedViewModel<NamedEntityTreeItemResponseModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedViewModel<NamedEntityTreeItemResponseModel>>> Children(CancellationToken cancellationToken, Guid parentId, int skip = 0, int take = 100)
{
PagedModel<IDictionaryItem> paginatedItems = await DictionaryItemService.GetPagedAsync(parentId, skip, take);
return Ok(PagedViewModel(await MapTreeItemViewModels(paginatedItems.Items), paginatedItems.Total));
}
}
@@ -1,77 +0,0 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Controllers.Tree;
using Umbraco.Cms.Api.Management.Routing;
using Umbraco.Cms.Api.Management.ViewModels;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Web.Common.Authorization;
namespace Umbraco.Cms.Api.Management.Controllers.Dictionary.Tree;
[VersionedApiBackOfficeRoute($"{Constants.Web.RoutePath.Tree}/dictionary")]
[ApiExplorerSettings(GroupName = "Dictionary")]
[Authorize(Policy = AuthorizationPolicies.TreeAccessDictionaryOrTemplates)]
// NOTE: at the moment dictionary items (renamed to dictionary tree) aren't supported by EntityService, so we have little use of the
// tree controller base. We'll keep it though, in the hope that we can mend EntityService.
public class DictionaryTreeControllerBase : NamedEntityTreeControllerBase<NamedEntityTreeItemResponseModel>
{
public DictionaryTreeControllerBase(IEntityService entityService, IDictionaryItemService dictionaryItemService)
: base(entityService) =>
DictionaryItemService = dictionaryItemService;
// dictionary items do not currently have a known UmbracoObjectType, so we'll settle with Unknown for now
protected override UmbracoObjectTypes ItemObjectType => UmbracoObjectTypes.Unknown;
protected IDictionaryItemService DictionaryItemService { get; }
protected async Task<IEnumerable<NamedEntityTreeItemResponseModel>> MapTreeItemViewModels(IEnumerable<IDictionaryItem> dictionaryItems)
=> await Task.WhenAll(dictionaryItems.Select(CreateEntityTreeItemViewModelAsync));
protected override async Task<ActionResult<IEnumerable<NamedEntityTreeItemResponseModel>>> GetAncestors(Guid descendantKey, bool includeSelf = true)
{
IDictionaryItem? dictionaryItem = await DictionaryItemService.GetAsync(descendantKey);
if (dictionaryItem is null)
{
// this looks weird - but we actually mimic how the rest of the ancestor (and children) endpoints actually work
return Ok(Enumerable.Empty<NamedEntityTreeItemResponseModel>());
}
var ancestors = new List<IDictionaryItem>();
if (includeSelf)
{
ancestors.Add(dictionaryItem);
}
while (dictionaryItem?.ParentId is not null)
{
dictionaryItem = await DictionaryItemService.GetAsync(dictionaryItem.ParentId.Value);
if (dictionaryItem is not null)
{
ancestors.Add(dictionaryItem);
}
}
NamedEntityTreeItemResponseModel[] viewModels = await Task.WhenAll(ancestors.Select(CreateEntityTreeItemViewModelAsync));
return Ok(viewModels.Reverse());
}
private async Task<NamedEntityTreeItemResponseModel> CreateEntityTreeItemViewModelAsync(IDictionaryItem dictionaryItem)
{
var hasChildren = await DictionaryItemService.CountChildrenAsync(dictionaryItem.Key) > 0;
return new NamedEntityTreeItemResponseModel
{
Name = dictionaryItem.ItemKey,
Id = dictionaryItem.Key,
HasChildren = hasChildren,
Parent = dictionaryItem.ParentId.HasValue
? new ReferenceByIdModel
{
Id = dictionaryItem.ParentId.Value
}
: null
};
}
}
@@ -1,28 +0,0 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
namespace Umbraco.Cms.Api.Management.Controllers.Dictionary.Tree;
[ApiVersion("1.0")]
public class RootDictionaryTreeController : DictionaryTreeControllerBase
{
public RootDictionaryTreeController(IEntityService entityService, IDictionaryItemService dictionaryItemService)
: base(entityService, dictionaryItemService)
{
}
[HttpGet("root")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedViewModel<NamedEntityTreeItemResponseModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedViewModel<NamedEntityTreeItemResponseModel>>> Root(CancellationToken cancellationToken, int skip = 0, int take = 100)
{
PagedModel<IDictionaryItem> paginatedItems = await DictionaryItemService.GetPagedAsync(null, skip, take);
return Ok(PagedViewModel(await MapTreeItemViewModels(paginatedItems.Items), paginatedItems.Total));
}
}
@@ -1,74 +0,0 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.Security.Authorization.Dictionary;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Api.Management.ViewModels.Dictionary;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Security.Authorization;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Cms.Web.Common.Authorization;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Management.Controllers.Dictionary;
[ApiVersion("1.0")]
public class UpdateDictionaryController : DictionaryControllerBase
{
private readonly IDictionaryItemService _dictionaryItemService;
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
private readonly IDictionaryPresentationFactory _dictionaryPresentationFactory;
private readonly IAuthorizationService _authorizationService;
public UpdateDictionaryController(
IDictionaryItemService dictionaryItemService,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
IDictionaryPresentationFactory dictionaryPresentationFactory,
IAuthorizationService authorizationService)
{
_dictionaryItemService = dictionaryItemService;
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
_dictionaryPresentationFactory = dictionaryPresentationFactory;
_authorizationService = authorizationService;
}
[HttpPut($"{{{nameof(id)}:guid}}")]
[MapToApiVersion("1.0")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> Update(
CancellationToken cancellationToken,
Guid id,
UpdateDictionaryItemRequestModel updateDictionaryItemRequestModel)
{
IDictionaryItem? current = await _dictionaryItemService.GetAsync(id);
if (current == null)
{
return DictionaryItemNotFound();
}
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
User,
new DictionaryPermissionResource(updateDictionaryItemRequestModel.Translations.Select(t => t.IsoCode)),
AuthorizationPolicies.DictionaryPermissionByResource);
if (authorizationResult.Succeeded is false)
{
return Forbidden();
}
IDictionaryItem updated = await _dictionaryPresentationFactory.MapUpdateModelToDictionaryItemAsync(current, updateDictionaryItemRequestModel);
Attempt<IDictionaryItem, DictionaryItemOperationStatus> result =
await _dictionaryItemService.UpdateAsync(updated, CurrentUserKey(_backOfficeSecurityAccessor));
return result.Success
? Ok()
: DictionaryItemOperationStatusResult(result.Status);
}
}
@@ -1,59 +0,0 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.Security.Authorization.Content;
using Umbraco.Cms.Api.Management.ViewModels.Document;
using Umbraco.Cms.Core.Actions;
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 ByKeyDocumentController : DocumentControllerBase
{
private readonly IAuthorizationService _authorizationService;
private readonly IContentEditingService _contentEditingService;
private readonly IDocumentPresentationFactory _documentPresentationFactory;
public ByKeyDocumentController(
IAuthorizationService authorizationService,
IContentEditingService contentEditingService,
IDocumentPresentationFactory documentPresentationFactory)
{
_authorizationService = authorizationService;
_contentEditingService = contentEditingService;
_documentPresentationFactory = documentPresentationFactory;
}
[HttpGet("{id:guid}")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(DocumentResponseModel), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> ByKey(CancellationToken cancellationToken, Guid id)
{
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)
{
return DocumentNotFound();
}
DocumentResponseModel model = await _documentPresentationFactory.CreateResponseModelAsync(content);
return Ok(model);
}
}

Some files were not shown because too many files have changed in this diff Show More