Compare commits
71
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ab0abca99 | ||
|
|
009a999e22 | ||
|
|
d3c1443b14 | ||
|
|
8a9db11422 | ||
|
|
d9fb6df16e | ||
|
|
14ed3348bf | ||
|
|
9e7a36865a | ||
|
|
db1d999721 | ||
|
|
18047a7cfb | ||
|
|
def7ebd48c | ||
|
|
8c2b1ebdc5 | ||
|
|
9227517a50 | ||
|
|
5322d0f7b5 | ||
|
|
a282cc5691 | ||
|
|
048f8bcdf9 | ||
|
|
7bcbc748d4 | ||
|
|
f30e6cfe7f | ||
|
|
026e80e026 | ||
|
|
a3b77cff63 | ||
|
|
4ca68d6995 | ||
|
|
280cb7f2b1 | ||
|
|
095a73132c | ||
|
|
cfb0fc23ac | ||
|
|
6620aca9fe | ||
|
|
2422a02b23 | ||
|
|
b4a9dc0770 | ||
|
|
e7411244fd | ||
|
|
9f357173c3 | ||
|
|
7dc6e3ed5b | ||
|
|
5dfff212d0 | ||
|
|
17615f966b | ||
|
|
7a2d6b6c63 | ||
|
|
343867f6ee | ||
|
|
b9837ac77c | ||
|
|
bb73ec6c68 | ||
|
|
ee2d7bbb1b | ||
|
|
5d48bc7371 | ||
|
|
59a4649528 | ||
|
|
f54b6033ab | ||
|
|
bf340cd7d4 | ||
|
|
313417cb91 | ||
|
|
7850078623 | ||
|
|
44bf3b77b3 | ||
|
|
8485458896 | ||
|
|
3d253f5f06 | ||
|
|
64287d7e1a | ||
|
|
edc78a5a4c | ||
|
|
8056a59a10 | ||
|
|
1a18d6c035 | ||
|
|
3e877182b5 | ||
|
|
0e4f883bc1 | ||
|
|
265cc62ab8 | ||
|
|
800c16970e | ||
|
|
6bd11bf233 | ||
|
|
a2fd82a3f3 | ||
|
|
7567990da1 | ||
|
|
c7d157bbfd | ||
|
|
abc312c9b4 | ||
|
|
2161edb871 | ||
|
|
e77e9c5691 | ||
|
|
e934a943b5 | ||
|
|
65bb2801b0 | ||
|
|
a0a4af6a0c | ||
|
|
e8d6cded2b | ||
|
|
c65204a146 | ||
|
|
95eb58587b | ||
|
|
86f3033334 | ||
|
|
25628a8b76 | ||
|
|
7552e315fb | ||
|
|
d15b5b221e | ||
|
|
0166727eee |
@@ -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
|
||||
|
||||
+62
-32
@@ -4,8 +4,8 @@
|
||||
|
||||
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.
|
||||
|
||||
@@ -13,6 +13,17 @@ 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 (  ) above.
|
||||
|
||||
## Getting Started:
|
||||
To run umbraco, we first need to initialize the client git submodule:
|
||||
* Execute `git submodule update --init` to get the files into Umbraco.Web.UI.Client project
|
||||
* If you are going to work on the Backoffice, you can either go to the Umbraco.Web.UI.Client folder and check out a new branch or set it up in your IDE, which will allow you to commit to each repository simultaneously:
|
||||
* **Rider**: Preferences -> Version Control -> Directory Mappings -> Click the '+' sign
|
||||
* If you get a white page delete Umbraco.Cms.StaticAssets\wwwroot\umbraco folder and run `npm ci && npm run build:for:cms` inside Umbraco.Web.UI.Client folder to clear out any leftover files from older versions.
|
||||
|
||||
### Latest version
|
||||
* If you want to get the latest changes from the client repository, run `git submodule update` again which will pull the latest main branch.
|
||||
|
||||
|
||||
## Debugging source locally
|
||||
|
||||
Did you read ["Are you sure"](#are-you-sure)?
|
||||
@@ -21,32 +32,29 @@ 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.
|
||||
|
||||
> [!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.
|
||||
|
||||
#### Debugging with VS Code
|
||||
|
||||
In order to build the Umbraco source code locally with Visual Studio Code, first make sure you have the following installed.
|
||||
|
||||
- [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/)
|
||||
* [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/)
|
||||
|
||||
Open the root folder of the repository in Visual Studio 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 Build`.
|
||||
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.
|
||||
|
||||
You can also run the tasks manually on the command line:
|
||||
|
||||
```
|
||||
cd src\Umbraco.Web.UI.Client
|
||||
npm i
|
||||
npm run build:for:cms
|
||||
npm run dev
|
||||
```
|
||||
|
||||
If you want to make changes to the UI, you can choose to run a front-end development server. To learn more please read the Umbraco.Web.UI.Client README.md [Run against a local Umbraco instance](../src/Umbraco.Web.UI.Client/.github/README.md#run-against-a-local-umbraco-instance) for more information.
|
||||
If you just want to build the UI Client to `Umbraco.Web.UI` then instead of running `dev`, you can do: `npm run build`.
|
||||
|
||||
The login screen is a different frontend build, for that one you can run it as follows:
|
||||
|
||||
@@ -58,6 +66,12 @@ npm run dev
|
||||
|
||||
If you just want to build the Login screen to `Umbraco.Web.UI` then instead of running `dev`, you can do: `npm run build`.
|
||||
|
||||
**The initial Gulp build might take a long time - don't worry, this will be faster on subsequent runs.**
|
||||
|
||||
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:
|
||||
|
||||
```
|
||||
@@ -72,18 +86,24 @@ When the page eventually loads in your web browser, you can follow the installer
|
||||
|
||||
In order to build the Umbraco source code locally with Visual Studio, first make sure you have the following installed.
|
||||
|
||||
- [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 v20+](https://nodejs.org/en/download/)
|
||||
- npm v10+ (installed with Node.js)
|
||||
- [Git command line](https://git-scm.com/download/)
|
||||
* [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/)
|
||||
|
||||
The easiest way to get started is to open `umbraco.sln` in Visual Studio.
|
||||
|
||||
Umbraco 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. There are two web projects in the solution, `Umbraco.Web.UI.Client` and `Umbraco.Web.UI.Login`. They each have their own build process and can be run separately. 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. It will automatically restore and build the client projects when you run it.
|
||||
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.
|
||||
|
||||
If you want to watch the UI Client to `Umbraco.Web.UI` then you can open a terminal in `src/Umbraco.Web.UI.Client` where you can run `npm run dev:mock` manually. This will launch the Vite dev server on http://localhost:5173 and watch for changes with mocked API responses.
|
||||
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.
|
||||
|
||||
You can also run `npm run dev:server` to run the Vite server against a local Umbraco instance. In this case, you need to have the .NET project running and accept connections from the Vite server. Please see the Umbraco.Web.UI.Client README.md [Run against a local Umbraco instance](../src/Umbraco.Web.UI.Client/.github/README.md#run-against-a-local-umbraco-instance) for more information.
|
||||
**The initial Gulp build might take a long time - don't worry, this will be faster on subsequent runs.**
|
||||
|
||||
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.
|
||||
|
||||
"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.
|
||||
|
||||
**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.**
|
||||
|
||||
@@ -95,7 +115,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:
|
||||
|
||||
@@ -107,18 +127,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.
|
||||
|
||||
@@ -126,14 +145,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
|
||||
|
||||
@@ -147,6 +165,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"
|
||||
[ starter kits ]: https://our.umbraco.com/packages/?category=Starter%20Kits&version=9 "Browse starter kits available for v9 on Our "
|
||||
### 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"
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||

|
||||
|
||||
### 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',
|
||||
},
|
||||
],
|
||||
},
|
||||
```
|
||||
|
||||
Let’s 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. Let’s start by looking at the call to retrieve the current status of the cache:
|
||||
|
||||

|
||||
|
||||
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. Let’s take the “Refresh status” button as an example:
|
||||
|
||||

|
||||
|
||||
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 “<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 |
+6
-6
@@ -13,9 +13,13 @@ changelog:
|
||||
- title: 💥 Breaking Changes
|
||||
labels:
|
||||
- category/breaking
|
||||
- title: 🐛 Bug Fixes
|
||||
labels:
|
||||
- type/bug
|
||||
- category/bug
|
||||
- type/improvement
|
||||
- title: 📄 Documentation
|
||||
labels:
|
||||
- documentation
|
||||
- category/documentation
|
||||
- title: 🏠 Internal
|
||||
labels:
|
||||
@@ -25,14 +29,10 @@ changelog:
|
||||
- dependencies
|
||||
- title: 🌈 A11Y
|
||||
labels:
|
||||
- accessibility
|
||||
- category/accessibility
|
||||
- title: 🚀 New Features
|
||||
labels:
|
||||
- type/feature
|
||||
- category/feature
|
||||
- type/enhancement
|
||||
- category/enhancement
|
||||
- title: 🐛 Bug Fixes
|
||||
- title: Other Changes
|
||||
labels:
|
||||
- '*'
|
||||
|
||||
@@ -3,26 +3,20 @@ 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
|
||||
@@ -33,39 +27,36 @@ env:
|
||||
|
||||
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
|
||||
submodules: true
|
||||
|
||||
# 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
|
||||
|
||||
@@ -1,65 +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 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
|
||||
@@ -47,6 +47,9 @@ NDependOut/
|
||||
QueryResult.htm
|
||||
tools/docfx/
|
||||
|
||||
# Ignore rule for clearing out Belle (avoid rebuilding all the time)
|
||||
preserve.belle
|
||||
|
||||
# csharp-docs
|
||||
/build/csharp-docs/api/
|
||||
/build/csharp-docs/_site/
|
||||
@@ -70,6 +73,10 @@ tools/docfx/
|
||||
/src/Umbraco.Cms.StaticAssets/wwwroot/umbraco/login
|
||||
|
||||
# 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/
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
[submodule "src/Umbraco.Web.UI.Client"]
|
||||
path = src/Umbraco.Web.UI.Client
|
||||
url = https://github.com/umbraco/Umbraco.CMS.Backoffice.git
|
||||
Vendored
+33
-121
@@ -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}"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Vendored
+7
-3
@@ -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
-12
@@ -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,12 @@
|
||||
<NeutralLanguage>en-US</NeutralLanguage>
|
||||
<Nullable>enable</Nullable>
|
||||
<WarningsAsErrors>nullable</WarningsAsErrors>
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
<TreatWarningsAsErrors>true</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,7 +31,7 @@
|
||||
<PropertyGroup>
|
||||
<GenerateCompatibilitySuppressionFile>false</GenerateCompatibilitySuppressionFile>
|
||||
<EnablePackageValidation>true</EnablePackageValidation>
|
||||
<PackageValidationBaselineVersion>15.0.0</PackageValidationBaselineVersion>
|
||||
<PackageValidationBaselineVersion>14.0.0</PackageValidationBaselineVersion>
|
||||
<EnableStrictModeForCompatibleFrameworksInPackage>true</EnableStrictModeForCompatibleFrameworksInPackage>
|
||||
<EnableStrictModeForCompatibleTfms>true</EnableStrictModeForCompatibleTfms>
|
||||
</PropertyGroup>
|
||||
|
||||
+43
-51
@@ -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="Nerdbank.GitVersioning" Version="3.6.139" />
|
||||
<GlobalPackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
|
||||
<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.0" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.0" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.0" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="9.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="9.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.FileProviders.Embedded" Version="9.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.FileProviders.Physical" Version="9.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http" Version="9.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Identity.Core" Version="9.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Identity.Stores" Version="9.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options" Version="9.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="9.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options.DataAnnotations" Version="9.0.0" />
|
||||
<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.Caching.Memory" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
|
||||
<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" />
|
||||
</ItemGroup>
|
||||
<!-- Umbraco packages -->
|
||||
<ItemGroup>
|
||||
@@ -45,7 +45,7 @@
|
||||
<PackageVersion Include="Asp.Versioning.Mvc" Version="8.1.0" />
|
||||
<PackageVersion Include="Asp.Versioning.Mvc.ApiExplorer" Version="8.1.0" />
|
||||
<PackageVersion Include="Dazinator.Extensions.FileProviders" Version="2.0.0" />
|
||||
<PackageVersion Include="Examine" Version="3.6.0" />
|
||||
<PackageVersion Include="Examine" Version="3.5.0" />
|
||||
<PackageVersion Include="Examine.Core" Version="3.5.0" />
|
||||
<PackageVersion Include="HtmlAgilityPack" Version="1.11.71" />
|
||||
<PackageVersion Include="JsonPatch.Net" Version="3.1.1" />
|
||||
@@ -58,46 +58,38 @@
|
||||
<PackageVersion Include="ncrontab" Version="3.3.3" />
|
||||
<PackageVersion Include="NPoco" Version="5.7.1" />
|
||||
<PackageVersion Include="NPoco.SqlServer" Version="5.7.1" />
|
||||
<PackageVersion Include="OpenIddict.Abstractions" Version="6.0.0-preview3.24551.41" />
|
||||
<PackageVersion Include="OpenIddict.AspNetCore" Version="6.0.0-preview3.24551.41" />
|
||||
<PackageVersion Include="OpenIddict.EntityFrameworkCore" Version="6.0.0-preview3.24551.41" />
|
||||
<PackageVersion Include="Serilog" Version="4.1.0" />
|
||||
<PackageVersion Include="OpenIddict.Abstractions" Version="5.7.0" />
|
||||
<PackageVersion Include="OpenIddict.AspNetCore" Version="5.7.0" />
|
||||
<PackageVersion Include="OpenIddict.EntityFrameworkCore" Version="5.7.0" />
|
||||
<PackageVersion Include="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="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" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
+93
-182
@@ -36,21 +36,21 @@ parameters:
|
||||
- name: integrationNonReleaseTestFilter
|
||||
displayName: TestFilter used for non-release type builds
|
||||
type: string
|
||||
default: "--filter TestCategory!=LongRunning&TestCategory!=NonCritical"
|
||||
default: '--filter TestCategory!=LongRunning&TestCategory!=NonCritical'
|
||||
- name: integrationReleaseTestFilter
|
||||
displayName: TestFilter used for release type builds
|
||||
type: string
|
||||
default: " "
|
||||
default: ' '
|
||||
- name: nonWindowsIntegrationNonReleaseTestFilter
|
||||
displayName: TestFilter used for non-release type builds on non Windows agents
|
||||
type: string
|
||||
default: "--filter TestCategory!=LongRunning&TestCategory!=NonCritical"
|
||||
default: '--filter TestCategory!=LongRunning&TestCategory!=NonCritical'
|
||||
- name: nonWindowsIntegrationReleaseTestFilter
|
||||
displayName: TestFilter used for release type builds on non Windows agents
|
||||
type: string
|
||||
default: " "
|
||||
default: ' '
|
||||
- name: isNightly
|
||||
displayName: "Is nightly build (used for MyGet feed)"
|
||||
displayName: 'Is nightly build (used for MyGet feed)'
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
@@ -75,17 +75,24 @@ stages:
|
||||
- job: A
|
||||
displayName: Build Umbraco CMS
|
||||
pool:
|
||||
vmImage: "windows-latest"
|
||||
vmImage: 'windows-latest'
|
||||
steps:
|
||||
- checkout: self
|
||||
submodules: false
|
||||
lfs: false,
|
||||
fetchDepth: 500
|
||||
- template: templates/backoffice-install.yml
|
||||
submodules: true
|
||||
- task: UseDotNet@2
|
||||
displayName: Use .NET SDK from global.json
|
||||
inputs:
|
||||
useGlobalJson: true
|
||||
- template: templates/backoffice-install.yml
|
||||
- script: npm run build:for:cms
|
||||
displayName: Run build (Bellissima)
|
||||
workingDirectory: src/Umbraco.Web.UI.Client
|
||||
- script: npm ci --no-fund --no-audit --prefer-offline
|
||||
displayName: Run npm ci (Login)
|
||||
workingDirectory: src/Umbraco.Web.UI.Login
|
||||
- script: npm run build
|
||||
displayName: Run npm build (Login)
|
||||
workingDirectory: src/Umbraco.Web.UI.Login
|
||||
- task: DotNetCoreCLI@2
|
||||
displayName: Run dotnet restore
|
||||
inputs:
|
||||
@@ -97,7 +104,7 @@ stages:
|
||||
inputs:
|
||||
command: build
|
||||
projects: $(solution)
|
||||
arguments: "--configuration $(buildConfiguration) --no-restore --property:ContinuousIntegrationBuild=true --property:GeneratePackageOnBuild=true --property:PackageOutputPath=$(Build.ArtifactStagingDirectory)/nupkg"
|
||||
arguments: '--configuration $(buildConfiguration) --no-restore --property:ContinuousIntegrationBuild=true --property:GeneratePackageOnBuild=true --property:PackageOutputPath=$(Build.ArtifactStagingDirectory)/nupkg'
|
||||
- task: PublishPipelineArtifact@1
|
||||
displayName: Publish nupkg
|
||||
inputs:
|
||||
@@ -112,12 +119,10 @@ stages:
|
||||
- job: B
|
||||
displayName: Build Bellissima Package
|
||||
pool:
|
||||
vmImage: "ubuntu-latest"
|
||||
vmImage: 'ubuntu-latest'
|
||||
steps:
|
||||
- checkout: self
|
||||
submodules: false
|
||||
lfs: false,
|
||||
fetchDepth: 500
|
||||
submodules: true
|
||||
- template: templates/backoffice-install.yml
|
||||
- script: npm run build:for:npm
|
||||
displayName: Run build:for:npm
|
||||
@@ -147,7 +152,7 @@ stages:
|
||||
- job:
|
||||
displayName: Build C# API Reference
|
||||
pool:
|
||||
vmImage: "windows-latest"
|
||||
vmImage: 'windows-latest'
|
||||
steps:
|
||||
- task: UseDotNet@2
|
||||
displayName: Use .NET SDK from global.json
|
||||
@@ -196,15 +201,12 @@ stages:
|
||||
- job:
|
||||
displayName: Build js API Reference
|
||||
pool:
|
||||
vmImage: "ubuntu-latest"
|
||||
vmImage: 'ubuntu-latest'
|
||||
variables:
|
||||
BASE_PATH: /v$(umbracoMajorVersion)/ui
|
||||
steps:
|
||||
- checkout: self
|
||||
submodules: false
|
||||
lfs: false,
|
||||
fetchDepth: 1
|
||||
fetchFilter: tree:0
|
||||
submodules: true
|
||||
- template: templates/backoffice-install.yml
|
||||
- script: npm run storybook:build
|
||||
displayName: Build Storybook
|
||||
@@ -253,19 +255,14 @@ stages:
|
||||
strategy:
|
||||
matrix:
|
||||
Windows:
|
||||
vmImage: "windows-latest"
|
||||
vmImage: 'windows-latest'
|
||||
Linux:
|
||||
vmImage: "ubuntu-latest"
|
||||
vmImage: 'ubuntu-latest'
|
||||
macOS:
|
||||
vmImage: "macOS-latest"
|
||||
vmImage: 'macOS-latest'
|
||||
pool:
|
||||
vmImage: $(vmImage)
|
||||
steps:
|
||||
- checkout: self
|
||||
submodules: false
|
||||
lfs: false,
|
||||
fetchDepth: 1
|
||||
fetchFilter: tree:0
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: Download build artifacts
|
||||
inputs:
|
||||
@@ -279,8 +276,8 @@ stages:
|
||||
displayName: Run dotnet test
|
||||
inputs:
|
||||
command: test
|
||||
projects: "tests/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj"
|
||||
arguments: "--configuration $(buildConfiguration) --no-build"
|
||||
projects: 'tests/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj'
|
||||
arguments: '--configuration $(buildConfiguration) --no-build'
|
||||
testRunTitle: Unit Tests - $(Agent.OS)
|
||||
|
||||
- stage: Integration
|
||||
@@ -291,47 +288,20 @@ stages:
|
||||
jobs:
|
||||
# Integration Tests (SQLite)
|
||||
- job:
|
||||
timeoutInMinutes: 180
|
||||
displayName: Integration Tests (SQLite)
|
||||
strategy:
|
||||
matrix:
|
||||
# Windows:
|
||||
# vmImage: 'windows-latest'
|
||||
# We split the tests into 3 parts for each OS to reduce the time it takes to run them on the pipeline
|
||||
LinuxPart1Of3:
|
||||
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)"
|
||||
LinuxPart2Of3:
|
||||
vmImage: "ubuntu-latest"
|
||||
# Filter tests that are part of the Umbraco.Infrastructure.Service namespace
|
||||
testFilter: "(FullyQualifiedName~Umbraco.Infrastructure.Service)"
|
||||
LinuxPart3Of3:
|
||||
vmImage: "ubuntu-latest"
|
||||
# Filter tests that are not part of the Umbraco.Infrastructure namespace. So this will run all tests that are not part of the Umbraco.Infrastructure namespace
|
||||
testFilter: "(FullyQualifiedName!~Umbraco.Infrastructure)"
|
||||
macOSPart1Of3:
|
||||
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)"
|
||||
macOSPart2Of3:
|
||||
vmImage: "macOS-latest"
|
||||
# Filter tests that are part of the Umbraco.Infrastructure.Service namespace
|
||||
testFilter: "(FullyQualifiedName~Umbraco.Infrastructure.Service)"
|
||||
macOSPart3Of3:
|
||||
vmImage: "macOS-latest"
|
||||
# Filter tests that are not part of the Umbraco.Infrastructure namespace.
|
||||
testFilter: "(FullyQualifiedName!~Umbraco.Infrastructure)"
|
||||
Windows:
|
||||
vmImage: 'windows-latest'
|
||||
Linux:
|
||||
vmImage: 'ubuntu-latest'
|
||||
macOS:
|
||||
vmImage: 'macOS-latest'
|
||||
pool:
|
||||
vmImage: $(vmImage)
|
||||
variables:
|
||||
Tests__Database__DatabaseType: "Sqlite"
|
||||
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
|
||||
@@ -349,16 +319,17 @@ stages:
|
||||
displayName: Run dotnet test
|
||||
inputs:
|
||||
command: test
|
||||
projects: "tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj"
|
||||
projects: 'tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj'
|
||||
testRunTitle: Integration Tests SQLite - $(Agent.OS)
|
||||
${{ if and(eq(variables['Agent.OS'],'Windows_NT'), or(variables.releaseTestFilter, parameters.forceReleaseTestFilter)) }}:
|
||||
arguments: '--filter "$(testFilter)" --configuration $(buildConfiguration) --no-build ${{parameters.integrationReleaseTestFilter}}'
|
||||
arguments: '--configuration $(buildConfiguration) --no-build ${{parameters.integrationReleaseTestFilter}}'
|
||||
${{ elseif eq(variables['Agent.OS'],'Windows_NT') }}:
|
||||
arguments: '--filter "$(testFilter)" --configuration $(buildConfiguration) --no-build ${{parameters.integrationNonReleaseTestFilter}}'
|
||||
arguments: '--configuration $(buildConfiguration) --no-build ${{parameters.integrationNonReleaseTestFilter}}'
|
||||
${{ elseif or(variables.releaseTestFilter, parameters.forceReleaseTestFilter) }}:
|
||||
arguments: '--filter "$(testFilter)" --configuration $(buildConfiguration) --no-build ${{parameters.nonWindowsIntegrationReleaseTestFilter}}'
|
||||
arguments: '--configuration $(buildConfiguration) --no-build ${{parameters.nonWindowsIntegrationReleaseTestFilter}}'
|
||||
${{ else }}:
|
||||
arguments: '--filter "$(testFilter)" --configuration $(buildConfiguration) --no-build ${{parameters.nonWindowsIntegrationNonReleaseTestFilter}}'
|
||||
arguments: '--configuration $(buildConfiguration) --no-build ${{parameters.nonWindowsIntegrationNonReleaseTestFilter}}'
|
||||
|
||||
# Integration Tests (SQL Server)
|
||||
- job:
|
||||
timeoutInMinutes: 180
|
||||
@@ -367,46 +338,15 @@ stages:
|
||||
displayName: Integration Tests (SQL Server)
|
||||
strategy:
|
||||
matrix:
|
||||
# We split the tests into 3 parts for each OS to reduce the time it takes to run them on the pipeline
|
||||
WindowsPart1Of3:
|
||||
vmImage: "windows-latest"
|
||||
Windows:
|
||||
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)"
|
||||
WindowsPart2Of3:
|
||||
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)"
|
||||
WindowsPart3Of3:
|
||||
vmImage: "windows-latest"
|
||||
Tests__Database__DatabaseType: LocalDb
|
||||
Tests__Database__SQLServerMasterConnectionString: N/A
|
||||
# Filter tests that are not part of the Umbraco.Infrastructure namespace. So this will run all tests that are not part of the Umbraco.Infrastructure namespace
|
||||
testFilter: "(FullyQualifiedName!~Umbraco.Infrastructure)"
|
||||
LinuxPart1Of3:
|
||||
vmImage: "ubuntu-latest"
|
||||
Linux:
|
||||
vmImage: 'ubuntu-latest'
|
||||
SA_PASSWORD: UmbracoIntegration123!
|
||||
Tests__Database__DatabaseType: SqlServer
|
||||
Tests__Database__SQLServerMasterConnectionString: "Server=(local);User Id=sa;Password=$(SA_PASSWORD);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)"
|
||||
LinuxPart2Of3:
|
||||
vmImage: "ubuntu-latest"
|
||||
SA_PASSWORD: UmbracoIntegration123!
|
||||
Tests__Database__DatabaseType: SqlServer
|
||||
Tests__Database__SQLServerMasterConnectionString: "Server=(local);User Id=sa;Password=$(SA_PASSWORD);TrustServerCertificate=True"
|
||||
# Filter tests that are part of the Umbraco.Infrastructure.Service namespace
|
||||
testFilter: "(FullyQualifiedName~Umbraco.Infrastructure.Service)"
|
||||
LinuxPart3Of3:
|
||||
vmImage: "ubuntu-latest"
|
||||
SA_PASSWORD: UmbracoIntegration123!
|
||||
Tests__Database__DatabaseType: SqlServer
|
||||
Tests__Database__SQLServerMasterConnectionString: "Server=(local);User Id=sa;Password=$(SA_PASSWORD);TrustServerCertificate=True"
|
||||
# Filter tests that are not part of the Umbraco.Infrastructure namespace. So this will run all tests that are not part of the Umbraco.Infrastructure namespace
|
||||
testFilter: "(FullyQualifiedName!~Umbraco.Infrastructure)"
|
||||
Tests__Database__SQLServerMasterConnectionString: 'Server=(local);User Id=sa;Password=$(SA_PASSWORD);TrustServerCertificate=True'
|
||||
pool:
|
||||
vmImage: $(vmImage)
|
||||
steps:
|
||||
@@ -436,16 +376,16 @@ stages:
|
||||
displayName: Run dotnet test
|
||||
inputs:
|
||||
command: test
|
||||
projects: "tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj"
|
||||
projects: 'tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj'
|
||||
testRunTitle: Integration Tests SQL Server - $(Agent.OS)
|
||||
${{ if and(eq(variables['Agent.OS'],'Windows_NT'), or(variables.releaseTestFilter, parameters.forceReleaseTestFilter)) }}:
|
||||
arguments: '--filter "$(testFilter)" --configuration $(buildConfiguration) --no-build ${{parameters.integrationReleaseTestFilter}}'
|
||||
arguments: '--configuration $(buildConfiguration) --no-build ${{parameters.integrationReleaseTestFilter}}'
|
||||
${{ elseif eq(variables['Agent.OS'],'Windows_NT') }}:
|
||||
arguments: '--filter "$(testFilter)" --configuration $(buildConfiguration) --no-build ${{parameters.integrationNonReleaseTestFilter}}'
|
||||
arguments: '--configuration $(buildConfiguration) --no-build ${{parameters.integrationNonReleaseTestFilter}}'
|
||||
${{ elseif or(variables.releaseTestFilter, parameters.forceReleaseTestFilter) }}:
|
||||
arguments: '--filter "$(testFilter)" --configuration $(buildConfiguration) --no-build ${{parameters.nonWindowsIntegrationReleaseTestFilter}}'
|
||||
arguments: '--configuration $(buildConfiguration) --no-build ${{parameters.nonWindowsIntegrationReleaseTestFilter}}'
|
||||
${{ else }}:
|
||||
arguments: '--filter "$(testFilter)" --configuration $(buildConfiguration) --no-build ${{parameters.nonWindowsIntegrationNonReleaseTestFilter}}'
|
||||
arguments: '--configuration $(buildConfiguration) --no-build ${{parameters.nonWindowsIntegrationNonReleaseTestFilter}}'
|
||||
|
||||
# Stop SQL Server
|
||||
- pwsh: docker stop mssql
|
||||
@@ -490,24 +430,10 @@ stages:
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN_PROVIDERNAME: Microsoft.Data.Sqlite
|
||||
strategy:
|
||||
matrix:
|
||||
LinuxPart1Of3:
|
||||
vmImage: "ubuntu-latest"
|
||||
testCommand: "npm run smokeTest -- --shard=1/3"
|
||||
LinuxPart2Of3:
|
||||
vmImage: "ubuntu-latest"
|
||||
testCommand: "npm run smokeTest -- --shard=2/3"
|
||||
LinuxPart3Of3:
|
||||
vmImage: "ubuntu-latest"
|
||||
testCommand: "npm run smokeTest -- --shard=3/3"
|
||||
WindowsPart1Of3:
|
||||
vmImage: "windows-latest"
|
||||
testCommand: "npm run smokeTest -- --shard=1/3"
|
||||
WindowsPart2Of3:
|
||||
vmImage: "windows-latest"
|
||||
testCommand: "npm run smokeTest -- --shard=2/3"
|
||||
WindowsPart3Of3:
|
||||
vmImage: "windows-latest"
|
||||
testCommand: "npm run smokeTest -- --shard=3/3"
|
||||
Linux:
|
||||
vmImage: 'ubuntu-latest'
|
||||
Windows:
|
||||
vmImage: 'windows-latest'
|
||||
pool:
|
||||
vmImage: $(vmImage)
|
||||
steps:
|
||||
@@ -580,6 +506,10 @@ stages:
|
||||
condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT'))
|
||||
workingDirectory: $(Agent.BuildDirectory)/app
|
||||
|
||||
# Ensures we have the package wait-on installed
|
||||
- pwsh: npm install wait-on
|
||||
displayName: Install wait-on package
|
||||
|
||||
# Wait for application to start responding to requests
|
||||
- pwsh: npx wait-on -v --interval 1000 --timeout 120000 $(ASPNETCORE_URLS)
|
||||
displayName: Wait for application
|
||||
@@ -591,7 +521,7 @@ stages:
|
||||
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
|
||||
|
||||
# Test
|
||||
- pwsh: $(testCommand)
|
||||
- pwsh: npm run smokeTestSqlite --ignore-certificate-errors
|
||||
displayName: Run Playwright tests
|
||||
continueOnError: true
|
||||
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
|
||||
@@ -612,7 +542,7 @@ stages:
|
||||
# Copy artifacts
|
||||
- pwsh: |
|
||||
if (Test-Path tests/Umbraco.Tests.AcceptanceTest/results/*) {
|
||||
Copy-Item tests/Umbraco.Tests.AcceptanceTest/results/* $(Build.ArtifactStagingDirectory) -Recurse
|
||||
Copy-Item tests/Umbraco.Tests.AcceptanceTest/results $(Build.ArtifactStagingDirectory) -Recurse
|
||||
}
|
||||
displayName: Copy Playwright results
|
||||
condition: succeededOrFailed()
|
||||
@@ -623,7 +553,8 @@ stages:
|
||||
condition: succeededOrFailed()
|
||||
inputs:
|
||||
targetPath: $(Build.ArtifactStagingDirectory)
|
||||
artifact: "Acceptance Test Results - $(Agent.JobName) - Attempt #$(System.JobAttempt)"
|
||||
artifact: 'Acceptance Tests - $(Agent.JobName) - Attempt #$(System.JobAttempt)'
|
||||
|
||||
- job:
|
||||
displayName: E2E Tests (SQL Server)
|
||||
variables:
|
||||
@@ -632,31 +563,13 @@ stages:
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN_PROVIDERNAME: Microsoft.Data.SqlClient
|
||||
strategy:
|
||||
matrix:
|
||||
${{ if eq(parameters.sqlServerLinuxAcceptanceTests, True) }}:
|
||||
LinuxPart1Of3:
|
||||
testCommand: "npm run smokeTestSqlite -- --shard=1/3"
|
||||
vmImage: "ubuntu-latest"
|
||||
${{ if eq(parameters.sqlServerLinuxAcceptanceTests, True) }} :
|
||||
Linux:
|
||||
vmImage: 'ubuntu-latest'
|
||||
SA_PASSWORD: $(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD)
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN: "Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);TrustServerCertificate=True"
|
||||
LinuxPart2Of3:
|
||||
testCommand: "npm run smokeTestSqlite -- --shard=2/3"
|
||||
vmImage: "ubuntu-latest"
|
||||
SA_PASSWORD: $(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD)
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN: "Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);TrustServerCertificate=True"
|
||||
LinuxPart3Of3:
|
||||
testCommand: "npm run smokeTestSqlite -- --shard=3/3"
|
||||
vmImage: "ubuntu-latest"
|
||||
SA_PASSWORD: $(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD)
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN: "Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);TrustServerCertificate=True"
|
||||
WindowsPart1Of3:
|
||||
vmImage: "windows-latest"
|
||||
testCommand: "npm run smokeTestSqlite -- --shard=1/3"
|
||||
WindowsPart2Of3:
|
||||
vmImage: "windows-latest"
|
||||
testCommand: "npm run smokeTestSqlite -- --shard=2/3"
|
||||
WindowsPart3Of3:
|
||||
vmImage: "windows-latest"
|
||||
testCommand: "npm run smokeTestSqlite -- --shard=3/3"
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN: 'Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);TrustServerCertificate=True'
|
||||
Windows:
|
||||
vmImage: 'windows-latest'
|
||||
pool:
|
||||
vmImage: $(vmImage)
|
||||
steps:
|
||||
@@ -748,7 +661,7 @@ stages:
|
||||
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
|
||||
|
||||
# Test
|
||||
- pwsh: $(testCommand)
|
||||
- pwsh: npm run smokeTest --ignore-certificate-errors
|
||||
displayName: Run Playwright tests
|
||||
continueOnError: true
|
||||
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
|
||||
@@ -778,7 +691,7 @@ stages:
|
||||
# Copy artifacts
|
||||
- pwsh: |
|
||||
if (Test-Path tests/Umbraco.Tests.AcceptanceTest/results/*) {
|
||||
Copy-Item tests/Umbraco.Tests.AcceptanceTest/results/* $(Build.ArtifactStagingDirectory) -Recurse
|
||||
Copy-Item tests/Umbraco.Tests.AcceptanceTest/results $(Build.ArtifactStagingDirectory) -Recurse
|
||||
}
|
||||
displayName: Copy Playwright results
|
||||
condition: succeededOrFailed()
|
||||
@@ -789,7 +702,8 @@ stages:
|
||||
condition: succeededOrFailed()
|
||||
inputs:
|
||||
targetPath: $(Build.ArtifactStagingDirectory)
|
||||
artifact: "Acceptance Test Results - $(Agent.JobName) - Attempt #$(System.JobAttempt)"
|
||||
artifact: 'Acceptance Tests - $(Agent.JobName) - Attempt #$(System.JobAttempt)'
|
||||
|
||||
###############################################
|
||||
## Release
|
||||
###############################################
|
||||
@@ -813,13 +727,13 @@ stages:
|
||||
- task: NuGetCommand@2
|
||||
displayName: NuGet push
|
||||
inputs:
|
||||
command: "push"
|
||||
command: 'push'
|
||||
packagesToPush: $(Build.ArtifactStagingDirectory)/**/*.nupkg
|
||||
nuGetFeedType: "external"
|
||||
nuGetFeedType: 'external'
|
||||
${{ if eq(parameters.isNightly, true) }}:
|
||||
publishFeedCredentials: "MyGet - Umbraco Nightly"
|
||||
publishFeedCredentials: 'MyGet - Umbraco Nightly'
|
||||
${{ else }}:
|
||||
publishFeedCredentials: "MyGet - Pre-releases"
|
||||
publishFeedCredentials: 'MyGet - Pre-releases'
|
||||
- job:
|
||||
displayName: Push to pre-release feed (npm)
|
||||
steps:
|
||||
@@ -843,8 +757,8 @@ stages:
|
||||
- task: npmAuthenticate@0
|
||||
displayName: Authenticate with npm (MyGet)
|
||||
inputs:
|
||||
workingFile: "$(Pipeline.Workspace)/npm/.npmrc"
|
||||
customEndpoint: "MyGet (npm) - Umbracoprereleases, MyGet (npm) - Umbraconightly"
|
||||
workingFile: '$(Pipeline.Workspace)/npm/.npmrc'
|
||||
customEndpoint: 'MyGet (npm) - Umbracoprereleases, MyGet (npm) - Umbraconightly'
|
||||
- bash: |
|
||||
# Setup temp npm project to load in defaults from the local .npmrc
|
||||
npm init -y
|
||||
@@ -874,10 +788,10 @@ stages:
|
||||
- task: NuGetCommand@2
|
||||
displayName: NuGet push
|
||||
inputs:
|
||||
command: "push"
|
||||
command: 'push'
|
||||
packagesToPush: $(Build.ArtifactStagingDirectory)/**/*.nupkg
|
||||
nuGetFeedType: "external"
|
||||
publishFeedCredentials: "NuGet - Umbraco.*"
|
||||
nuGetFeedType: 'external'
|
||||
publishFeedCredentials: 'NuGet - Umbraco.*'
|
||||
|
||||
- stage: Deploy_Npm
|
||||
displayName: Npm release
|
||||
@@ -898,7 +812,7 @@ stages:
|
||||
displayName: Authenticate with npm
|
||||
inputs:
|
||||
workingFile: $(Pipeline.Workspace)/npm/.npmrc
|
||||
customEndpoint: "NPM - Umbraco Backoffice"
|
||||
customEndpoint: 'NPM - Umbraco Backoffice'
|
||||
- script: |
|
||||
# Setup temp npm project to load in defaults from the local .npmrc
|
||||
npm init -y
|
||||
@@ -911,7 +825,7 @@ stages:
|
||||
|
||||
- stage: Upload_API_Docs
|
||||
pool:
|
||||
vmImage: "windows-latest" # Apparently AzureFileCopy is windows only :(
|
||||
vmImage: 'windows-latest' # Apparently AzureFileCopy is windows only :(
|
||||
variables:
|
||||
umbracoMajorVersion: $[ stageDependencies.Build.A.outputs['build.NBGV_VersionMajor'] ]
|
||||
displayName: Upload API Documentation
|
||||
@@ -933,15 +847,14 @@ stages:
|
||||
inputs:
|
||||
archiveFilePatterns: $(Build.SourcesDirectory)/csharp-docs.zip
|
||||
destinationFolder: $(Build.ArtifactStagingDirectory)/csharp-docs
|
||||
overwriteExistingFiles: true
|
||||
- task: AzureFileCopy@4
|
||||
displayName: "Copy C# Docs to blob storage"
|
||||
displayName: 'Copy C# Docs to blob storage'
|
||||
inputs:
|
||||
SourcePath: "$(Build.ArtifactStagingDirectory)/csharp-docs/*"
|
||||
SourcePath: '$(Build.ArtifactStagingDirectory)/csharp-docs/*'
|
||||
azureSubscription: umbraco-storage
|
||||
Destination: AzureBlob
|
||||
storage: umbracoapidocs
|
||||
ContainerName: "$web"
|
||||
ContainerName: '$web'
|
||||
BlobPrefix: v$(umbracoMajorVersion)/csharp
|
||||
CleanTargetBeforeCopy: true
|
||||
- job:
|
||||
@@ -957,15 +870,14 @@ stages:
|
||||
inputs:
|
||||
archiveFilePatterns: $(Build.SourcesDirectory)/ui-docs.zip
|
||||
destinationFolder: $(Build.ArtifactStagingDirectory)/ui-docs
|
||||
overwriteExistingFiles: true
|
||||
- task: AzureFileCopy@4
|
||||
displayName: "Copy Storybook to blob storage"
|
||||
displayName: 'Copy Storybook to blob storage'
|
||||
inputs:
|
||||
SourcePath: "$(Build.ArtifactStagingDirectory)/ui-docs/*"
|
||||
SourcePath: '$(Build.ArtifactStagingDirectory)/ui-docs/*'
|
||||
azureSubscription: umbraco-storage
|
||||
Destination: AzureBlob
|
||||
storage: umbracoapidocs
|
||||
ContainerName: "$web"
|
||||
ContainerName: '$web'
|
||||
BlobPrefix: v$(umbracoMajorVersion)/ui
|
||||
CleanTargetBeforeCopy: true
|
||||
- job:
|
||||
@@ -981,14 +893,13 @@ stages:
|
||||
inputs:
|
||||
archiveFilePatterns: $(Build.SourcesDirectory)/ui-api-docs.zip
|
||||
destinationFolder: $(Build.ArtifactStagingDirectory)/ui-api-docs
|
||||
overwriteExistingFiles: true
|
||||
- task: AzureFileCopy@4
|
||||
displayName: "Copy UI API Docs to blob storage"
|
||||
displayName: 'Copy UI API Docs to blob storage'
|
||||
inputs:
|
||||
SourcePath: "$(Build.ArtifactStagingDirectory)/ui-api-docs/*"
|
||||
SourcePath: '$(Build.ArtifactStagingDirectory)/ui-api-docs/*'
|
||||
azureSubscription: umbraco-storage
|
||||
Destination: AzureBlob
|
||||
storage: umbracoapidocs
|
||||
ContainerName: "$web"
|
||||
ContainerName: '$web'
|
||||
BlobPrefix: v$(umbracoMajorVersion)/ui-api
|
||||
CleanTargetBeforeCopy: true
|
||||
|
||||
@@ -3,13 +3,12 @@ name: Nightly_E2E_Test_$(TeamProject)_$(Build.DefinitionName)_$(SourceBranchName
|
||||
pr: none
|
||||
trigger: none
|
||||
|
||||
schedules:
|
||||
- cron: '0 0 * * *'
|
||||
displayName: Daily midnight build
|
||||
branches:
|
||||
include:
|
||||
- v14/dev
|
||||
- v15/dev
|
||||
# schedules:
|
||||
# - cron: '0 0 * * *'
|
||||
# displayName: Daily midnight build
|
||||
# branches:
|
||||
# include:
|
||||
# - v14/dev
|
||||
|
||||
variables:
|
||||
nodeVersion: 20
|
||||
@@ -23,6 +22,12 @@ variables:
|
||||
npm_config_cache: $(Pipeline.Workspace)/.npm_client
|
||||
NODE_OPTIONS: --max_old_space_size=16384
|
||||
|
||||
parameters:
|
||||
- name: runSmokeTests
|
||||
displayName: Run the smoke tests
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
stages:
|
||||
###############################################
|
||||
## Build
|
||||
@@ -109,24 +114,10 @@ stages:
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN_PROVIDERNAME: Microsoft.Data.Sqlite
|
||||
strategy:
|
||||
matrix:
|
||||
LinuxPart1Of3:
|
||||
vmImage: "ubuntu-latest"
|
||||
testCommand: "npm run test -- --shard=1/3"
|
||||
LinuxPart2Of3:
|
||||
vmImage: "ubuntu-latest"
|
||||
testCommand: "npm run test -- --shard=2/3"
|
||||
LinuxPart3Of3:
|
||||
vmImage: "ubuntu-latest"
|
||||
testCommand: "npm run test -- --shard=3/3"
|
||||
WindowsPart1Of3:
|
||||
vmImage: "windows-latest"
|
||||
testCommand: "npm run test -- --shard=1/3"
|
||||
WindowsPart2Of3:
|
||||
vmImage: "windows-latest"
|
||||
testCommand: "npm run test -- --shard=2/3"
|
||||
WindowsPart3Of3:
|
||||
vmImage: "windows-latest"
|
||||
testCommand: "npm run test -- --shard=3/3"
|
||||
Linux:
|
||||
vmImage: 'ubuntu-latest'
|
||||
Windows:
|
||||
vmImage: 'windows-latest'
|
||||
pool:
|
||||
vmImage: $(vmImage)
|
||||
steps:
|
||||
@@ -214,7 +205,10 @@ stages:
|
||||
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
|
||||
|
||||
# Test
|
||||
- pwsh: $(testCommand)
|
||||
- ${{ if eq(parameters.runSmokeTests, true) }}:
|
||||
pwsh: npm run smokeTestSqlite --ignore-certificate-errors
|
||||
${{ else }}:
|
||||
pwsh: npm run testSqlite --ignore-certificate-errors
|
||||
displayName: Run Playwright tests
|
||||
continueOnError: true
|
||||
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
|
||||
@@ -235,7 +229,7 @@ stages:
|
||||
# Copy artifacts
|
||||
- pwsh: |
|
||||
if (Test-Path tests/Umbraco.Tests.AcceptanceTest/results/*) {
|
||||
Copy-Item tests/Umbraco.Tests.AcceptanceTest/results/* $(Build.ArtifactStagingDirectory) -Recurse
|
||||
Copy-Item tests/Umbraco.Tests.AcceptanceTest/results $(Build.ArtifactStagingDirectory) -Recurse
|
||||
}
|
||||
displayName: Copy Playwright results
|
||||
condition: succeededOrFailed()
|
||||
@@ -246,7 +240,7 @@ stages:
|
||||
condition: succeededOrFailed()
|
||||
inputs:
|
||||
targetPath: $(Build.ArtifactStagingDirectory)
|
||||
artifact: "Acceptance Test Results - $(Agent.JobName) - Attempt #$(System.JobAttempt)"
|
||||
artifact: 'Acceptance Tests - $(Agent.JobName) - Attempt #$(System.JobAttempt)'
|
||||
|
||||
- job:
|
||||
displayName: E2E Tests (SQL Server)
|
||||
@@ -257,30 +251,12 @@ stages:
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN_PROVIDERNAME: Microsoft.Data.SqlClient
|
||||
strategy:
|
||||
matrix:
|
||||
LinuxPart1Of3:
|
||||
testCommand: "npm run testSqlite -- --shard=1/3"
|
||||
vmImage: "ubuntu-latest"
|
||||
Linux:
|
||||
vmImage: 'ubuntu-latest'
|
||||
SA_PASSWORD: $(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD)
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN: "Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);TrustServerCertificate=True"
|
||||
LinuxPart2Of3:
|
||||
testCommand: "npm run testSqlite -- --shard=2/3"
|
||||
vmImage: "ubuntu-latest"
|
||||
SA_PASSWORD: $(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD)
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN: "Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);TrustServerCertificate=True"
|
||||
LinuxPart3Of3:
|
||||
testCommand: "npm run testSqlite -- --shard=3/3"
|
||||
vmImage: "ubuntu-latest"
|
||||
SA_PASSWORD: $(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD)
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN: "Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);TrustServerCertificate=True"
|
||||
WindowsPart1Of3:
|
||||
vmImage: "windows-latest"
|
||||
testCommand: "npm run testSqlite -- --shard=1/3"
|
||||
WindowsPart2Of3:
|
||||
vmImage: "windows-latest"
|
||||
testCommand: "npm run testSqlite -- --shard=2/3"
|
||||
WindowsPart3Of3:
|
||||
vmImage: "windows-latest"
|
||||
testCommand: "npm run testSqlite -- --shard=3/3"
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN: 'Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);TrustServerCertificate=True'
|
||||
Windows:
|
||||
vmImage: 'windows-latest'
|
||||
pool:
|
||||
vmImage: $(vmImage)
|
||||
steps:
|
||||
@@ -376,7 +352,10 @@ stages:
|
||||
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
|
||||
|
||||
# Test
|
||||
- pwsh: $(testCommand)
|
||||
- ${{ if eq(parameters.runSmokeTests, true) }}:
|
||||
pwsh: npm run smokeTest --ignore-certificate-errors
|
||||
${{ else }}:
|
||||
pwsh: npm run test --ignore-certificate-errors
|
||||
displayName: Run Playwright tests
|
||||
continueOnError: true
|
||||
workingDirectory: tests/Umbraco.Tests.AcceptanceTest
|
||||
@@ -406,7 +385,7 @@ stages:
|
||||
# Copy artifacts
|
||||
- pwsh: |
|
||||
if (Test-Path tests/Umbraco.Tests.AcceptanceTest/results/*) {
|
||||
Copy-Item tests/Umbraco.Tests.AcceptanceTest/results/* $(Build.ArtifactStagingDirectory) -Recurse
|
||||
Copy-Item tests/Umbraco.Tests.AcceptanceTest/results $(Build.ArtifactStagingDirectory) -Recurse
|
||||
}
|
||||
displayName: Copy Playwright results
|
||||
condition: succeededOrFailed()
|
||||
@@ -417,4 +396,4 @@ stages:
|
||||
condition: succeededOrFailed()
|
||||
inputs:
|
||||
targetPath: $(Build.ArtifactStagingDirectory)
|
||||
artifact: "Acceptance Test Results - $(Agent.JobName) - Attempt #$(System.JobAttempt)"
|
||||
artifact: 'Acceptance Tests - $(Agent.JobName) - Attempt #$(System.JobAttempt)'
|
||||
|
||||
+2
-3
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"sdk": {
|
||||
"version": "9.0.100",
|
||||
"rollForward": "latestFeature",
|
||||
"allowPrerelease": false
|
||||
"version": "8.0.300",
|
||||
"rollForward": "latestFeature"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,7 +6,6 @@ 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;
|
||||
@@ -16,23 +14,22 @@ public class ConfigureUmbracoSwaggerGenOptions : IConfigureOptions<SwaggerGenOpt
|
||||
{
|
||||
private readonly IOperationIdSelector _operationIdSelector;
|
||||
private readonly ISchemaIdSelector _schemaIdSelector;
|
||||
private readonly ISubTypesSelector _subTypesSelector;
|
||||
|
||||
[Obsolete("Use non-obsolete constructor. This will be removed in Umbraco 16.")]
|
||||
[Obsolete("Use non-obsolete constructor. This will be removed in Umbraco 15.")]
|
||||
public ConfigureUmbracoSwaggerGenOptions(
|
||||
IOptions<ApiVersioningOptions> apiVersioningOptions,
|
||||
IOperationIdSelector operationIdSelector,
|
||||
ISchemaIdSelector schemaIdSelector)
|
||||
: this(operationIdSelector, schemaIdSelector)
|
||||
{
|
||||
}
|
||||
|
||||
public ConfigureUmbracoSwaggerGenOptions(
|
||||
IOperationIdSelector operationIdSelector,
|
||||
ISchemaIdSelector schemaIdSelector)
|
||||
: this(operationIdSelector, schemaIdSelector, StaticServiceProvider.Instance.GetRequiredService<ISubTypesSelector>())
|
||||
{ }
|
||||
|
||||
public ConfigureUmbracoSwaggerGenOptions(
|
||||
IOperationIdSelector operationIdSelector,
|
||||
ISchemaIdSelector schemaIdSelector,
|
||||
ISubTypesSelector subTypesSelector)
|
||||
{
|
||||
_operationIdSelector = operationIdSelector;
|
||||
_schemaIdSelector = schemaIdSelector;
|
||||
_subTypesSelector = subTypesSelector;
|
||||
}
|
||||
|
||||
public void Configure(SwaggerGenOptions swaggerGenOptions)
|
||||
@@ -49,25 +46,26 @@ public class ConfigureUmbracoSwaggerGenOptions : IConfigureOptions<SwaggerGenOpt
|
||||
swaggerGenOptions.CustomOperationIds(description => _operationIdSelector.OperationId(description));
|
||||
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}";
|
||||
}
|
||||
|
||||
@@ -23,8 +23,6 @@ public static class UmbracoBuilderApiExtensions
|
||||
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;
|
||||
|
||||
@@ -41,13 +41,13 @@ public static class UmbracoBuilderAuthExtensions
|
||||
.SetTokenEndpointUris(
|
||||
Paths.MemberApi.TokenEndpoint.TrimStart(Constants.CharArrays.ForwardSlash),
|
||||
Paths.BackOfficeApi.TokenEndpoint.TrimStart(Constants.CharArrays.ForwardSlash))
|
||||
.SetEndSessionEndpointUris(
|
||||
.SetLogoutEndpointUris(
|
||||
Paths.MemberApi.LogoutEndpoint.TrimStart(Constants.CharArrays.ForwardSlash),
|
||||
Paths.BackOfficeApi.LogoutEndpoint.TrimStart(Constants.CharArrays.ForwardSlash))
|
||||
.SetRevocationEndpointUris(
|
||||
Paths.MemberApi.RevokeEndpoint.TrimStart(Constants.CharArrays.ForwardSlash),
|
||||
Paths.BackOfficeApi.RevokeEndpoint.TrimStart(Constants.CharArrays.ForwardSlash))
|
||||
.SetUserInfoEndpointUris(
|
||||
.SetUserinfoEndpointUris(
|
||||
Paths.MemberApi.UserinfoEndpoint.TrimStart(Constants.CharArrays.ForwardSlash));
|
||||
|
||||
// Enable authorization code flow with PKCE
|
||||
@@ -56,16 +56,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
|
||||
|
||||
@@ -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,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,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);
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@
|
||||
<Title>Umbraco CMS - API Common</Title>
|
||||
<Description>Contains the bits and pieces that are shared between the Umbraco CMS APIs.</Description>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
</ItemGroup>
|
||||
@@ -14,9 +13,10 @@
|
||||
<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>
|
||||
@@ -24,5 +24,6 @@
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Umbraco.Core\Umbraco.Core.csproj" />
|
||||
<ProjectReference Include="..\Umbraco.Web.Common\Umbraco.Web.Common.csproj" />
|
||||
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
-1
@@ -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>();
|
||||
|
||||
+20
-29
@@ -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)
|
||||
{
|
||||
@@ -53,31 +70,5 @@ public class ConfigureUmbracoMemberAuthenticationDeliveryApiSwaggerGenOptions :
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
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,4 +1,3 @@
|
||||
using System.Diagnostics;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@@ -47,17 +46,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)
|
||||
{
|
||||
|
||||
@@ -45,7 +45,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)
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -5,7 +5,6 @@ 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,7 +12,6 @@ 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;
|
||||
@@ -27,47 +25,22 @@ namespace Umbraco.Cms.Api.Delivery.Controllers.Security;
|
||||
[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 +49,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 +75,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 +100,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 +128,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)
|
||||
{
|
||||
|
||||
+1
-26
@@ -5,7 +5,6 @@ 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,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,29 @@ 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)
|
||||
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 +39,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 +51,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)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ public class MemberApplicationManager : OpenIdDictApplicationManagerBase, IMembe
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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,15 +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;
|
||||
|
||||
public ApiMediaQueryService(IPublishedMediaCache publishedMediaCache, ILogger<ApiMediaQueryService> logger, IMediaNavigationQueryService mediaNavigationQueryService)
|
||||
public ApiMediaQueryService(IPublishedSnapshotAccessor publishedSnapshotAccessor, ILogger<ApiMediaQueryService> logger)
|
||||
{
|
||||
_publishedMediaCache = publishedMediaCache;
|
||||
_publishedSnapshotAccessor = publishedSnapshotAccessor;
|
||||
_logger = logger;
|
||||
_mediaNavigationQueryService = mediaNavigationQueryService;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -55,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)
|
||||
{
|
||||
@@ -71,7 +69,7 @@ internal sealed class ApiMediaQueryService : IApiMediaQueryService
|
||||
break;
|
||||
}
|
||||
|
||||
currentChildren = resolvedMedia.Children(null, _publishedMediaCache, _mediaNavigationQueryService);
|
||||
currentChildren = resolvedMedia.Children;
|
||||
}
|
||||
|
||||
return resolvedMedia;
|
||||
@@ -104,7 +102,7 @@ internal sealed class ApiMediaQueryService : IApiMediaQueryService
|
||||
? mediaCache.GetById(parentKey)
|
||||
: TryGetByPath(childrenOf, mediaCache);
|
||||
|
||||
return parent?.Children(null, _publishedMediaCache, _mediaNavigationQueryService) ?? Array.Empty<IPublishedContent>();
|
||||
return parent?.Children ?? Array.Empty<IPublishedContent>();
|
||||
}
|
||||
|
||||
private IEnumerable<IPublishedContent>? ApplyFilters(IEnumerable<IPublishedContent> source, IEnumerable<string> filters)
|
||||
|
||||
@@ -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;
|
||||
@@ -42,31 +42,42 @@ internal sealed class RequestRedirectService : RoutingServiceBase, IRequestRedir
|
||||
{
|
||||
requestedPath = requestedPath.EnsureStartsWith("/");
|
||||
|
||||
IPublishedContent? startItem = GetStartItem();
|
||||
|
||||
// must append the root content url segment if it is not hidden by config, because
|
||||
// the URL tracking is based on the actual URL, including the root content url segment
|
||||
if (_globalSettings.HideTopLevelNodeFromPath == false)
|
||||
if (_globalSettings.HideTopLevelNodeFromPath == false && startItem?.UrlSegment != null)
|
||||
{
|
||||
IPublishedContent? startItem = GetStartItem();
|
||||
if (startItem?.UrlSegment != null)
|
||||
{
|
||||
requestedPath = $"{startItem.UrlSegment.EnsureStartsWith("/")}{requestedPath}";
|
||||
}
|
||||
requestedPath = $"{startItem.UrlSegment.EnsureStartsWith("/")}{requestedPath}";
|
||||
}
|
||||
|
||||
var culture = _requestCultureService.GetRequestedCulture();
|
||||
|
||||
// append the configured domain content ID to the path if we have a domain bound request,
|
||||
// because URL tracking registers the tracked url like "{domain content ID}/{content path}"
|
||||
Uri contentRoute = GetDefaultRequestUri(requestedPath);
|
||||
DomainAndUri? domainAndUri = GetDomainAndUriForRoute(contentRoute);
|
||||
if (domainAndUri != null)
|
||||
// important: redirect URLs are always tracked without trailing slashes
|
||||
requestedPath = requestedPath.TrimEnd("/");
|
||||
IRedirectUrl? redirectUrl = _redirectUrlService.GetMostRecentRedirectUrl(requestedPath, culture);
|
||||
|
||||
// if a redirect URL was not found, try by appending the start item ID because URL tracking might have tracked
|
||||
// a redirect with "{root content ID}/{content path}"
|
||||
if (redirectUrl is null && startItem is not null)
|
||||
{
|
||||
requestedPath = GetContentRoute(domainAndUri, contentRoute);
|
||||
culture ??= domainAndUri.Culture;
|
||||
redirectUrl = _redirectUrlService.GetMostRecentRedirectUrl($"{startItem.Id}{requestedPath}", culture);
|
||||
}
|
||||
|
||||
// still no redirect URL found - try looking for a configured domain if we have a domain bound request,
|
||||
// because URL tracking might have tracked a redirect with "{domain content ID}/{content path}"
|
||||
if (redirectUrl is null)
|
||||
{
|
||||
Uri contentRoute = GetDefaultRequestUri(requestedPath);
|
||||
DomainAndUri? domainAndUri = GetDomainAndUriForRoute(contentRoute);
|
||||
if (domainAndUri is not null)
|
||||
{
|
||||
requestedPath = GetContentRoute(domainAndUri, contentRoute);
|
||||
culture ??= domainAndUri.Culture;
|
||||
redirectUrl = _redirectUrlService.GetMostRecentRedirectUrl(requestedPath, culture);
|
||||
}
|
||||
}
|
||||
|
||||
// important: redirect URLs are always tracked without trailing slashes
|
||||
IRedirectUrl? redirectUrl = _redirectUrlService.GetMostRecentRedirectUrl(requestedPath.TrimEnd("/"), culture);
|
||||
IPublishedContent? content = redirectUrl != null
|
||||
? _apiPublishedContentCache.GetById(redirectUrl.ContentKey)
|
||||
: null;
|
||||
|
||||
@@ -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,11 @@
|
||||
<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>
|
||||
<!-- TODO: [ASP0019] use IHeaderDictionary.Append or the indexer to append or set headers,
|
||||
and remove this override -->
|
||||
<WarningsNotAsErrors>ASP0019</WarningsNotAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Umbraco.Cms.Api.Common\Umbraco.Cms.Api.Common.csproj" />
|
||||
<ProjectReference Include="..\Umbraco.Web.Common\Umbraco.Web.Common.csproj" />
|
||||
|
||||
+1
@@ -31,6 +31,7 @@ public class ConfigureUmbracoManagementApiSwaggerGenOptions : IConfigureOptions<
|
||||
});
|
||||
|
||||
swaggerGenOptions.OperationFilter<ResponseHeaderOperationFilter>();
|
||||
swaggerGenOptions.SelectSubTypesUsing(_umbracoJsonTypeInfoResolver.FindSubTypes);
|
||||
swaggerGenOptions.UseOneOfForPolymorphism();
|
||||
|
||||
// Ensure all types that implements the IOpenApiDiscriminator have a $type property in the OpenApi schema with the default value (The class name) that is expected by the server
|
||||
|
||||
@@ -51,7 +51,7 @@ public class BackOfficeLoginController : Controller
|
||||
|
||||
if (string.IsNullOrEmpty(model.UmbracoUrl))
|
||||
{
|
||||
model.UmbracoUrl = _hostingEnvironment.ToAbsolute(Constants.System.DefaultUmbracoPath);
|
||||
model.UmbracoUrl = _hostingEnvironment.ToAbsolute(_globalSettings.UmbracoPath);
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(model.ReturnUrl))
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ namespace Umbraco.Cms.Api.Management.Controllers.Content;
|
||||
public abstract class ContentCollectionControllerBase<TContent, TCollectionResponseModel, TValueResponseModelBase, TVariantResponseModel> : ManagementApiControllerBase
|
||||
where TContent : class, IContentBase
|
||||
where TCollectionResponseModel : ContentResponseModelBase<TValueResponseModelBase, TVariantResponseModel>
|
||||
where TValueResponseModelBase : ValueResponseModelBase
|
||||
where TValueResponseModelBase : ValueModelBase
|
||||
where TVariantResponseModel : VariantResponseModelBase
|
||||
{
|
||||
private readonly IUmbracoMapper _mapper;
|
||||
|
||||
@@ -4,7 +4,6 @@ using Umbraco.Cms.Api.Management.ViewModels.Content;
|
||||
using Umbraco.Cms.Core.Mapping;
|
||||
using Umbraco.Cms.Core.Models.ContentEditing;
|
||||
using Umbraco.Cms.Core.Models.ContentEditing.Validation;
|
||||
using Umbraco.Cms.Core.PropertyEditors.Validation;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
@@ -101,7 +100,7 @@ public abstract class ContentControllerBase : ManagementApiControllerBase
|
||||
|
||||
var errors = new SortedDictionary<string, string[]>();
|
||||
|
||||
var validationErrorExpressionRoot = $"$.{nameof(ContentModelBase<TValueModel, TVariantModel>.Values).ToFirstLowerInvariant()}";
|
||||
var missingPropertyModels = new List<PropertyValidationResponseModel>();
|
||||
foreach (PropertyValidationError validationError in validationResult.ValidationErrors)
|
||||
{
|
||||
TValueModel? requestValue = requestModel.Values.FirstOrDefault(value =>
|
||||
@@ -110,16 +109,13 @@ public abstract class ContentControllerBase : ManagementApiControllerBase
|
||||
&& value.Segment == validationError.Segment);
|
||||
if (requestValue is null)
|
||||
{
|
||||
errors.Add(
|
||||
$"{validationErrorExpressionRoot}[{JsonPathExpression.MissingPropertyValue(validationError.Alias, validationError.Culture, validationError.Segment)}].{nameof(ValueModelBase.Value)}",
|
||||
validationError.ErrorMessages);
|
||||
missingPropertyModels.Add(MapMissingProperty(validationError));
|
||||
continue;
|
||||
}
|
||||
|
||||
var index = requestModel.Values.IndexOf(requestValue);
|
||||
errors.Add(
|
||||
$"$.{nameof(ContentModelBase<TValueModel, TVariantModel>.Values).ToFirstLowerInvariant()}[{index}].{nameof(ValueModelBase.Value).ToFirstLowerInvariant()}{validationError.JsonPath}",
|
||||
validationError.ErrorMessages);
|
||||
var key = $"$.{nameof(ContentModelBase<TValueModel, TVariantModel>.Values).ToFirstLowerInvariant()}[{index}].{nameof(ValueModelBase.Value).ToFirstLowerInvariant()}{validationError.JsonPath}";
|
||||
errors.Add(key, validationError.ErrorMessages);
|
||||
}
|
||||
|
||||
return OperationStatusResult(status, problemDetailsBuilder
|
||||
@@ -127,6 +123,16 @@ public abstract class ContentControllerBase : ManagementApiControllerBase
|
||||
.WithTitle("Validation failed")
|
||||
.WithDetail("One or more properties did not pass validation")
|
||||
.WithRequestModelErrors(errors)
|
||||
.WithExtension("missingValues", missingPropertyModels.ToArray())
|
||||
.Build()));
|
||||
}
|
||||
|
||||
private PropertyValidationResponseModel MapMissingProperty(PropertyValidationError source) =>
|
||||
new()
|
||||
{
|
||||
Alias = source.Alias,
|
||||
Segment = source.Segment,
|
||||
Culture = source.Culture,
|
||||
Messages = source.ErrorMessages,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace Umbraco.Cms.Api.Management.Controllers.DataType;
|
||||
|
||||
[VersionedApiBackOfficeRoute(Constants.UdiEntityType.DataType)]
|
||||
[ApiExplorerSettings(GroupName = "Data Type")]
|
||||
[Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentsOrMediaOrMembersOrContentTypes)]
|
||||
[Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentOrMediaOrContentTypes)]
|
||||
public abstract class DataTypeControllerBase : ManagementApiControllerBase
|
||||
{
|
||||
protected IActionResult DataTypeOperationStatusResult(DataTypeOperationStatus status) =>
|
||||
|
||||
@@ -2,14 +2,13 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Api.Management.Factories;
|
||||
using Umbraco.Cms.Api.Management.Security.Authorization.Content;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Document;
|
||||
using Umbraco.Cms.Core.Actions;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Security.Authorization;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.Querying;
|
||||
using Umbraco.Cms.Web.Common.Authorization;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
@@ -19,42 +18,17 @@ namespace Umbraco.Cms.Api.Management.Controllers.Document;
|
||||
public class ByKeyDocumentController : DocumentControllerBase
|
||||
{
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
private readonly IContentEditingService _contentEditingService;
|
||||
private readonly IDocumentPresentationFactory _documentPresentationFactory;
|
||||
private readonly IContentQueryService _contentQueryService;
|
||||
|
||||
[Obsolete("Scheduled for removal in v17")]
|
||||
public ByKeyDocumentController(
|
||||
IAuthorizationService authorizationService,
|
||||
IContentEditingService contentEditingService,
|
||||
IDocumentPresentationFactory documentPresentationFactory)
|
||||
{
|
||||
_authorizationService = authorizationService;
|
||||
_contentEditingService = contentEditingService;
|
||||
_documentPresentationFactory = documentPresentationFactory;
|
||||
_contentQueryService = StaticServiceProvider.Instance.GetRequiredService<IContentQueryService>();
|
||||
}
|
||||
|
||||
// needed for greedy selection until other constructor remains in v17
|
||||
[Obsolete("Scheduled for removal in v17")]
|
||||
public ByKeyDocumentController(
|
||||
IAuthorizationService authorizationService,
|
||||
IContentEditingService contentEditingService,
|
||||
IDocumentPresentationFactory documentPresentationFactory,
|
||||
IContentQueryService contentQueryService)
|
||||
{
|
||||
_authorizationService = authorizationService;
|
||||
_documentPresentationFactory = documentPresentationFactory;
|
||||
_contentQueryService = contentQueryService;
|
||||
}
|
||||
|
||||
[ActivatorUtilitiesConstructor]
|
||||
public ByKeyDocumentController(
|
||||
IAuthorizationService authorizationService,
|
||||
IDocumentPresentationFactory documentPresentationFactory,
|
||||
IContentQueryService contentQueryService)
|
||||
{
|
||||
_authorizationService = authorizationService;
|
||||
_documentPresentationFactory = documentPresentationFactory;
|
||||
_contentQueryService = contentQueryService;
|
||||
}
|
||||
|
||||
[HttpGet("{id:guid}")]
|
||||
@@ -73,16 +47,13 @@ public class ByKeyDocumentController : DocumentControllerBase
|
||||
return Forbidden();
|
||||
}
|
||||
|
||||
var contentWithScheduleAttempt = await _contentQueryService.GetWithSchedulesAsync(id);
|
||||
|
||||
if (contentWithScheduleAttempt.Success == false)
|
||||
IContent? content = await _contentEditingService.GetAsync(id);
|
||||
if (content == null)
|
||||
{
|
||||
return ContentQueryOperationStatusResult(contentWithScheduleAttempt.Status);
|
||||
return DocumentNotFound();
|
||||
}
|
||||
|
||||
DocumentResponseModel model = await _documentPresentationFactory.CreateResponseModelAsync(
|
||||
contentWithScheduleAttempt.Result!.Content,
|
||||
contentWithScheduleAttempt.Result.Schedules);
|
||||
DocumentResponseModel model = await _documentPresentationFactory.CreateResponseModelAsync(content);
|
||||
return Ok(model);
|
||||
}
|
||||
}
|
||||
|
||||
-58
@@ -1,58 +0,0 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.Factories;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Document;
|
||||
using Umbraco.Cms.Core.Actions;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Security.Authorization;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Web.Common.Authorization;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.Document;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
public class ByKeyPublishedDocumentController : DocumentControllerBase
|
||||
{
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
private readonly IContentEditingService _contentEditingService;
|
||||
private readonly IDocumentPresentationFactory _documentPresentationFactory;
|
||||
|
||||
public ByKeyPublishedDocumentController(
|
||||
IAuthorizationService authorizationService,
|
||||
IContentEditingService contentEditingService,
|
||||
IDocumentPresentationFactory documentPresentationFactory)
|
||||
{
|
||||
_authorizationService = authorizationService;
|
||||
_contentEditingService = contentEditingService;
|
||||
_documentPresentationFactory = documentPresentationFactory;
|
||||
}
|
||||
|
||||
[HttpGet("{id:guid}/published")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(PublishedDocumentResponseModel), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> ByKeyPublished(CancellationToken cancellationToken, Guid id)
|
||||
{
|
||||
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
|
||||
User,
|
||||
ContentPermissionResource.WithKeys(ActionBrowse.ActionLetter, id),
|
||||
AuthorizationPolicies.ContentPermissionByResource);
|
||||
|
||||
if (!authorizationResult.Succeeded)
|
||||
{
|
||||
return Forbidden();
|
||||
}
|
||||
|
||||
IContent? content = await _contentEditingService.GetAsync(id);
|
||||
if (content == null || content.Published is false)
|
||||
{
|
||||
return DocumentNotFound();
|
||||
}
|
||||
|
||||
PublishedDocumentResponseModel model = await _documentPresentationFactory.CreatePublishedResponseModelAsync(content);
|
||||
return Ok(model);
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -15,7 +15,7 @@ namespace Umbraco.Cms.Api.Management.Controllers.Document.Collection;
|
||||
[VersionedApiBackOfficeRoute($"{Constants.Web.RoutePath.Collection}/{Constants.UdiEntityType.Document}")]
|
||||
[ApiExplorerSettings(GroupName = nameof(Constants.UdiEntityType.Document))]
|
||||
[Authorize(Policy = AuthorizationPolicies.TreeAccessDocuments)]
|
||||
public abstract class DocumentCollectionControllerBase : ContentCollectionControllerBase<IContent, DocumentCollectionResponseModel, DocumentValueResponseModel, DocumentVariantResponseModel>
|
||||
public abstract class DocumentCollectionControllerBase : ContentCollectionControllerBase<IContent, DocumentCollectionResponseModel, DocumentValueModel, DocumentVariantResponseModel>
|
||||
{
|
||||
protected DocumentCollectionControllerBase(IUmbracoMapper mapper)
|
||||
: base(mapper)
|
||||
|
||||
@@ -170,15 +170,4 @@ public abstract class DocumentControllerBase : ContentControllerBase
|
||||
.WithTitle("Unknown content operation status.")
|
||||
.Build()),
|
||||
});
|
||||
|
||||
protected IActionResult ContentQueryOperationStatusResult(ContentQueryOperationStatus status)
|
||||
=> OperationStatusResult(status, problemDetailsBuilder => status switch
|
||||
{
|
||||
ContentQueryOperationStatus.ContentNotFound => NotFound(problemDetailsBuilder
|
||||
.WithTitle("The document could not be found")
|
||||
.Build()),
|
||||
_ => StatusCode(StatusCodes.Status500InternalServerError, problemDetailsBuilder
|
||||
.WithTitle("Unknown content query status.")
|
||||
.Build()),
|
||||
});
|
||||
}
|
||||
|
||||
+1
-1
@@ -49,7 +49,7 @@ public class GetPublicAccessDocumentController : DocumentControllerBase
|
||||
}
|
||||
|
||||
Attempt<PublicAccessEntry?, PublicAccessOperationStatus> accessAttempt =
|
||||
await _publicAccessService.GetEntryByContentKeyWithoutAncestorsAsync(id);
|
||||
await _publicAccessService.GetEntryByContentKeyAsync(id);
|
||||
|
||||
if (accessAttempt.Success is false || accessAttempt.Result is null)
|
||||
{
|
||||
|
||||
@@ -46,7 +46,7 @@ public class PublishDocumentController : DocumentControllerBase
|
||||
{
|
||||
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
|
||||
User,
|
||||
ContentPermissionResource.WithKeys(ActionPublish.ActionLetter, id, requestModel.PublishSchedules.Where(x => x.Culture is not null).Select(x=>x.Culture!)),
|
||||
ContentPermissionResource.WithKeys(ActionPublish.ActionLetter, id, requestModel.PublishSchedules.Where(x=>x.Culture is not null).Select(x=>x.Culture!)),
|
||||
AuthorizationPolicies.ContentPermissionByResource);
|
||||
|
||||
if (!authorizationResult.Succeeded)
|
||||
@@ -54,7 +54,7 @@ public class PublishDocumentController : DocumentControllerBase
|
||||
return Forbidden();
|
||||
}
|
||||
|
||||
Attempt<List<CulturePublishScheduleModel>, ContentPublishingOperationStatus> modelResult = _documentPresentationFactory.CreateCulturePublishScheduleModels(requestModel);
|
||||
Attempt<CultureAndScheduleModel, ContentPublishingOperationStatus> modelResult = _documentPresentationFactory.CreateCultureAndScheduleModel(requestModel);
|
||||
|
||||
if (modelResult.Success is false)
|
||||
{
|
||||
|
||||
+3
-4
@@ -3,7 +3,6 @@ using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
|
||||
using Umbraco.Cms.Api.Management.ViewModels;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Mapping;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
@@ -23,7 +22,7 @@ public class AreReferencedDocumentController : DocumentControllerBase
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a paged list of the items used in any kind of relation from selected keys.
|
||||
/// Gets a page list of the items used in any kind of relation from selected keys.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Used when bulk deleting content/media and bulk unpublishing content (delete and unpublish on List view).
|
||||
@@ -38,11 +37,11 @@ public class AreReferencedDocumentController : DocumentControllerBase
|
||||
int skip = 0,
|
||||
int take = 20)
|
||||
{
|
||||
PagedModel<Guid> distinctByKeyItemsWithReferencedRelations = await _trackedReferencesSkipTakeService.GetPagedKeysWithDependentReferencesAsync(ids, Constants.ObjectTypes.Document, skip, take);
|
||||
PagedModel<RelationItemModel> distinctByKeyItemsWithReferencedRelations = await _trackedReferencesSkipTakeService.GetPagedItemsWithRelationsAsync(ids, skip, take, true);
|
||||
var pagedViewModel = new PagedViewModel<ReferenceByIdModel>
|
||||
{
|
||||
Total = distinctByKeyItemsWithReferencedRelations.Total,
|
||||
Items = _umbracoMapper.MapEnumerable<Guid, ReferenceByIdModel>(distinctByKeyItemsWithReferencedRelations.Items),
|
||||
Items = _umbracoMapper.MapEnumerable<RelationItemModel, ReferenceByIdModel>(distinctByKeyItemsWithReferencedRelations.Items),
|
||||
};
|
||||
|
||||
return await Task.FromResult(pagedViewModel);
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ public class ReferencedByDocumentController : DocumentControllerBase
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a paged list of tracked references for the current item, so you can see where an item is being used.
|
||||
/// Gets a page list of tracked references for the current item, so you can see where an item is being used.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Used by info tabs on content, media etc. and for the delete and unpublish of single items.
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ public class ReferencedDescendantsDocumentController : DocumentControllerBase
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a paged list of the descendant nodes of the current item used in any kind of relation.
|
||||
/// Gets a page list of the child nodes of the current item used in any kind of relation.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Used when deleting and unpublishing a single item to check if this item has any descending items that are in any
|
||||
|
||||
+1
-2
@@ -1,4 +1,4 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.Controllers.Tree;
|
||||
using Umbraco.Cms.Api.Management.Factories;
|
||||
@@ -54,7 +54,6 @@ public abstract class DocumentTreeControllerBase : UserStartNodeTreeControllerBa
|
||||
responseModel.IsProtected = _publicAccessService.IsProtected(entity.Path);
|
||||
responseModel.IsTrashed = entity.Trashed;
|
||||
responseModel.Id = entity.Key;
|
||||
responseModel.CreateDate = entity.CreateDate;
|
||||
|
||||
responseModel.Variants = _documentPresentationFactory.CreateVariantsItemResponseModels(documentEntitySlim);
|
||||
responseModel.DocumentType = _documentPresentationFactory.CreateDocumentTypeReferenceResponseModel(documentEntitySlim);
|
||||
|
||||
+1
-27
@@ -12,7 +12,6 @@ using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.Document;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
[ApiVersion("1.1")]
|
||||
public class ValidateUpdateDocumentController : UpdateDocumentControllerBase
|
||||
{
|
||||
private readonly IContentEditingService _contentEditingService;
|
||||
@@ -33,35 +32,10 @@ public class ValidateUpdateDocumentController : UpdateDocumentControllerBase
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
[Obsolete("Please use version 1.1 of this API. Will be removed in V16.")]
|
||||
public async Task<IActionResult> Validate(CancellationToken cancellationToken, Guid id, UpdateDocumentRequestModel requestModel)
|
||||
=> await HandleRequest(id, requestModel, async () =>
|
||||
{
|
||||
var validateUpdateDocumentRequestModel = new ValidateUpdateDocumentRequestModel
|
||||
{
|
||||
Values = requestModel.Values,
|
||||
Variants = requestModel.Variants,
|
||||
Template = requestModel.Template,
|
||||
Cultures = null
|
||||
};
|
||||
|
||||
ValidateContentUpdateModel model = _documentEditingPresentationFactory.MapValidateUpdateModel(validateUpdateDocumentRequestModel);
|
||||
Attempt<ContentValidationResult, ContentEditingOperationStatus> result = await _contentEditingService.ValidateUpdateAsync(id, model);
|
||||
|
||||
return result.Success
|
||||
? Ok()
|
||||
: DocumentEditingOperationStatusResult(result.Status, requestModel, result.Result);
|
||||
});
|
||||
|
||||
[HttpPut("{id:guid}/validate")]
|
||||
[MapToApiVersion("1.1")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> ValidateV1_1(CancellationToken cancellationToken, Guid id, ValidateUpdateDocumentRequestModel requestModel)
|
||||
=> await HandleRequest(id, requestModel, async () =>
|
||||
{
|
||||
ValidateContentUpdateModel model = _documentEditingPresentationFactory.MapValidateUpdateModel(requestModel);
|
||||
ContentUpdateModel model = _documentEditingPresentationFactory.MapUpdateModel(requestModel);
|
||||
Attempt<ContentValidationResult, ContentEditingOperationStatus> result = await _contentEditingService.ValidateUpdateAsync(id, model);
|
||||
|
||||
return result.Success
|
||||
|
||||
+1
-1
@@ -32,7 +32,7 @@ public class ItemDocumentTypeItemController : DocumentTypeItemControllerBase
|
||||
return Ok(Enumerable.Empty<DocumentTypeItemResponseModel>());
|
||||
}
|
||||
|
||||
IEnumerable<IContentType> contentTypes = _contentTypeService.GetMany(ids);
|
||||
IEnumerable<IContentType> contentTypes = _contentTypeService.GetAll(ids);
|
||||
List<DocumentTypeItemResponseModel> responseModels = _mapper.MapEnumerable<IContentType, DocumentTypeItemResponseModel>(contentTypes);
|
||||
return await Task.FromResult(Ok(responseModels));
|
||||
}
|
||||
|
||||
+1
-1
@@ -35,7 +35,7 @@ public class SearchDocumentTypeItemController : DocumentTypeItemControllerBase
|
||||
return await Task.FromResult(Ok(new PagedModel<DocumentTypeItemResponseModel> { Total = searchResult.Total }));
|
||||
}
|
||||
|
||||
IEnumerable<IContentType> contentTypes = _contentTypeService.GetMany(searchResult.Items.Select(item => item.Key).ToArray().EmptyNull());
|
||||
IEnumerable<IContentType> contentTypes = _contentTypeService.GetAll(searchResult.Items.Select(item => item.Key).ToArray().EmptyNull());
|
||||
var result = new PagedModel<DocumentTypeItemResponseModel>
|
||||
{
|
||||
Items = _mapper.MapEnumerable<IContentType, DocumentTypeItemResponseModel>(contentTypes),
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ public class DocumentTypeTreeControllerBase : FolderTreeControllerBase<DocumentT
|
||||
protected override DocumentTypeTreeItemResponseModel[] MapTreeItemViewModels(Guid? parentKey, IEntitySlim[] entities)
|
||||
{
|
||||
var contentTypes = _contentTypeService
|
||||
.GetMany(entities.Select(entity => entity.Id).ToArray())
|
||||
.GetAll(entities.Select(entity => entity.Id).ToArray())
|
||||
.ToDictionary(contentType => contentType.Id);
|
||||
|
||||
return entities.Select(entity =>
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ namespace Umbraco.Cms.Api.Management.Controllers.Media.Collection;
|
||||
[VersionedApiBackOfficeRoute($"{Constants.Web.RoutePath.Collection}/{Constants.UdiEntityType.Media}")]
|
||||
[ApiExplorerSettings(GroupName = nameof(Constants.UdiEntityType.Media))]
|
||||
[Authorize(Policy = AuthorizationPolicies.SectionAccessMedia)]
|
||||
public abstract class MediaCollectionControllerBase : ContentCollectionControllerBase<IMedia, MediaCollectionResponseModel, MediaValueResponseModel, MediaVariantResponseModel>
|
||||
public abstract class MediaCollectionControllerBase : ContentCollectionControllerBase<IMedia, MediaCollectionResponseModel, MediaValueModel, MediaVariantResponseModel>
|
||||
{
|
||||
protected MediaCollectionControllerBase(IUmbracoMapper mapper)
|
||||
: base(mapper)
|
||||
|
||||
+2
-3
@@ -3,7 +3,6 @@ using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
|
||||
using Umbraco.Cms.Api.Management.ViewModels;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Mapping;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
@@ -39,11 +38,11 @@ public class AreReferencedMediaController : MediaControllerBase
|
||||
int skip = 0,
|
||||
int take = 20)
|
||||
{
|
||||
PagedModel<Guid> distinctByKeyItemsWithReferencedRelations = await _trackedReferencesSkipTakeService.GetPagedKeysWithDependentReferencesAsync(ids, Constants.ObjectTypes.Media, skip, take);
|
||||
PagedModel<RelationItemModel> distinctByKeyItemsWithReferencedRelations = await _trackedReferencesSkipTakeService.GetPagedItemsWithRelationsAsync(ids, skip, take, true);
|
||||
var pagedViewModel = new PagedViewModel<ReferenceByIdModel>
|
||||
{
|
||||
Total = distinctByKeyItemsWithReferencedRelations.Total,
|
||||
Items = _umbracoMapper.MapEnumerable<Guid, ReferenceByIdModel>(distinctByKeyItemsWithReferencedRelations.Items),
|
||||
Items = _umbracoMapper.MapEnumerable<RelationItemModel, ReferenceByIdModel>(distinctByKeyItemsWithReferencedRelations.Items),
|
||||
};
|
||||
|
||||
return await Task.FromResult(pagedViewModel);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.Controllers.Tree;
|
||||
using Umbraco.Cms.Api.Management.Factories;
|
||||
@@ -50,7 +50,6 @@ public class MediaTreeControllerBase : UserStartNodeTreeControllerBase<MediaTree
|
||||
{
|
||||
responseModel.IsTrashed = entity.Trashed;
|
||||
responseModel.Id = entity.Key;
|
||||
responseModel.CreateDate = entity.CreateDate;
|
||||
|
||||
responseModel.Variants = _mediaPresentationFactory.CreateVariantsItemResponseModels(mediaEntitySlim);
|
||||
responseModel.MediaType = _mediaPresentationFactory.CreateMediaTypeReferenceResponseModel(mediaEntitySlim);
|
||||
|
||||
+1
-1
@@ -32,7 +32,7 @@ public class ItemMediaTypeItemController : MediaTypeItemControllerBase
|
||||
return Ok(Enumerable.Empty<MediaTypeItemResponseModel>());
|
||||
}
|
||||
|
||||
IEnumerable<IMediaType> mediaTypes = _mediaTypeService.GetMany(ids);
|
||||
IEnumerable<IMediaType> mediaTypes = _mediaTypeService.GetAll(ids);
|
||||
List<MediaTypeItemResponseModel> responseModels = _mapper.MapEnumerable<IMediaType, MediaTypeItemResponseModel>(mediaTypes);
|
||||
return await Task.FromResult(Ok(responseModels));
|
||||
}
|
||||
|
||||
+1
-1
@@ -35,7 +35,7 @@ public class SearchMediaTypeItemController : MediaTypeItemControllerBase
|
||||
return await Task.FromResult(Ok(new PagedModel<MediaTypeItemResponseModel> { Total = searchResult.Total }));
|
||||
}
|
||||
|
||||
IEnumerable<IMediaType> mediaTypes = _mediaTypeService.GetMany(searchResult.Items.Select(item => item.Key).ToArray().EmptyNull());
|
||||
IEnumerable<IMediaType> mediaTypes = _mediaTypeService.GetAll(searchResult.Items.Select(item => item.Key).ToArray().EmptyNull());
|
||||
var result = new PagedModel<MediaTypeItemResponseModel>
|
||||
{
|
||||
Items = _mapper.MapEnumerable<IMediaType, MediaTypeItemResponseModel>(mediaTypes),
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@ public class MediaTypeTreeControllerBase : FolderTreeControllerBase<MediaTypeTre
|
||||
protected override MediaTypeTreeItemResponseModel[] MapTreeItemViewModels(Guid? parentKey, IEntitySlim[] entities)
|
||||
{
|
||||
var mediaTypes = _mediaTypeService
|
||||
.GetMany(entities.Select(entity => entity.Id).ToArray())
|
||||
.GetAll(entities.Select(entity => entity.Id).ToArray())
|
||||
.ToDictionary(contentType => contentType.Id);
|
||||
|
||||
return entities.Select(entity =>
|
||||
|
||||
+1
-1
@@ -32,7 +32,7 @@ public class ItemMemberTypeItemController : MemberTypeItemControllerBase
|
||||
return Ok(Enumerable.Empty<MemberTypeItemResponseModel>());
|
||||
}
|
||||
|
||||
IEnumerable<IMemberType> memberTypes = _memberTypeService.GetMany(ids);
|
||||
IEnumerable<IMemberType> memberTypes = _memberTypeService.GetAll(ids);
|
||||
List<MemberTypeItemResponseModel> responseModels = _mapper.MapEnumerable<IMemberType, MemberTypeItemResponseModel>(memberTypes);
|
||||
return Ok(responseModels);
|
||||
}
|
||||
|
||||
+1
-1
@@ -34,7 +34,7 @@ public class SearchMemberTypeItemController : MemberTypeItemControllerBase
|
||||
return await Task.FromResult(Ok(new PagedModel<MemberTypeItemResponseModel> { Total = searchResult.Total }));
|
||||
}
|
||||
|
||||
IEnumerable<IMemberType> memberTypes = _memberTypeService.GetMany(searchResult.Items.Select(item => item.Key).ToArray());
|
||||
IEnumerable<IMemberType> memberTypes = _memberTypeService.GetAll(searchResult.Items.Select(item => item.Key).ToArray());
|
||||
var result = new PagedModel<MemberTypeItemResponseModel>
|
||||
{
|
||||
Items = _mapper.MapEnumerable<IMemberType, MemberTypeItemResponseModel>(memberTypes),
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ public class MemberTypeTreeControllerBase : NamedEntityTreeControllerBase<Member
|
||||
protected override MemberTypeTreeItemResponseModel[] MapTreeItemViewModels(Guid? parentKey, IEntitySlim[] entities)
|
||||
{
|
||||
var memberTypes = _memberTypeService
|
||||
.GetMany(entities.Select(entity => entity.Id).ToArray())
|
||||
.GetAll(entities.Select(entity => entity.Id).ToArray())
|
||||
.ToDictionary(contentType => contentType.Id);
|
||||
|
||||
return entities.Select(entity =>
|
||||
|
||||
+10
-3
@@ -1,18 +1,25 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.PublishedCache;
|
||||
|
||||
[Obsolete("This controller no longer serves a purpose")]
|
||||
[ApiVersion("1.0")]
|
||||
public class CollectPublishedCacheController : PublishedCacheControllerBase
|
||||
{
|
||||
private readonly IPublishedSnapshotService _publishedSnapshotService;
|
||||
|
||||
public CollectPublishedCacheController(IPublishedSnapshotService publishedSnapshotService)
|
||||
=> _publishedSnapshotService = publishedSnapshotService;
|
||||
|
||||
[HttpPost("collect")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(StatusCodes.Status501NotImplemented)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> Collect(CancellationToken cancellationToken)
|
||||
{
|
||||
return StatusCode(StatusCodes.Status501NotImplemented);
|
||||
GC.Collect();
|
||||
await _publishedSnapshotService.CollectAsync();
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
|
||||
+4
-3
@@ -8,16 +8,17 @@ namespace Umbraco.Cms.Api.Management.Controllers.PublishedCache;
|
||||
[ApiVersion("1.0")]
|
||||
public class RebuildPublishedCacheController : PublishedCacheControllerBase
|
||||
{
|
||||
private readonly IDatabaseCacheRebuilder _databaseCacheRebuilder;
|
||||
private readonly IPublishedSnapshotService _publishedSnapshotService;
|
||||
|
||||
public RebuildPublishedCacheController(IDatabaseCacheRebuilder databaseCacheRebuilder) => _databaseCacheRebuilder = databaseCacheRebuilder;
|
||||
public RebuildPublishedCacheController(IPublishedSnapshotService publishedSnapshotService)
|
||||
=> _publishedSnapshotService = publishedSnapshotService;
|
||||
|
||||
[HttpPost("rebuild")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> Rebuild(CancellationToken cancellationToken)
|
||||
{
|
||||
_databaseCacheRebuilder.Rebuild();
|
||||
_publishedSnapshotService.Rebuild();
|
||||
return await Task.FromResult(Ok());
|
||||
}
|
||||
}
|
||||
|
||||
+8
-5
@@ -1,18 +1,21 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.PublishedCache;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
[Obsolete("This no longer relevant since snapshots are no longer used")]
|
||||
public class StatusPublishedCacheController : PublishedCacheControllerBase
|
||||
{
|
||||
private readonly IPublishedSnapshotStatus _publishedSnapshotStatus;
|
||||
|
||||
public StatusPublishedCacheController(IPublishedSnapshotStatus publishedSnapshotStatus)
|
||||
=> _publishedSnapshotStatus = publishedSnapshotStatus;
|
||||
|
||||
[HttpGet("status")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(StatusCodes.Status501NotImplemented)]
|
||||
[ProducesResponseType(typeof(string), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<string>> Status(CancellationToken cancellationToken)
|
||||
{
|
||||
return StatusCode(StatusCodes.Status501NotImplemented);
|
||||
}
|
||||
=> await Task.FromResult(Ok(_publishedSnapshotStatus.GetStatus()));
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
|
||||
using Umbraco.Cms.Api.Management.Controllers.Content;
|
||||
@@ -54,7 +54,6 @@ public abstract class RecycleBinControllerBase<TItem> : ContentControllerBase
|
||||
var viewModel = new TItem
|
||||
{
|
||||
Id = entity.Key,
|
||||
CreateDate = entity.CreateDate,
|
||||
HasChildren = entity.HasChildren,
|
||||
Parent = parentKey.HasValue
|
||||
? new ItemReferenceByIdResponseModel
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using Asp.Versioning;
|
||||
using Examine;
|
||||
using Examine.Lucene.Search;
|
||||
using Examine.Search;
|
||||
using Lucene.Net.QueryParsers.Classic;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
@@ -53,13 +52,12 @@ public class QuerySearcherController : SearcherControllerBase
|
||||
ISearchResults results;
|
||||
|
||||
// NativeQuery will work for a single word/phrase too (but depends on the implementation) the lucene one will work.
|
||||
// Due to examine changes we need to supply the skipTakeMaxResults, see https://github.com/umbraco/Umbraco-CMS/issues/17920 for more info
|
||||
try
|
||||
{
|
||||
results = searcher
|
||||
.CreateQuery()
|
||||
.NativeQuery(term)
|
||||
.Execute(new LuceneQueryOptions(skip, take, skipTakeMaxResults: skip + take));
|
||||
.Execute(QueryOptions.SkipTake(skip, take));
|
||||
}
|
||||
catch (ParseException)
|
||||
{
|
||||
|
||||
@@ -44,7 +44,6 @@ public class BackOfficeController : SecurityControllerBase
|
||||
private readonly IBackOfficeTwoFactorOptions _backOfficeTwoFactorOptions;
|
||||
private readonly IUserTwoFactorLoginService _userTwoFactorLoginService;
|
||||
private readonly IBackOfficeExternalLoginService _externalLoginService;
|
||||
private readonly IBackOfficeUserClientCredentialsManager _backOfficeUserClientCredentialsManager;
|
||||
|
||||
private const string RedirectFlowParameter = "flow";
|
||||
private const string RedirectStatusParameter = "status";
|
||||
@@ -58,8 +57,7 @@ public class BackOfficeController : SecurityControllerBase
|
||||
ILogger<BackOfficeController> logger,
|
||||
IBackOfficeTwoFactorOptions backOfficeTwoFactorOptions,
|
||||
IUserTwoFactorLoginService userTwoFactorLoginService,
|
||||
IBackOfficeExternalLoginService externalLoginService,
|
||||
IBackOfficeUserClientCredentialsManager backOfficeUserClientCredentialsManager)
|
||||
IBackOfficeExternalLoginService externalLoginService)
|
||||
{
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
_backOfficeSignInManager = backOfficeSignInManager;
|
||||
@@ -69,7 +67,6 @@ public class BackOfficeController : SecurityControllerBase
|
||||
_backOfficeTwoFactorOptions = backOfficeTwoFactorOptions;
|
||||
_userTwoFactorLoginService = userTwoFactorLoginService;
|
||||
_externalLoginService = externalLoginService;
|
||||
_backOfficeUserClientCredentialsManager = backOfficeUserClientCredentialsManager;
|
||||
}
|
||||
|
||||
[HttpPost("login")]
|
||||
@@ -191,21 +188,13 @@ public class BackOfficeController : SecurityControllerBase
|
||||
OpenIddictRequest? request = context.GetOpenIddictServerRequest();
|
||||
if (request == 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 we keep member authentication away from this endpoint
|
||||
if (request.ClientId is 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()
|
||||
@@ -213,63 +202,6 @@ public class BackOfficeController : SecurityControllerBase
|
||||
: await AuthorizeExternal(request);
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
[HttpPost("token")]
|
||||
[MapToApiVersion("1.0")]
|
||||
public async Task<IActionResult> Token()
|
||||
{
|
||||
HttpContext context = _httpContextAccessor.GetRequiredHttpContext();
|
||||
OpenIddictRequest? request = context.GetOpenIddictServerRequest();
|
||||
if (request == null)
|
||||
{
|
||||
return BadRequest(new OpenIddictResponse
|
||||
{
|
||||
Error = "No context found",
|
||||
ErrorDescription = "Unable to obtain context from the current request."
|
||||
});
|
||||
}
|
||||
|
||||
if (request.IsAuthorizationCodeGrantType() || request.IsRefreshTokenGrantType())
|
||||
{
|
||||
// attempt to authorize against the supplied the authorization code
|
||||
AuthenticateResult authenticateResult = await context.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."
|
||||
});
|
||||
}
|
||||
|
||||
// ensure the client ID and secret are valid (verified by OpenIddict)
|
||||
if (!request.IsClientCredentialsGrantType())
|
||||
{
|
||||
throw new InvalidOperationException("The requested grant type is not supported.");
|
||||
}
|
||||
|
||||
// grab the user associated with the client ID
|
||||
BackOfficeIdentityUser? associatedUser = await _backOfficeUserClientCredentialsManager.FindUserAsync(request.ClientId!);
|
||||
if (associatedUser is not null)
|
||||
{
|
||||
// log current datetime as last login (this also ensures that the user is not flagged as inactive)
|
||||
associatedUser.LastLoginDateUtc = DateTime.UtcNow;
|
||||
await _backOfficeUserManager.UpdateAsync(associatedUser);
|
||||
|
||||
return await SignInBackOfficeUser(associatedUser, request);
|
||||
}
|
||||
|
||||
// if this happens, the OpenIddict applications have somehow gone out of sync with the Umbraco users
|
||||
_logger.LogError("The user associated with the client ID ({clientId}) could not be found", request.ClientId);
|
||||
|
||||
return BadRequest(new OpenIddictResponse
|
||||
{
|
||||
Error = "Authorization failed",
|
||||
ErrorDescription = "The user associated with the supplied 'client_id' could not be found."
|
||||
});
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
[HttpGet("signout")]
|
||||
[MapToApiVersion("1.0")]
|
||||
@@ -518,7 +450,7 @@ public class BackOfficeController : SecurityControllerBase
|
||||
|
||||
private static IActionResult DefaultChallengeResult() => new ChallengeResult(Constants.Security.BackOfficeAuthenticationType);
|
||||
|
||||
private RedirectResult CallbackErrorRedirectWithStatus(string flowType, string status, IEnumerable<IdentityError> identityErrors)
|
||||
private RedirectResult CallbackErrorRedirectWithStatus( string flowType, string status, IEnumerable<IdentityError> identityErrors)
|
||||
{
|
||||
var redirectUrl = _securitySettings.Value.BackOfficeHost + "/" +
|
||||
_securitySettings.Value.AuthorizeCallbackErrorPathName.TrimStart('/').AppendQueryStringToUrl(
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.StaticFiles;
|
||||
using Microsoft.Extensions.FileProviders;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Api.Management.Routing;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.Security;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
[VersionedApiBackOfficeRoute(Common.Security.Paths.BackOfficeApi.EndpointTemplate + "/graphics")]
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
public class BackOfficeGraphicsController : Controller
|
||||
{
|
||||
public const string LogoRouteName = nameof(BackOfficeGraphicsController) + "." + nameof(Logo);
|
||||
public const string LoginBackGroundRouteName = nameof(BackOfficeGraphicsController) + "." + nameof(LoginBackground);
|
||||
public const string LoginLogoRouteName = nameof(BackOfficeGraphicsController) + "." + nameof(LoginLogo);
|
||||
public const string LoginLogoAlternativeRouteName = nameof(BackOfficeGraphicsController) + "." + nameof(LoginLogoAlternative);
|
||||
|
||||
private readonly IOptions<ContentSettings> _contentSettings;
|
||||
private readonly IContentTypeProvider _contentTypeProvider;
|
||||
private readonly IWebHostEnvironment _webHostEnvironment;
|
||||
|
||||
public BackOfficeGraphicsController(IOptions<ContentSettings> contentSettings, IOptions<StaticFileOptions> staticFileOptions, IWebHostEnvironment webHostEnvironment)
|
||||
{
|
||||
_contentSettings = contentSettings;
|
||||
_webHostEnvironment = webHostEnvironment;
|
||||
_contentTypeProvider = staticFileOptions.Value.ContentTypeProvider ?? new FileExtensionContentTypeProvider();
|
||||
}
|
||||
|
||||
[HttpGet("login-background", Name = LoginBackGroundRouteName)]
|
||||
[AllowAnonymous]
|
||||
[MapToApiVersion("1.0")]
|
||||
public IActionResult LoginBackground() => HandleFileRequest(_contentSettings.Value.LoginBackgroundImage);
|
||||
|
||||
[HttpGet("logo", Name = LogoRouteName)]
|
||||
[AllowAnonymous]
|
||||
[MapToApiVersion("1.0")]
|
||||
public IActionResult Logo() => HandleFileRequest(_contentSettings.Value.BackOfficeLogo);
|
||||
|
||||
[HttpGet("login-logo", Name = LoginLogoRouteName)]
|
||||
[AllowAnonymous]
|
||||
[MapToApiVersion("1.0")]
|
||||
public IActionResult LoginLogo() => HandleFileRequest(_contentSettings.Value.LoginLogoImage);
|
||||
|
||||
[HttpGet("login-logo-alternative", Name = LoginLogoAlternativeRouteName)]
|
||||
[AllowAnonymous]
|
||||
[MapToApiVersion("1.0")]
|
||||
public IActionResult LoginLogoAlternative() => HandleFileRequest(_contentSettings.Value.LoginLogoImageAlternative);
|
||||
|
||||
private IActionResult HandleFileRequest(string virtualPath)
|
||||
{
|
||||
var filePath = $"{Constants.SystemDirectories.Umbraco}/{virtualPath}".TrimStart(Constants.CharArrays.Tilde);
|
||||
IFileInfo fileInfo = _webHostEnvironment.WebRootFileProvider.GetFileInfo(filePath);
|
||||
|
||||
if (fileInfo.PhysicalPath is null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (_contentTypeProvider.TryGetContentType(fileInfo.PhysicalPath, out var contentType))
|
||||
{
|
||||
Stream fileStream = fileInfo.CreateReadStream();
|
||||
return File(fileStream, contentType);
|
||||
}
|
||||
|
||||
return StatusCode(StatusCodes.Status412PreconditionFailed);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,8 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Api.Management.Security;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Server;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
@@ -16,7 +14,6 @@ public class ConfigurationServerController : ServerControllerBase
|
||||
{
|
||||
private readonly SecuritySettings _securitySettings;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly IBackOfficeExternalLoginProviders _externalLoginProviders;
|
||||
|
||||
[Obsolete("Use the constructor that accepts all arguments. Will be removed in V16.")]
|
||||
public ConfigurationServerController(IOptions<SecuritySettings> securitySettings)
|
||||
@@ -24,21 +21,13 @@ public class ConfigurationServerController : ServerControllerBase
|
||||
{
|
||||
}
|
||||
|
||||
[Obsolete("Use the constructor that accepts all arguments. Will be removed in V16.")]
|
||||
public ConfigurationServerController(IOptions<SecuritySettings> securitySettings, IOptions<GlobalSettings> globalSettings)
|
||||
: this(securitySettings, globalSettings, StaticServiceProvider.Instance.GetRequiredService<IBackOfficeExternalLoginProviders>())
|
||||
{
|
||||
}
|
||||
|
||||
[ActivatorUtilitiesConstructor]
|
||||
public ConfigurationServerController(IOptions<SecuritySettings> securitySettings, IOptions<GlobalSettings> globalSettings, IBackOfficeExternalLoginProviders externalLoginProviders)
|
||||
public ConfigurationServerController(IOptions<SecuritySettings> securitySettings, IOptions<GlobalSettings> globalSettings)
|
||||
{
|
||||
_securitySettings = securitySettings.Value;
|
||||
_globalSettings = globalSettings.Value;
|
||||
_externalLoginProviders = externalLoginProviders;
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
[HttpGet("configuration")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(ServerConfigurationResponseModel), StatusCodes.Status200OK)]
|
||||
@@ -47,8 +36,7 @@ public class ConfigurationServerController : ServerControllerBase
|
||||
var responseModel = new ServerConfigurationResponseModel
|
||||
{
|
||||
AllowPasswordReset = _securitySettings.AllowPasswordReset,
|
||||
VersionCheckPeriod = _globalSettings.VersionCheckPeriod,
|
||||
AllowLocalLogin = _externalLoginProviders.HasDenyLocalLogin() is false,
|
||||
VersionCheckPeriod = _globalSettings.VersionCheckPeriod
|
||||
};
|
||||
|
||||
return Task.FromResult<IActionResult>(Ok(responseModel));
|
||||
|
||||
+4
-13
@@ -1,6 +1,5 @@
|
||||
using System.Diagnostics;
|
||||
using System.Linq.Expressions;
|
||||
using System.Runtime.Versioning;
|
||||
using System.Text;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
@@ -9,9 +8,7 @@ using Umbraco.Cms.Api.Management.ViewModels.Template.Query;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.Models.TemplateQuery;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.Navigation;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.Template.Query;
|
||||
@@ -23,8 +20,6 @@ public class ExecuteTemplateQueryController : TemplateQueryControllerBase
|
||||
private readonly IVariationContextAccessor _variationContextAccessor;
|
||||
private readonly IPublishedValueFallback _publishedValueFallback;
|
||||
private readonly IContentTypeService _contentTypeService;
|
||||
private readonly IPublishedContentCache _contentCache;
|
||||
private readonly IDocumentNavigationQueryService _documentNavigationQueryService;
|
||||
|
||||
private static readonly string _indent = $"{Environment.NewLine} ";
|
||||
|
||||
@@ -32,16 +27,12 @@ public class ExecuteTemplateQueryController : TemplateQueryControllerBase
|
||||
IPublishedContentQuery publishedContentQuery,
|
||||
IVariationContextAccessor variationContextAccessor,
|
||||
IPublishedValueFallback publishedValueFallback,
|
||||
IContentTypeService contentTypeService,
|
||||
IPublishedContentCache contentCache,
|
||||
IDocumentNavigationQueryService documentNavigationQueryService)
|
||||
IContentTypeService contentTypeService)
|
||||
{
|
||||
_publishedContentQuery = publishedContentQuery;
|
||||
_variationContextAccessor = variationContextAccessor;
|
||||
_publishedValueFallback = publishedValueFallback;
|
||||
_contentTypeService = contentTypeService;
|
||||
_contentCache = contentCache;
|
||||
_documentNavigationQueryService = documentNavigationQueryService;
|
||||
}
|
||||
|
||||
[HttpPost("execute")]
|
||||
@@ -60,7 +51,7 @@ public class ExecuteTemplateQueryController : TemplateQueryControllerBase
|
||||
timer.Stop();
|
||||
|
||||
var contentTypeIconsByKey = _contentTypeService
|
||||
.GetMany(results.Select(content => content.ContentType.Key).Distinct())
|
||||
.GetAll(results.Select(content => content.ContentType.Key).Distinct())
|
||||
.ToDictionary(contentType => contentType.Key, contentType => contentType.Icon);
|
||||
|
||||
return await Task.FromResult(Ok(new TemplateQueryResultResponseModel
|
||||
@@ -118,13 +109,13 @@ public class ExecuteTemplateQueryController : TemplateQueryControllerBase
|
||||
queryExpression.Append($".ChildrenOfType(\"{model.DocumentTypeAlias}\")");
|
||||
return rootContent == null
|
||||
? Enumerable.Empty<IPublishedContent>()
|
||||
: rootContent.ChildrenOfType(_variationContextAccessor, _contentCache, _documentNavigationQueryService, model.DocumentTypeAlias);
|
||||
: rootContent.ChildrenOfType(_variationContextAccessor, model.DocumentTypeAlias);
|
||||
}
|
||||
|
||||
queryExpression.Append(".Children()");
|
||||
return rootContent == null
|
||||
? Enumerable.Empty<IPublishedContent>()
|
||||
: rootContent.Children(_variationContextAccessor, _contentCache, _documentNavigationQueryService);
|
||||
: rootContent.Children(_variationContextAccessor);
|
||||
}
|
||||
|
||||
private IEnumerable<IPublishedContent> ApplyFiltering(IEnumerable<TemplateQueryExecuteFilterPresentationModel>? filters, IEnumerable<IPublishedContent> contentQuery, StringBuilder queryExpression)
|
||||
|
||||
@@ -38,10 +38,6 @@ public class TemplateControllerBase : ManagementApiControllerBase
|
||||
.WithTitle("Master template not found")
|
||||
.WithDetail("The master template referenced in the template was not found.")
|
||||
.Build()),
|
||||
TemplateOperationStatus.MasterTemplateCannotBeDeleted => BadRequest(problemDetailsBuilder
|
||||
.WithTitle("Master template cannot be deleted")
|
||||
.WithDetail("The master templates cannot be deleted. Please ensure the template is not a master template before you delete.")
|
||||
.Build()),
|
||||
_ => StatusCode(StatusCodes.Status500InternalServerError, problemDetailsBuilder
|
||||
.WithTitle("Unknown template operation status.")
|
||||
.Build()),
|
||||
|
||||
+4
-1
@@ -1,4 +1,4 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Common.Builders;
|
||||
using Umbraco.Cms.Api.Management.Routing;
|
||||
@@ -17,6 +17,9 @@ public abstract class TemporaryFileControllerBase : ManagementApiControllerBase
|
||||
.WithTitle("File extension not allowed")
|
||||
.WithDetail("The file extension is not allowed.")
|
||||
.Build()),
|
||||
TemporaryFileOperationStatus.InvalidFileName => BadRequest(problemDetailsBuilder
|
||||
.WithTitle("The provided file name is not valid")
|
||||
.Build()),
|
||||
TemporaryFileOperationStatus.KeyAlreadyUsed => BadRequest(problemDetailsBuilder
|
||||
.WithTitle("Key already used")
|
||||
.WithDetail("The specified key is already used.")
|
||||
|
||||
-29
@@ -1,29 +0,0 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Core.Security.OperationStatus;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.User.ClientCredentials;
|
||||
|
||||
[ApiExplorerSettings(GroupName = "User")]
|
||||
public abstract class ClientCredentialsUserControllerBase : UserControllerBase
|
||||
{
|
||||
protected IActionResult BackOfficeUserClientCredentialsOperationStatusResult(BackOfficeUserClientCredentialsOperationStatus status) =>
|
||||
OperationStatusResult(status, problemDetailsBuilder => status switch
|
||||
{
|
||||
BackOfficeUserClientCredentialsOperationStatus.InvalidUser => BadRequest(problemDetailsBuilder
|
||||
.WithTitle("Invalid user")
|
||||
.WithDetail("The specified user does not support this operation. Possibly caused by a mismatched client ID or an inapplicable user type.")
|
||||
.Build()),
|
||||
BackOfficeUserClientCredentialsOperationStatus.DuplicateClientId => BadRequest(problemDetailsBuilder
|
||||
.WithTitle("Duplicate client ID")
|
||||
.WithDetail("The specified client ID is already in use. Choose another client ID.")
|
||||
.Build()),
|
||||
BackOfficeUserClientCredentialsOperationStatus.InvalidClientId => BadRequest(problemDetailsBuilder
|
||||
.WithTitle("Invalid client ID")
|
||||
.WithDetail("The specified client ID is invalid. A valid client ID can only contain [a-z], [A-Z], [0-9], and [-._~]. Furthermore, including the prefix it cannot be longer than 255 characters.")
|
||||
.Build()),
|
||||
_ => StatusCode(StatusCodes.Status500InternalServerError, problemDetailsBuilder
|
||||
.WithTitle("Unknown client credentials operation status.")
|
||||
.Build()),
|
||||
});
|
||||
}
|
||||
-49
@@ -1,49 +0,0 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.User.ClientCredentials;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Security.Authorization;
|
||||
using Umbraco.Cms.Core.Security.OperationStatus;
|
||||
using Umbraco.Cms.Web.Common.Authorization;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.User.ClientCredentials;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
public class CreateClientCredentialsUserController : ClientCredentialsUserControllerBase
|
||||
{
|
||||
private readonly IBackOfficeUserClientCredentialsManager _backOfficeUserClientCredentialsManager;
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
|
||||
public CreateClientCredentialsUserController(
|
||||
IBackOfficeUserClientCredentialsManager backOfficeUserClientCredentialsManager,
|
||||
IAuthorizationService authorizationService)
|
||||
{
|
||||
_backOfficeUserClientCredentialsManager = backOfficeUserClientCredentialsManager;
|
||||
_authorizationService = authorizationService;
|
||||
}
|
||||
|
||||
[HttpPost("{id:guid}/client-credentials")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
public async Task<IActionResult> Create(CancellationToken cancellationToken, Guid id, CreateUserClientCredentialsRequestModel model)
|
||||
{
|
||||
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
|
||||
User,
|
||||
UserPermissionResource.WithKeys(id),
|
||||
AuthorizationPolicies.UserPermissionByResource);
|
||||
|
||||
if (!authorizationResult.Succeeded)
|
||||
{
|
||||
return Forbidden();
|
||||
}
|
||||
|
||||
Attempt<BackOfficeUserClientCredentialsOperationStatus> result = await _backOfficeUserClientCredentialsManager.SaveAsync(id, model.ClientId, model.ClientSecret);
|
||||
return result.Success
|
||||
? Ok()
|
||||
: BackOfficeUserClientCredentialsOperationStatusResult(result.Result);
|
||||
}
|
||||
}
|
||||
-48
@@ -1,48 +0,0 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Security.Authorization;
|
||||
using Umbraco.Cms.Core.Security.OperationStatus;
|
||||
using Umbraco.Cms.Web.Common.Authorization;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.User.ClientCredentials;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
public class DeleteClientCredentialsUserController : ClientCredentialsUserControllerBase
|
||||
{
|
||||
private readonly IBackOfficeUserClientCredentialsManager _backOfficeUserClientCredentialsManager;
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
|
||||
public DeleteClientCredentialsUserController(
|
||||
IBackOfficeUserClientCredentialsManager backOfficeUserClientCredentialsManager,
|
||||
IAuthorizationService authorizationService)
|
||||
{
|
||||
_backOfficeUserClientCredentialsManager = backOfficeUserClientCredentialsManager;
|
||||
_authorizationService = authorizationService;
|
||||
}
|
||||
|
||||
[HttpDelete("{id:guid}/client-credentials/{clientId}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
public async Task<IActionResult> Delete(CancellationToken cancellationToken, Guid id, string clientId)
|
||||
{
|
||||
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
|
||||
User,
|
||||
UserPermissionResource.WithKeys(id),
|
||||
AuthorizationPolicies.UserPermissionByResource);
|
||||
|
||||
if (!authorizationResult.Succeeded)
|
||||
{
|
||||
return Forbidden();
|
||||
}
|
||||
|
||||
Attempt<BackOfficeUserClientCredentialsOperationStatus> result = await _backOfficeUserClientCredentialsManager.DeleteAsync(id, clientId);
|
||||
return result.Success
|
||||
? Ok()
|
||||
: BackOfficeUserClientCredentialsOperationStatusResult(result.Result);
|
||||
}
|
||||
}
|
||||
-43
@@ -1,43 +0,0 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Security.Authorization;
|
||||
using Umbraco.Cms.Web.Common.Authorization;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.User.ClientCredentials;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
public class GetAllClientCredentialsUserController : ClientCredentialsUserControllerBase
|
||||
{
|
||||
private readonly IBackOfficeUserClientCredentialsManager _backOfficeUserClientCredentialsManager;
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
|
||||
public GetAllClientCredentialsUserController(
|
||||
IBackOfficeUserClientCredentialsManager backOfficeUserClientCredentialsManager,
|
||||
IAuthorizationService authorizationService)
|
||||
{
|
||||
_backOfficeUserClientCredentialsManager = backOfficeUserClientCredentialsManager;
|
||||
_authorizationService = authorizationService;
|
||||
}
|
||||
|
||||
[HttpGet("{id:guid}/client-credentials")]
|
||||
[ProducesResponseType(typeof(IEnumerable<string>), StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> GetAll(CancellationToken cancellationToken, Guid id)
|
||||
{
|
||||
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
|
||||
User,
|
||||
UserPermissionResource.WithKeys(id),
|
||||
AuthorizationPolicies.UserPermissionByResource);
|
||||
|
||||
if (!authorizationResult.Succeeded)
|
||||
{
|
||||
return Forbidden();
|
||||
}
|
||||
|
||||
IEnumerable<string> clientIds = await _backOfficeUserClientCredentialsManager.GetClientIdsAsync(id);
|
||||
return Ok(clientIds);
|
||||
}
|
||||
}
|
||||
@@ -128,10 +128,6 @@ public abstract class UserOrCurrentUserControllerBase : ManagementApiControllerB
|
||||
.WithTitle("Self password reset not allowed")
|
||||
.WithDetail("It is not allowed to reset the password for the account you are logged in to.")
|
||||
.Build()),
|
||||
UserOperationStatus.InvalidUserType => BadRequest(problemDetailsBuilder
|
||||
.WithTitle("Invalid user type")
|
||||
.WithDetail("The target user type does not support this operation.")
|
||||
.Build()),
|
||||
UserOperationStatus.Forbidden => Forbidden(),
|
||||
_ => StatusCode(StatusCodes.Status500InternalServerError, problemDetailsBuilder
|
||||
.WithTitle("Unknown user operation status.")
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.Factories;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Webhook;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Webhook.Logs;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.Webhook.Logs;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
public class AllWebhookLogController : WebhookLogControllerBase
|
||||
{
|
||||
private readonly IWebhookLogService _webhookLogService;
|
||||
private readonly IWebhookPresentationFactory _webhookPresentationFactory;
|
||||
|
||||
public AllWebhookLogController(IWebhookLogService webhookLogService, IWebhookPresentationFactory webhookPresentationFactory)
|
||||
{
|
||||
_webhookLogService = webhookLogService;
|
||||
_webhookPresentationFactory = webhookPresentationFactory;
|
||||
}
|
||||
|
||||
[HttpGet("logs")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(WebhookResponseModel), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> Logs(CancellationToken cancellationToken, int skip = 0, int take = 100)
|
||||
{
|
||||
PagedModel<WebhookLog> logs = await _webhookLogService.Get(skip, take);
|
||||
IEnumerable<WebhookLogResponseModel> logResponseModels = logs.Items.Select(x => _webhookPresentationFactory.CreateResponseModel(x));
|
||||
return Ok(logResponseModels);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.Routing;
|
||||
using Umbraco.Cms.Core;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.Webhook.Logs;
|
||||
|
||||
[VersionedApiBackOfficeRoute($"{Constants.UdiEntityType.Webhook}")]
|
||||
[ApiExplorerSettings(GroupName = "Webhook")]
|
||||
public class WebhookLogControllerBase : ManagementApiControllerBase;
|
||||
-1
@@ -81,7 +81,6 @@ internal static class BackOfficeAuthPolicyBuilderExtensions
|
||||
AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessDocuments, Constants.Applications.Content);
|
||||
AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessDocumentsOrDocumentTypes, Constants.Applications.Content, Constants.Applications.Settings);
|
||||
AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessDocumentOrMediaOrContentTypes, Constants.Applications.Content, Constants.Applications.Settings, Constants.Applications.Media);
|
||||
AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessDocumentsOrMediaOrMembersOrContentTypes, Constants.Applications.Content, Constants.Applications.Media, Constants.Applications.Members, Constants.Applications.Settings);
|
||||
AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessDocumentTypes, Constants.Applications.Settings);
|
||||
AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessLanguages, Constants.Applications.Settings);
|
||||
AddAllowedApplicationsPolicy(AuthorizationPolicies.TreeAccessMediaTypes, Constants.Applications.Settings);
|
||||
|
||||
@@ -10,7 +10,7 @@ internal static class MemberBuilderExtensions
|
||||
{
|
||||
internal static IUmbracoBuilder AddMember(this IUmbracoBuilder builder)
|
||||
{
|
||||
builder.Services.AddSingleton<IMemberPresentationFactory, MemberPresentationFactory>();
|
||||
builder.Services.AddTransient<IMemberPresentationFactory, MemberPresentationFactory>();
|
||||
builder.Services.AddTransient<IMemberEditingPresentationFactory, MemberEditingPresentationFactory>();
|
||||
|
||||
builder.WithCollectionBuilder<MapDefinitionCollectionBuilder>().Add<MemberMapDefinition>();
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user