Compare commits

..
Author SHA1 Message Date
Andy Butland dcbbed4160 Fixed failures in unit tests. 2025-05-06 05:35:21 +02:00
Andy Butland e94e165593 Fixed pipeline definition. 2025-05-06 05:17:05 +02:00
Andy ButlandandGitHub 34709be6cc Merge commit from fork
* Backport user enumeration fix.

* Bump gitversion.

* Remove full path details from exception when requesting a path outside of the physical file system's root.

* Adds randomness to failed login duration.
2025-05-06 05:11:04 +02:00
mole a62fa93c77 Use windows agent for nuget push 2025-04-28 10:58:29 +02:00
Andy Butland ab31fbb0aa Bump version to 13.8.1. 2025-04-17 11:14:17 +02:00
Andy Butland a486d5df33 Bumped version to 13.8.0. 2025-04-10 07:38:20 +02:00
Andy Butland 0e0aca55af Verify endpoint selection candidates with host attribute are ignored if request doesn't match the configured hosts. (#18820) 2025-04-04 06:54:56 +02:00
Andy Butland 3e9ff6b5cb Check we have matched a preview URL by ID when exiting preview. (#18841) 2025-04-03 10:39:47 +02:00
Jacob OvergaardandGitHub fdca086a47 build(deps): bump @umbraco-ui/uui from 1.12.2 to 1.13.0 (#18830) 2025-03-26 14:32:29 +00:00
Andreas ZerbstandGitHub 42a81beeac V13 hotfix sqlserver integration tests (#18744)
* Disabled encrypt

* Skips integration tests for SQl Server on releases

* Removed encrypt
2025-03-20 11:59:03 +01:00
Andy Butland 9284b9e0b1 Make preview check for delivery API content case insensitive. (#18731) 2025-03-20 06:58:57 +01:00
Andy ButlandandGitHub 5570583f70 Fixes issue with macro rendering in an RTE when GUIDs are used for backoffice document routes (#18691)
* Fixes issue with macro rendering in an RTE when GUIDs are used for backoffice document routes.

* Fixed null reference error.
2025-03-18 11:10:23 +01:00
Andy ButlandandGitHub eb979625d1 Render folders before files in static files picker. (#18701) 2025-03-18 11:05:54 +01:00
14650 changed files with 412735 additions and 602002 deletions
-1
View File
@@ -1,4 +1,3 @@
**/*
!tests/Umbraco.Tests.Integration/bin/**
!tests/Umbraco.Tests.UnitTests/bin/**
**/node_modules
-1
View File
@@ -46,7 +46,6 @@
*.xml text=auto
*.resx text=auto
*.yml text eol=lf core.whitespace whitespace=tab-in-indent,trailing-space,tabwidth=2
*.sh eol=lf
*.csproj text=auto merge=union
*.vbproj text=auto merge=union
+69 -86
View File
@@ -1,13 +1,11 @@
# Umbraco CMS Build
This guide will explain how you can build the Umbraco CMS from the source code. You will most likely want to do this if your are setting up a local development environment for contributing code updates to the project. You will need this in order to develop and test your fix or feature.
## Are you sure?
In order to use Umbraco as a CMS and build your website with it, you should not build it yourself. If you're reading this then you're trying to contribute to Umbraco or you're debugging a complex issue.
- Are you about to [create a pull request for Umbraco][contribution guidelines]?
- Are you trying to get to the bottom of a problem in your existing Umbraco installation?
- Are you about to [create a pull request for Umbraco][contribution guidelines]?
- Are you trying to get to the bottom of a problem in your existing Umbraco installation?
If the answer is yes, please read on. Otherwise, make sure to head on over [to the download page](https://our.umbraco.com/download) and start using Umbraco CMS as intended.
@@ -15,7 +13,8 @@ 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.
## Working with the Umbraco source code
## Debugging source locally
Did you read ["Are you sure"](#are-you-sure)?
@@ -23,109 +22,82 @@ Did you read ["Are you sure"](#are-you-sure)?
If you want to run a build without debugging, see [Building from source](#building-from-source) below. This runs the build in the same way it is run on our build servers.
If you've got this far and are keen to get stuck in helping us fix a bug or implement a feature, great! Please read on...
#### Debugging with VS Code
### Prerequisites
In order to build the Umbraco source code locally with Visual Studio Code, first make sure you have the following installed.
In order to work with the Umbraco source code locally, first make sure you have the following installed.
* [Visual Studio Code](https://code.visualstudio.com/)
* [dotnet SDK v7+](https://dotnet.microsoft.com/en-us/download)
* [Node.js v14+](https://nodejs.org/en/download/)
* npm v7+ (installed with Node.js)
* [Git command line](https://git-scm.com/download/)
- Your favourite IDE: [Visual Studio 2022 v17+ with .NET 7+](https://visualstudio.microsoft.com/vs/), [Rider](https://www.jetbrains.com/rider/) or [Visual Studio Code](https://code.visualstudio.com/)
- [dotnet SDK v9+](https://dotnet.microsoft.com/en-us/download)
- [Node.js v20+](https://nodejs.org/en/download/)
- npm v10+ (installed with Node.js)
- [Git command line](https://git-scm.com/download/)
Open the root folder of the repository in Visual Studio Code.
### Familiarizing yourself with the code
To build the front end you'll need to open the command pallet (<kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>P</kbd>) and run `>Tasks: Run Task` followed by `Client Watch` and then run the `Client Build` task in the same way.
Umbraco is a .NET application using C#. The solution is broken down into multiple projects. There are several class libraries. The `Umbraco.Web.UI` project is the main project that hosts the back office and login screen. This is the project you will want to run to see your changes.
There are two web projects in the solution with client-side assets based on TypeScript, `Umbraco.Web.UI.Client` and `Umbraco.Web.UI.Login`.
There are a few different ways to work locally when implementing features or fixing issues with the Umbraco CMS. Depending on whether you are working solely on the front-end, solely on the back-end, or somewhere in between, you may find different workflows work best for you.
Here are some suggestions based on how we work on developing Umbraco at HQ.
### First checkout
When you first clone the source code, build the whole solution via your IDE. You can then start the `Umbraco.Web.UI` project via the IDE or the command line and should find everything across front and back-end is built and running.
You can also run the tasks manually on the command line:
```
cd <solution root>\src\Umbraco.Web.UI
dotnet run --no-build
cd src\Umbraco.Web.UI.Client
npm i
npm run dev
```
When the page loads in your web browser, you can follow the installer to set up a database for debugging. When complete, you will have an empty Umbraco installation to begin working with. You may also wish to install a [starter kit][https://marketplace.umbraco.com/category/themes-&-starter-kits] to ease your debugging.
If you just want to build the UI Client to `Umbraco.Web.UI` then instead of running `dev`, you can do: `npm run build`.
### Back-end only changes
If you are working on back-end only features, when switching branches or pulling down the latest from GitHub, you will find the front-end getting rebuilt periodically when you look to build the back-end changes. This can take a while and slow you down. So if for a period of time you don't care about changes in the front-end, you can disable this build step.
Go to `Umbraco.Cms.StaticAssets.csproj` and comment out the following lines of MsBuild by adding a REM statement in front:
The login screen is a different frontend build, for that one you can run it as follows:
```
REM npm ci --no-fund --no-audit --prefer-offline
REM npm run build:for:cms
cd src\Umbraco.Web.UI.Login
npm i
npm run dev
```
Just be careful not to include this change in your PR.
If you just want to build the Login screen to `Umbraco.Web.UI` then instead of running `dev`, you can do: `npm run build`.
### Front-end only changes
**The initial Gulp build might take a long time - don't worry, this will be faster on subsequent runs.**
Conversely, if you are working on front-end only, you want to build the back-end once and then run it. Before you do so, update the configuration in `appSettings.json` to add the following under `Umbraco:Cms:Security`:
You might run into [Gulp quirks](#gulp-quirks).
The caching for the back office has been described as 'aggressive' so we often find it's best when making back office changes to [disable caching in the browser (check "Disable cache" on the "Network" tab of developer tools)][disable browser caching] to help you to see the changes you're making.
To run the C# portion of the project, either hit <kbd>F5</kbd> to begin debugging, or manually using the command line:
```
"BackOfficeHost": "http://localhost:5173",
"AuthorizeCallbackPathName": "/oauth_complete",
"AuthorizeCallbackLogoutPathName": "/logout",
"AuthorizeCallbackErrorPathName": "/error"
dotnet watch --project .\src\Umbraco.Web.UI\Umbraco.Web.UI.csproj
```
Then run Umbraco from the command line.
**The initial C# build might take a _really_ long time (seriously, go and make a cup of coffee!) - but don't worry, this will be faster on subsequent runs.**
```
cd <solution root>\src\Umbraco.Web.UI
dotnet run --no-build
```
When the page eventually loads in your web browser, you can follow the installer to set up a database for debugging. You may also wish to install a [starter kit][starter kits] to ease your debugging.
In another terminal window, run the following to watch the front-end changes and launch Umbraco using the URL indicated from this task.
#### Debugging with Visual Studio
```
cd <solution root>\src\Umbraco.Web.UI.Client
npm run dev:server
```
In order to build the Umbraco source code locally with Visual Studio, first make sure you have the following installed.
You'll find as you make changes to the front-end files, the updates will be picked up and your browser refreshed automatically.
* [Visual Studio 2022 v17+ with .NET 7+](https://visualstudio.microsoft.com/vs/) ([the community edition is free](https://www.visualstudio.com/thank-you-downloading-visual-studio/?sku=Community&rel=15) for you to use to contribute to Open Source projects)
* [Node.js v14+](https://nodejs.org/en/download/)
* npm v7+ (installed with Node.js)
* [Git command line](https://git-scm.com/download/)
> [!NOTE]
> The caching for the back office has been described as 'aggressive' so we often find it's best when making back office changes to [disable caching in the browser (check "Disable cache" on the "Network" tab of developer tools)][disable browser caching] to help you to see the changes you're making.
The easiest way to get started is to open `umbraco.sln` in Visual Studio.
Whilst most of the backoffice code lives in `Umbraco.Web.UI.Client`, the login screen is in a separate project. If you do any work with that you can build with:
To build the front end, you'll first need to run `cd src\Umbraco.Web.UI.Client && npm install` in the command line (or `cd src\Umbraco.Web.UI.Client; npm install` in PowerShell). Then find the Task Runner Explorer (View → Other Windows → Task Runner Explorer) and run the `build` task under `Gulpfile.js`. You may need to refresh the Task Runner Explorer before the tasks load.
```
cd <solution root>\src\Umbraco.Web.UI.Login
npm run build
```
If you're working on the backoffice, you may wish to run the `dev` command instead while you're working with it, so changes are copied over to the appropriate directories and you can refresh your browser to view the results of your changes.
In both front-end projects, if you've refreshed your branch from the latest on GitHub you may need to update front-end dependencies.
**The initial Gulp build might take a long time - don't worry, this will be faster on subsequent runs.**
To do that, run:
You might run into [Gulp quirks](#gulp-quirks).
```
npm ci --no-fund --no-audit --prefer-offline
```
The caching for the back office has been described as 'aggressive' so we often find it's best when making back office changes to [disable caching in the browser (check "Disable cache" on the "Network" tab of developer tools)][disable browser caching] to help you to see the changes you're making.
### Full-stack changes
"The rest" is a C# based codebase, which is mostly ASP.NET Core MVC based. You can make changes, build them in Visual Studio, and hit <kbd>F5</kbd> to see the result.
If working across both front and back-end, follow both methods and use `dotnet watch`, or re-run `dotnet run` (or `dotnet build` followed by `dotnet run --no-build`) whenever you need to update the back-end code.
**The initial C# build might take a _really_ long time (seriously, go and make a cup of coffee!) - but don't worry, this will be faster on subsequent runs.**
Request and response models used by the management APIs are made available client-side as generated code. If you make changes to the management API, you can re-generate the typed client code with:
```
cd <solution root>\src\Umbraco.Web.UI.Client
npm run generate:server-api-dev
```
Please also update the `OpenApi.json` file held in the solution by copying and pasting the output from `/umbraco/swagger/management/swagger.json`.
When the page eventually loads in your web browser, you can follow the installer to set up a database for debugging. You may also wish to install a [starter kit][starter kits] to ease your debugging.
## Building from source
@@ -133,7 +105,7 @@ Did you read ["Are you sure"](#are-you-sure)?
Do note that this is only required if you want to test out your custom changes in a separate site (not the one in the Umbraco.Web.UI), if you just want to test your changes you can run the included test site using: `dotnet run` from `src/Umbraco.Web.UI/`
You may want to build a set of NuGet packages with your changes, this can be done using the dotnet pack command.
You may want to build a set of NuGet packages with your changes, this can be done using the dotnet pack command.
First enter the root of the project in a command line environment, and then use the following command to build the NuGet packages:
@@ -145,18 +117,17 @@ You can then add these as a local NuGet feed using the following command:
`dotnet nuget add source <Path to Build.Out folder> -n MyLocalFeed`
This will add a local nuget feed with the name "MyLocalFeed" and you'll now be able to use your custom built NuGet packages.
This will add a local nuget feed with the name "MyLocalFeed" and you'll now be able to use your custom built NuGet packages.
### Cleaning up
Once the solution has been used to run a site, one may want to "reset" the solution in order to run a fresh new site again.
The easiest way to do this by deleting the following files and folders:
* src/Umbraco.Web.UI/appsettings.json
* src/Umbraco.Web.UI/umbraco/Data
- src/Umbraco.Web.UI/appsettings.json
- src/Umbraco.Web.UI/umbraco/Data
You only have to remove the connection strings from the appsettings, but removing the data folder ensures that the sqlite database gets deleted too.
You only have to remove the connection strings from the appsettings, but removing the data folder ensures that the sqlite database gets deleted too.
Next time you run a build the `appsettings.json` file will be re-created in its default state.
@@ -164,14 +135,13 @@ This will leave media files and views around, but in most cases, it will be enou
To perform a more complete clear, you will want to also delete the content of the media, views, scripts... directories.
The following command will force remove all untracked files and directories, whether they are ignored by Git or not. Combined with `git reset` it can recreate a pristine working directory.
The following command will force remove all untracked files and directories, whether they are ignored by Git or not. Combined with `git reset` it can recreate a pristine working directory.
git clean -xdf .
For git documentation see:
- git [clean](https://git-scm.com/docs/git-clean)
- git [reset](https://git-scm.com/docs/git-reset)
* git [clean](<https://git-scm.com/docs/git-clean>)
* git [reset](<https://git-scm.com/docs/git-reset>)
## Azure DevOps
@@ -185,5 +155,18 @@ The produced artifacts are published in a container that can be downloaded from
Git might have issues dealing with long file paths during build. You may want/need to enable `core.longpaths` support (see [this page](https://github.com/msysgit/msysgit/wiki/Git-cannot-create-a-file-or-directory-with-a-long-path) for details).
[ contribution guidelines]: CONTRIBUTING.md "Read the guide to contributing for more details on contributing to Umbraco"
### Gulp Quirks
You may need to run the following commands to set up gulp properly:
```
npm cache clean --force
npm ci
npm run build
```
[ contribution guidelines]: CONTRIBUTING.md "Read the guide to contributing for more details on contributing to Umbraco"
[ starter kits ]: https://our.umbraco.com/packages/?category=Starter%20Kits&version=9 "Browse starter kits available for v9 on Our "
[ disable browser caching ]: https://techwiser.com/disable-cache-google-chrome-firefox "Instructions on how to disable browser caching in Chrome and Firefox"
+13 -22
View File
@@ -4,43 +4,34 @@
These contribution guidelines are mostly just that - guidelines, not rules. This is what we've found to work best over the years, but if you choose to ignore them, we still love you! 💖 Use your best judgement, and feel free to propose changes to this document in a pull request.
## Getting Started
We have a guide on [what to consider before you start](contributing-before-you-start.md) and more detailed guides at the end of this article.
## Contribution guide
This guide describes each step to make your first contribution:
The following steps are a quick-start guide:
1. **Fork**
Create a fork of [`Umbraco-CMS` on GitHub](https://github.com/umbraco/Umbraco-CMS)
![Fork the repository](img/forkrepository.png)
2. **Clone**
When GitHub has created your fork, you can clone it in your favorite Git tool or on the command line with `git clone https://github.com/[YourUsername]/Umbraco-CMS`.
![Clone the fork](img/clonefork.png)
![Clone the fork](img/clonefork.png)
3. **Switch to the correct branch**
Switch to the `contrib` branch
4. **Branch out**
4. **Build**
Create a new branch based on `contrib` and name it after the issue you're fixing, For example: `v15/bugfix/18132-rte-tinymce-onchange-value-check`.
Build your fork of Umbraco locally [as described in the build documentation](BUILD.md), you can build with any IDE that supports dotnet or the command line.
Please follow this format for branches: `v{major}/{feature|bugfix|task}/{issue}-{description}`.
5. **Branch**
This is a development branch for the particular issue you're working on, in this case a bug-fix for issue number `18132` that affects Umbraco v.15.
Don't commit to `contrib`, create a new branch first.
5. **Build or run a Development Server**
You can build or run a Development Server with any IDE that supports DotNet or the command line.
Read [Build or run a Development Server](BUILD.md) for the right approach to your needs.
Create a new branch now and name it after the issue you're fixing, we usually follow the format: `temp/12345`. This means it's a temporary branch for the particular issue you're working on, in this case issue number `12345`. Don't commit to `contrib`, create a new branch first.
6. **Change**
@@ -50,7 +41,7 @@ This guide describes each step to make your first contribution:
Done? Yay! 🎉
Remember to commit to your branch. When it's ready push the changes to your fork on GitHub.
Remember to commit to your new `temp` branch, and don't commit to `contrib`. Then you can push the changes up to your fork on GitHub.
8. **Create pull request**
@@ -61,7 +52,7 @@ This guide describes each step to make your first contribution:
## Further contribution guides
- [Before you start](contributing-before-you-start.md)
- [Finding your first issue: Up for grabs](contributing-first-issue.md)
- [Finding your first issue: Up for grabs](contributing-before-you-start.md)
- [Contributing to the new backoffice](https://docs.umbraco.com/umbraco-backoffice/)
- [Unwanted changes](contributing-unwanted-changes.md)
- [Other ways to contribute](contributing-other-ways-to-contribute.md)
+2 -2
View File
@@ -6,8 +6,8 @@ body:
- type: input
id: "version"
attributes:
label: "Which Umbraco version are you using?"
description: "Please write the *exact* version, example: `10.1.0`. Use the help icon in the Umbraco backoffice to find the version you're using"
label: "Which Umbraco version are you using? (Please write the *exact* version, example: 10.1.0)"
description: "Use the help icon in the Umbraco backoffice to find the version you're using"
validations:
required: true
- type: textarea
+2 -2
View File
@@ -4,11 +4,11 @@ contact_links:
url: https://github.com/umbraco/Umbraco-CMS/discussions/new?category=features-and-ideas
about: Start a new discussion when you have ideas or feature requests, eventually discussions can turn into plans
- name: ⁉️ Support Question
url: https://forum.umbraco.com
url: https://our.umbraco.com
about: This issue tracker is NOT meant for support questions. If you have a question, please join us on the forum.
- name: 📖 Documentation Issue
url: https://github.com/umbraco/UmbracoDocs/issues
about: Documentation issues should be reported on the Umbraco documentation repository.
- name: 🔐 Security Issue
url: https://umbraco.com/trust-center/security-and-umbraco/how-to-report-a-vulnerability-in-umbraco/
url: https://umbraco.com/about-us/trust-center/security-and-umbraco/how-to-report-a-vulnerability-in-umbraco/
about: Discovered a Security Issue in Umbraco?
+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.
+23 -29
View File
@@ -1,54 +1,48 @@
# [Umbraco CMS](https://umbraco.com)
[![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](../LICENSE.md)
[![NuGet Version](https://img.shields.io/nuget/v/Umbraco.Cms)](https://www.nuget.org/packages/Umbraco.Cms)
[![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)
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](CONTRIBUTING.md)
[![Forum](https://img.shields.io/badge/help-forum-blue)](https://forum.umbraco.com)
[![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)
![Mastodon Follow](https://img.shields.io/mastodon/follow/110661369750014952?domain=https%3A%2F%2Fumbracocommunity.social)
[![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/).
-30
View File
@@ -1,30 +0,0 @@
# Bellissima release instructions
## Build
> _See internal documentation on the build/release workflow._
## GitHub Release Notes
To generate release notes on GitHub.
- Go to the [**Releases** area](https://github.com/umbraco/Umbraco.CMS.Backoffice/releases)
- Press the [**"Draft a new release"** button](https://github.com/umbraco/Umbraco.CMS.Backoffice/releases/new)
- In the combobox for "Choose a tag", expand then select or enter the next version number, e.g. `release-14.2.0`
- If the tag does not already exist, an option labelled "Create new tag: release-14.2.0 on publish" will appear, select that option
- In the combobox for "Target: main", expand then select the release branch for the next version, e.g. `release/14.2`
- In the combobox for "Previous tag: auto":
- If the next release is an RC, then you can leave as `auto`
- Otherwise, select the previous stable version, e.g. `release-14.1.1`
- Press the **"Generate release notes"** button, this will populate the main textarea
- Change the title to match the version, e.g. `14.2.0`
- Check the details, view in the "Preview" tab
- What type of release is this?
- If it's an RC, then check "Set as a pre-release"
- If it's stable, then check "Set as the latest release"
- Once you're happy with the contents and ready to save...
- If you need more time to review, press the **"Save draft"** button and you can come back to it later
- If you are ready to make the release notes public, then press **"Publish release"** button! :tada:
> If you're curious about how the content is generated, take a look at the `release.yml` configuration:
> https://github.com/umbraco/Umbraco.CMS.Backoffice/blob/main/.github/release.yml
-230
View File
@@ -1,230 +0,0 @@
# Contribution Guidelines
## Thoughts, links, and questions
In the high probability that you are porting something from angular JS then here are a few helpful tips for using Lit:
Here is the LIT documentation and playground: [https://lit.dev](https://lit.dev)
### What is the process of contribution?
- Read the [README](README.md) to learn how to get the project up and running
- Find an issue marked as [community/up-for-grabs](https://github.com/umbraco/Umbraco.CMS.Backoffice/issues?q=is%3Aissue+is%3Aopen+label%3Acommunity%2Fup-for-grabs) - note that some are also marked [good first issue](https://github.com/umbraco/Umbraco.CMS.Backoffice/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) which indicates they are simple to get started on
- Umbraco HQ owns the Management API on the backend, so features can be worked on in the frontend only when there is an API, or otherwise if no API is required
- A contribution should be made in a fork of the repository
- Once a contribution is ready, a pull request should be made to this repository and HQ will assign a reviewer
- A pull request should always indicate what part of a feature it tries to solve, i.e. does it close the targeted issue (if any) or does the developer expect Umbraco HQ to take over
## Contributing in general terms
A lot of the UI has already been migrated to the new backoffice. Generally speaking, one would find a feature on the projects board, locate the UI in the old backoffice (v11 is fine), convert it to Lit components using the UI library, put the business logic into a store/service, write tests, and make a pull request.
We are also very keen to receive contributions towards **documentation, unit testing, package development, accessibility, and just general testing of the UI.**
## The Management API
The management API is the colloquial term used to describe the new backoffice API. It is built as a .NET Web API, has a Swagger endpoint (/umbraco/swagger), and outputs an OpenAPI v3 schema, that the frontend consumes.
The frontend has an API formatter that takes the OpenAPI schema file and converts it into a set of TypeScript classes and interfaces.
**Current schema for API:**
[https://raw.githubusercontent.com/umbraco/Umbraco-CMS/v13/dev/src/Umbraco.Cms.Api.Management/OpenApi.json](https://raw.githubusercontent.com/umbraco/Umbraco-CMS/v15/dev/src/Umbraco.Cms.Api.Management/OpenApi.json)
**How to convert it:**
- Run `npm run generate:server-api`
## A contribution example
### Example: Published Cache Status Dashboard
![Published Status Dashboard](/.github/img/contributing/published-cache-status-dashboard.png)
### Boilerplate (example using Lit)
Links for Lit examples and documentation:
- [https://lit.dev](https://lit.dev)
- [https://lit.dev/docs/](https://lit.dev/docs/)
- [https://lit.dev/playground/](https://lit.dev/playground/)
### Functionality
**HTML**
The simplest approach is to copy over the HTML from the old backoffice into a new Lit element (check existing elements in the repository, e.g. if you are working with a dashboard, then check other dashboards, etc.). Once the HTML is inside the `render` method, it is often enough to simply replace `<umb-***>` elements with `<uui-***>` and replace a few of the attributes. In general, we try to build as much UI with Umbraco UI Library as possible.
**Controller**
The old AngularJS controllers will have to be converted into modern TypeScript and will have to use our new services and stores. We try to abstract as much away as possible, and mostly you will have to make API calls and let the rest of the system handle things like error handling and so on. In the case of this dashboard, we only have a few GET and POST requests. Looking at the new Management API, we find the PublishedCacheService, which is the new API controller to serve data to the dashboard.
To make the first button work, which simply just requests a new status from the server, we must make a call to `PublishedCacheService.getPublishedCacheStatus()`. An additional thing here is to wrap that in a friendly function called `tryExecuteAndNotify`, which is something we make available to developers to automatically handle the responses coming from the server and additionally use the Notifications to notify of any errors:
```typescript
import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources';
import { PublishedCacheService } from '@umbraco-cms/backoffice/external/backend-api';
private _getStatus() {
const { data: status } = await tryExecuteAndNotify(this, PublishedCacheService.getPublishedCacheStatus());
if (status) {
// we now have the status
console.log(status);
}
}
```
### State (buttons, etc)
It is a good idea to make buttons indicate a loading state when awaiting an API call. All `<uui-button>` support the `.state` property, which you can set around API calls:
```typescript
@state()
private _buttonState: UUIButtonState = undefined;
private _getStatus() {
this._buttonState = 'waiting';
[...await...]
this._buttonState = 'success';
}
```
## Making the dashboard visible
### Add to internal manifests
All items are declared in a `manifests.ts` file, which is located in each section directory.
To declare the Published Cache Status Dashboard as a new manifest, we need to add the section as a new json object that would look like this:
```typescript
{
type: 'dashboard',
alias: 'Umb.Dashboard.PublishedStatus',
name: 'Published Status Dashboard',
elementName: 'umb-dashboard-published-status',
element: () => import('./published-status/dashboard-published-status.element.js'),
weight: 200,
meta: {
label: 'Published Status',
pathname: 'published-status',
},
conditions: [
{
alias: 'Umb.Condition.SectionAlias',
match: 'Umb.Section.Settings',
},
],
},
```
Lets go through each of these properties…
- Type: can be one of the following:
- section - examples include: `Content`, `Media`
- dashboard - a view within a section. Examples include: the welcome dashboard
- propertyEditorUi
- editorView
- propertyAction
- tree
- editor
- treeItemAction
- Alias: is the unique key used to identify this item.
- Name: is the human-readable name for this item.
- ElementName: this is the customElementName declared on the element at the top of the file i.e
```typescript
@customElement('umb-dashboard-published-status')
```
- Js: references a function call to import the file that the element is declared within
- Weight: allows us to specify the order in which the dashboard will be displayed within the tabs bar
- Meta: allows us to reference additional data - in our case, we can specify the label that is shown in the tabs bar and the pathname that will be displayed in the URL
- Conditions: allows us to specify the conditions that must be met for the dashboard to be displayed. In our case, we are specifying that the dashboard will only be displayed within the Settings section
## API mock handlers
Running the app with `npm run dev`, you will quickly notice the API requests turn into 404 errors. To hit the API, we need to add a mock handler to define the endpoints that our dashboard will call. In the case of the Published Cache Status section, we have several calls to work through. Lets start by looking at the call to retrieve the current status of the cache:
![Published Status Dashboard](/.github/img/contributing/status-of-cache.png)
From the existing functionality, we can see that this is a string message that is received as part of a `GET` request from the server.
So to define this, we must first add a handler for the Published Status called `published-status.handlers.ts` within the mocks/domains folder. In this file we will have code that looks like the following:
```typescript
const { rest } = window.MockServiceWorker;
import { umbracoPath } from "@umbraco-cms/backoffice/utils";
export const handlers = [
rest.get(umbracoPath("/published-cache/status"), (_req, res, ctx) => {
return res(
// Respond with a 200 status code
ctx.status(200),
ctx.json<string>(
"Database cache is ok. ContentStore contains 1 item and has 1 generation and 0 snapshot. MediaStore contains 5 items and has 1 generation and 0 snapshot."
)
);
}),
];
```
This is defining the `GET` path that we will call through the resource: `/published-cache/status`
It returns a `200 OK` response and a string value with the current “status” of the published cache for us to use within the element
An example `POST` is similar. Lets take the “Refresh status” button as an example:
![Published Status Dashboard](/.github/img/contributing/refresh-status.png)
From our existing functionality, we can see that this makes a `POST` call to the server to prompt a reload of the published cache. So we would add a new endpoint to the mock handler that would look like:
```typescript
rest.post(umbracoPath('/published-cache/reload'), async (_req, res, ctx) => {
return res(
// Simulate a 1 second delay for the benefit of the UI
ctx.delay(1000)
// Respond with a 201 status code
ctx.status(201)
);
})
```
Which is defining a new `POST` endpoint that we can add to the core API fetcher using the path `/published-cache/reload`.
This call returns a simple `OK` status code and no other object.
## Storybook stories
We try to make good Storybook stories for new components, which is a nice way to work with a component in an isolated state. Imagine you are working with a dialog on page 3 and have to navigate back to that every time you make a change - this is now eliminated with Storybook as you can just make a story that displays that step. Storybook can only show one component at a time, so it also helps us to isolate view logic into more and smaller components, which in turn are more testable.
In-depth: [https://storybook.js.org/docs/web-components/get-started/introduction](https://storybook.js.org/docs/web-components/get-started/introduction)
Reference: [https://ambitious-stone-0033b3603.1.azurestaticapps.net/](https://ambitious-stone-0033b3603.1.azurestaticapps.net/)
- Locally: `npm run storybook`
For Umbraco UI stories, please navigate to [https://uui.umbraco.com/](https://uui.umbraco.com/)
## Testing
There are two testing tools on the backoffice: unit testing and end-to-end testing.
### Unit testing
We are using a tool called Web Test Runner which spins up a bunch of browsers using Playwright with the well-known jasmine/chai syntax. It is expected that any new component/element has a test file named “&lt;component>.test.ts”. It will automatically be picked up and there are a set of standard tests we apply to all components, which checks that they are registered correctly and they pass accessibility testing through Axe.
Working with playwright: [https://playwright.dev/docs/intro](https://playwright.dev/docs/intro)
## Putting it all together
When we are finished with the dashboard we will hopefully have something akin to this [real-world example of the actual dashboard that was migrated](https://github.com/umbraco/Umbraco.CMS.Backoffice/tree/main/src/backoffice/settings/dashboards/published-status).
Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

-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: 📄 Documentation
labels:
- documentation
- category/documentation
- title: 🏠 Internal
labels:
- internal
- title: 📦 Dependencies
labels:
- dependencies
- title: 🌈 A11Y
labels:
- accessibility
- category/accessibility
- title: 🚀 New Features
labels:
- type/feature
- category/feature
- type/enhancement
- category/enhancement
- title: 🐛 Bug Fixes
labels:
- '*'
+28 -39
View File
@@ -3,69 +3,58 @@ name: "Code scanning - action"
on:
push:
branches:
- "*/dev"
- "*/contrib"
- "contrib"
- "release/*"
- '*/dev'
- '*/contrib'
pull_request:
# The branches below must be a subset of the branches above
branches:
- "*/dev"
- "*/contrib"
- "contrib"
- "release/*"
schedule:
- cron: "33 2 * * 1"
- '*/dev'
- '*/contrib'
permissions:
contents: read
env:
dotnetVersion: 9.x
dotnetIncludePreviewVersions: "preview"
dotnetVersion: 8.x
dotnetIncludePreviewVersions: true
solution: umbraco.sln
buildConfiguration: SkipTests
DOTNET_NOLOGO: true
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:
name: Analyze (${{ matrix.language }})
permissions:
actions: read # for github/codeql-action/init to get workflow details
contents: read # for actions/checkout to fetch code
security-events: write # for github/codeql-action/analyze to upload SARIF results
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- language: csharp
build-mode: none
- language: javascript-typescript
build-mode: none
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
# We use the setup-dotnet action to set up .NET Core, otherwise the CodeQL CLI will not work with preview versions.
- name: Setup .NET from global.json
uses: actions/setup-dotnet@v4
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
config-file: ./.github/config/codeql-config.yml
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
config-file: ./.github/config/codeql-config.yml
- name: Use .NET ${{ env.dotnetVersion }}
uses: actions/setup-dotnet@v2
with:
dotnet-version: ${{ env.dotnetVersion }}
include-prerelease: ${{ env.dotnetIncludePreviewVersions }}
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: "/language:${{matrix.language}}"
- name: Run dotnet restore
run: dotnet restore ${{ env.solution }}
- name: Run dotnet build
run: dotnet build ${{ env.solution }} --configuration ${{ env.buildConfiguration }} --no-restore -p:ContinuousIntegrationBuild=true
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
-67
View File
@@ -1,67 +0,0 @@
name: Test Backoffice
on:
push:
branches:
- contrib
- release/*
- v*/dev
paths:
- src/Umbraco.Web.UI.Client/**
pull_request:
branches:
- contrib
- release/*
- v*/dev
paths:
- src/Umbraco.Web.UI.Client/**
# Allows GitHub to use this workflow to validate the merge queue
merge_group:
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
env:
NODE_OPTIONS: --max_old_space_size=16384
jobs:
build:
runs-on: ubuntu-latest
defaults:
run:
working-directory: src/Umbraco.Web.UI.Client
steps:
- uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version-file: src/Umbraco.Web.UI.Client/.nvmrc
cache: npm
cache-dependency-path: ./src/Umbraco.Web.UI.Client/package-lock.json
- run: npm ci --no-audit --no-fund --prefer-offline
- name: Check for circular dependencies
run: node devops/circular/index.js src
- run: npm run lint:errors
- run: npm run generate:tsconfig
- run: npm run generate:icons
- run: npm run build:for:cms
- run: npm run check:paths
- run: npm run generate:jsonschema:dist
test:
runs-on: ubuntu-latest
defaults:
run:
working-directory: src/Umbraco.Web.UI.Client
steps:
- uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version-file: src/Umbraco.Web.UI.Client/.nvmrc
cache: npm
cache-dependency-path: ./src/Umbraco.Web.UI.Client/package-lock.json
- run: npm ci --no-audit --no-fund --prefer-offline
- run: npx playwright install --with-deps
- run: npm test
+12 -11
View File
@@ -47,6 +47,13 @@ NDependOut/
QueryResult.htm
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/
@@ -61,15 +68,13 @@ tools/docfx/
/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/
/src/Umbraco.Web.UI.Client/[Bb]uild/[Bb]elle/
/src/Umbraco.Web.UI.Client/src/[Ll]ess/*.css
/src/Umbraco.Web.UI.Client/TESTS-*.xml
/src/Umbraco.Web.UI/wwwroot/[Mm]edia/
/src/Umbraco.Web.UI/App_Code/
/src/Umbraco.Web.UI/App_Plugins/
@@ -86,7 +91,6 @@ tools/docfx/
# 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
@@ -99,13 +103,10 @@ tools/docfx/
# 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
+33 -121
View File
@@ -1,123 +1,35 @@
{
"version": "0.2.0",
"compounds": [
{
"name": "Backoffice Launch (Vite + .NET Core)",
"configurations": [
"Backoffice Launch Vite (Chrome)",
".NET Core Serve with External Auth (web)"
],
"stopAll": true,
"presentation": {
"group": "1"
}
}
],
"configurations": [
{
"name": "Backoffice Launch Vite (Chrome)",
"request": "launch",
"env": {
"VITE_UMBRACO_USE_MSW": "${input:AskForMockServer}"
},
"runtimeExecutable": "npx",
"runtimeArgs": ["vite"],
"type": "node",
"cwd": "${workspaceFolder}/src/Umbraco.Web.UI.Client",
"skipFiles": ["<node_internals>/**", "node_modules/**"],
"smartStep": true,
"autoAttachChildProcesses": true,
"serverReadyAction": {
"killOnServerStop": true,
"action": "debugWithChrome",
"pattern": "Local: http://localhost:([0-9]+)",
"uriFormat": "http://localhost:%s",
"webRoot": "${workspaceFolder}/src/Umbraco.Web.UI.Client"
},
"presentation": {
"group": "2"
}
},
{
"name": "Backoffice Attach Vite (Chrome)",
"request": "launch",
"type": "chrome",
"smartStep": true,
"url": "http://localhost:5173/",
"skipFiles": ["<node_internals>/**", "node_modules/**"],
"webRoot": "${workspaceFolder}/src/Umbraco.Web.UI.Client",
"presentation": {
"group": "2"
}
},
{
// Use IntelliSense to find out which attributes exist for C# debugging
// Use hover for the description of the existing attributes
// For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md
"name": ".NET Core Launch (web)",
"type": "coreclr",
"request": "launch",
"program": "dotnet",
"args": ["run"],
"cwd": "${workspaceFolder}/src/Umbraco.Web.UI",
"stopAtEntry": false,
"requireExactSource": false,
// Enable launching a web browser when ASP.NET Core starts. For more information: https://aka.ms/VSCode-CS-LaunchJson-WebBrowser
"serverReadyAction": {
"action": "openExternally",
"pattern": "\\\\bNow listening on:\\\\s+(https?://\\\\S+)"
},
"env": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"sourceFileMap": {
"/Views": "${workspaceFolder}/Umbraco.Web.UI/Views"
},
"presentation": {
"group": "3"
}
},
{
"name": ".NET Core Attach",
"type": "coreclr",
"request": "attach",
"processId": "${command:pickProcess}",
"presentation": {
"group": "3"
}
},
{
"name": ".NET Core Serve with External Auth (web)",
"type": "coreclr",
"request": "launch",
"program": "dotnet",
"args": ["run"],
"cwd": "${workspaceFolder}/src/Umbraco.Web.UI",
"stopAtEntry": false,
"requireExactSource": false,
"checkForDevCert": true,
"env": {
"ASPNETCORE_ENVIRONMENT": "Development",
"ASPNETCORE_URLS": "https://localhost:44339",
"UMBRACO__CMS__SECURITY__BACKOFFICEHOST": "http://localhost:5173",
"UMBRACO__CMS__SECURITY__AUTHORIZECALLBACKPATHNAME": "/oauth_complete",
"UMBRACO__CMS__SECURITY__AUTHORIZECALLBACKLOGOUTPATHNAME": "/logout",
"UMBRACO__CMS__SECURITY__AUTHORIZECALLBACKERRORPATHNAME": "/error"
},
"sourceFileMap": {
"/Views": "${workspaceFolder}/Umbraco.Web.UI/Views"
},
"presentation": {
"group": "3"
}
}
],
"inputs": [
{
"id": "AskForMockServer",
"type": "promptString",
"description": "Use Mock Service Worker (MSW) for Backoffice API calls (off requires a running server)?",
"default": "off"
}
]
"version": "0.2.0",
"configurations": [
{
// Use IntelliSense to find out which attributes exist for C# debugging
// Use hover for the description of the existing attributes
// For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md
"name": ".NET Core Launch (web)",
"type": "coreclr",
"request": "launch",
"program": "dotnet",
"args": ["run"],
"cwd": "${workspaceFolder}/src/Umbraco.Web.UI",
"stopAtEntry": false,
"requireExactSource": false,
// Enable launching a web browser when ASP.NET Core starts. For more information: https://aka.ms/VSCode-CS-LaunchJson-WebBrowser
"serverReadyAction": {
"action": "openExternally",
"pattern": "\\\\bNow listening on:\\\\s+(https?://\\\\S+)"
},
"env": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"sourceFileMap": {
"/Views": "${workspaceFolder}/Views"
}
},
{
"name": ".NET Core Attach",
"type": "coreclr",
"request": "attach",
"processId": "${command:pickProcess}"
}
]
}
-1
View File
@@ -1 +0,0 @@
../src/Umbraco.Web.UI.Client/.vscode/lit.code-snippets
-5
View File
@@ -1,5 +0,0 @@
{
"cSpell.words": [
"unprovide"
]
}
+7 -3
View File
@@ -27,9 +27,11 @@
"promptOnClose": true,
"group": "build",
"type": "npm",
"script": "build:for:cms",
"script": "build",
"path": "src/Umbraco.Web.UI.Client/",
"problemMatcher": []
"problemMatcher": [
"$gulp-tsc"
]
},
{
"label": "Client Watch",
@@ -39,7 +41,9 @@
"type": "npm",
"script": "dev",
"path": "src/Umbraco.Web.UI.Client/",
"problemMatcher": []
"problemMatcher": [
"$gulp-tsc"
]
},
{
"label": "Dotnet build",
+3 -13
View File
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<Project>
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<Company>Umbraco HQ</Company>
<Authors>Umbraco</Authors>
<Copyright>Copyright © Umbraco $([System.DateTime]::Today.ToString('yyyy'))</Copyright>
@@ -14,21 +14,11 @@
<NeutralLanguage>en-US</NeutralLanguage>
<Nullable>enable</Nullable>
<WarningsAsErrors>nullable</WarningsAsErrors>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
<ImplicitUsings>enable</ImplicitUsings>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<WarnOnPackingNonPackableProject>false</WarnOnPackingNonPackableProject>
</PropertyGroup>
<PropertyGroup>
<!--
TODO: Fix and remove overrides:
[NU5104] Warning As Error: A stable release of a package should not have a prerelease dependency. Either modify the version spec of dependency
-->
<NoWarn>$(NoWarn),NU5104,SA1309</NoWarn>
<WarningsNotAsErrors>$(WarningsNotAsErrors),NU5104,SA1600</WarningsNotAsErrors>
</PropertyGroup>
<!-- SourceLink -->
<PropertyGroup>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
@@ -40,12 +30,12 @@
<PropertyGroup>
<GenerateCompatibilitySuppressionFile>false</GenerateCompatibilitySuppressionFile>
<EnablePackageValidation>true</EnablePackageValidation>
<PackageValidationBaselineVersion>15.0.0</PackageValidationBaselineVersion>
<PackageValidationBaselineVersion>13.0.0</PackageValidationBaselineVersion>
<EnableStrictModeForCompatibleFrameworksInPackage>true</EnableStrictModeForCompatibleFrameworksInPackage>
<EnableStrictModeForCompatibleTfms>true</EnableStrictModeForCompatibleTfms>
</PropertyGroup>
<!-- Calculate version only once for the whole repository -->
<!-- Calculate version only once for the whole repository -->
<PropertyGroup>
<GitVersionBaseDirectory>$(MSBuildThisFileDirectory)</GitVersionBaseDirectory>
</PropertyGroup>
+58 -58
View File
@@ -5,35 +5,35 @@
</PropertyGroup>
<!-- Global packages (private, build-time packages for all projects) -->
<ItemGroup>
<GlobalPackageReference Include="Nerdbank.GitVersioning" Version="3.6.146" />
<GlobalPackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
<GlobalPackageReference Include="Nerdbank.GitVersioning" Version="3.6.139" />
<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>
<!-- Microsoft packages -->
<ItemGroup>
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="9.0.0" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.11" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="8.0.11" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.10.0" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.2" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.2" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.2" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.10" />
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.FileProviders.Embedded" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.FileProviders.Physical" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.Identity.Core" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.Identity.Stores" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.Options.DataAnnotations" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.Caching.Hybrid" Version="9.0.0-preview.9.24556.5" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="8.0.11" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.11" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.11" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.11" />
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.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" />
<PackageVersion Include="Microsoft.Extensions.FileProviders.Embedded" Version="8.0.11" />
<PackageVersion Include="Microsoft.Extensions.FileProviders.Physical" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.Identity.Core" Version="8.0.11" />
<PackageVersion Include="Microsoft.Extensions.Identity.Stores" Version="8.0.11" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="8.0.2" />
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Options.DataAnnotations" Version="8.0.0" />
<PackageVersion Include="System.Runtime.Caching" Version="8.0.1" />
</ItemGroup>
<!-- Umbraco packages -->
<ItemGroup>
@@ -42,62 +42,62 @@
</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.7.1" />
<PackageVersion Include="Examine.Core" Version="3.7.1" />
<PackageVersion Include="HtmlAgilityPack" Version="1.11.74" />
<PackageVersion Include="JsonPatch.Net" Version="3.1.1" />
<PackageVersion Include="Examine" Version="3.7.0" />
<PackageVersion Include="Examine.Core" Version="3.7.0" />
<PackageVersion Include="HtmlAgilityPack" Version="1.11.71" />
<PackageVersion Include="K4os.Compression.LZ4" Version="1.3.8" />
<PackageVersion Include="MailKit" Version="4.10.0" />
<PackageVersion Include="MailKit" Version="4.8.0" />
<PackageVersion Include="Markdown" Version="2.2.1" />
<PackageVersion Include="MessagePack" Version="2.5.192" />
<PackageVersion Include="MiniProfiler.AspNetCore.Mvc" Version="4.3.8" />
<PackageVersion Include="MiniProfiler.Shared" Version="4.5.4" />
<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="6.1.1" />
<PackageVersion Include="OpenIddict.AspNetCore" Version="6.1.1" />
<PackageVersion Include="OpenIddict.EntityFrameworkCore" Version="6.1.1" />
<PackageVersion Include="Serilog" Version="4.2.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="3.0.0" />
<PackageVersion Include="Serilog.Enrichers.Thread" Version="4.0.0" />
<PackageVersion Include="Serilog.Expressions" Version="5.0.0" />
<PackageVersion Include="Serilog.Enrichers.Process" Version="2.0.2" />
<PackageVersion Include="Serilog.Enrichers.Thread" Version="3.1.0" />
<PackageVersion Include="Serilog.Expressions" Version="4.0.0" />
<PackageVersion Include="Serilog.Extensions.Hosting" Version="8.0.0" />
<PackageVersion Include="Serilog.Formatting.Compact" Version="3.0.0" />
<PackageVersion Include="Serilog.Formatting.Compact.Reader" Version="4.0.0" />
<PackageVersion Include="Serilog.Formatting.Compact" Version="2.0.0" />
<PackageVersion Include="Serilog.Formatting.Compact.Reader" Version="3.0.0" />
<PackageVersion Include="Serilog.Settings.Configuration" Version="8.0.4" />
<PackageVersion Include="Serilog.Sinks.Async" Version="2.1.0" />
<PackageVersion Include="Serilog.Sinks.File" Version="6.0.0" />
<PackageVersion Include="Serilog.Sinks.Map" Version="2.0.0" />
<PackageVersion Include="Serilog.Sinks.Async" Version="1.5.0" />
<PackageVersion Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageVersion Include="Serilog.Sinks.Map" Version="1.0.2" />
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.7" />
<PackageVersion Include="SixLabors.ImageSharp.Web" Version="3.1.3" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="7.1.0" />
<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) -->
<ItemGroup>
<!-- Microsoft.EntityFrameworkCore.SqlServer and NPoco.SqlServer brings in a vulnerable version of Azure.Identity -->
<!-- Take top-level depedendency on Azure.Identity, because Microsoft.EntityFrameworkCore.SqlServer depends on a vulnerable version -->
<!-- Both Microsoft.EntityFrameworkCore.SqlServer and NPoco.SqlServer bring in a vulnerable version of Azure.Identity -->
<PackageVersion Include="Azure.Identity" Version="1.13.1" />
<!-- Microsoft.EntityFrameworkCore.SqlServer brings in a vulnerable version of System.Runtime.Caching -->
<PackageVersion Include="System.Runtime.Caching" Version="9.0.0" />
<!-- Dazinator.Extensions.FileProviders brings in a vulnerable version of System.Net.Http -->
<PackageVersion Include="System.Net.Http" Version="4.3.4" />
<!-- Examine brings in a vulnerable version of System.Security.Cryptography.Xml -->
<PackageVersion Include="System.Security.Cryptography.Xml" Version="9.0.0" />
<!-- Dazinator.Extensions.FileProviders and MiniProfiler.AspNetCore.Mvc brings in a vulnerable version of System.Text.RegularExpressions -->
<PackageVersion Include="System.Security.Cryptography.Xml" Version="8.0.2" />
<!-- Both Dazinator.Extensions.FileProviders and MiniProfiler.AspNetCore.Mvc bring in a vulnerable version of System.Text.RegularExpressions -->
<PackageVersion Include="System.Text.RegularExpressions" Version="4.3.1" />
<!-- OpenIddict.AspNetCore, Npoco.SqlServer and Microsoft.EntityFrameworkCore.SqlServer brings in a vulnerable version of Microsoft.IdentityModel.JsonWebTokens -->
<!-- Take top-level depedendency on Microsoft.IdentityModel.JsonWebTokens, because OpenIddict.AspNetCore, Npoco.SqlServer and Microsoft.EntityFrameworkCore.SqlServer depends on a vulnerable version -->
<PackageVersion Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.2.1" />
<!-- Azure.Identity, Microsoft.EntityFrameworkCore.SqlServer and Dazinator.Extensions.FileProviders brings in a legacy version of System.Text.Encodings.Web -->
<PackageVersion Include="System.Text.Encodings.Web" Version="9.0.0" />
<!-- NPoco.SqlServer brings in a vulnerable version of Microsoft.Data.SqlClient -->
<PackageVersion Include="Microsoft.Data.SqlClient" Version="5.2.2" />
<!-- Examine.Lucene brings in a vulnerable version of Lucene.Net.Replicator -->
<!-- Both OpenIddict.AspNetCore, Npoco.SqlServer and Microsoft.EntityFrameworkCore.SqlServer bring in a vulnerable version of Microsoft.IdentityModel.JsonWebTokens -->
<PackageVersion Include="Microsoft.IdentityModel.JsonWebTokens" Version="7.7.1" />
<!-- Examine.Lucene bring in a vulnerable version of Lucene.Net.Replicator -->
<PackageVersion Include="Lucene.Net.Replicator" Version="4.8.0-beta00017" />
<!-- 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>
-579
View File
@@ -1,579 +0,0 @@
Third-Party Notices
===================
This file contains notices and attributions for third-party software used in the Umbraco CMS project.
It is not a license and does not grant any rights to use the third-party software.
Umbraco CMS is licensed under the MIT License, which can be found in the LICENSE file.
---
@openid/AppAuth-JS: An OpenID Connect and OAuth 2.0 client library for JavaScript
URL: https://github.com/openid/AppAuth-JS
License: Apache License, Version 2.0
Copyright: 2017 Google Inc.
---
AutoFixture: Write maintainable unit tests, faster
URL: https://github.com/AutoFixture/AutoFixture
License: MIT License
Copyright: 2013 Mark Seemann
---
Asp.Versioning.Mvc: A library for ASP.NET Core versioning
URL: https://github.com/dotnet/aspnet-api-versioning
License: MIT License
Copyright: .NET Foundation and contributors
---
Babel: A JavaScript compiler
URL: https://babeljs.io/
License: MIT License
Copyright: 2014-present Sebastian McKenzie and other contributors
---
BenchmarkDotNet: Powerful .NET library for benchmarking
URL: https://github.com/dotnet/BenchmarkDotNet
License: MIT License
Copyright: .NET Foundation and Contributors
---
Bogus: A simple and sane data generator for populating objects that supports different locales.
URL: https://github.com/bchavez/Bogus
License: MIT License
Copyright: 2015 Brian Chavez
---
CommandLineParser: Terse syntax C# command line parser for .NET
URL: https://github.com/commandlineparser/commandline
License: MIT License
Copyright: 2005-2015 Giacomo Stelluti Scala & Contributors
---
cross-env: A CLI tool to set environment variables across platforms
URL: https://github.com/kentcdodds/cross-env
License: MIT License
Copyright: 2017 Kent C. Dodds
---
Dazinator.Extensions.FileProviders: A library for file provider extensions
URL: https://github.com/dazinator/Dazinator.Extensions.FileProviders
License: MIT License
Copyright: 2016 Darrell
---
DOMPurify: A DOM-only XSS sanitizer for HTML, MathML and SVG
URL: https://github.com/cure53/DOMPurify
License: Apache License, Version 2.0
Copyright: 2025 Dr.-Ing. Mario Heiderich, Cure53
---
Element Internals Polyfill: A polyfill for the Element Internals API
URL: https://github.com/calebdwilliams/element-internals-polyfill
License: MIT License
Copyright: 2021 Caleb Williams
---
Eslint: A tool for identifying and reporting on patterns in JavaScript
URL: https://eslint.org/
License: MIT License
Copyright: OpenJS Foundation and other contributors
---
Examine: A search and indexing library for .NET
URL: https://github.com/Shazwazza/Examine
License: Microsoft Public License (Ms-PL)
Copyright: 2023 Shannon Deminick
---
Glob: A library for matching file paths using glob patterns
URL: https://github.com/isaacs/node-glob
License: ISC License
Copyright: 2009-2023 Isaac Z. Schlueter and Contributors
---
Globals: A library for managing global variables in JavaScript
URL: https://github.com/sindresorhus/globals
License: MIT License
Copyright: Sindre Sorhus
---
Html Agility Pack: An HTML parser for .NET
URL: https://html-agility-pack.net/
License: MIT License
Copyright: ZZZ Projects Inc.
---
ImageSharp: A cross-platform library for processing images in .NET
URL: https://github.com/SixLabors/ImageSharp
License: Apache License, Version 2.0 under the Six Labors Split License
Copyright: Six Labors
---
jsdiff: A JavaScript text differencing implementation
URL: https://github.com/kpdecker/jsdiff
License: BSD 3-Clause License
Copyright: 2009-2015 Kevin Decker <kpdecker@gmail.com>
---
JsonPatch.Net: A library for JSON Patch (RFC 6902) in .NET
URL: https://github.com/json-everything/json-everything
License: MIT License
Copyright: .NET Foundation and Contributors
---
K4os.Compression.LZ4: A fast LZ4 compression library for .NET
URL: https://github.com/MiloszKrajewski/K4os.Compression.LZ4
License: MIT License
Copyright: 2017 Milosz Krajewski
---
Lit: A simple library for building fast, lightweight web components
URL: https://lit.dev
License: BSD 3-Clause License
Copyright: 2020 Google LLC. All rights reserved.
---
Lucide: Beautiful & consistent icons for the web
URL: https://lucide.dev/
License: ISC License
Copyright: 2013-2022 Cole Bemis
Copyright: 2022 Lucide Contributors
---
Madge: A dependency graph generator for JavaScript
URL: https://github.com/pahen/madge
License: MIT License
Copyright: 2017 Patrik Henningsson
---
MailKit: A library for sending email in .NET
URL: https://github.com/jstedfast/MailKit
License: MIT License
Copyright: 2013-2024 .NET Foundation and Contributors
---
Markdown: A library for parsing and compiling Markdown
URL: https://github.com/hey-red/Markdown
License: MIT License
Copyright: 2018 red
---
marked: A markdown parser and compiler
URL: https://marked.js.org/
License: MIT License
Copyright: 2011-2018, Christopher Jeffrey (https://github.com/chjj/)
Copyright: 2018+, MarkedJS (https://github.com/markedjs/)
---
Message Pack: The extremely fast MessagePack serializer for C#
URL: https://github.com/MessagePack-CSharp/MessagePack-CSharp
License: MIT License
Copyright: 2017 Yoshifumi Kawai and contributors
---
Miniprofiler: A mini profiler for .NET
URL: https://github.com/MiniProfiler/dotnet
License: MIT License
Copyright: .NET MiniProfiler Contributors
---
Monaco Editor: A browser-based code editor
URL: https://microsoft.github.io/monaco-editor/
License: MIT License
Copyright: 2016-present Microsoft Corporation
---
Moq: A mocking library for .NET
URL: https://github.com/moq/moq
License: BSD 3-Clause License
Copyright: 2007 Clarius Consulting, Manas Technology Solutions, InSTEDD, and Contributors.
---
Mock Service Worker (MSW): A library for mocking API requests in JavaScript
URL: https://mswjs.io/
License: MIT License
Copyright: 2018present Artem Zakharchenko
---
NCrontab: A cron schedule parser for .NET
URL: https://github.com/atifaziz/NCrontab
License: Apache License, Version 2.0
Copyright: 2001 The OpenSymphony Group
Copyright: 2008 Atif Aziz
---
Nerdbank.GitVersioning: A library for versioning .NET projects
URL: https://github.com/dotnet/Nerdbank.GitVersioning
License: MIT License
Copyright: .NET Foundation and Contributors
---
NJsonSchema: A JSON schema validator for .NET
URL: https://github.com/RicoSuter/NJsonSchema
License: MIT License
Copyright: 2022 Rico Suter
---
NPoco: A micro ORM for .NET
URL: https://github.com/schotime/NPoco
License: Apache License, Version 2.0
Copyright: Schotime
---
NUnit: A unit testing framework for .NET
URL: https://github.com/nunit/nunit
License: MIT License
Copyright: Charlie Poole, Rob Prouse and Contributors
---
Open Web Components: A set of standards and libraries for building web components
URL: https://open-wc.org/
License: MIT License
Copyright: 2018 open-wc
---
Openapi-ts: The OpenAPI to TypeScript codegen
URL: https://github.com/hey-api/openapi-ts
License: MIT License
Copyright: Hey API
---
OpenIddict: A simple and flexible OpenID Connect server for ASP.NET Core
URL: https://github.com/openiddict/openiddict-core
License: Apache License, Version 2.0
Copyright: Kévin Chalet
---
Playwright: A Node.js library to automate browser testing
URL: https://playwright.dev/
License: Apache License, Version 2.0
Copyright: 2025 Microsoft Corporation
---
Playwright-msw: A library to wrap Mock Service Worker with Playwright
URL: https://github.com/valendres/playwright-msw
License: MIT License
Copyright: 2022 Peter Weller
---
Prettier: An opinionated code formatter
URL: https://prettier.io/
License: MIT License
Copyright: James Long and contributors
---
Remark-gfm: A GitHub Flavored Markdown plugin for Remark
URL: https://github.com/remarkjs/remark-gfm
License: MIT License
Copyright: Titus Wormer
---
Rollup: A module bundler for JavaScript
URL: https://rollupjs.org/
License: MIT License
Copyright: 2015-present Rollup contributors
---
Rollup Plugins: A collection of Rollup plugins
URL: https://github.com/rollup/plugins
License: MIT License
Copyright: 2019-present Rollup Plugins contributors
---
Rollup-plugin-esbuild: A Rollup plugin for using esbuild
URL: https://github.com/egoist/rollup-plugin-esbuild
License: MIT License
Copyright: 2020 EGOIST
---
Rollup-plugin-import-css: A Rollup plugin for importing CSS files
URL: https://github.com/jleeson/rollup-plugin-import-css
License: MIT License
Copyright: 2020 Jacob Leeson
---
rxjs: Reactive Extensions for JavaScript
URL: https://rxjs.dev/
License: Apache License, Version 2.0
Copyright: 2015-present Ben Lesh <ben@benlesh.com>, Google, Inc., Netflix, Inc., Microsoft Corp., and contributors
---
Serilog: A diagnostic logging library for .NET
URL: https://github.com/serilog/serilog
License: Apache License, Version 2.0
Copyright: Serilog Contributors
---
Simple Icons: A set of SVG icons for popular brands
URL: https://simpleicons.org/
License: CC0 1.0 Universal License
Copyright: Simple Icons Contributors
---
Storybook: A UI component explorer for Web Components
URL: https://storybook.js.org/
License: MIT License
Copyright: 2024 Storybook
---
StyleCop.Analyzers: Analyzers for StyleCop
URL: https://github.com/DotNetAnalyzers/StyleCopAnalyzers
License: MIT License
Copyright: Tunnel Vision Laboratories, LLC
---
SVGO: A tool for optimizing SVG files
URL: https://svgo.dev/
License: MIT License
Copyright: Kir Belevich
---
Swashbuckle.AspNetCore: A library for generating Swagger documentation for ASP.NET Core APIs
URL: https://github.com/domaindrivendev/Swashbuckle.AspNetCore
License: MIT License
Copyright: 2016 Richard Morris
---
Tiny Glob: A tiny globbing library for Node.js
URL: https://github.com/terkelg/tiny-glob
License: MIT License
Copyright: 2018 Terkel
---
TinyMCE, version 6.x: A rich text editor for the web
URL: https://www.tiny.cloud/
License: MIT License
Copyright: 2022 Ephox Corporation DBA Tiny Technologies, Inc.
---
Tiptap: A renderless rich-text editor for the web
URL: https://tiptap.dev/
License: MIT License
Copyright: 2025 Tiptap GmbH
---
Tsc-alias: A TypeScript compiler plugin for aliasing module paths
URL: https://github.com/justkey007/tsc-alias
License: MIT License
Copyright: 2018 Justkey
---
Typedoc: A documentation generator for TypeScript projects
URL: https://typedoc.org/
License: Apache License, Version 2.0
Copyright: Gerrit Birkeland and Contributors
---
Typescript: A typed superset of JavaScript that compiles to plain JavaScript
URL: https://www.typescriptlang.org/
License: Apache License, Version 2.0
Copyright: 2012-present Microsoft Corporation
---
Typescript-eslint: A set of tools for linting TypeScript code
URL: https://github.com/typescript-eslint/typescript-eslint
License: MIT License
Copyright: 2019 typescript-eslint and other contributors
---
Typescript-json-schema: A library for generating JSON schema from TypeScript types
URL: https://github.com/YousefED/typescript-json-schema
License: BSD 3-Clause License
Copyright: 2016 typescript-json-schema contributors
---
Umbraco.Code: Provides code-level tools for Umbraco
URL: https://github.com/umbraco/Umbraco-Code
License: MIT License
Copyright: 2005-present Umbraco A/S
---
Umbraco.GitVersioning.Extensions: Utilities for Nerdbank.GitVersioning
URL: https://github.com/umbraco/Umbraco.GitVersioning.Extensions
License: MIT License
Copyright: 2005-present Umbraco A/S
---
Umbraco.JsonSchema.Extensions: Utilities for JSON schema generation
URL: https://github.com/umbraco/Umbraco.JsonSchema.Extensions
License: MIT License
Copyright: 2005-present Umbraco A/S
---
Umbraco UI Library: A set of UI components for building web applications
URL: https://uui.umbraco.com/
License: MIT License
Copyright: 2005-present Umbraco A/S
---
uuid: A library for generating unique identifiers
URL: https://github.com/uuidjs/uuid
License: MIT License
Copyright: 2010-2020 Robert Kieffer and other contributors
---
Vite: A fast build tool and development server for modern web projects
URL: https://vite.dev/
License: MIT License
Copyright: 2019-present VoidZero Inc. and Vite contributors
---
Vite-plugin-static-copy: A Vite plugin for copying static files
URL: https://github.com/sapphi-red/vite-plugin-static-copy
License: MIT License
Copyright: 2021 sapphi-red
---
Vite-tsconfig-paths: A Vite plugin for resolving TypeScript paths
URL: https://github.com/aleclarson/vite-tsconfig-paths
License: MIT License
Copyright: Alec Larson
---
Web Component Analyzer: A tool for analyzing web components
URL: https://github.com/runem/web-component-analyzer
License: MIT License
Copyright: 2019 Rune Mehlsen
-10
View File
@@ -1,10 +0,0 @@
{
"folders": [
{
"path": "src/Umbraco.Web.UI.Client"
},
{
"path": "src/Umbraco.Web.UI.Login"
}
]
}
+366 -388
View File
File diff suppressed because it is too large Load Diff
-75
View File
@@ -1,75 +0,0 @@
parameters:
- name: testFolder
type: string
default: ''
- name: buildConfiguration
type: string
default: ''
- name: additionalEnvironmentVariables
type: boolean
default: False
steps:
- pwsh: |
dotnet restore UmbracoProject
cp $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest.UmbracoProject/*.cs UmbracoProject
displayName: Restore project
workingDirectory: $(Agent.BuildDirectory)/app
# Update application to use necessary app settings
- pwsh: |
$sourcePath = "$(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/tests/${{ parameters.testFolder }}/AdditionalSetup"
$destinationPath = "UmbracoProject"
$jsonFiles = Get-ChildItem -Path $sourcePath -Filter "*.json"
if ($jsonFiles) {
$jsonFiles | ForEach-Object {
Write-Host "Copying: $($_.FullName)"
Copy-Item -Path $_.FullName -Destination $destinationPath -Force
}
} else {
Write-Host "No JSON files found."
}
displayName: Update application to use necessary app settings
workingDirectory: $(Agent.BuildDirectory)/app
# Update application to use necessary App_Plugins
- pwsh: |
$sourcePath = "$(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/tests/${{ parameters.testFolder }}/AdditionalSetup"
$destinationPath = "UmbracoProject"
$appPluginsFolders = Get-ChildItem -Path $sourcePath -Directory -Filter "App_Plugins"
if ($appPluginsFolders) {
foreach ($folder in $appPluginsFolders) {
Write-Host "Copying folder: $($folder.FullName)"
Copy-Item -Path $folder.FullName -Destination $destinationPath -Recurse -Force
}
} else {
Write-Host "No App_Plugins found."
}
displayName: Update application to use necessary app plugins
workingDirectory: $(Agent.BuildDirectory)/app
# Update application to use necessary classes
- pwsh: |
$sourcePath = "$(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest/tests/${{ parameters.testFolder }}/AdditionalSetup"
$destinationPath = "UmbracoProject"
$csharpFiles = Get-ChildItem -Path $sourcePath -Filter "*.cs"
if ($csharpFiles) {
$csharpFiles | ForEach-Object {
Write-Host "Copying: $($_.FullName)"
Copy-Item -Path $_.FullName -Destination $destinationPath -Force
}
} else {
Write-Host "No C# files found."
}
displayName: Update application to use necessary classes
workingDirectory: $(Agent.BuildDirectory)/app
- ${{ if eq(parameters.additionalEnvironmentVariables, False) }}:
- pwsh: |
dotnet build UmbracoProject --configuration ${{ parameters.buildConfiguration }} --no-restore
dotnet dev-certs https
displayName: Build application
workingDirectory: $(Agent.BuildDirectory)/app
condition: succeeded()
@@ -1,47 +0,0 @@
parameters:
- name: SA_PASSWORD
type: string
default: ''
- name: buildConfiguration
type: string
default: ''
- name: additionalEnvironmentVariables
type: boolean
default: False
- name: DatabaseType
type: string
default: ''
steps:
# Skips the SQLServer setup if the databaseType does not match
- ${{ if eq(parameters.DatabaseType, 'SQLServer') }}:
# Start SQL Server Linux
- powershell: docker run --name mssql -d -p 1433:1433 -e "ACCEPT_EULA=Y" -e "MSSQL_SA_PASSWORD=${{ parameters.SA_PASSWORD }}" mcr.microsoft.com/mssql/server:2022-latest
displayName: Start SQL Server Docker image (Linux)
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
# Start SQL Server LocalDB Windows
- pwsh: SqlLocalDB start MSSQLLocalDB
displayName: Start SQL Server LocalDB (Windows)
condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT'))
# If we want to add additional environment variables to the run step, then we will skip these
- ${{ if eq(parameters.additionalEnvironmentVariables, False) }}:
# Run application for Linux
- bash: |
nohup dotnet run --project UmbracoProject --configuration ${{ parameters.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
# Run application for Windows
- pwsh: |
$process = Start-Process dotnet "run --project UmbracoProject --configuration ${{ parameters.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
-105
View File
@@ -1,105 +0,0 @@
parameters:
- name: ASPNETCORE_URLS
type: string
default: ''
- name: testCommand
type: string
default: ''
- name: port
type: string
default: ''
- name: AZUREB2CTESTUSEREMAIL
type: string
default: ''
- name: AZUREB2CTESTUSERPASSWORD
type: string
default: ''
- name: DatabaseType
type: string
default: ''
steps:
# Ensures we have the package wait-on installed
- pwsh: npm install wait-on
displayName: Install wait-on package
# Wait for either the port of the aspnetcore url
- pwsh: |
$Port = "${{ parameters.port }}"
$Url = "${{ parameters.ASPNETCORE_URLS }}"
if ($Port -ne "") {
Write-Host "Waiting on TCP port $Port"
npx wait-on -v --interval 1000 --timeout 120000 "tcp:$Port"
} else {
Write-Host "Waiting on URL $Url"
npx wait-on -v --interval 1000 --timeout 120000 "$Url"
}
displayName: Wait for application
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
# Install Playwright and dependencies
- pwsh: npx playwright install chromium
displayName: Install Playwright only with Chromium browser
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
# Test
- pwsh: ${{ parameters.testCommand }}
displayName: Run Playwright tests
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
env:
CI: true
CommitId: $(Build.SourceVersion)
AgentOs: $(Agent.OS)
AZUREADB2CTESTUSEREMAIL: ${{ parameters.AZUREB2CTESTUSEREMAIL }}
AZUREADB2CTESTUSERPASSWORD: ${{ parameters.AZUREB2CTESTUSERPASSWORD }}
# Stop application
- bash: kill -15 $(AcceptanceTestProcessId)
displayName: Stop application (Linux)
condition: and(succeededOrFailed(), ne(variables.AcceptanceTestProcessId, ''), eq(variables['Agent.OS'], 'Linux'))
- pwsh: Stop-Process -Id $(AcceptanceTestProcessId)
displayName: Stop application (Windows)
condition: and(succeededOrFailed(), ne(variables.AcceptanceTestProcessId, ''), eq(variables['Agent.OS'], 'Windows_NT'))
- ${{ if eq(parameters.DatabaseType, 'SQLServer') }}:
# Stop SQL Server
- pwsh: docker stop mssql
displayName: Stop SQL Server Docker image (Linux)
condition: and(succeededOrFailed(), eq(variables['Agent.OS'], 'Linux'))
- pwsh: SqlLocalDB stop MSSQLLocalDB
displayName: Stop SQL Server LocalDB (Windows)
condition: and(succeededOrFailed(), 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 Test Results - $(Agent.JobName) - Attempt #$(System.JobAttempt)"
# Publish test results
- task: PublishTestResults@2
displayName: "Publish test results"
condition: succeededOrFailed()
inputs:
testResultsFormat: 'JUnit'
testResultsFiles: '*.xml'
searchFolder: "tests/Umbraco.Tests.AcceptanceTest/results"
testRunTitle: "$(Agent.JobName)"
-69
View File
@@ -1,69 +0,0 @@
parameters:
- name: nodeVersion
type: string
default: ''
- name: PlaywrightUserEmail
type: string
default: ''
- name: PlaywrightPassword
type: string
default: ''
- name: ASPNETCORE_URLS
type: string
default: ''
- name: npm_config_cache
type: string
default: ''
steps:
- 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=${{ parameters.PlaywrightUserEmail }}
UMBRACO_USER_PASSWORD=${{ parameters.PlaywrightPassword }}
URL=${{ parameters.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: ${{ parameters.npm_config_cache }}
- script: npm ci --no-fund --no-audit --prefer-offline
workingDirectory: $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest
displayName: Restore NPM packages
# Install Template
- 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
displayName: Install Template
workingDirectory: $(Agent.BuildDirectory)/app
-629
View File
@@ -1,629 +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
# - v15/dev
parameters:
- name: differentAppSettingsAcceptanceTests
displayName: Run acceptance tests with different app settings
type: boolean
default: false
- name: skipDefaultConfigAcceptanceTests
displayName: Skip tests with DefaultConfig
type: boolean
default: false
- name: skipIntegrationTests
displayName: Skip integration tests
type: boolean
default: false
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
stages:
###############################################
## Build
###############################################
- stage: Build
jobs:
- job: A
displayName: Build Umbraco CMS
pool:
vmImage: "windows-latest"
steps:
- checkout: self
submodules: false
lfs: false,
fetchDepth: 500
- template: templates/backoffice-install.yml
- task: UseDotNet@2
displayName: Use .NET SDK from global.json
inputs:
useGlobalJson: true
- 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
- job: B
displayName: Build Bellissima Package
pool:
vmImage: "ubuntu-latest"
steps:
- checkout: self
submodules: false
lfs: false,
fetchDepth: 500
- 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: Integration
displayName: Integration Tests
dependsOn: Build
condition: ${{ eq(parameters.skipIntegrationTests, false) }}
jobs:
# Integration Tests (SQLite)
- job:
timeoutInMinutes: 180
displayName: Integration Tests (SQLite)
strategy:
matrix:
# Windows:
# vmImage: 'windows-latest'
# We split the tests into 4 parts for each OS to reduce the time it takes to run them on the pipeline
LinuxPart1Of4:
vmImage: "ubuntu-latest"
# Filter tests that are part of the Umbraco.Infrastructure namespace but not part of the Umbraco.Infrastructure.Service namespace
testFilter: "(FullyQualifiedName~Umbraco.Infrastructure) & (FullyQualifiedName!~Umbraco.Infrastructure.Service)"
LinuxPart2Of4:
vmImage: "ubuntu-latest"
# Filter tests that are part of the Umbraco.Infrastructure.Service namespace
testFilter: "(FullyQualifiedName~Umbraco.Infrastructure.Service)"
LinuxPart3Of4:
vmImage: "ubuntu-latest"
# Filter tests that are not part of the Umbraco.Infrastructure and ManagementApi namespace. So this will run all tests that are not part of the Umbraco.Infrastructure namespace
testFilter: "(FullyQualifiedName!~Umbraco.Infrastructure) & (FullyQualifiedName!~ManagementApi)"
LinuxPart4Of4:
vmImage: "ubuntu-latest"
# Filter tests that are part of the ManagementApi namespace
testFilter: "(FullyQualifiedName~ManagementApi)"
macOSPart1Of4:
vmImage: "macOS-latest"
# Filter tests that are part of the Umbraco.Infrastructure namespace but not part of the Umbraco.Infrastructure.Service namespace
testFilter: "(FullyQualifiedName~Umbraco.Infrastructure) & (FullyQualifiedName!~Umbraco.Infrastructure.Service)"
macOSPart2Of4:
vmImage: "macOS-latest"
# Filter tests that are part of the Umbraco.Infrastructure.Service namespace
testFilter: "(FullyQualifiedName~Umbraco.Infrastructure.Service)"
macOSPart3Of4:
vmImage: "macOS-latest"
# Filter tests that are not part of the Umbraco.Infrastructure and ManagementApi namespace.
testFilter: "(FullyQualifiedName!~Umbraco.Infrastructure) & (FullyQualifiedName!~ManagementApi)"
macOSPart4Of4:
vmImage: "macOS-latest"
# Filter tests that are part of the ManagementApi namespace.
testFilter: "(FullyQualifiedName~ManagementApi)"
pool:
vmImage: $(vmImage)
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
inputs:
artifact: build_output
path: $(Build.SourcesDirectory)
- task: UseDotNet@2
displayName: Use .NET SDK from global.json
inputs:
useGlobalJson: true
# Test
- task: DotNetCoreCLI@2
displayName: Run dotnet test
inputs:
command: test
projects: "tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj"
testRunTitle: Integration Tests SQLite - $(Agent.OS)
arguments: '--filter "$(testFilter)" --configuration $(buildConfiguration) --no-build'
# Integration Tests (SQL Server)
- job:
timeoutInMinutes: 180
displayName: Integration Tests (SQL Server)
variables:
SA_PASSWORD: UmbracoAcceptance123!
strategy:
matrix:
# We split the tests into 4 parts for each OS to reduce the time it takes to run them on the pipeline
WindowsPart1Of4:
vmImage: "windows-latest"
Tests__Database__DatabaseType: LocalDb
Tests__Database__SQLServerMasterConnectionString: N/A
# Filter tests that are part of the Umbraco.Infrastructure namespace but not part of the Umbraco.Infrastructure.Service namespace
testFilter: "(FullyQualifiedName~Umbraco.Infrastructure) & (FullyQualifiedName!~Umbraco.Infrastructure.Service)"
WindowsPart2Of4:
vmImage: "windows-latest"
Tests__Database__DatabaseType: LocalDb
Tests__Database__SQLServerMasterConnectionString: N/A
# Filter tests that are part of the Umbraco.Infrastructure.Service namespace
testFilter: "(FullyQualifiedName~Umbraco.Infrastructure.Service)"
WindowsPart3Of4:
vmImage: "windows-latest"
Tests__Database__DatabaseType: LocalDb
Tests__Database__SQLServerMasterConnectionString: N/A
# Filter tests that are not part of the Umbraco.Infrastructure and ManagementApi namespace.
testFilter: "(FullyQualifiedName!~Umbraco.Infrastructure) & (FullyQualifiedName!~ManagementApi)"
WindowsPart4Of4:
vmImage: "windows-latest"
Tests__Database__DatabaseType: LocalDb
Tests__Database__SQLServerMasterConnectionString: N/A
# Filter tests that are part of the ManagementApi namespace.
testFilter: "(FullyQualifiedName~ManagementApi)"
LinuxPart1Of4:
vmImage: "ubuntu-latest"
Tests__Database__DatabaseType: SqlServer
Tests__Database__SQLServerMasterConnectionString: "Server=(local);User Id=sa;Password=$(SA_PASSWORD);Encrypt=True;TrustServerCertificate=True"
# Filter tests that are part of the Umbraco.Infrastructure namespace but not part of the Umbraco.Infrastructure.Service namespace
testFilter: "(FullyQualifiedName~Umbraco.Infrastructure) & (FullyQualifiedName!~Umbraco.Infrastructure.Service)"
LinuxPart2Of4:
vmImage: "ubuntu-latest"
Tests__Database__DatabaseType: SqlServer
Tests__Database__SQLServerMasterConnectionString: "Server=(local);User Id=sa;Password=$(SA_PASSWORD);Encrypt=True;TrustServerCertificate=True"
# Filter tests that are part of the Umbraco.Infrastructure.Service namespace
testFilter: "(FullyQualifiedName~Umbraco.Infrastructure.Service)"
LinuxPart3Of4:
vmImage: "ubuntu-latest"
Tests__Database__DatabaseType: SqlServer
Tests__Database__SQLServerMasterConnectionString: "Server=(local);User Id=sa;Password=$(SA_PASSWORD);Encrypt=True;TrustServerCertificate=True"
# Filter tests that are not part of the Umbraco.Infrastructure and ManagementApi namespace.
testFilter: "(FullyQualifiedName!~Umbraco.Infrastructure) & (FullyQualifiedName!~ManagementApi)"
LinuxPart4Of4:
vmImage: "ubuntu-latest"
Tests__Database__DatabaseType: SqlServer
Tests__Database__SQLServerMasterConnectionString: "Server=(local);User Id=sa;Password=$(SA_PASSWORD);Encrypt=True;TrustServerCertificate=True"
# Filter tests that are part of the ManagementApi namespace.
testFilter: "(FullyQualifiedName~ManagementApi)"
pool:
vmImage: $(vmImage)
steps:
# Setup test environment
- task: DownloadPipelineArtifact@2
displayName: Download build artifacts
inputs:
artifact: build_output
path: $(Build.SourcesDirectory)
- task: UseDotNet@2
displayName: Use .NET SDK from global.json
inputs:
useGlobalJson: true
# 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'))
- powershell: |
$maxAttempts = 12
$attempt = 0
$status = ""
while (($status -ne 'running') -and ($attempt -lt $maxAttempts)) {
Start-Sleep -Seconds 5
# We use the docker inspect command to check the status of the container. If the container is not running, we wait 5 seconds and try again. And if reaches 12 attempts, we fail the build.
$status = docker inspect -f '{{.State.Status}}' mssql
if ($status -ne 'running') {
Write-Host "Waiting for SQL Server to be ready... Attempt $($attempt + 1)"
$attempt++
}
}
if ($status -eq 'running') {
Write-Host "SQL Server container is running"
docker ps -a
} else {
Write-Host "SQL Server did not become ready in time. Last known status: $status"
docker logs mssql
exit 1
}
displayName: Wait for SQL Server to be ready (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'))
# Test
- task: DotNetCoreCLI@2
displayName: Run dotnet test
inputs:
command: test
projects: "tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj"
testRunTitle: Integration Tests SQL Server - $(Agent.OS)
arguments: '--filter "$(testFilter)" --configuration $(buildConfiguration) --no-build'
# 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'))
- stage: DefaultConfigE2E
displayName: Default Config 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
condition: ${{ eq(parameters.skipDefaultConfigAcceptanceTests, false) }}
variables:
# Connection string
CONNECTIONSTRINGS__UMBRACODBDSN: Data Source=Umbraco;Mode=Memory;Cache=Shared;Foreign Keys=True;Pooling=True
CONNECTIONSTRINGS__UMBRACODBDSN_PROVIDERNAME: Microsoft.Data.Sqlite
DatabaseType: SQLite
strategy:
matrix:
LinuxPart1Of3:
vmImage: "ubuntu-latest"
testFolder: "DefaultConfig"
testCommand: "npm run testSqlite -- --shard=1/3"
LinuxPart2Of3:
vmImage: "ubuntu-latest"
testFolder: "DefaultConfig"
testCommand: "npm run testSqlite -- --shard=2/3"
LinuxPart3Of3:
vmImage: "ubuntu-latest"
testFolder: "DefaultConfig"
testCommand: "npm run testSqlite -- --shard=3/3"
WindowsPart1Of3:
vmImage: "windows-latest"
testFolder: "DefaultConfig"
testCommand: "npm run testSqlite -- --shard=1/3"
WindowsPart2Of3:
vmImage: "windows-latest"
testFolder: "DefaultConfig"
testCommand: "npm run testSqlite -- --shard=2/3"
WindowsPart3Of3:
vmImage: "windows-latest"
testFolder: "DefaultConfig"
testCommand: "npm run testSqlite -- --shard=3/3"
pool:
vmImage: $(vmImage)
steps:
# Setup test environment Template
- template: nightly-E2E-setup-template.yml
parameters:
nodeVersion: ${{ variables.nodeVersion }}
PlaywrightUserEmail: ${{ variables.UMBRACO__CMS__UNATTENDED__UNATTENDEDUSEREMAIL }}
PlaywrightPassword: ${{ variables.UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD }}
ASPNETCORE_URLS: ${{ variables.ASPNETCORE_URLS }}
npm_config_cache: ${{ variables.npm_config_cache }}
- pwsh: |
dotnet restore UmbracoProject
cp $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest.UmbracoProject/*.cs UmbracoProject
displayName: Restore project
workingDirectory: $(Agent.BuildDirectory)/app
- pwsh: |
dotnet build UmbracoProject --configuration ${{ variables.buildConfiguration }} --no-restore
dotnet dev-certs https
displayName: Build application
workingDirectory: $(Agent.BuildDirectory)/app
condition: succeeded()
# Run application Template
- template: nightly-E2E-run-application-template.yml
parameters:
DatabaseType: ${{ variables.DatabaseType }}
buildConfiguration: ${{ variables.buildConfiguration }}
# Run tests Template
- template: nightly-E2E-run-tests-template.yml
parameters:
testCommand: $(testCommand)
ASPNETCORE_URLS: ${{ variables.ASPNETCORE_URLS }}
DatabaseType: ${{ variables.DatabaseType }}
- job:
displayName: E2E Tests (SQL Server)
timeoutInMinutes: 180
condition: ${{ eq(parameters.skipDefaultConfigAcceptanceTests, false) }}
variables:
# Connection string
CONNECTIONSTRINGS__UMBRACODBDSN: Data Source=(localdb)\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\Umbraco.mdf;Integrated Security=True
CONNECTIONSTRINGS__UMBRACODBDSN_PROVIDERNAME: Microsoft.Data.SqlClient
DatabaseType: SQLServer
SA_PASSWORD: UmbracoAcceptance123!
strategy:
matrix:
LinuxPart1Of3:
testCommand: "npm run test -- --shard=1/3"
testFolder: "DefaultConfig"
vmImage: "ubuntu-latest"
CONNECTIONSTRINGS__UMBRACODBDSN: "Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);Encrypt=True;TrustServerCertificate=True"
LinuxPart2Of3:
testCommand: "npm run test -- --shard=2/3"
testFolder: "DefaultConfig"
vmImage: "ubuntu-latest"
CONNECTIONSTRINGS__UMBRACODBDSN: "Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);Encrypt=True;TrustServerCertificate=True"
LinuxPart3Of3:
testCommand: "npm run test -- --shard=3/3"
testFolder: "DefaultConfig"
vmImage: "ubuntu-latest"
CONNECTIONSTRINGS__UMBRACODBDSN: "Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);Encrypt=True;TrustServerCertificate=True"
WindowsPart1Of3:
testCommand: "npm run test -- --shard=1/3"
testFolder: "DefaultConfig"
vmImage: "windows-latest"
WindowsPart2Of3:
testCommand: "npm run test -- --shard=2/3"
testFolder: "DefaultConfig"
vmImage: "windows-latest"
WindowsPart3Of3:
testCommand: "npm run test -- --shard=3/3"
testFolder: "DefaultConfig"
vmImage: "windows-latest"
pool:
vmImage: $(vmImage)
steps:
# Setup test environment Template
- template: nightly-E2E-setup-template.yml
parameters:
nodeVersion: ${{ variables.nodeVersion }}
PlaywrightUserEmail: ${{ variables.UMBRACO__CMS__UNATTENDED__UNATTENDEDUSEREMAIL }}
PlaywrightPassword: ${{ variables.UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD }}
ASPNETCORE_URLS: ${{ variables.ASPNETCORE_URLS }}
npm_config_cache: ${{ variables.npm_config_cache }}
- pwsh: |
dotnet restore UmbracoProject
cp $(Build.SourcesDirectory)/tests/Umbraco.Tests.AcceptanceTest.UmbracoProject/*.cs UmbracoProject
displayName: Restore project
workingDirectory: $(Agent.BuildDirectory)/app
- pwsh: |
dotnet build UmbracoProject --configuration ${{ variables.buildConfiguration }} --no-restore
dotnet dev-certs https
displayName: Build application
workingDirectory: $(Agent.BuildDirectory)/app
condition: succeeded()
# Run application Template
- template: nightly-E2E-run-application-template.yml
parameters:
SA_PASSWORD: ${{ variables.SA_PASSWORD }}
buildConfiguration: ${{ variables.buildConfiguration }}
DatabaseType: ${{ variables.DatabaseType }}
# Run tests Template
- template: nightly-E2E-run-tests-template.yml
parameters:
testCommand: $(testCommand)
ASPNETCORE_URLS: ${{ variables.ASPNETCORE_URLS }}
DatabaseType: ${{ variables.DatabaseType }}
- stage: AdditionalConfigE2E
displayName: Additional Config E2E Tests
dependsOn: Build
variables:
npm_config_cache: $(Pipeline.Workspace)/.npm_e2e
ASPNETCORE_URLS: https://localhost:44331
PlaywrightPassword: UmbracoAcceptance123!
PlaywrightUserEmail: playwright@umbraco.com
jobs:
- job:
displayName: E2E Tests with Different App settings (SQL Server)
condition: ${{ eq(parameters.differentAppSettingsAcceptanceTests, true) }}
timeoutInMinutes: 180
variables:
SA_PASSWORD: UmbracoAcceptance123!
DatabaseType: SQLServer
strategy:
matrix:
# UnattendedInstallConfig
WindowsUnattendedInstallConfig:
vmImage: "windows-latest"
testFolder: "UnattendedInstallConfig"
testCommand: "npx playwright test --project=unattendedInstallConfig --grep=InstallSQLServer"
port: 44331
additionalEnvironmentVariables: false
# DeliveryApiConfig
WindowsDeliveryApiConfig:
vmImage: "windows-latest"
testFolder: "DeliveryApi"
port: ''
testCommand: "npx playwright test --project=deliveryApi"
CONNECTIONSTRINGS__UMBRACODBDSN: Data Source=(localdb)\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\Umbraco.mdf;Integrated Security=True
CONNECTIONSTRINGS__UMBRACODBDSN_PROVIDERNAME: Microsoft.Data.SqlClient
additionalEnvironmentVariables: false
LinuxDeliveryApiConfig:
vmImage: "ubuntu-latest"
testFolder: "DeliveryApi"
port: ''
testCommand: "npx playwright test --project=deliveryApi"
CONNECTIONSTRINGS__UMBRACODBDSN: Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);Encrypt=True;TrustServerCertificate=True
CONNECTIONSTRINGS__UMBRACODBDSN_PROVIDERNAME: Microsoft.Data.SqlClient
additionalEnvironmentVariables: false
# ExternalLogin AzureADB2C
WindowsExternalLoginAzureADB2C:
vmImage: "windows-latest"
testFolder: "ExternalLogin\\AzureADB2C"
testCommand: "npx playwright test --project=externalLoginAzureADB2C"
port: 44331
packageName: "Microsoft.AspNetCore.Authentication.OpenIdConnect"
packageVersion: "9.0.8"
CONNECTIONSTRINGS__UMBRACODBDSN: Data Source=(localdb)\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\Umbraco.mdf;Integrated Security=True
CONNECTIONSTRINGS__UMBRACODBDSN_PROVIDERNAME: Microsoft.Data.SqlClient
additionalEnvironmentVariables: true
pool:
vmImage: $(vmImage)
steps:
# Setup test environment Template
- template: nightly-E2E-setup-template.yml
parameters:
nodeVersion: ${{ variables.nodeVersion }}
PlaywrightUserEmail: ${{ variables.PlaywrightUserEmail }}
PlaywrightPassword: ${{ variables.PlaywrightPassword }}
ASPNETCORE_URLS: ${{ variables.ASPNETCORE_URLS }}
npm_config_cache: ${{ variables.npm_config_cache }}
# Install NuGet package if specified in the matrix
- pwsh: |
Write-Host "Installing package $(packageName) version $(packageVersion)"
dotnet add package $(packageName) --version $(packageVersion)
displayName: "Install NuGet package: $(packageName)"
workingDirectory: $(Agent.BuildDirectory)/app/UmbracoProject
condition: and(succeeded(), ne(variables['packageName'], ''), ne(variables['packageVersion'], ''))
# Build application Template
- template: nightly-E2E-build-template.yml
parameters:
testFolder: $(testFolder)
buildConfiguration: ${{ variables.buildConfiguration }}
additionalEnvironmentVariables: ${{ eq(variables['additionalEnvironmentVariables'], true) }}
# Build application for AzureADB2C
- pwsh: |
dotnet build UmbracoProject --configuration ${{ variables.buildConfiguration }} --no-restore
dotnet dev-certs https
displayName: Build application for AzureADB2C
workingDirectory: $(Agent.BuildDirectory)/app
env:
AZUREADB2CDOMAIN: $(AZUREB2CDOMAIN)
AZUREADB2CTENANT: $(AZUREB2CTENANT)
AZUREADB2CPOLICY: $(AZUREB2CPOLICY)
AZUREADB2CCLIENTID: $(AZUREB2CCLIENTID)
AZUREADB2CCLIENTSECRET: $(AZUREB2CCLIENTSECRET)
condition: and(succeeded(), eq(variables['testFolder'], 'ExternalLogin\AzureADB2C'))
# Run application Template
- template: nightly-E2E-run-application-template.yml
parameters:
SA_PASSWORD: ${{ variables.SA_PASSWORD }}
additionalEnvironmentVariables: ${{ eq(variables['additionalEnvironmentVariables'], true ) }}
buildConfiguration: ${{ variables.buildConfiguration }}
DatabaseType: ${{ variables.DatabaseType }}
# Run application for Linux with additional Environment Variables for Azure AD
- bash: |
nohup dotnet run --project UmbracoProject --configuration ${{ variables.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'), eq(variables['testFolder'], 'ExternalLogin\AzureADB2C'))
workingDirectory: $(Agent.BuildDirectory)/app
env:
AZUREADB2CDOMAIN: $(AZUREB2CDOMAIN)
AZUREADB2CTENANT: $(AZUREB2CTENANT)
AZUREADB2CPOLICY: $(AZUREB2CPOLICY)
AZUREADB2CCLIENTID: $(AZUREB2CCLIENTID)
AZUREADB2CCLIENTSECRET: $(AZUREB2CCLIENTSECRET)
# Run application for Windows with additional Environment Variables for Azure AD
- pwsh: |
$process = Start-Process dotnet "run --project UmbracoProject --configuration ${{ variables.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'), eq(variables['testFolder'], 'ExternalLogin\AzureADB2C'))
workingDirectory: $(Agent.BuildDirectory)/app
env:
AZUREADB2CDOMAIN: $(AZUREB2CDOMAIN)
AZUREADB2CTENANT: $(AZUREB2CTENANT)
AZUREADB2CPOLICY: $(AZUREB2CPOLICY)
AZUREADB2CCLIENTID: $(AZUREB2CCLIENTID)
AZUREADB2CCLIENTSECRET: $(AZUREB2CCLIENTSECRET)
# Run tests Template
- template: nightly-E2E-run-tests-template.yml
parameters:
testCommand: $(testCommand)
ASPNETCORE_URLS: ${{ variables.ASPNETCORE_URLS }}
port: $(port)
AZUREB2CTESTUSEREMAIL: $(AZUREB2CTESTUSEREMAIL)
AZUREB2CTESTUSERPASSWORD: $(AZUREB2CTESTUSERPASSWORD)
DatabaseType: ${{ variables.DatabaseType }}
+1 -2
View File
@@ -9,10 +9,9 @@ schedules:
branches:
include:
- v10/dev
- v12/dev
- v13/dev
- v14/dev
- v15/dev
- v16/dev
steps:
- checkout: none
-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
+2 -3
View File
@@ -1,7 +1,6 @@
{
"sdk": {
"version": "9.0.100",
"rollForward": "latestFeature",
"allowPrerelease": false
"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;
}
@@ -1,5 +1,4 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.Extensions.DependencyInjection;
@@ -7,32 +6,24 @@ using Microsoft.Extensions.Options;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
using Umbraco.Cms.Api.Common.OpenApi;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Common.Configuration;
public class ConfigureUmbracoSwaggerGenOptions : IConfigureOptions<SwaggerGenOptions>
{
private readonly IOptions<ApiVersioningOptions> _apiVersioningOptions;
private readonly IOperationIdSelector _operationIdSelector;
private readonly ISchemaIdSelector _schemaIdSelector;
private readonly ISubTypesSelector _subTypesSelector;
[Obsolete("Use non-obsolete constructor. This will be removed in Umbraco 16.")]
public ConfigureUmbracoSwaggerGenOptions(
IOptions<ApiVersioningOptions> apiVersioningOptions,
IOperationIdSelector operationIdSelector,
ISchemaIdSelector schemaIdSelector)
: this(operationIdSelector, schemaIdSelector, StaticServiceProvider.Instance.GetRequiredService<ISubTypesSelector>())
{ }
public ConfigureUmbracoSwaggerGenOptions(
IOperationIdSelector operationIdSelector,
ISchemaIdSelector schemaIdSelector,
ISubTypesSelector subTypesSelector)
{
_apiVersioningOptions = apiVersioningOptions;
_operationIdSelector = operationIdSelector;
_schemaIdSelector = schemaIdSelector;
_subTypesSelector = subTypesSelector;
}
public void Configure(SwaggerGenOptions swaggerGenOptions)
@@ -43,31 +34,32 @@ 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 (api.ActionDescriptor is ControllerActionDescriptor controllerActionDescriptor
&& controllerActionDescriptor.MethodInfo.HasMapToApiAttribute(name))
if (string.IsNullOrWhiteSpace(api.GroupName))
{
return true;
return false;
}
ApiVersionMetadata apiVersionMetadata = api.ActionDescriptor.GetApiVersionMetadata();
return apiVersionMetadata.Name == name
|| (string.IsNullOrEmpty(apiVersionMetadata.Name) && name == DefaultApiConfiguration.ApiName);
if (api.ActionDescriptor is ControllerActionDescriptor controllerActionDescriptor)
{
return controllerActionDescriptor.MethodInfo.HasMapToApiAttribute(name);
}
return false;
});
swaggerGenOptions.TagActionsBy(api => new[] { api.GroupName });
swaggerGenOptions.OrderActionsBy(ActionOrderBy);
swaggerGenOptions.SchemaFilter<EnumSchemaFilter>();
swaggerGenOptions.CustomSchemaIds(_schemaIdSelector.SchemaId);
swaggerGenOptions.SelectSubTypesUsing(_subTypesSelector.SubTypes);
swaggerGenOptions.SupportNonNullableReferenceTypes();
}
// see https://github.com/domaindrivendev/Swashbuckle.AspNetCore#change-operation-sort-order-eg-for-ui-sorting
private static string ActionOrderBy(ApiDescription apiDesc)
=> $"{apiDesc.GroupName}_{apiDesc.ActionDescriptor.AttributeRouteInfo?.Template ?? apiDesc.ActionDescriptor.RouteValues["controller"]}_{(apiDesc.ActionDescriptor.RouteValues.TryGetValue("action", out var action) ? action : null)}_{apiDesc.HttpMethod}";
=> $"{apiDesc.GroupName}_{apiDesc.ActionDescriptor.AttributeRouteInfo?.Template ?? apiDesc.ActionDescriptor.RouteValues["controller"]}_{apiDesc.ActionDescriptor.RouteValues["action"]}_{apiDesc.HttpMethod}";
}
@@ -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,20 +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.AddSingleton<ISubTypesSelector, SubTypesSelector>();
builder.Services.AddSingleton<ISubTypesHandler, SubTypesHandler>();
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,20 +31,17 @@ 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))
.SetEndSessionEndpointUris(
Paths.MemberApi.LogoutEndpoint.TrimStart(Constants.CharArrays.ForwardSlash),
Paths.BackOfficeApi.LogoutEndpoint.TrimStart(Constants.CharArrays.ForwardSlash))
Paths.MemberApi.TokenEndpoint.TrimStart(Constants.CharArrays.ForwardSlash))
.SetLogoutEndpointUris(
Paths.MemberApi.LogoutEndpoint.TrimStart(Constants.CharArrays.ForwardSlash))
.SetRevocationEndpointUris(
Paths.MemberApi.RevokeEndpoint.TrimStart(Constants.CharArrays.ForwardSlash),
Paths.BackOfficeApi.RevokeEndpoint.TrimStart(Constants.CharArrays.ForwardSlash))
.SetUserInfoEndpointUris(
Paths.MemberApi.RevokeEndpoint.TrimStart(Constants.CharArrays.ForwardSlash))
.SetUserinfoEndpointUris(
Paths.MemberApi.UserinfoEndpoint.TrimStart(Constants.CharArrays.ForwardSlash));
// Enable authorization code flow with PKCE
@@ -56,16 +50,12 @@ public static class UmbracoBuilderAuthExtensions
.RequireProofKeyForCodeExchange()
.AllowRefreshTokenFlow();
// Enable the client credentials flow.
options.AllowClientCredentialsFlow();
// Register the ASP.NET Core host and configure for custom authentication endpoint.
options
.UseAspNetCore()
.EnableAuthorizationEndpointPassthrough()
.EnableTokenEndpointPassthrough()
.EnableEndSessionEndpointPassthrough()
.EnableUserInfoEndpointPassthrough();
.EnableLogoutEndpointPassthrough()
.EnableUserinfoEndpointPassthrough();
// Enable reference tokens
// - see https://documentation.openiddict.com/configuration/token-storage.html
@@ -73,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)
@@ -106,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.
@@ -130,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,8 +0,0 @@
namespace Umbraco.Cms.Api.Common.OpenApi;
public interface ISubTypesHandler
{
bool CanHandle(Type type, string documentName);
IEnumerable<Type> Handle(Type type);
}
@@ -1,6 +0,0 @@
namespace Umbraco.Cms.Api.Common.OpenApi;
public interface ISubTypesSelector
{
IEnumerable<Type> SubTypes(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,25 +0,0 @@
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace Umbraco.Cms.Api.Common.OpenApi;
/// <summary>
/// This filter explicitly removes all security schemes from a named OpenAPI document.
/// </summary>
public class RemoveSecuritySchemesDocumentFilter : IDocumentFilter
{
private readonly string _documentName;
public RemoveSecuritySchemesDocumentFilter(string documentName)
=> _documentName = documentName;
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
{
if (context.DocumentName != _documentName)
{
return;
}
swaggerDoc.Components.SecuritySchemes.Clear();
}
}
@@ -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);
}
}
@@ -1,20 +0,0 @@
using Umbraco.Cms.Api.Common.Serialization;
namespace Umbraco.Cms.Api.Common.OpenApi;
public class SubTypesHandler : ISubTypesHandler
{
private readonly IUmbracoJsonTypeInfoResolver _umbracoJsonTypeInfoResolver;
public SubTypesHandler(IUmbracoJsonTypeInfoResolver umbracoJsonTypeInfoResolver)
=> _umbracoJsonTypeInfoResolver = umbracoJsonTypeInfoResolver;
protected virtual bool CanHandle(Type type)
=> type.Namespace?.StartsWith("Umbraco.Cms") is true;
public virtual bool CanHandle(Type type, string documentName)
=> CanHandle(type);
public virtual IEnumerable<Type> Handle(Type type)
=> _umbracoJsonTypeInfoResolver.FindSubTypes(type);
}
@@ -1,60 +0,0 @@
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Api.Common.Serialization;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Hosting;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Common.OpenApi;
public class SubTypesSelector : ISubTypesSelector
{
private readonly IOptions<GlobalSettings> _settings;
private readonly IHostingEnvironment _hostingEnvironment;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IEnumerable<ISubTypesHandler> _subTypeHandlers;
private readonly IUmbracoJsonTypeInfoResolver _umbracoJsonTypeInfoResolver;
public SubTypesSelector(
IOptions<GlobalSettings> settings,
IHostingEnvironment hostingEnvironment,
IHttpContextAccessor httpContextAccessor,
IEnumerable<ISubTypesHandler> subTypeHandlers,
IUmbracoJsonTypeInfoResolver umbracoJsonTypeInfoResolver)
{
_settings = settings;
_hostingEnvironment = hostingEnvironment;
_httpContextAccessor = httpContextAccessor;
_subTypeHandlers = subTypeHandlers;
_umbracoJsonTypeInfoResolver = umbracoJsonTypeInfoResolver;
}
public IEnumerable<Type> SubTypes(Type type)
{
var backOfficePath = _settings.Value.GetBackOfficePath(_hostingEnvironment);
var swaggerPath = $"{backOfficePath}/swagger";
if (_httpContextAccessor.HttpContext?.Request.Path.StartsWithSegments(swaggerPath) ?? false)
{
// Split the path into segments
var segments = _httpContextAccessor.HttpContext.Request.Path.Value!
.Substring(swaggerPath.Length)
.TrimStart(Constants.CharArrays.ForwardSlash)
.Split(Constants.CharArrays.ForwardSlash);
// Extract the document name from the path
var documentName = segments[0];
// Find the first handler that can handle the type / document name combination
ISubTypesHandler? handler = _subTypeHandlers.FirstOrDefault(h => h.CanHandle(type, documentName));
if (handler != null)
{
return handler.Handle(type);
}
}
// Default implementation to maintain backwards compatibility
return _umbracoJsonTypeInfoResolver.FindSubTypes(type);
}
}
@@ -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)
@@ -9,14 +9,13 @@
</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" />
</ItemGroup>
<ItemGroup>
<!-- Both OpenIddict.AspNetCore, Npoco.SqlServer and Microsoft.EntityFrameworkCore.SqlServer bring in a vulnerable version of Microsoft.IdentityModel.JsonWebTokens -->
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens"/>
<!-- Take top-level depedendency on OpenIddict.AspNetCore depends on a vulnerable version -->
<PackageReference Include="Microsoft.Extensions.Caching.Memory" />
</ItemGroup>
@@ -18,12 +18,7 @@ internal sealed class DeliveryApiOutputCachePolicy : IOutputCachePolicy
.RequestServices
.GetRequiredService<IRequestPreviewService>();
IApiAccessService apiAccessService = context
.HttpContext
.RequestServices
.GetRequiredService<IApiAccessService>();
context.EnableOutputCaching = requestPreviewService.IsPreview() is false && apiAccessService.HasPublicAccess();
context.EnableOutputCaching = requestPreviewService.IsPreview() is false;
context.ResponseExpirationTimeSpan = _duration;
return ValueTask.CompletedTask;
@@ -1,19 +0,0 @@
using Microsoft.AspNetCore.OutputCaching;
namespace Umbraco.Cms.Api.Delivery.Caching;
internal sealed class NoOutputCachePolicy : IOutputCachePolicy
{
ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context, CancellationToken cancellationToken)
{
context.EnableOutputCaching = false;
return ValueTask.CompletedTask;
}
ValueTask IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context, CancellationToken cancellationToken)
=> ValueTask.CompletedTask;
ValueTask IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context, CancellationToken cancellationToken)
=> ValueTask.CompletedTask;
}
@@ -21,7 +21,6 @@ public class ConfigureUmbracoDeliveryApiSwaggerGenOptions: IConfigureOptions<Swa
});
swaggerGenOptions.DocumentFilter<MimeTypeDocumentFilter>(DeliveryApiConfiguration.ApiName);
swaggerGenOptions.DocumentFilter<RemoveSecuritySchemesDocumentFilter>(DeliveryApiConfiguration.ApiName);
swaggerGenOptions.OperationFilter<SwaggerContentDocumentationFilter>();
swaggerGenOptions.OperationFilter<SwaggerMediaDocumentationFilter>();
@@ -17,16 +17,33 @@ namespace Umbraco.Cms.Api.Delivery.Configuration;
/// </remarks>
public class ConfigureUmbracoMemberAuthenticationDeliveryApiSwaggerGenOptions : IConfigureOptions<SwaggerGenOptions>
{
private const string AuthSchemeName = "UmbracoMember";
private const string AuthSchemeName = "Umbraco Member";
public void Configure(SwaggerGenOptions options)
{
options.AddSecurityDefinition(
AuthSchemeName,
new OpenApiSecurityScheme
{
In = ParameterLocation.Header,
Name = AuthSchemeName,
Type = SecuritySchemeType.OAuth2,
Description = "Umbraco Member Authentication",
Flows = new OpenApiOAuthFlows
{
AuthorizationCode = new OpenApiOAuthFlow
{
AuthorizationUrl = new Uri(Paths.MemberApi.AuthorizationEndpoint, UriKind.Relative),
TokenUrl = new Uri(Paths.MemberApi.TokenEndpoint, UriKind.Relative)
}
}
});
// add security requirements for content API operations
options.DocumentFilter<DeliveryApiSecurityFilter>();
options.OperationFilter<DeliveryApiSecurityFilter>();
}
private class DeliveryApiSecurityFilter : SwaggerFilterBase<ContentApiControllerBase>, IOperationFilter, IDocumentFilter
private class DeliveryApiSecurityFilter : SwaggerFilterBase<ContentApiControllerBase>, IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
@@ -48,36 +65,10 @@ public class ConfigureUmbracoMemberAuthenticationDeliveryApiSwaggerGenOptions :
Id = AuthSchemeName,
}
},
[]
new string[] { }
}
}
};
}
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
{
if (context.DocumentName != DeliveryApiConfiguration.ApiName)
{
return;
}
swaggerDoc.Components.SecuritySchemes.Add(
AuthSchemeName,
new OpenApiSecurityScheme
{
In = ParameterLocation.Header,
Name = AuthSchemeName,
Type = SecuritySchemeType.OAuth2,
Description = "Umbraco Member Authentication",
Flows = new OpenApiOAuthFlows
{
AuthorizationCode = new OpenApiOAuthFlow
{
AuthorizationUrl = new Uri(Paths.MemberApi.AuthorizationEndpoint, UriKind.Relative),
TokenUrl = new Uri(Paths.MemberApi.TokenEndpoint, UriKind.Relative)
}
}
});
}
}
}
@@ -1,10 +1,12 @@
using System.Diagnostics;
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;
@@ -14,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,
@@ -47,17 +75,19 @@ public class ByIdContentApiController : ContentApiItemControllerBase
private async Task<IActionResult> HandleRequest(Guid id)
{
IPublishedContent? contentItem = await ApiPublishedContentCache.GetByIdAsync(id).ConfigureAwait(false);
IPublishedContent? contentItem = ApiPublishedContentCache.GetById(id);
if (contentItem is null)
{
return NotFound();
}
IActionResult? deniedAccessResult = await HandleMemberAccessAsync(contentItem, _requestMemberAccessService).ConfigureAwait(false);
IActionResult? deniedAccessResult = await HandleMemberAccessAsync(contentItem, _requestMemberAccessService);
if (deniedAccessResult is not null)
{
return deniedAccessResult;
}
IApiContentResponse? apiContentResponse = ApiContentResponseBuilder.Build(contentItem);
if (apiContentResponse is null)
{
@@ -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,
@@ -45,7 +74,7 @@ public class ByIdsContentApiController : ContentApiItemControllerBase
private async Task<IActionResult> HandleRequest(HashSet<Guid> ids)
{
IPublishedContent[] contentItems = (await ApiPublishedContentCache.GetByIdsAsync(ids).ConfigureAwait(false)).ToArray();
IPublishedContent[] contentItems = ApiPublishedContentCache.GetByIds(ids).ToArray();
IActionResult? deniedAccessResult = await HandleMemberAccessAsync(contentItems, _requestMemberAccessService);
if (deniedAccessResult is not null)
@@ -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)
{
@@ -12,10 +12,8 @@ namespace Umbraco.Cms.Api.Delivery.Controllers.Media;
[ApiVersion("2.0")]
public class ByIdMediaApiController : MediaApiControllerBase
{
public ByIdMediaApiController(
IPublishedMediaCache publishedMediaCache,
IApiMediaWithCropsResponseBuilder apiMediaWithCropsResponseBuilder)
: base(publishedMediaCache, apiMediaWithCropsResponseBuilder)
public ByIdMediaApiController(IPublishedSnapshotAccessor publishedSnapshotAccessor, IApiMediaWithCropsResponseBuilder apiMediaWithCropsResponseBuilder)
: base(publishedSnapshotAccessor, apiMediaWithCropsResponseBuilder)
{
}
@@ -13,8 +13,8 @@ namespace Umbraco.Cms.Api.Delivery.Controllers.Media;
[ApiVersion("2.0")]
public class ByIdsMediaApiController : MediaApiControllerBase
{
public ByIdsMediaApiController(IPublishedMediaCache publishedMediaCache, IApiMediaWithCropsResponseBuilder apiMediaWithCropsResponseBuilder)
: base(publishedMediaCache, apiMediaWithCropsResponseBuilder)
public ByIdsMediaApiController(IPublishedSnapshotAccessor publishedSnapshotAccessor, IApiMediaWithCropsResponseBuilder apiMediaWithCropsResponseBuilder)
: base(publishedSnapshotAccessor, apiMediaWithCropsResponseBuilder)
{
}
@@ -16,10 +16,10 @@ public class ByPathMediaApiController : MediaApiControllerBase
private readonly IApiMediaQueryService _apiMediaQueryService;
public ByPathMediaApiController(
IPublishedMediaCache publishedMediaCache,
IPublishedSnapshotAccessor publishedSnapshotAccessor,
IApiMediaWithCropsResponseBuilder apiMediaWithCropsResponseBuilder,
IApiMediaQueryService apiMediaQueryService)
: base(publishedMediaCache, apiMediaWithCropsResponseBuilder)
: base(publishedSnapshotAccessor, apiMediaWithCropsResponseBuilder)
=> _apiMediaQueryService = apiMediaQueryService;
[HttpGet("item/{*path}")]
@@ -20,15 +20,18 @@ namespace Umbraco.Cms.Api.Delivery.Controllers.Media;
public abstract class MediaApiControllerBase : DeliveryApiControllerBase
{
private readonly IApiMediaWithCropsResponseBuilder _apiMediaWithCropsResponseBuilder;
private IPublishedMediaCache _publishedMediaCache;
private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor;
private IPublishedMediaCache? _publishedMediaCache;
protected MediaApiControllerBase(IPublishedMediaCache publishedMediaCache, IApiMediaWithCropsResponseBuilder apiMediaWithCropsResponseBuilder)
protected MediaApiControllerBase(IPublishedSnapshotAccessor publishedSnapshotAccessor, IApiMediaWithCropsResponseBuilder apiMediaWithCropsResponseBuilder)
{
_publishedMediaCache = publishedMediaCache;
_publishedSnapshotAccessor = publishedSnapshotAccessor;
_apiMediaWithCropsResponseBuilder = apiMediaWithCropsResponseBuilder;
}
protected IPublishedMediaCache PublishedMediaCache => _publishedMediaCache;
protected IPublishedMediaCache PublishedMediaCache => _publishedMediaCache
??= _publishedSnapshotAccessor.GetRequiredPublishedSnapshot().Media
?? throw new InvalidOperationException("Could not obtain the published media cache");
protected IApiMediaWithCropsResponse BuildApiMediaWithCrops(IPublishedContent media)
=> _apiMediaWithCropsResponseBuilder.Build(media);
@@ -21,10 +21,10 @@ public class QueryMediaApiController : MediaApiControllerBase
private readonly IApiMediaQueryService _apiMediaQueryService;
public QueryMediaApiController(
IPublishedMediaCache publishedMediaCache,
IPublishedSnapshotAccessor publishedSnapshotAccessor,
IApiMediaWithCropsResponseBuilder apiMediaWithCropsResponseBuilder,
IApiMediaQueryService apiMediaQueryService)
: base(publishedMediaCache, apiMediaWithCropsResponseBuilder)
: base(publishedSnapshotAccessor, apiMediaWithCropsResponseBuilder)
=> _apiMediaQueryService = apiMediaQueryService;
[HttpGet]
@@ -1,11 +1,10 @@
using System.Security.Claims;
using System.Security.Claims;
using Asp.Versioning;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using OpenIddict.Abstractions;
@@ -13,61 +12,36 @@ using OpenIddict.Server.AspNetCore;
using Umbraco.Cms.Api.Delivery.Routing;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.DependencyInjection;
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
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IMemberSignInManager _memberSignInManager;
private readonly IMemberManager _memberManager;
private readonly IMemberClientCredentialsManager _memberClientCredentialsManager;
private readonly DeliveryApiSettings _deliveryApiSettings;
private readonly ILogger<MemberController> _logger;
[Obsolete("Please use the non-obsolete constructor. Will be removed in V16.")]
public MemberController(
IHttpContextAccessor httpContextAccessor,
IMemberSignInManager memberSignInManager,
IMemberManager memberManager,
IOptions<DeliveryApiSettings> deliveryApiSettings,
ILogger<MemberController> logger)
: this(memberSignInManager, memberManager, StaticServiceProvider.Instance.GetRequiredService<IMemberClientCredentialsManager>(), deliveryApiSettings, logger)
{
}
[Obsolete("Please use the non-obsolete constructor. Will be removed in V16.")]
public MemberController(
IHttpContextAccessor httpContextAccessor,
IMemberSignInManager memberSignInManager,
IMemberManager memberManager,
IMemberClientCredentialsManager memberClientCredentialsManager,
IOptions<DeliveryApiSettings> deliveryApiSettings,
ILogger<MemberController> logger)
: this(memberSignInManager, memberManager, memberClientCredentialsManager, deliveryApiSettings, logger)
{
}
[ActivatorUtilitiesConstructor]
public MemberController(
IMemberSignInManager memberSignInManager,
IMemberManager memberManager,
IMemberClientCredentialsManager memberClientCredentialsManager,
IOptions<DeliveryApiSettings> deliveryApiSettings,
ILogger<MemberController> logger)
{
_httpContextAccessor = httpContextAccessor;
_memberSignInManager = memberSignInManager;
_memberManager = memberManager;
_memberClientCredentialsManager = memberClientCredentialsManager;
_logger = logger;
_deliveryApiSettings = deliveryApiSettings.Value;
}
@@ -76,31 +50,25 @@ public class MemberController : DeliveryApiControllerBase
[MapToApiVersion("1.0")]
public async Task<IActionResult> Authorize()
{
// the Authorize endpoint is not allowed unless authorization code flow is enabled.
if (_deliveryApiSettings.MemberAuthorization?.AuthorizationCodeFlow?.Enabled is not true)
// in principle this is not necessary for now, since the member application has been removed, thus making
// the member client ID invalid for the authentication code flow. However, if we ever add additional flows
// to the API, we should perform this check, so we might as well include it upfront.
if (_deliveryApiSettings.MemberAuthorizationIsEnabled() is false)
{
return BadRequest(new OpenIddictResponse
{
Error = "Not allowed", ErrorDescription = "Member authorization is not allowed."
});
return BadRequest("Member authorization is not allowed.");
}
OpenIddictRequest? request = HttpContext.GetOpenIddictServerRequest();
HttpContext context = _httpContextAccessor.GetRequiredHttpContext();
OpenIddictRequest? request = context.GetOpenIddictServerRequest();
if (request is null)
{
return BadRequest(new OpenIddictResponse
{
Error = "No context found", ErrorDescription = "Unable to obtain context from the current request."
});
return BadRequest("Unable to obtain OpenID data from the current request.");
}
// make sure this endpoint ONLY handles member authentication
if (request.ClientId is not Constants.OAuthClientIds.Member)
{
return BadRequest(new OpenIddictResponse
{
Error = "Invalid 'client ID'", ErrorDescription = "The specified 'client_id' is not valid."
});
return BadRequest("The specified client ID cannot be used here.");
}
return request.IdentityProvider.IsNullOrWhiteSpace()
@@ -108,50 +76,6 @@ public class MemberController : DeliveryApiControllerBase
: await AuthorizeExternal(request);
}
[HttpPost("token")]
[MapToApiVersion("1.0")]
public async Task<IActionResult> Token()
{
OpenIddictRequest? request = HttpContext.GetOpenIddictServerRequest();
if (request is null)
{
return BadRequest(new OpenIddictResponse
{
Error = "No context found", ErrorDescription = "Unable to obtain context from the current request."
});
}
// authorization code flow or refresh token flow?
if ((request.IsAuthorizationCodeGrantType() || request.IsRefreshTokenGrantType()) && _deliveryApiSettings.MemberAuthorization?.AuthorizationCodeFlow?.Enabled is true)
{
// attempt to authorize against the supplied the authorization code
AuthenticateResult authenticateResult = await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
return authenticateResult is { Succeeded: true, Principal: not null }
? new SignInResult(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, authenticateResult.Principal)
: BadRequest(new OpenIddictResponse
{
Error = "Authorization failed", ErrorDescription = "The supplied authorization could not be verified."
});
}
// client credentials flow?
if (request.IsClientCredentialsGrantType() && _deliveryApiSettings.MemberAuthorization?.ClientCredentialsFlow?.Enabled is true)
{
// if we get here, the client ID and secret are valid (verified by OpenIddict)
MemberIdentityUser? member = await _memberClientCredentialsManager.FindMemberAsync(request.ClientId!);
return member is not null
? await SignInMember(member, request)
: BadRequest(new OpenIddictResponse
{
Error = "Authorization failed", ErrorDescription = "Invalid 'client_id' or client configuration."
});
}
throw new InvalidOperationException("The requested grant type is not supported.");
}
[HttpGet("signout")]
[MapToApiVersion("1.0")]
public async Task<IActionResult> Signout()
@@ -177,10 +101,7 @@ public class MemberController : DeliveryApiControllerBase
if (member is null)
{
_logger.LogError("The member with username {userName} was successfully authorized, but could not be retrieved by the member manager", userName);
return BadRequest(new OpenIddictResponse
{
Error = "Authorization failed", ErrorDescription = "The member associated with the supplied 'client_id' could not be found."
});
return BadRequest("The member could not be found.");
}
return await SignInMember(member, request);
@@ -208,10 +129,7 @@ public class MemberController : DeliveryApiControllerBase
if (member is null)
{
_logger.LogError("A member was successfully authorized using external authentication, but could not be retrieved by the member manager");
return BadRequest(new OpenIddictResponse
{
Error = "Authorization failed", ErrorDescription = "The member associated with the supplied 'client_id' could not be found."
});
return BadRequest("The member could not be found.");
}
// update member authentication tokens if succeeded
@@ -21,7 +21,6 @@ using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Infrastructure.Security;
using Umbraco.Cms.Web.Common.ApplicationBuilder;
@@ -62,7 +61,6 @@ public static class UmbracoBuilderExtensions
builder.Services.AddTransient<IMemberApplicationManager, MemberApplicationManager>();
builder.Services.AddTransient<IRequestMemberAccessService, RequestMemberAccessService>();
builder.Services.AddTransient<ICurrentMemberClaimsProvider, CurrentMemberClaimsProvider>();
builder.Services.AddScoped<IMemberClientCredentialsManager, MemberClientCredentialsManager>();
builder.Services.ConfigureOptions<ConfigureUmbracoDeliveryApiSwaggerGenOptions>();
builder.AddUmbracoApiOpenApiUI();
@@ -106,7 +104,7 @@ public static class UmbracoBuilderExtensions
builder.Services.AddOutputCache(options =>
{
options.AddBasePolicy(build => build.AddPolicy<NoOutputCachePolicy>());
options.AddBasePolicy(_ => { });
if (outputCacheSettings.ContentDuration.TotalSeconds > 0)
{
@@ -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,11 +1,10 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Infrastructure.Security;
@@ -17,19 +16,16 @@ internal sealed class InitializeMemberApplicationNotificationHandler : INotifica
private readonly ILogger<InitializeMemberApplicationNotificationHandler> _logger;
private readonly DeliveryApiSettings _deliveryApiSettings;
private readonly IServiceScopeFactory _serviceScopeFactory;
private readonly IMemberClientCredentialsManager _memberClientCredentialsManager;
public InitializeMemberApplicationNotificationHandler(
IRuntimeState runtimeState,
IOptions<DeliveryApiSettings> deliveryApiSettings,
ILogger<InitializeMemberApplicationNotificationHandler> logger,
IServiceScopeFactory serviceScopeFactory,
IMemberClientCredentialsManager memberClientCredentialsManager)
IServiceScopeFactory serviceScopeFactory)
{
_runtimeState = runtimeState;
_logger = logger;
_serviceScopeFactory = serviceScopeFactory;
_memberClientCredentialsManager = memberClientCredentialsManager;
_deliveryApiSettings = deliveryApiSettings.Value;
}
@@ -45,12 +41,6 @@ internal sealed class InitializeMemberApplicationNotificationHandler : INotifica
using IServiceScope scope = _serviceScopeFactory.CreateScope();
IMemberApplicationManager memberApplicationManager = scope.ServiceProvider.GetRequiredService<IMemberApplicationManager>();
await HandleMemberApplication(memberApplicationManager, cancellationToken);
await HandleMemberClientCredentialsApplication(memberApplicationManager, cancellationToken);
}
private async Task HandleMemberApplication(IMemberApplicationManager memberApplicationManager, CancellationToken cancellationToken)
{
if (_deliveryApiSettings.MemberAuthorization?.AuthorizationCodeFlow?.Enabled is not true)
{
await memberApplicationManager.DeleteMemberApplicationAsync(cancellationToken);
@@ -76,21 +66,6 @@ internal sealed class InitializeMemberApplicationNotificationHandler : INotifica
cancellationToken);
}
private async Task HandleMemberClientCredentialsApplication(IMemberApplicationManager memberApplicationManager, CancellationToken cancellationToken)
{
if (_deliveryApiSettings.MemberAuthorization?.ClientCredentialsFlow?.Enabled is not true)
{
// disabled
return;
}
IEnumerable<MemberClientCredentials> memberClientCredentials = await _memberClientCredentialsManager.GetAllAsync();
foreach (MemberClientCredentials memberClientCredential in memberClientCredentials)
{
await memberApplicationManager.EnsureMemberClientCredentialsApplicationAsync(memberClientCredential.ClientId, memberClientCredential.ClientSecret, cancellationToken);
}
}
private bool ValidateRedirectUrls(Uri[] redirectUrls)
{
if (redirectUrls.Any() is false)
@@ -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;
}
}
@@ -37,12 +37,12 @@ public abstract class ContainsFilterBase : IFilterHandler
{
GroupCollection groups = QueryParserRegex.Match(filter).Groups;
if (groups.Count != 3 || groups.TryGetValue("operator", out Group? operatorGroup) is false || groups.TryGetValue("value", out Group? valueGroup) is false)
if (groups.Count != 3 || groups.ContainsKey("operator") is false || groups.ContainsKey("value") is false)
{
return DefaultFilterOption();
}
FilterOperation? filterOperation = ParseFilterOperation(operatorGroup.Value);
FilterOperation? filterOperation = ParseFilterOperation(groups["operator"].Value);
if (filterOperation.HasValue is false)
{
return DefaultFilterOption();
@@ -51,7 +51,7 @@ public abstract class ContainsFilterBase : IFilterHandler
return new FilterOption
{
FieldName = FieldName,
Values = [valueGroup.Value],
Values = new[] { groups["value"].Value },
Operator = filterOperation.Value
};
@@ -1,6 +1,4 @@
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Extensions;
@@ -9,45 +7,15 @@ namespace Umbraco.Cms.Api.Delivery.Querying;
public abstract class QueryOptionBase
{
private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor;
private readonly IRequestRoutingService _requestRoutingService;
private readonly IRequestPreviewService _requestPreviewService;
private readonly IApiDocumentUrlService _apiDocumentUrlService;
private readonly IVariationContextAccessor _variationContextAccessor;
[Obsolete("Please use the non-obsolete constructor. Will be removed in V17.")]
public QueryOptionBase(
IPublishedContentCache publishedContentCache,
IPublishedSnapshotAccessor publishedSnapshotAccessor,
IRequestRoutingService requestRoutingService)
: this(
requestRoutingService,
StaticServiceProvider.Instance.GetRequiredService<IRequestPreviewService>(),
StaticServiceProvider.Instance.GetRequiredService<IApiDocumentUrlService>(),
StaticServiceProvider.Instance.GetRequiredService<IVariationContextAccessor>())
{
}
[Obsolete("Please use the non-obsolete constructor. Will be removed in V17.")]
public QueryOptionBase(
IPublishedContentCache publishedContentCache,
IRequestRoutingService requestRoutingService,
IRequestPreviewService requestPreviewService,
IRequestCultureService requestCultureService,
IApiDocumentUrlService apiDocumentUrlService,
IVariationContextAccessor variationContextAccessor)
: this(requestRoutingService, requestPreviewService, apiDocumentUrlService, variationContextAccessor)
{
}
public QueryOptionBase(
IRequestRoutingService requestRoutingService,
IRequestPreviewService requestPreviewService,
IApiDocumentUrlService apiDocumentUrlService,
IVariationContextAccessor variationContextAccessor)
{
_publishedSnapshotAccessor = publishedSnapshotAccessor;
_requestRoutingService = requestRoutingService;
_requestPreviewService = requestPreviewService;
_apiDocumentUrlService = apiDocumentUrlService;
_variationContextAccessor = variationContextAccessor;
}
protected Guid? GetGuidFromQuery(string queryStringValue)
@@ -62,11 +30,12 @@ public abstract class QueryOptionBase
return id;
}
IPublishedSnapshot publishedSnapshot = _publishedSnapshotAccessor.GetRequiredPublishedSnapshot();
// Check if the passed value is a path of a content item
var contentRoute = _requestRoutingService.GetContentRoute(queryStringValue);
return _apiDocumentUrlService.GetDocumentKeyByRoute(
contentRoute,
_variationContextAccessor.VariationContext?.Culture,
_requestPreviewService.IsPreview());
IPublishedContent? contentItem = publishedSnapshot.Content?.GetByRoute(contentRoute);
return contentItem?.Key;
}
}
@@ -4,74 +4,28 @@ using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Services.Navigation;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Delivery.Querying.Selectors;
public sealed class AncestorsSelector : QueryOptionBase, ISelectorHandler
{
private readonly IDocumentNavigationQueryService _navigationQueryService;
private const string AncestorsSpecifier = "ancestors:";
private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor;
private readonly IRequestPreviewService _requestPreviewService;
[Obsolete("Please use the non-obsolete constructor. Will be removed in V17.")]
public AncestorsSelector(
IPublishedContentCache publishedContentCache,
IRequestRoutingService requestRoutingService,
IDocumentNavigationQueryService navigationQueryService,
IRequestPreviewService requestPreviewService)
: this(
requestRoutingService,
requestPreviewService,
StaticServiceProvider.Instance.GetRequiredService<IApiDocumentUrlService>(),
StaticServiceProvider.Instance.GetRequiredService<IVariationContextAccessor>(),
navigationQueryService)
public AncestorsSelector(IPublishedSnapshotAccessor publishedSnapshotAccessor,
IRequestRoutingService requestRoutingService)
: this(publishedSnapshotAccessor, requestRoutingService, StaticServiceProvider.Instance.GetRequiredService<IRequestPreviewService>())
{
}
[Obsolete("Please use the non-obsolete constructor. Will be removed in V17.")]
public AncestorsSelector(
IPublishedContentCache publishedContentCache,
IRequestRoutingService requestRoutingService,
IDocumentNavigationQueryService navigationQueryService)
: this(
requestRoutingService,
StaticServiceProvider.Instance.GetRequiredService<IRequestPreviewService>(),
StaticServiceProvider.Instance.GetRequiredService<IApiDocumentUrlService>(),
StaticServiceProvider.Instance.GetRequiredService<IVariationContextAccessor>(),
navigationQueryService)
{
}
[Obsolete("Use the constructor that takes all parameters. Scheduled for removal in V17.")]
public AncestorsSelector(IPublishedContentCache publishedContentCache, IRequestRoutingService requestRoutingService)
: this(
requestRoutingService,
StaticServiceProvider.Instance.GetRequiredService<IRequestPreviewService>(),
StaticServiceProvider.Instance.GetRequiredService<IApiDocumentUrlService>(),
StaticServiceProvider.Instance.GetRequiredService<IVariationContextAccessor>(),
StaticServiceProvider.Instance.GetRequiredService<IDocumentNavigationQueryService>())
{
}
public AncestorsSelector(
IRequestRoutingService requestRoutingService,
IRequestPreviewService requestPreviewService,
IApiDocumentUrlService apiDocumentUrlService,
IVariationContextAccessor variationContextAccessor,
IDocumentNavigationQueryService navigationQueryService)
: base(requestRoutingService, requestPreviewService, apiDocumentUrlService, variationContextAccessor)
=> _navigationQueryService = navigationQueryService;
[Obsolete("Use the constructor that takes all parameters. Scheduled for removal in V17.")]
public AncestorsSelector(
IRequestRoutingService requestRoutingService,
IPublishedContentCache publishedContentCache,
IRequestPreviewService requestPreviewService,
IApiDocumentUrlService apiDocumentUrlService,
IVariationContextAccessor variationContextAccessor,
IDocumentNavigationQueryService navigationQueryService)
: this(requestRoutingService, requestPreviewService, apiDocumentUrlService, variationContextAccessor, navigationQueryService)
public AncestorsSelector(IPublishedSnapshotAccessor publishedSnapshotAccessor, IRequestRoutingService requestRoutingService, IRequestPreviewService requestPreviewService)
: base(publishedSnapshotAccessor, requestRoutingService)
{
_publishedSnapshotAccessor = publishedSnapshotAccessor;
_requestPreviewService = requestPreviewService;
}
/// <inheritdoc />
@@ -84,7 +38,7 @@ public sealed class AncestorsSelector : QueryOptionBase, ISelectorHandler
var fieldValue = selector[AncestorsSpecifier.Length..];
Guid? id = GetGuidFromQuery(fieldValue);
if (id is null || _navigationQueryService.TryGetAncestorsKeys(id.Value, out IEnumerable<Guid> ancestorKeys) is false)
if (id is null)
{
// Setting the Value to "" since that would yield no results.
// It won't be appropriate to return null here since if we reached this,
@@ -96,10 +50,27 @@ public sealed class AncestorsSelector : QueryOptionBase, ISelectorHandler
};
}
IPublishedContentCache contentCache = _publishedSnapshotAccessor.GetRequiredPublishedSnapshot()?.Content
?? throw new InvalidOperationException("Could not obtain the content cache");
IPublishedContent? contentItem = contentCache.GetById(_requestPreviewService.IsPreview(), id.Value);
if (contentItem is null)
{
// no such content item, make sure the selector does not yield any results
return new SelectorOption
{
FieldName = AncestorsSelectorIndexer.FieldName,
Values = Array.Empty<string>()
};
}
var ancestorKeys = contentItem.Ancestors().Select(a => a.Key.ToString("D")).ToArray();
return new SelectorOption
{
FieldName = AncestorsSelectorIndexer.FieldName,
Values = ancestorKeys.Select(key => key.ToString("D")).ToArray()
Values = ancestorKeys
};
}
}
@@ -1,8 +1,5 @@
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Delivery.Indexing.Selectors;
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Extensions;
@@ -12,33 +9,8 @@ public sealed class ChildrenSelector : QueryOptionBase, ISelectorHandler
{
private const string ChildrenSpecifier = "children:";
[Obsolete("Please use the non-obsolete constructor. Will be removed in V17.")]
public ChildrenSelector(IPublishedContentCache publishedContentCache, IRequestRoutingService requestRoutingService)
: this(
requestRoutingService,
StaticServiceProvider.Instance.GetRequiredService<IRequestPreviewService>(),
StaticServiceProvider.Instance.GetRequiredService<IApiDocumentUrlService>(),
StaticServiceProvider.Instance.GetRequiredService<IVariationContextAccessor>())
{
}
[Obsolete("Please use the non-obsolete constructor. Will be removed in V17.")]
public ChildrenSelector(
IPublishedContentCache publishedContentCache,
IRequestRoutingService requestRoutingService,
IRequestPreviewService requestPreviewService,
IApiDocumentUrlService apiDocumentUrlService,
IVariationContextAccessor variationContextAccessor)
: this(requestRoutingService, requestPreviewService, apiDocumentUrlService, variationContextAccessor)
{
}
public ChildrenSelector(
IRequestRoutingService requestRoutingService,
IRequestPreviewService requestPreviewService,
IApiDocumentUrlService apiDocumentUrlService,
IVariationContextAccessor variationContextAccessor)
: base(requestRoutingService, requestPreviewService, apiDocumentUrlService, variationContextAccessor)
public ChildrenSelector(IPublishedSnapshotAccessor publishedSnapshotAccessor, IRequestRoutingService requestRoutingService)
: base(publishedSnapshotAccessor, requestRoutingService)
{
}
@@ -1,8 +1,5 @@
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Api.Delivery.Indexing.Selectors;
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Extensions;
@@ -12,33 +9,8 @@ public sealed class DescendantsSelector : QueryOptionBase, ISelectorHandler
{
private const string DescendantsSpecifier = "descendants:";
[Obsolete("Please use the non-obsolete constructor. Will be removed in V17.")]
public DescendantsSelector(IPublishedContentCache publishedContentCache, IRequestRoutingService requestRoutingService)
: this(
requestRoutingService,
StaticServiceProvider.Instance.GetRequiredService<IRequestPreviewService>(),
StaticServiceProvider.Instance.GetRequiredService<IApiDocumentUrlService>(),
StaticServiceProvider.Instance.GetRequiredService<IVariationContextAccessor>())
{
}
[Obsolete("Please use the non-obsolete constructor. Will be removed in V17.")]
public DescendantsSelector(
IPublishedContentCache publishedContentCache,
IRequestRoutingService requestRoutingService,
IRequestPreviewService requestPreviewService,
IApiDocumentUrlService apiDocumentUrlService,
IVariationContextAccessor variationContextAccessor)
: this(requestRoutingService, requestPreviewService, apiDocumentUrlService, variationContextAccessor)
{
}
public DescendantsSelector(
IRequestRoutingService requestRoutingService,
IRequestPreviewService requestPreviewService,
IApiDocumentUrlService apiDocumentUrlService,
IVariationContextAccessor variationContextAccessor)
: base(requestRoutingService, requestPreviewService, apiDocumentUrlService, variationContextAccessor)
public DescendantsSelector(IPublishedSnapshotAccessor publishedSnapshotAccessor, IRequestRoutingService requestRoutingService)
: base(publishedSnapshotAccessor, requestRoutingService)
{
}
@@ -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,12 +36,12 @@ 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,
OpenIddictConstants.Permissions.Endpoints.Token,
OpenIddictConstants.Permissions.Endpoints.EndSession,
OpenIddictConstants.Permissions.Endpoints.Logout,
OpenIddictConstants.Permissions.Endpoints.Revocation,
OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode,
OpenIddictConstants.Permissions.GrantTypes.RefreshToken,
@@ -64,26 +64,4 @@ public class MemberApplicationManager : OpenIdDictApplicationManagerBase, IMembe
public async Task DeleteMemberApplicationAsync(CancellationToken cancellationToken = default)
=> await Delete(Constants.OAuthClientIds.Member, cancellationToken);
public async Task EnsureMemberClientCredentialsApplicationAsync(string clientId, string clientSecret, CancellationToken cancellationToken = default)
{
var applicationDescriptor = new OpenIddictApplicationDescriptor
{
DisplayName = $"Umbraco client credentials member access: {clientId}",
ClientId = clientId,
ClientSecret = clientSecret,
ClientType = OpenIddictConstants.ClientTypes.Confidential,
Permissions =
{
OpenIddictConstants.Permissions.Endpoints.Token,
OpenIddictConstants.Permissions.Endpoints.Revocation,
OpenIddictConstants.Permissions.GrantTypes.ClientCredentials
}
};
await CreateOrUpdate(applicationDescriptor, cancellationToken);
}
public async Task DeleteMemberClientCredentialsApplicationAsync(string clientId, CancellationToken cancellationToken = default)
=> await Delete(clientId, cancellationToken);
}
@@ -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,
@@ -79,14 +90,10 @@ internal sealed class ApiContentQueryProvider : IApiContentQueryProvider
return new PagedModel<Guid>();
}
List<Guid> items = [];
foreach (ISearchResult result in results)
{
if (result.Values.TryGetValue(ItemIdFieldName, out string? value))
{
items.Add(Guid.Parse(value));
}
}
Guid[] items = results
.Where(r => r.Values.ContainsKey(ItemIdFieldName))
.Select(r => Guid.Parse(r.Values[ItemIdFieldName]))
.ToArray();
return new PagedModel<Guid>(results.TotalItemCount, items);
}
@@ -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,
@@ -4,7 +4,6 @@ using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Services.Navigation;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Extensions;
@@ -13,21 +12,13 @@ namespace Umbraco.Cms.Api.Delivery.Services;
/// <inheritdoc />
internal sealed class ApiMediaQueryService : IApiMediaQueryService
{
private readonly IPublishedMediaCache _publishedMediaCache;
private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor;
private readonly ILogger<ApiMediaQueryService> _logger;
private readonly IMediaNavigationQueryService _mediaNavigationQueryService;
private readonly IPublishedMediaStatusFilteringService _publishedMediaStatusFilteringService;
public ApiMediaQueryService(
IPublishedMediaCache publishedMediaCache,
ILogger<ApiMediaQueryService> logger,
IMediaNavigationQueryService mediaNavigationQueryService,
IPublishedMediaStatusFilteringService publishedMediaStatusFilteringService)
public ApiMediaQueryService(IPublishedSnapshotAccessor publishedSnapshotAccessor, ILogger<ApiMediaQueryService> logger)
{
_publishedMediaCache = publishedMediaCache;
_publishedSnapshotAccessor = publishedSnapshotAccessor;
_logger = logger;
_mediaNavigationQueryService = mediaNavigationQueryService;
_publishedMediaStatusFilteringService = publishedMediaStatusFilteringService;
}
/// <inheritdoc/>
@@ -61,7 +52,8 @@ internal sealed class ApiMediaQueryService : IApiMediaQueryService
=> TryGetByPath(path, GetRequiredPublishedMediaCache());
private IPublishedMediaCache GetRequiredPublishedMediaCache()
=> _publishedMediaCache;
=> _publishedSnapshotAccessor.GetRequiredPublishedSnapshot().Media
?? throw new InvalidOperationException("Could not obtain the published media cache");
private IPublishedContent? TryGetByPath(string path, IPublishedMediaCache mediaCache)
{
@@ -77,7 +69,7 @@ internal sealed class ApiMediaQueryService : IApiMediaQueryService
break;
}
currentChildren = resolvedMedia.Children(_mediaNavigationQueryService, _publishedMediaStatusFilteringService);
currentChildren = resolvedMedia.Children;
}
return resolvedMedia;
@@ -110,7 +102,7 @@ internal sealed class ApiMediaQueryService : IApiMediaQueryService
? mediaCache.GetById(parentKey)
: TryGetByPath(childrenOf, mediaCache);
return parent?.Children(_mediaNavigationQueryService, _publishedMediaStatusFilteringService) ?? Array.Empty<IPublishedContent>();
return parent?.Children ?? Array.Empty<IPublishedContent>();
}
private IEnumerable<IPublishedContent>? ApplyFilters(IEnumerable<IPublishedContent> source, IEnumerable<string> filters)
@@ -185,7 +177,7 @@ internal sealed class ApiMediaQueryService : IApiMediaQueryService
}
private static Attempt<PagedModel<Guid>, ApiMediaQueryOperationStatus> PagedResult(IEnumerable<IPublishedContent> children, int skip, int take)
private Attempt<PagedModel<Guid>, ApiMediaQueryOperationStatus> PagedResult(IEnumerable<IPublishedContent> children, int skip, int take)
{
IPublishedContent[] childrenAsArray = children as IPublishedContent[] ?? children.ToArray();
var result = new PagedModel<Guid>
@@ -1,4 +1,4 @@
using System.Security.Claims;
using System.Security.Claims;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
@@ -21,7 +21,7 @@ internal sealed class RequestRedirectService : RoutingServiceBase, IRequestRedir
private readonly GlobalSettings _globalSettings;
public RequestRedirectService(
IDomainCache domainCache,
IPublishedSnapshotAccessor publishedSnapshotAccessor,
IHttpContextAccessor httpContextAccessor,
IRequestStartItemProviderAccessor requestStartItemProviderAccessor,
IRequestCultureService requestCultureService,
@@ -29,7 +29,7 @@ internal sealed class RequestRedirectService : RoutingServiceBase, IRequestRedir
IApiPublishedContentCache apiPublishedContentCache,
IApiContentRouteBuilder apiContentRouteBuilder,
IOptions<GlobalSettings> globalSettings)
: base(domainCache, httpContextAccessor, requestStartItemProviderAccessor)
: base(publishedSnapshotAccessor, httpContextAccessor, requestStartItemProviderAccessor)
{
_requestCultureService = requestCultureService;
_redirectUrlService = redirectUrlService;
@@ -1,6 +1,5 @@
using Microsoft.AspNetCore.Http;
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Routing;
@@ -13,11 +12,11 @@ internal sealed class RequestRoutingService : RoutingServiceBase, IRequestRoutin
private readonly IRequestCultureService _requestCultureService;
public RequestRoutingService(
IDomainCache domainCache,
IPublishedSnapshotAccessor publishedSnapshotAccessor,
IHttpContextAccessor httpContextAccessor,
IRequestStartItemProviderAccessor requestStartItemProviderAccessor,
IRequestCultureService requestCultureService)
: base(domainCache, httpContextAccessor, requestStartItemProviderAccessor) =>
: base(publishedSnapshotAccessor, httpContextAccessor, requestStartItemProviderAccessor) =>
_requestCultureService = requestCultureService;
/// <inheritdoc />
@@ -3,34 +3,29 @@ using Umbraco.Cms.Core;
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Services.Navigation;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Delivery.Services;
internal sealed class RequestStartItemProvider : RequestHeaderHandler, IRequestStartItemProvider
{
private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor;
private readonly IVariationContextAccessor _variationContextAccessor;
private readonly IRequestPreviewService _requestPreviewService;
private readonly IDocumentNavigationQueryService _documentNavigationQueryService;
private readonly IPublishedContentCache _publishedContentCache;
// this provider lifetime is Scope, so we can cache this as a field
private IPublishedContent? _requestedStartContent;
public RequestStartItemProvider(
IHttpContextAccessor httpContextAccessor,
IPublishedSnapshotAccessor publishedSnapshotAccessor,
IVariationContextAccessor variationContextAccessor,
IRequestPreviewService requestPreviewService,
IDocumentNavigationQueryService documentNavigationQueryService,
IPublishedContentCache publishedContentCache)
IRequestPreviewService requestPreviewService)
: base(httpContextAccessor)
{
_publishedSnapshotAccessor = publishedSnapshotAccessor;
_variationContextAccessor = variationContextAccessor;
_requestPreviewService = requestPreviewService;
_documentNavigationQueryService = documentNavigationQueryService;
_publishedContentCache = publishedContentCache;
}
/// <inheritdoc/>
@@ -47,10 +42,13 @@ internal sealed class RequestStartItemProvider : RequestHeaderHandler, IRequestS
return null;
}
_documentNavigationQueryService.TryGetRootKeys(out IEnumerable<Guid> rootKeys);
IEnumerable<IPublishedContent> rootContent = rootKeys
.Select(rootKey => _publishedContentCache.GetById(_requestPreviewService.IsPreview(), rootKey))
.WhereNotNull();
if (_publishedSnapshotAccessor.TryGetPublishedSnapshot(out IPublishedSnapshot? publishedSnapshot) == false ||
publishedSnapshot?.Content == null)
{
return null;
}
IEnumerable<IPublishedContent> rootContent = publishedSnapshot.Content.GetAtRoot(_requestPreviewService.IsPreview());
_requestedStartContent = Guid.TryParse(headerValue, out Guid key)
? rootContent.FirstOrDefault(c => c.Key == key)
@@ -9,16 +9,16 @@ namespace Umbraco.Cms.Api.Delivery.Services;
internal abstract class RoutingServiceBase
{
private readonly IDomainCache _domainCache;
private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IRequestStartItemProviderAccessor _requestStartItemProviderAccessor;
protected RoutingServiceBase(
IDomainCache domainCache,
IPublishedSnapshotAccessor publishedSnapshotAccessor,
IHttpContextAccessor httpContextAccessor,
IRequestStartItemProviderAccessor requestStartItemProviderAccessor)
{
_domainCache = domainCache;
_publishedSnapshotAccessor = publishedSnapshotAccessor;
_httpContextAccessor = httpContextAccessor;
_requestStartItemProviderAccessor = requestStartItemProviderAccessor;
}
@@ -40,9 +40,15 @@ internal abstract class RoutingServiceBase
protected DomainAndUri? GetDomainAndUriForRoute(Uri contentUrl)
{
IEnumerable<Domain> domains = _domainCache.GetAll(false);
IDomainCache? domainCache = _publishedSnapshotAccessor.GetRequiredPublishedSnapshot().Domains;
if (domainCache == null)
{
throw new InvalidOperationException("Could not obtain the domain cache in the current context");
}
return DomainUtilities.SelectDomain(domains, contentUrl, defaultCulture: _domainCache.DefaultCulture);
IEnumerable<Domain> domains = domainCache.GetAll(false);
return DomainUtilities.SelectDomain(domains, contentUrl, defaultCulture: domainCache.DefaultCulture);
}
protected IPublishedContent? GetStartItem()
@@ -3,16 +3,7 @@
<Title>Umbraco CMS - Delivery API</Title>
<Description>Contains the presentation layer for the Umbraco CMS Delivery API.</Description>
</PropertyGroup>
<PropertyGroup>
<!--
TODO: Fix and remove overrides:
[ASP0019] use IHeaderDictionary.Append or the indexer to append or set headers
[CS0618/CS0612] update obsolete references
-->
<WarningsNotAsErrors>$(WarningsNotAsErrors),ASP0019,CS0618,CS0612</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,65 +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.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(Constants.System.DefaultUmbracoPath);
}
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);
}
}

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